Compare commits
	
		
			10 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 70c29c778c | |||
| 0fc302699e | |||
| dcb7ca2df3 | |||
| ccbb0415e4 | |||
| 496f54cedd | |||
| 83b5ecebeb | |||
| 53b5cbed07 | |||
| 352fe79791 | |||
| a95d5a96a0 | |||
| ece7bb9a94 | 
							
								
								
									
										13
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,5 +1,18 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-10-23 - 1.12.6 - fix(dependencies) | ||||
| Bump FontAwesome to ^7.1.0 and add local claude settings | ||||
|  | ||||
| - Updated @fortawesome packages (@fortawesome/fontawesome-svg-core, @fortawesome/free-brands-svg-icons, @fortawesome/free-regular-svg-icons, @fortawesome/free-solid-svg-icons) to ^7.1.0 in package.json | ||||
| - Added .claude/settings.local.json to configure local Claude/tooling permissions for repository operations | ||||
|  | ||||
| ## 2025-09-23 - 1.12.5 - fix(ci) | ||||
| Add local permissions settings for development | ||||
|  | ||||
| - Adds a new local settings file: .claude/settings.local.json | ||||
| - Provides explicit permission entries for development tasks (allow running pnpm scripts, reading files, searching/replacing patterns, activating project, and helper tooling) | ||||
| - Intended for local dev environment to enable tool automation without changing repository code | ||||
|  | ||||
| ## 2025-09-20 - 1.12.4 - fix(ci) | ||||
| Add local assistant settings to enable permitted dev tooling commands | ||||
|  | ||||
|   | ||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "@design.estate/dees-catalog", | ||||
|   "version": "1.12.4", | ||||
|   "version": "1.12.6", | ||||
|   "private": false, | ||||
|   "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", | ||||
|   "main": "dist_ts_web/index.js", | ||||
| @@ -19,10 +19,10 @@ | ||||
|     "@design.estate/dees-domtools": "^2.3.3", | ||||
|     "@design.estate/dees-element": "^2.1.2", | ||||
|     "@design.estate/dees-wcctools": "^1.2.0", | ||||
|     "@fortawesome/fontawesome-svg-core": "^7.0.1", | ||||
|     "@fortawesome/free-brands-svg-icons": "^7.0.1", | ||||
|     "@fortawesome/free-regular-svg-icons": "^7.0.1", | ||||
|     "@fortawesome/free-solid-svg-icons": "^7.0.1", | ||||
|     "@fortawesome/fontawesome-svg-core": "^7.1.0", | ||||
|     "@fortawesome/free-brands-svg-icons": "^7.1.0", | ||||
|     "@fortawesome/free-regular-svg-icons": "^7.1.0", | ||||
|     "@fortawesome/free-solid-svg-icons": "^7.1.0", | ||||
|     "@push.rocks/smarti18n": "^1.0.4", | ||||
|     "@push.rocks/smartpromise": "^4.2.0", | ||||
|     "@push.rocks/smartstring": "^4.1.0", | ||||
|   | ||||
							
								
								
									
										56
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										56
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -18,17 +18,17 @@ importers: | ||||
|         specifier: ^1.2.0 | ||||
|         version: 1.2.0 | ||||
|       '@fortawesome/fontawesome-svg-core': | ||||
|         specifier: ^7.0.1 | ||||
|         version: 7.0.1 | ||||
|         specifier: ^7.1.0 | ||||
|         version: 7.1.0 | ||||
|       '@fortawesome/free-brands-svg-icons': | ||||
|         specifier: ^7.0.1 | ||||
|         version: 7.0.1 | ||||
|         specifier: ^7.1.0 | ||||
|         version: 7.1.0 | ||||
|       '@fortawesome/free-regular-svg-icons': | ||||
|         specifier: ^7.0.1 | ||||
|         version: 7.0.1 | ||||
|         specifier: ^7.1.0 | ||||
|         version: 7.1.0 | ||||
|       '@fortawesome/free-solid-svg-icons': | ||||
|         specifier: ^7.0.1 | ||||
|         version: 7.0.1 | ||||
|         specifier: ^7.1.0 | ||||
|         version: 7.1.0 | ||||
|       '@push.rocks/smarti18n': | ||||
|         specifier: ^1.0.4 | ||||
|         version: 1.0.4 | ||||
| @@ -774,24 +774,24 @@ packages: | ||||
|   '@esm-bundle/chai@4.3.4-fix.0': | ||||
|     resolution: {integrity: sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==} | ||||
|  | ||||
|   '@fortawesome/fontawesome-common-types@7.0.1': | ||||
|     resolution: {integrity: sha512-0VpNtO5cNe1/HQWMkl4OdncYK/mv9hnBte0Ew0n6DMzmo3Q3WzDFABHm6LeNTipt5zAyhQ6Ugjiu8aLaEjh1gg==} | ||||
|   '@fortawesome/fontawesome-common-types@7.1.0': | ||||
|     resolution: {integrity: sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==} | ||||
|     engines: {node: '>=6'} | ||||
|  | ||||
|   '@fortawesome/fontawesome-svg-core@7.0.1': | ||||
|     resolution: {integrity: sha512-x0cR55ILVqFpUioSMf6ebpRCMXMcheGN743P05W2RB5uCNpJUqWIqW66Lap8PfL/lngvjTbZj0BNSUweIr/fHQ==} | ||||
|   '@fortawesome/fontawesome-svg-core@7.1.0': | ||||
|     resolution: {integrity: sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==} | ||||
|     engines: {node: '>=6'} | ||||
|  | ||||
|   '@fortawesome/free-brands-svg-icons@7.0.1': | ||||
|     resolution: {integrity: sha512-6xPmn5SrND/GM0+W33E77x05+aDn6RpR02eWd8eLdN0IxY0vXa5yU/ugaAKloOVxiG9w2330TSRsbJYL6c57Ow==} | ||||
|   '@fortawesome/free-brands-svg-icons@7.1.0': | ||||
|     resolution: {integrity: sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==} | ||||
|     engines: {node: '>=6'} | ||||
|  | ||||
|   '@fortawesome/free-regular-svg-icons@7.0.1': | ||||
|     resolution: {integrity: sha512-4V9fHbHjcx9Qu4O99AM5B4zuEDfB4zajk1I77hEzOxPN00f8g3484Aeq6WpfFcmookvjLE3Pr71Dhf/lqw7tbA==} | ||||
|   '@fortawesome/free-regular-svg-icons@7.1.0': | ||||
|     resolution: {integrity: sha512-0e2fdEyB4AR+e6kU4yxwA/MonnYcw/CsMEP9lH82ORFi9svA6/RhDyhxIv5mlJaldmaHLLYVTb+3iEr+PDSZuQ==} | ||||
|     engines: {node: '>=6'} | ||||
|  | ||||
|   '@fortawesome/free-solid-svg-icons@7.0.1': | ||||
|     resolution: {integrity: sha512-esKuSrl1WMOTMDLNt38i16VfLe/gRZt2ZAJ3Yw7slfs7sj583MKqNFqO57zmhknk1Sya6f9Wys89aCzIJkcqlg==} | ||||
|   '@fortawesome/free-solid-svg-icons@7.1.0': | ||||
|     resolution: {integrity: sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==} | ||||
|     engines: {node: '>=6'} | ||||
|  | ||||
|   '@git.zone/tsbuild@2.6.8': | ||||
| @@ -6662,23 +6662,23 @@ snapshots: | ||||
|     dependencies: | ||||
|       '@types/chai': 4.3.20 | ||||
|  | ||||
|   '@fortawesome/fontawesome-common-types@7.0.1': {} | ||||
|   '@fortawesome/fontawesome-common-types@7.1.0': {} | ||||
|  | ||||
|   '@fortawesome/fontawesome-svg-core@7.0.1': | ||||
|   '@fortawesome/fontawesome-svg-core@7.1.0': | ||||
|     dependencies: | ||||
|       '@fortawesome/fontawesome-common-types': 7.0.1 | ||||
|       '@fortawesome/fontawesome-common-types': 7.1.0 | ||||
|  | ||||
|   '@fortawesome/free-brands-svg-icons@7.0.1': | ||||
|   '@fortawesome/free-brands-svg-icons@7.1.0': | ||||
|     dependencies: | ||||
|       '@fortawesome/fontawesome-common-types': 7.0.1 | ||||
|       '@fortawesome/fontawesome-common-types': 7.1.0 | ||||
|  | ||||
|   '@fortawesome/free-regular-svg-icons@7.0.1': | ||||
|   '@fortawesome/free-regular-svg-icons@7.1.0': | ||||
|     dependencies: | ||||
|       '@fortawesome/fontawesome-common-types': 7.0.1 | ||||
|       '@fortawesome/fontawesome-common-types': 7.1.0 | ||||
|  | ||||
|   '@fortawesome/free-solid-svg-icons@7.0.1': | ||||
|   '@fortawesome/free-solid-svg-icons@7.1.0': | ||||
|     dependencies: | ||||
|       '@fortawesome/fontawesome-common-types': 7.0.1 | ||||
|       '@fortawesome/fontawesome-common-types': 7.1.0 | ||||
|  | ||||
|   '@git.zone/tsbuild@2.6.8': | ||||
|     dependencies: | ||||
| @@ -6801,10 +6801,8 @@ snapshots: | ||||
|     transitivePeerDependencies: | ||||
|       - '@nuxt/kit' | ||||
|       - '@swc/helpers' | ||||
|       - bufferutil | ||||
|       - react | ||||
|       - supports-color | ||||
|       - utf-8-validate | ||||
|       - vue | ||||
|  | ||||
|   '@hapi/bourne@3.0.0': {} | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@design.estate/dees-catalog', | ||||
|   version: '1.12.4', | ||||
|   version: '1.12.6', | ||||
|   description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' | ||||
| } | ||||
|   | ||||
| @@ -52,6 +52,9 @@ export class DeesPdfViewer extends DeesElement { | ||||
|   @property({ type: Array }) | ||||
|   private thumbnailData: Array<{page: number, rendered: boolean}> = []; | ||||
|  | ||||
|   @property({ type: Array }) | ||||
|   private pageData: Array<{page: number, rendered: boolean, rendering: boolean}> = []; | ||||
|  | ||||
|   private pdfDocument: any; | ||||
|   private renderState: RenderState = 'idle'; | ||||
|   private renderAbortController: AbortController | null = null; | ||||
| @@ -60,16 +63,21 @@ export class DeesPdfViewer extends DeesElement { | ||||
|   private currentRenderTask: any = null; | ||||
|   private currentRenderPromise: Promise<void> | null = null; | ||||
|   private thumbnailRenderTasks: any[] = []; | ||||
|   private pageRenderTasks: Map<number, any> = new Map(); | ||||
|   private canvas: HTMLCanvasElement | undefined; | ||||
|   private ctx: CanvasRenderingContext2D | undefined; | ||||
|   private viewerMain: HTMLElement | null = null; | ||||
|   private resizeObserver?: ResizeObserver; | ||||
|   private intersectionObserver?: IntersectionObserver; | ||||
|   private scrollThrottleTimeout?: number; | ||||
|   private viewportDimensions = { width: 0, height: 0 }; | ||||
|   private viewportMode: 'auto' | 'page-fit' | 'page-width' | 'custom' = 'auto'; | ||||
|   private readonly MANUAL_MIN_ZOOM = 0.5; | ||||
|   private readonly MANUAL_MAX_ZOOM = 3; | ||||
|   private readonly ABSOLUTE_MIN_ZOOM = 0.1; | ||||
|   private readonly ABSOLUTE_MAX_ZOOM = 4; | ||||
|   private readonly PAGE_GAP = 20; | ||||
|   private readonly RENDER_BUFFER = 3; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
| @@ -150,6 +158,13 @@ export class DeesPdfViewer extends DeesElement { | ||||
|             </div> | ||||
|  | ||||
|             <div class="toolbar-group toolbar-group--end"> | ||||
|               <button | ||||
|                 class="toolbar-button" | ||||
|                 @click=${() => this.showSidebar = !this.showSidebar} | ||||
|                 title="${this.showSidebar ? 'Hide thumbnails' : 'Show thumbnails'}" | ||||
|               > | ||||
|                 <dees-icon icon="${this.showSidebar ? 'lucide:SidebarClose' : 'lucide:Sidebar'}"></dees-icon> | ||||
|               </button> | ||||
|               <button | ||||
|                 class="toolbar-button" | ||||
|                 @click=${this.downloadPdf} | ||||
| @@ -201,15 +216,25 @@ export class DeesPdfViewer extends DeesElement { | ||||
|             </div> | ||||
|           ` : ''} | ||||
|  | ||||
|           <div class="viewer-main"> | ||||
|           <div class="viewer-main" @scroll=${this.handleScroll}> | ||||
|             ${this.loading ? html` | ||||
|               <div class="loading-container"> | ||||
|                 <div class="loading-spinner"></div> | ||||
|                 <div class="loading-text">Loading PDF...</div> | ||||
|               </div> | ||||
|             ` : html` | ||||
|               <div class="pages-container"> | ||||
|                 ${repeat( | ||||
|                   this.pageData, | ||||
|                   (item) => item.page, | ||||
|                   (item) => html` | ||||
|                     <div class="page-wrapper" data-page="${item.page}"> | ||||
|                       <div class="canvas-container"> | ||||
|                 <canvas id="pdf-canvas"></canvas> | ||||
|                         <canvas class="page-canvas" data-page="${item.page}"></canvas> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   ` | ||||
|                 )} | ||||
|               </div> | ||||
|             `} | ||||
|           </div> | ||||
| @@ -234,6 +259,14 @@ export class DeesPdfViewer extends DeesElement { | ||||
|     await super.disconnectedCallback(); | ||||
|     this.resizeObserver?.disconnect(); | ||||
|     this.resizeObserver = undefined; | ||||
|     this.intersectionObserver?.disconnect(); | ||||
|     this.intersectionObserver = undefined; | ||||
|  | ||||
|     // Clear scroll timeout | ||||
|     if (this.scrollThrottleTimeout) { | ||||
|       clearTimeout(this.scrollThrottleTimeout); | ||||
|       this.scrollThrottleTimeout = undefined; | ||||
|     } | ||||
|  | ||||
|     // Mark as disposed and clean up | ||||
|     this.renderState = 'disposed'; | ||||
| @@ -257,11 +290,15 @@ export class DeesPdfViewer extends DeesElement { | ||||
|       await this.loadPdf(); | ||||
|     } | ||||
|  | ||||
|     // Only re-render thumbnails when sidebar becomes visible and document is loaded | ||||
|     if (changedProperties.has('showSidebar') && this.showSidebar && this.pdfDocument && this.renderState === 'rendered') { | ||||
|     // Re-render thumbnails when sidebar becomes visible and document is loaded | ||||
|     if (changedProperties.has('showSidebar') && this.showSidebar && this.pdfDocument) { | ||||
|       // Use requestAnimationFrame to ensure DOM is ready | ||||
|       await new Promise(resolve => requestAnimationFrame(resolve)); | ||||
|       // Force re-render of thumbnails by resetting their rendered state | ||||
|       this.thumbnailData.forEach(thumb => thumb.rendered = false); | ||||
|       await this.renderThumbnails(); | ||||
|       // Re-setup intersection observer for lazy loading of pages | ||||
|       this.setupIntersectionObserver(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -283,42 +320,39 @@ export class DeesPdfViewer extends DeesElement { | ||||
|       this.currentPage = this.initialPage; | ||||
|       this.resolveInitialViewportMode(); | ||||
|  | ||||
|       // Initialize thumbnail data array | ||||
|       // Initialize thumbnail and page data arrays | ||||
|       this.thumbnailData = Array.from({length: this.totalPages}, (_, i) => ({ | ||||
|         page: i + 1, | ||||
|         rendered: false | ||||
|       })); | ||||
|  | ||||
|       // Set loading to false to render the canvas | ||||
|       this.pageData = Array.from({length: this.totalPages}, (_, i) => ({ | ||||
|         page: i + 1, | ||||
|         rendered: false, | ||||
|         rendering: false | ||||
|       })); | ||||
|  | ||||
|       // Set loading to false to render the pages | ||||
|       this.loading = false; | ||||
|       await this.updateComplete; | ||||
|       this.ensureViewerRefs(); | ||||
|       this.setupIntersectionObserver(); | ||||
|  | ||||
|       // Wait for next frame to ensure DOM is ready | ||||
|       await new Promise(resolve => requestAnimationFrame(resolve)); | ||||
|       if (signal.aborted) return; | ||||
|  | ||||
|       // Always re-acquire canvas references | ||||
|       this.canvas = this.shadowRoot?.querySelector('#pdf-canvas') as HTMLCanvasElement; | ||||
|  | ||||
|       if (!this.canvas) { | ||||
|         console.error('Canvas element not found in DOM'); | ||||
|         this.renderState = 'error'; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       this.ctx = this.canvas.getContext('2d'); | ||||
|  | ||||
|       if (!this.ctx) { | ||||
|         console.error('Failed to acquire 2D rendering context'); | ||||
|         this.renderState = 'error'; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       this.renderState = 'rendering-main'; | ||||
|       await this.renderPage(this.currentPage); | ||||
|  | ||||
|       // Render initial visible pages | ||||
|       await this.renderVisiblePages(); | ||||
|       if (signal.aborted) return; | ||||
|  | ||||
|       // Scroll to initial page | ||||
|       if (this.initialPage > 1) { | ||||
|         await this.scrollToPage(this.initialPage, false); | ||||
|       } | ||||
|  | ||||
|       if (this.showSidebar) { | ||||
|         // Ensure sidebar is in DOM after loading = false | ||||
|         await this.updateComplete; | ||||
| @@ -338,82 +372,214 @@ export class DeesPdfViewer extends DeesElement { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async renderPage(pageNum: number) { | ||||
|     if (!this.pdfDocument || !this.canvas || !this.ctx) return; | ||||
|   private setupIntersectionObserver() { | ||||
|     if (this.intersectionObserver) { | ||||
|       this.intersectionObserver.disconnect(); | ||||
|     } | ||||
|  | ||||
|     // Wait for any existing render to complete | ||||
|     if (this.currentRenderPromise) { | ||||
|       try { | ||||
|         await this.currentRenderPromise; | ||||
|       } catch (error) { | ||||
|         // Ignore errors from previous renders | ||||
|     this.intersectionObserver = new IntersectionObserver( | ||||
|       (entries) => { | ||||
|         for (const entry of entries) { | ||||
|           const pageWrapper = entry.target as HTMLElement; | ||||
|           const pageNum = parseInt(pageWrapper.dataset.page || '1'); | ||||
|  | ||||
|           if (entry.isIntersecting) { | ||||
|             this.renderPageIfNeeded(pageNum); | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         root: this.viewerMain, | ||||
|         rootMargin: `${this.RENDER_BUFFER * 100}px 0px`, | ||||
|         threshold: 0.01 | ||||
|       } | ||||
|     ); | ||||
|  | ||||
|     // Observe all page wrappers | ||||
|     const pageWrappers = this.shadowRoot?.querySelectorAll('.page-wrapper'); | ||||
|     if (pageWrappers) { | ||||
|       pageWrappers.forEach(wrapper => { | ||||
|         this.intersectionObserver?.observe(wrapper); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|     // Create a new promise for this render | ||||
|     this.currentRenderPromise = this._doRenderPage(pageNum); | ||||
|   private async renderVisiblePages() { | ||||
|     if (!this.viewerMain) return; | ||||
|  | ||||
|     // Find visible pages based on scroll position | ||||
|     const clientHeight = this.viewerMain.clientHeight; | ||||
|  | ||||
|     for (const pageInfo of this.pageData) { | ||||
|       const pageWrapper = this.shadowRoot?.querySelector(`.page-wrapper[data-page="${pageInfo.page}"]`) as HTMLElement; | ||||
|       if (!pageWrapper) continue; | ||||
|  | ||||
|       const rect = pageWrapper.getBoundingClientRect(); | ||||
|       const viewerRect = this.viewerMain.getBoundingClientRect(); | ||||
|       const relativeTop = rect.top - viewerRect.top; | ||||
|       const relativeBottom = relativeTop + rect.height; | ||||
|  | ||||
|       // Check if page is visible or within buffer zone | ||||
|       const buffer = this.RENDER_BUFFER * clientHeight; | ||||
|       if (relativeBottom >= -buffer && relativeTop <= clientHeight + buffer) { | ||||
|         await this.renderPageIfNeeded(pageInfo.page); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async renderPageIfNeeded(pageNum: number) { | ||||
|     const pageInfo = this.pageData.find(p => p.page === pageNum); | ||||
|     if (!pageInfo || pageInfo.rendered || pageInfo.rendering) return; | ||||
|  | ||||
|     pageInfo.rendering = true; | ||||
|  | ||||
|     try { | ||||
|       await this.currentRenderPromise; | ||||
|     } finally { | ||||
|       this.currentRenderPromise = null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _doRenderPage(pageNum: number) { | ||||
|     if (!this.pdfDocument || !this.canvas || !this.ctx) return; | ||||
|  | ||||
|     this.pageRendering = true; | ||||
|  | ||||
|     try { | ||||
|       const page = await this.pdfDocument.getPage(pageNum); | ||||
|       if (!this.ctx) { | ||||
|         console.error('Unable to acquire canvas rendering context'); | ||||
|         this.pageRendering = false; | ||||
|       const canvas = this.shadowRoot?.querySelector(`.page-canvas[data-page="${pageNum}"]`) as HTMLCanvasElement; | ||||
|       if (!canvas) { | ||||
|         pageInfo.rendering = false; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const page = await this.pdfDocument.getPage(pageNum); | ||||
|       const viewport = this.computeViewport(page); | ||||
|  | ||||
|       this.canvas.height = viewport.height; | ||||
|       this.canvas.width = viewport.width; | ||||
|       this.canvas.style.width = `${viewport.width}px`; | ||||
|       this.canvas.style.height = `${viewport.height}px`; | ||||
|       // Set canvas dimensions | ||||
|       canvas.height = viewport.height; | ||||
|       canvas.width = viewport.width; | ||||
|       canvas.style.width = `${viewport.width}px`; | ||||
|       canvas.style.height = `${viewport.height}px`; | ||||
|  | ||||
|       const ctx = canvas.getContext('2d'); | ||||
|       if (!ctx) { | ||||
|         page.cleanup?.(); | ||||
|         pageInfo.rendering = false; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const renderContext = { | ||||
|         canvasContext: this.ctx, | ||||
|         canvasContext: ctx, | ||||
|         viewport: viewport, | ||||
|       }; | ||||
|  | ||||
|       // Store the render task | ||||
|       this.currentRenderTask = page.render(renderContext); | ||||
|       await this.currentRenderTask.promise; | ||||
|       const renderTask = page.render(renderContext); | ||||
|       this.pageRenderTasks.set(pageNum, renderTask); | ||||
|  | ||||
|       this.currentRenderTask = null; | ||||
|       this.pageRendering = false; | ||||
|       await renderTask.promise; | ||||
|  | ||||
|       // Clean up the page object | ||||
|       page.cleanup?.(); | ||||
|       pageInfo.rendered = true; | ||||
|       pageInfo.rendering = false; | ||||
|       this.pageRenderTasks.delete(pageNum); | ||||
|  | ||||
|       if (this.pageNumPending !== null) { | ||||
|         const nextPage = this.pageNumPending; | ||||
|         this.pageNumPending = null; | ||||
|         await this.renderPage(nextPage); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       // Ignore cancellation errors | ||||
|       // Update page data to reflect rendered state | ||||
|       this.requestUpdate('pageData'); | ||||
|     } catch (error: any) { | ||||
|       if (error?.name !== 'RenderingCancelledException') { | ||||
|         console.error('Error rendering page:', error); | ||||
|         console.error(`Error rendering page ${pageNum}:`, error); | ||||
|       } | ||||
|       this.currentRenderTask = null; | ||||
|       this.pageRendering = false; | ||||
|       pageInfo.rendering = false; | ||||
|       this.pageRenderTasks.delete(pageNum); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private queueRenderPage(pageNum: number) { | ||||
|     if (this.pageRendering) { | ||||
|       this.pageNumPending = pageNum; | ||||
|   private handleScroll = () => { | ||||
|     // Throttle scroll events | ||||
|     if (this.scrollThrottleTimeout) { | ||||
|       clearTimeout(this.scrollThrottleTimeout); | ||||
|     } | ||||
|  | ||||
|     this.scrollThrottleTimeout = window.setTimeout(() => { | ||||
|       this.updateCurrentPage(); | ||||
|       this.renderVisiblePages(); | ||||
|     }, 50); | ||||
|   } | ||||
|  | ||||
|   private updateCurrentPage() { | ||||
|     if (!this.viewerMain) return; | ||||
|  | ||||
|     const scrollTop = this.viewerMain.scrollTop; | ||||
|     const clientHeight = this.viewerMain.clientHeight; | ||||
|     const centerY = scrollTop + clientHeight / 2; | ||||
|  | ||||
|     // Find which page is at the center of the viewport | ||||
|     for (let i = 0; i < this.pageData.length; i++) { | ||||
|       const pageWrapper = this.shadowRoot?.querySelector(`.page-wrapper[data-page="${i + 1}"]`) as HTMLElement; | ||||
|       if (!pageWrapper) continue; | ||||
|  | ||||
|       const rect = pageWrapper.getBoundingClientRect(); | ||||
|       const viewerRect = this.viewerMain.getBoundingClientRect(); | ||||
|       const relativeTop = rect.top - viewerRect.top + scrollTop; | ||||
|       const relativeBottom = relativeTop + rect.height; | ||||
|  | ||||
|       if (centerY >= relativeTop && centerY <= relativeBottom) { | ||||
|         if (this.currentPage !== i + 1) { | ||||
|           this.currentPage = i + 1; | ||||
|           // Scroll the thumbnail into view if sidebar is visible | ||||
|           if (this.showSidebar) { | ||||
|             this.scrollThumbnailIntoView(i + 1); | ||||
|           } | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private scrollThumbnailIntoView(pageNum: number) { | ||||
|     const thumbnail = this.shadowRoot?.querySelector(`.thumbnail[data-page="${pageNum}"]`) as HTMLElement; | ||||
|     const sidebarContent = this.shadowRoot?.querySelector('.sidebar-content') as HTMLElement; | ||||
|  | ||||
|     if (thumbnail && sidebarContent) { | ||||
|       // Get the thumbnail's position relative to the sidebar | ||||
|       const thumbnailRect = thumbnail.getBoundingClientRect(); | ||||
|       const sidebarRect = sidebarContent.getBoundingClientRect(); | ||||
|  | ||||
|       // Check if thumbnail is outside the visible area | ||||
|       const isAbove = thumbnailRect.top < sidebarRect.top; | ||||
|       const isBelow = thumbnailRect.bottom > sidebarRect.bottom; | ||||
|  | ||||
|       if (isAbove || isBelow) { | ||||
|         // Calculate the scroll position to center the thumbnail | ||||
|         const thumbnailOffset = thumbnail.offsetTop; | ||||
|         const thumbnailHeight = thumbnail.offsetHeight; | ||||
|         const sidebarHeight = sidebarContent.clientHeight; | ||||
|         const targetScrollTop = thumbnailOffset - (sidebarHeight / 2) + (thumbnailHeight / 2); | ||||
|  | ||||
|         // Scroll the sidebar to center the thumbnail | ||||
|         sidebarContent.scrollTo({ | ||||
|           top: Math.max(0, targetScrollTop), | ||||
|           behavior: 'smooth' | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async scrollToPage(pageNum: number, smooth: boolean = true) { | ||||
|     await this.updateComplete; | ||||
|     const pageWrapper = this.shadowRoot?.querySelector(`.page-wrapper[data-page="${pageNum}"]`) as HTMLElement; | ||||
|     if (pageWrapper && this.viewerMain) { | ||||
|       // Calculate the offset of the page wrapper relative to the viewer | ||||
|       const pageRect = pageWrapper.getBoundingClientRect(); | ||||
|       const viewerRect = this.viewerMain.getBoundingClientRect(); | ||||
|       const currentScrollTop = this.viewerMain.scrollTop; | ||||
|  | ||||
|       // Calculate the target scroll position | ||||
|       const targetScrollTop = currentScrollTop + (pageRect.top - viewerRect.top) - this.viewerMain.clientTop; | ||||
|  | ||||
|       // Scroll to the calculated position | ||||
|       if (smooth) { | ||||
|         this.viewerMain.scrollTo({ | ||||
|           top: targetScrollTop, | ||||
|           behavior: 'smooth' | ||||
|         }); | ||||
|       } else { | ||||
|       this.renderPage(pageNum); | ||||
|         this.viewerMain.scrollTop = targetScrollTop; | ||||
|       } | ||||
|  | ||||
|       // Update current page | ||||
|       this.currentPage = pageNum; | ||||
|  | ||||
|       // Ensure the page is rendered | ||||
|       await this.renderPageIfNeeded(pageNum); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -448,41 +614,56 @@ export class DeesPdfViewer extends DeesElement { | ||||
|  | ||||
|     try { | ||||
|       await this.updateComplete; | ||||
|       const thumbnails = this.shadowRoot?.querySelectorAll('.thumbnail-canvas') as NodeListOf<HTMLCanvasElement>; | ||||
|       const thumbnailWidth = 176; // Fixed width for thumbnails (200px container - 24px padding) | ||||
|       const thumbnails = this.shadowRoot?.querySelectorAll('.thumbnail') as NodeListOf<HTMLElement>; | ||||
|       const thumbnailCanvases = this.shadowRoot?.querySelectorAll('.thumbnail-canvas') as NodeListOf<HTMLCanvasElement>; | ||||
|       const sidebarContent = this.shadowRoot?.querySelector('.sidebar-content') as HTMLElement; | ||||
|  | ||||
|       // Get the actual available width for thumbnails (sidebar width minus padding) | ||||
|       const sidebarStyles = window.getComputedStyle(sidebarContent); | ||||
|       const sidebarPadding = parseFloat(sidebarStyles.paddingLeft) + parseFloat(sidebarStyles.paddingRight); | ||||
|       const maxThumbnailWidth = 200 - sidebarPadding - 4; // Account for border | ||||
|  | ||||
|       // Clear all canvases first to prevent conflicts | ||||
|       for (const canvas of Array.from(thumbnails)) { | ||||
|       for (const canvas of Array.from(thumbnailCanvases)) { | ||||
|         const context = canvas.getContext('2d'); | ||||
|         if (context) { | ||||
|           context.clearRect(0, 0, canvas.width, canvas.height); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       for (const canvas of Array.from(thumbnails)) { | ||||
|       for (let i = 0; i < thumbnailCanvases.length; i++) { | ||||
|         if (signal?.aborted) return; | ||||
|  | ||||
|         const canvas = thumbnailCanvases[i]; | ||||
|         const thumbnail = thumbnails[i]; | ||||
|         const pageNum = parseInt(canvas.dataset.page || '1'); | ||||
|         const page = await this.pdfDocument.getPage(pageNum); | ||||
|  | ||||
|         // Calculate scale to fit thumbnail width while maintaining aspect ratio | ||||
|         // Get the page's natural dimensions | ||||
|         const initialViewport = page.getViewport({ scale: 1 }); | ||||
|         const scale = thumbnailWidth / initialViewport.width; | ||||
|  | ||||
|         // Calculate scale to fit within the max thumbnail width | ||||
|         const scale = maxThumbnailWidth / initialViewport.width; | ||||
|         const viewport = page.getViewport({ scale }); | ||||
|  | ||||
|         // Set canvas dimensions to actual render size | ||||
|         canvas.width = viewport.width; | ||||
|         canvas.height = viewport.height; | ||||
|  | ||||
|         // Also set the display size via style to ensure proper display | ||||
|         // Set the display size via style to ensure proper display | ||||
|         canvas.style.width = `${viewport.width}px`; | ||||
|         canvas.style.height = `${viewport.height}px`; | ||||
|  | ||||
|         // Set the actual thumbnail container height | ||||
|         thumbnail.style.height = `${viewport.height}px`; | ||||
|         thumbnail.style.minHeight = `${viewport.height}px`; | ||||
|  | ||||
|         const context = canvas.getContext('2d'); | ||||
|         if (!context) { | ||||
|           page.cleanup?.(); | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|         const renderContext = { | ||||
|           canvasContext: context, | ||||
|           viewport: viewport, | ||||
| @@ -514,45 +695,27 @@ export class DeesPdfViewer extends DeesElement { | ||||
|  | ||||
|   private previousPage() { | ||||
|     if (this.currentPage > 1) { | ||||
|       this.currentPage--; | ||||
|       this.queueRenderPage(this.currentPage); | ||||
|       this.scrollToPage(this.currentPage - 1); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private nextPage() { | ||||
|     if (this.currentPage < this.totalPages) { | ||||
|       this.currentPage++; | ||||
|       this.queueRenderPage(this.currentPage); | ||||
|       this.scrollToPage(this.currentPage + 1); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async goToPage(pageNum: number) { | ||||
|     if (pageNum >= 1 && pageNum <= this.totalPages) { | ||||
|       this.currentPage = pageNum; | ||||
|  | ||||
|       // Ensure canvas references are available | ||||
|       if (!this.canvas || !this.ctx) { | ||||
|         await this.updateComplete; | ||||
|         this.canvas = this.shadowRoot?.querySelector('#pdf-canvas') as HTMLCanvasElement; | ||||
|         this.ctx = this.canvas?.getContext('2d') || null; | ||||
|       } | ||||
|  | ||||
|       if (this.canvas && this.ctx) { | ||||
|         this.queueRenderPage(this.currentPage); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private handleThumbnailClick(e: Event) { | ||||
|     const target = e.currentTarget as HTMLElement; | ||||
|     const pageNum = parseInt(target.dataset.page || '1'); | ||||
|     this.goToPage(pageNum); | ||||
|     this.scrollToPage(pageNum); | ||||
|   } | ||||
|  | ||||
|   private handlePageInput(e: Event) { | ||||
|     const input = e.target as HTMLInputElement; | ||||
|     const pageNum = parseInt(input.value); | ||||
|     this.goToPage(pageNum); | ||||
|     this.scrollToPage(pageNum); | ||||
|   } | ||||
|  | ||||
|   private zoomIn() { | ||||
| @@ -560,7 +723,7 @@ export class DeesPdfViewer extends DeesElement { | ||||
|     this.viewportMode = 'custom'; | ||||
|     if (nextZoom !== this.currentZoom) { | ||||
|       this.currentZoom = nextZoom; | ||||
|       this.queueRenderPage(this.currentPage); | ||||
|       this.reRenderAllPages(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -569,24 +732,50 @@ export class DeesPdfViewer extends DeesElement { | ||||
|     this.viewportMode = 'custom'; | ||||
|     if (nextZoom !== this.currentZoom) { | ||||
|       this.currentZoom = nextZoom; | ||||
|       this.queueRenderPage(this.currentPage); | ||||
|       this.reRenderAllPages(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private resetZoom() { | ||||
|     this.viewportMode = 'custom'; | ||||
|     this.currentZoom = 1; | ||||
|     this.queueRenderPage(this.currentPage); | ||||
|     this.reRenderAllPages(); | ||||
|   } | ||||
|  | ||||
|   private fitToPage() { | ||||
|     this.viewportMode = 'page-fit'; | ||||
|     this.queueRenderPage(this.currentPage); | ||||
|     this.reRenderAllPages(); | ||||
|   } | ||||
|  | ||||
|   private fitToWidth() { | ||||
|     this.viewportMode = 'page-width'; | ||||
|     this.queueRenderPage(this.currentPage); | ||||
|     this.reRenderAllPages(); | ||||
|   } | ||||
|  | ||||
|   private reRenderAllPages() { | ||||
|     // Clear all rendered pages to force re-render with new zoom | ||||
|     this.pageData.forEach(page => { | ||||
|       page.rendered = false; | ||||
|       page.rendering = false; | ||||
|     }); | ||||
|  | ||||
|     // Cancel any ongoing render tasks | ||||
|     this.pageRenderTasks.forEach(task => { | ||||
|       try { | ||||
|         task.cancel(); | ||||
|       } catch (error) { | ||||
|         // Ignore cancellation errors | ||||
|       } | ||||
|     }); | ||||
|     this.pageRenderTasks.clear(); | ||||
|  | ||||
|     // Request update to re-render pages | ||||
|     this.requestUpdate(); | ||||
|  | ||||
|     // Render visible pages after update | ||||
|     this.updateComplete.then(() => { | ||||
|       this.renderVisiblePages(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private downloadPdf() { | ||||
| @@ -653,11 +842,34 @@ export class DeesPdfViewer extends DeesElement { | ||||
|       this.resizeObserver = new ResizeObserver(() => { | ||||
|         this.measureViewportDimensions(); | ||||
|         if (this.pdfDocument) { | ||||
|           this.queueRenderPage(this.currentPage); | ||||
|           // Re-render all pages when viewport size changes | ||||
|           this.reRenderAllPages(); | ||||
|         } | ||||
|       }); | ||||
|       this.resizeObserver.observe(this.viewerMain); | ||||
|       this.measureViewportDimensions(); | ||||
|  | ||||
|       // Prevent scroll propagation to parent when scrolling inside viewer | ||||
|       this.viewerMain.addEventListener('wheel', (e) => { | ||||
|         const element = e.currentTarget as HTMLElement; | ||||
|         const scrollTop = element.scrollTop; | ||||
|         const scrollHeight = element.scrollHeight; | ||||
|         const clientHeight = element.clientHeight; | ||||
|         const deltaY = e.deltaY; | ||||
|  | ||||
|         // Check if we're at the boundaries | ||||
|         const isAtTop = scrollTop === 0; | ||||
|         const isAtBottom = Math.abs(scrollTop + clientHeight - scrollHeight) < 1; | ||||
|  | ||||
|         // Prevent propagation if we're scrolling within bounds | ||||
|         if ((deltaY < 0 && !isAtTop) || (deltaY > 0 && !isAtBottom)) { | ||||
|           e.stopPropagation(); | ||||
|         } else if ((deltaY < 0 && isAtTop) || (deltaY > 0 && isAtBottom)) { | ||||
|           // Prevent default and propagation when at boundaries | ||||
|           e.preventDefault(); | ||||
|           e.stopPropagation(); | ||||
|         } | ||||
|       }, { passive: false }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -760,6 +972,16 @@ export class DeesPdfViewer extends DeesElement { | ||||
|     // Clear the render task reference | ||||
|     this.currentRenderTask = null; | ||||
|  | ||||
|     // Cancel any page render tasks | ||||
|     this.pageRenderTasks.forEach(task => { | ||||
|       try { | ||||
|         task.cancel(); | ||||
|       } catch (error) { | ||||
|         // Ignore cancellation errors | ||||
|       } | ||||
|     }); | ||||
|     this.pageRenderTasks.clear(); | ||||
|  | ||||
|     // Cancel any thumbnail render tasks | ||||
|     for (const task of (this.thumbnailRenderTasks || [])) { | ||||
|       try { | ||||
| @@ -775,6 +997,7 @@ export class DeesPdfViewer extends DeesElement { | ||||
|     this.pageRendering = false; | ||||
|     this.pageNumPending = null; | ||||
|     this.thumbnailData = []; | ||||
|     this.pageData = []; | ||||
|     this.documentId = ''; | ||||
|  | ||||
|     // Clear canvas content | ||||
|   | ||||
| @@ -9,6 +9,7 @@ export const viewerStyles = [ | ||||
|       height: 600px; | ||||
|       position: relative; | ||||
|       font-family: 'Geist Sans', sans-serif; | ||||
|       contain: layout style; | ||||
|     } | ||||
|  | ||||
|     .pdf-viewer { | ||||
| @@ -17,6 +18,8 @@ export const viewerStyles = [ | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(215 20% 10%)')}; | ||||
|       position: relative; | ||||
|       overflow: hidden; | ||||
|     } | ||||
|  | ||||
|     .toolbar { | ||||
| @@ -109,6 +112,7 @@ export const viewerStyles = [ | ||||
|       display: flex; | ||||
|       overflow: hidden; | ||||
|       position: relative; | ||||
|       min-height: 0; | ||||
|     } | ||||
|  | ||||
|     .sidebar { | ||||
| @@ -117,6 +121,8 @@ export const viewerStyles = [ | ||||
|       border-right: 1px solid ${cssManager.bdTheme('hsl(214 31% 91%)', 'hsl(217 25% 22%)')}; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       height: 100%; | ||||
|       overflow: hidden; | ||||
|     } | ||||
|  | ||||
|     .sidebar-header { | ||||
| @@ -156,10 +162,11 @@ export const viewerStyles = [ | ||||
|     .sidebar-content { | ||||
|       flex: 1; | ||||
|       overflow-y: auto; | ||||
|       overflow-x: hidden; | ||||
|       padding: 12px; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       gap: 12px; | ||||
|       display: block; | ||||
|       overscroll-behavior: contain; | ||||
|       min-height: 0; | ||||
|     } | ||||
|  | ||||
|     .thumbnail { | ||||
| @@ -169,11 +176,16 @@ export const viewerStyles = [ | ||||
|       cursor: pointer; | ||||
|       border: 2px solid transparent; | ||||
|       transition: border-color 0.15s ease; | ||||
|       background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 18%)')}; | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: center; | ||||
|       min-height: 100px; | ||||
|       background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(215 20% 18%)')}; | ||||
|       display: block; | ||||
|       width: 100%; | ||||
|       margin-bottom: 12px; | ||||
|       /* Default A4 aspect ratio (297mm / 210mm ≈ 1.414) */ | ||||
|       min-height: calc(176px * 1.414); | ||||
|     } | ||||
|  | ||||
|     .thumbnail:last-child { | ||||
|       margin-bottom: 0; | ||||
|     } | ||||
|  | ||||
|     .thumbnail:hover { | ||||
| @@ -186,7 +198,7 @@ export const viewerStyles = [ | ||||
|  | ||||
|     .thumbnail-canvas { | ||||
|       display: block; | ||||
|       max-width: 100%; | ||||
|       width: 100%; | ||||
|       height: auto; | ||||
|       image-rendering: -webkit-optimize-contrast; | ||||
|       image-rendering: crisp-edges; | ||||
| @@ -206,17 +218,21 @@ export const viewerStyles = [ | ||||
|  | ||||
|     .viewer-main { | ||||
|       flex: 1; | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: center; | ||||
|       overflow: auto; | ||||
|       overflow-y: auto; | ||||
|       overflow-x: hidden; | ||||
|       padding: 20px; | ||||
|       scroll-behavior: smooth; | ||||
|       overscroll-behavior: contain; | ||||
|       min-height: 0; | ||||
|       position: relative; | ||||
|     } | ||||
|  | ||||
|     .loading-container { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       align-items: center; | ||||
|       justify-content: center; | ||||
|       height: 100%; | ||||
|       gap: 16px; | ||||
|       color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')}; | ||||
|     } | ||||
| @@ -241,6 +257,19 @@ export const viewerStyles = [ | ||||
|       font-weight: 500; | ||||
|     } | ||||
|  | ||||
|     .pages-container { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       align-items: center; | ||||
|       gap: 20px; | ||||
|     } | ||||
|  | ||||
|     .page-wrapper { | ||||
|       display: flex; | ||||
|       justify-content: center; | ||||
|       width: 100%; | ||||
|     } | ||||
|  | ||||
|     .canvas-container { | ||||
|       background: white; | ||||
|       box-shadow: 0 2px 12px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.3)')}; | ||||
| @@ -249,7 +278,7 @@ export const viewerStyles = [ | ||||
|       display: inline-block; | ||||
|     } | ||||
|  | ||||
|     #pdf-canvas { | ||||
|     .page-canvas { | ||||
|       display: block; | ||||
|       image-rendering: -webkit-optimize-contrast; | ||||
|       image-rendering: crisp-edges; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user