From bb883ce3414a40f260e4d8a9ac25f10a768b6e86 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 20 Sep 2025 21:36:04 +0000 Subject: [PATCH] feat(dees-pdf-preview): enhance hover functionality and page indicator display feat(dees-pdf-viewer): improve input handling and remove unused variables --- ts_web/elements/dees-pdf-preview/component.ts | 110 +++++++++++------- ts_web/elements/dees-pdf-preview/styles.ts | 40 ++++++- ts_web/elements/dees-pdf-viewer/component.ts | 11 +- 3 files changed, 107 insertions(+), 54 deletions(-) diff --git a/ts_web/elements/dees-pdf-preview/component.ts b/ts_web/elements/dees-pdf-preview/component.ts index 3f60478..464091f 100644 --- a/ts_web/elements/dees-pdf-preview/component.ts +++ b/ts_web/elements/dees-pdf-preview/component.ts @@ -1,10 +1,9 @@ -import { DeesElement, property, html, customElement, domtools, type TemplateResult, css, cssManager } from '@design.estate/dees-element'; +import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element'; import { PdfManager } from '../dees-pdf-shared/PdfManager.js'; import { CanvasPool, type PooledCanvas } from '../dees-pdf-shared/CanvasPool.js'; import { PerformanceMonitor, throttle } from '../dees-pdf-shared/utils.js'; import { previewStyles } from './styles.js'; import { demo as demoFunc } from './demo.js'; -import { DeesContextmenu } from '../dees-contextmenu.js'; import '../dees-icon.js'; declare global { @@ -22,10 +21,7 @@ export class DeesPdfPreview extends DeesElement { public pdfUrl: string = ''; @property({ type: Number }) - public maxPages: number = 3; - - @property({ type: Number }) - public stackOffset: number = 8; + public currentPreviewPage: number = 1; @property({ type: Boolean }) public clickable: boolean = true; @@ -42,8 +38,12 @@ export class DeesPdfPreview extends DeesElement { @property({ type: Boolean }) private error: boolean = false; + @property({ type: Boolean }) + private isHovering: boolean = false; + private renderPagesTask: Promise | null = null; private renderPagesQueued: boolean = false; + private hoverPageNumber: number = 1; private observer: IntersectionObserver; private pdfDocument: any; @@ -62,6 +62,9 @@ export class DeesPdfPreview extends DeesElement {
${this.loading ? html`
@@ -79,10 +82,19 @@ export class DeesPdfPreview extends DeesElement { ${!this.loading && !this.error ? html`
- ${this.getStackedCanvases()} +
- ${this.pageCount > 0 ? html` + ${this.pageCount > 1 && this.isHovering ? html` +
+ Page ${this.currentPreviewPage} of ${this.pageCount} +
+ ` : ''} + + ${this.pageCount > 0 && !this.isHovering ? html`
${this.pageCount} page${this.pageCount > 1 ? 's' : ''} @@ -100,27 +112,34 @@ export class DeesPdfPreview extends DeesElement { `; } - private getStackedCanvases(): TemplateResult[] { - const pagesToShow = Math.min(this.pageCount, this.maxPages); - const canvases: TemplateResult[] = []; + private handleMouseEnter() { + this.isHovering = true; + } - for (let i = pagesToShow - 1; i >= 0; i--) { - const offset = i * this.stackOffset; - canvases.push(html` - - `); + private handleMouseLeave() { + this.isHovering = false; + // Reset to first page when not hovering + if (this.currentPreviewPage !== 1) { + this.currentPreviewPage = 1; + void this.scheduleRenderPages(); } + } - return canvases; + private handleMouseMove(e: MouseEvent) { + if (!this.isHovering || this.pageCount <= 1) return; + + const rect = this.getBoundingClientRect(); + const x = e.clientX - rect.left; + const width = rect.width; + + // Calculate which page to show based on horizontal position + const percentage = Math.max(0, Math.min(1, x / width)); + const newPage = Math.ceil(percentage * this.pageCount) || 1; + + if (newPage !== this.currentPreviewPage) { + this.currentPreviewPage = newPage; + void this.scheduleRenderPages(); + } } public async connectedCallback() { @@ -175,6 +194,7 @@ export class DeesPdfPreview extends DeesElement { try { this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl); this.pageCount = this.pdfDocument.numPages; + this.currentPreviewPage = 1; this.loadedPdfUrl = this.pdfUrl; await this.updateComplete; @@ -222,35 +242,34 @@ export class DeesPdfPreview extends DeesElement { private async performRenderPages() { if (!this.pdfDocument) return; - const canvasElements = this.shadowRoot?.querySelectorAll('.preview-canvas') as NodeListOf; - const pagesToRender = Math.min(this.pageCount, this.maxPages); + + const canvas = this.shadowRoot?.querySelector('.preview-canvas') as HTMLCanvasElement; + if (!canvas) return; // Release old canvases this.clearCanvases(); - const maxStackOffset = (pagesToRender - 1) * this.stackOffset; - this.cacheElements(); - const { availableWidth, availableHeight } = this.getAvailableStackSize(maxStackOffset); + // Get available size for the preview + const { availableWidth, availableHeight } = this.getAvailableSize(); - // Render pages in reverse order (back to front for stacking) - for (let i = 0; i < pagesToRender; i++) { - const canvas = canvasElements[i]; - if (!canvas) continue; - - const pageNum = parseInt(canvas.dataset.page || '1'); + try { + // Get the page to render + const pageNum = this.currentPreviewPage; const page = await this.pdfDocument.getPage(pageNum); // Calculate scale to fit within available area while keeping aspect ratio const initialViewport = page.getViewport({ scale: 1 }); const scaleX = availableWidth > 0 ? availableWidth / initialViewport.width : 0; const scaleY = availableHeight > 0 ? availableHeight / initialViewport.height : 0; - const scale = Math.min(scaleX || 0.5, scaleY || scaleX || 0.5, 0.75); + const scale = Math.min(scaleX || 0.5, scaleY || scaleX || 0.5, 1.0); + if (!Number.isFinite(scale) || scale <= 0) { page.cleanup?.(); - continue; + return; } + const viewport = page.getViewport({ scale }); // Acquire canvas from pool @@ -278,6 +297,8 @@ export class DeesPdfPreview extends DeesElement { // Release page to free memory page.cleanup(); + } catch (error) { + console.error(`Failed to render page ${this.currentPreviewPage}:`, error); } } @@ -300,6 +321,8 @@ export class DeesPdfPreview extends DeesElement { this.renderPagesQueued = false; this.pageCount = 0; + this.currentPreviewPage = 1; + this.isHovering = false; this.previewContainer = null; this.stackElement = null; this.loadedPdfUrl = null; @@ -332,6 +355,7 @@ export class DeesPdfPreview extends DeesElement { } this.cleanup(); this.rendered = false; + this.currentPreviewPage = 1; // Check if in viewport and render if so if (this.observer) { @@ -342,7 +366,7 @@ export class DeesPdfPreview extends DeesElement { } } - if ((changedProperties.has('maxPages') || changedProperties.has('stackOffset')) && this.rendered) { + if (changedProperties.has('currentPreviewPage') && this.rendered) { await this.scheduleRenderPages(); } } @@ -430,7 +454,7 @@ export class DeesPdfPreview extends DeesElement { this.resizeObserver.observe(this); } - private getAvailableStackSize(maxStackOffset: number) { + private getAvailableSize() { if (!this.stackElement) { return { availableWidth: 0, @@ -439,8 +463,8 @@ export class DeesPdfPreview extends DeesElement { } const rect = this.stackElement.getBoundingClientRect(); - const availableWidth = Math.max(rect.width - maxStackOffset, 0); - const availableHeight = Math.max(rect.height - maxStackOffset, 0); + const availableWidth = Math.max(rect.width, 0); + const availableHeight = Math.max(rect.height, 0); return { availableWidth, availableHeight }; } diff --git a/ts_web/elements/dees-pdf-preview/styles.ts b/ts_web/elements/dees-pdf-preview/styles.ts index ad03c3a..d27f168 100644 --- a/ts_web/elements/dees-pdf-preview/styles.ts +++ b/ts_web/elements/dees-pdf-preview/styles.ts @@ -36,18 +36,25 @@ export const previewStyles = [ position: relative; width: 100%; height: 100%; - padding: 20px; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; box-sizing: border-box; } .preview-canvas { - position: absolute; + position: relative; background: white; border: 1px solid ${cssManager.bdTheme('hsl(214 31% 88%)', 'hsl(217 25% 30%)')}; border-radius: 4px; display: block; + max-width: 100%; + max-height: 100%; + object-fit: contain; image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; + box-shadow: 0 2px 8px ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(0, 0, 0, 0.2)')}; } .preview-info { @@ -153,6 +160,35 @@ export const previewStyles = [ font-size: 32px; } + .preview-page-indicator { + position: absolute; + top: 12px; + left: 12px; + right: 12px; + padding: 6px 10px; + background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.75)', 'hsl(0 0% 100% / 0.85)')}; + color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')}; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + text-align: center; + backdrop-filter: blur(8px); + z-index: 15; + pointer-events: none; + animation: fadeIn 0.2s ease; + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + /* Responsive sizes */ :host([size="small"]) .preview-container { width: 150px; diff --git a/ts_web/elements/dees-pdf-viewer/component.ts b/ts_web/elements/dees-pdf-viewer/component.ts index 57f91ac..3635c4f 100644 --- a/ts_web/elements/dees-pdf-viewer/component.ts +++ b/ts_web/elements/dees-pdf-viewer/component.ts @@ -1,11 +1,9 @@ -import { DeesElement, property, html, customElement, domtools, type TemplateResult, type CSSResult, css, cssManager } from '@design.estate/dees-element'; +import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element'; import { keyed } from 'lit/directives/keyed.js'; import { repeat } from 'lit/directives/repeat.js'; -import { DeesInputBase } from '../dees-input-base.js'; import { PdfManager } from '../dees-pdf-shared/PdfManager.js'; import { viewerStyles } from './styles.js'; import { demo as demoFunc } from './demo.js'; -import { DeesContextmenu } from '../dees-contextmenu.js'; import '../dees-icon.js'; declare global { @@ -68,7 +66,6 @@ export class DeesPdfViewer extends DeesElement { private resizeObserver?: ResizeObserver; private viewportDimensions = { width: 0, height: 0 }; private viewportMode: 'auto' | 'page-fit' | 'page-width' | 'custom' = 'auto'; - private loadedPdfUrl: string | null = null; private readonly MANUAL_MIN_ZOOM = 0.5; private readonly MANUAL_MAX_ZOOM = 3; private readonly ABSOLUTE_MIN_ZOOM = 0.1; @@ -96,7 +93,7 @@ export class DeesPdfViewer extends DeesElement { type="number" min="1" max="${this.totalPages}" - .value=${this.currentPage} + .value=${String(this.currentPage)} @change=${this.handlePageInput} class="page-input" /> @@ -334,7 +331,6 @@ export class DeesPdfViewer extends DeesElement { } this.renderState = 'rendered'; - this.loadedPdfUrl = this.pdfUrl; } catch (error) { console.error('Error loading PDF:', error); this.loading = false; @@ -795,9 +791,6 @@ export class DeesPdfViewer extends DeesElement { } } - // Clear the loaded URL reference - this.loadedPdfUrl = null; - // Finally null the document reference this.pdfDocument = null;