feat(dees-pdf-preview): enhance hover functionality and page indicator display

feat(dees-pdf-viewer): improve input handling and remove unused variables
This commit is contained in:
2025-09-20 21:36:04 +00:00
parent d9703d3ce3
commit bb883ce341
3 changed files with 107 additions and 54 deletions

View File

@@ -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 { PdfManager } from '../dees-pdf-shared/PdfManager.js';
import { CanvasPool, type PooledCanvas } from '../dees-pdf-shared/CanvasPool.js'; import { CanvasPool, type PooledCanvas } from '../dees-pdf-shared/CanvasPool.js';
import { PerformanceMonitor, throttle } from '../dees-pdf-shared/utils.js'; import { PerformanceMonitor, throttle } from '../dees-pdf-shared/utils.js';
import { previewStyles } from './styles.js'; import { previewStyles } from './styles.js';
import { demo as demoFunc } from './demo.js'; import { demo as demoFunc } from './demo.js';
import { DeesContextmenu } from '../dees-contextmenu.js';
import '../dees-icon.js'; import '../dees-icon.js';
declare global { declare global {
@@ -22,10 +21,7 @@ export class DeesPdfPreview extends DeesElement {
public pdfUrl: string = ''; public pdfUrl: string = '';
@property({ type: Number }) @property({ type: Number })
public maxPages: number = 3; public currentPreviewPage: number = 1;
@property({ type: Number })
public stackOffset: number = 8;
@property({ type: Boolean }) @property({ type: Boolean })
public clickable: boolean = true; public clickable: boolean = true;
@@ -42,8 +38,12 @@ export class DeesPdfPreview extends DeesElement {
@property({ type: Boolean }) @property({ type: Boolean })
private error: boolean = false; private error: boolean = false;
@property({ type: Boolean })
private isHovering: boolean = false;
private renderPagesTask: Promise<void> | null = null; private renderPagesTask: Promise<void> | null = null;
private renderPagesQueued: boolean = false; private renderPagesQueued: boolean = false;
private hoverPageNumber: number = 1;
private observer: IntersectionObserver; private observer: IntersectionObserver;
private pdfDocument: any; private pdfDocument: any;
@@ -62,6 +62,9 @@ export class DeesPdfPreview extends DeesElement {
<div <div
class="preview-container ${this.loading ? 'loading' : ''} ${this.error ? 'error' : ''} ${this.clickable ? 'clickable' : ''}" class="preview-container ${this.loading ? 'loading' : ''} ${this.error ? 'error' : ''} ${this.clickable ? 'clickable' : ''}"
@click=${this.handleClick} @click=${this.handleClick}
@mouseenter=${this.handleMouseEnter}
@mouseleave=${this.handleMouseLeave}
@mousemove=${this.handleMouseMove}
> >
${this.loading ? html` ${this.loading ? html`
<div class="preview-loading"> <div class="preview-loading">
@@ -79,10 +82,19 @@ export class DeesPdfPreview extends DeesElement {
${!this.loading && !this.error ? html` ${!this.loading && !this.error ? html`
<div class="preview-stack"> <div class="preview-stack">
${this.getStackedCanvases()} <canvas
class="preview-canvas"
data-page="${this.currentPreviewPage}"
></canvas>
</div> </div>
${this.pageCount > 0 ? html` ${this.pageCount > 1 && this.isHovering ? html`
<div class="preview-page-indicator">
Page ${this.currentPreviewPage} of ${this.pageCount}
</div>
` : ''}
${this.pageCount > 0 && !this.isHovering ? html`
<div class="preview-info"> <div class="preview-info">
<dees-icon icon="lucide:FileText"></dees-icon> <dees-icon icon="lucide:FileText"></dees-icon>
<span class="preview-pages">${this.pageCount} page${this.pageCount > 1 ? 's' : ''}</span> <span class="preview-pages">${this.pageCount} page${this.pageCount > 1 ? 's' : ''}</span>
@@ -100,27 +112,34 @@ export class DeesPdfPreview extends DeesElement {
`; `;
} }
private getStackedCanvases(): TemplateResult[] { private handleMouseEnter() {
const pagesToShow = Math.min(this.pageCount, this.maxPages); this.isHovering = true;
const canvases: TemplateResult[] = []; }
for (let i = pagesToShow - 1; i >= 0; i--) { private handleMouseLeave() {
const offset = i * this.stackOffset; this.isHovering = false;
canvases.push(html` // Reset to first page when not hovering
<canvas if (this.currentPreviewPage !== 1) {
class="preview-canvas" this.currentPreviewPage = 1;
data-page="${i + 1}" void this.scheduleRenderPages();
style="
top: ${offset}px;
left: ${offset}px;
z-index: ${pagesToShow - i};
${i > 0 ? `box-shadow: 0 2px 8px rgba(0, 0, 0, ${0.1 + (i * 0.05)});` : ''}
"
></canvas>
`);
} }
}
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() { public async connectedCallback() {
@@ -175,6 +194,7 @@ export class DeesPdfPreview extends DeesElement {
try { try {
this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl); this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl);
this.pageCount = this.pdfDocument.numPages; this.pageCount = this.pdfDocument.numPages;
this.currentPreviewPage = 1;
this.loadedPdfUrl = this.pdfUrl; this.loadedPdfUrl = this.pdfUrl;
await this.updateComplete; await this.updateComplete;
@@ -222,35 +242,34 @@ export class DeesPdfPreview extends DeesElement {
private async performRenderPages() { private async performRenderPages() {
if (!this.pdfDocument) return; if (!this.pdfDocument) return;
const canvasElements = this.shadowRoot?.querySelectorAll('.preview-canvas') as NodeListOf<HTMLCanvasElement>;
const pagesToRender = Math.min(this.pageCount, this.maxPages); const canvas = this.shadowRoot?.querySelector('.preview-canvas') as HTMLCanvasElement;
if (!canvas) return;
// Release old canvases // Release old canvases
this.clearCanvases(); this.clearCanvases();
const maxStackOffset = (pagesToRender - 1) * this.stackOffset;
this.cacheElements(); 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) try {
for (let i = 0; i < pagesToRender; i++) { // Get the page to render
const canvas = canvasElements[i]; const pageNum = this.currentPreviewPage;
if (!canvas) continue;
const pageNum = parseInt(canvas.dataset.page || '1');
const page = await this.pdfDocument.getPage(pageNum); const page = await this.pdfDocument.getPage(pageNum);
// Calculate scale to fit within available area while keeping aspect ratio // Calculate scale to fit within available area while keeping aspect ratio
const initialViewport = page.getViewport({ scale: 1 }); const initialViewport = page.getViewport({ scale: 1 });
const scaleX = availableWidth > 0 ? availableWidth / initialViewport.width : 0; const scaleX = availableWidth > 0 ? availableWidth / initialViewport.width : 0;
const scaleY = availableHeight > 0 ? availableHeight / initialViewport.height : 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) { if (!Number.isFinite(scale) || scale <= 0) {
page.cleanup?.(); page.cleanup?.();
continue; return;
} }
const viewport = page.getViewport({ scale }); const viewport = page.getViewport({ scale });
// Acquire canvas from pool // Acquire canvas from pool
@@ -278,6 +297,8 @@ export class DeesPdfPreview extends DeesElement {
// Release page to free memory // Release page to free memory
page.cleanup(); 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.renderPagesQueued = false;
this.pageCount = 0; this.pageCount = 0;
this.currentPreviewPage = 1;
this.isHovering = false;
this.previewContainer = null; this.previewContainer = null;
this.stackElement = null; this.stackElement = null;
this.loadedPdfUrl = null; this.loadedPdfUrl = null;
@@ -332,6 +355,7 @@ export class DeesPdfPreview extends DeesElement {
} }
this.cleanup(); this.cleanup();
this.rendered = false; this.rendered = false;
this.currentPreviewPage = 1;
// Check if in viewport and render if so // Check if in viewport and render if so
if (this.observer) { 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(); await this.scheduleRenderPages();
} }
} }
@@ -430,7 +454,7 @@ export class DeesPdfPreview extends DeesElement {
this.resizeObserver.observe(this); this.resizeObserver.observe(this);
} }
private getAvailableStackSize(maxStackOffset: number) { private getAvailableSize() {
if (!this.stackElement) { if (!this.stackElement) {
return { return {
availableWidth: 0, availableWidth: 0,
@@ -439,8 +463,8 @@ export class DeesPdfPreview extends DeesElement {
} }
const rect = this.stackElement.getBoundingClientRect(); const rect = this.stackElement.getBoundingClientRect();
const availableWidth = Math.max(rect.width - maxStackOffset, 0); const availableWidth = Math.max(rect.width, 0);
const availableHeight = Math.max(rect.height - maxStackOffset, 0); const availableHeight = Math.max(rect.height, 0);
return { availableWidth, availableHeight }; return { availableWidth, availableHeight };
} }

View File

@@ -36,18 +36,25 @@ export const previewStyles = [
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 20px; display: flex;
align-items: center;
justify-content: center;
padding: 16px;
box-sizing: border-box; box-sizing: border-box;
} }
.preview-canvas { .preview-canvas {
position: absolute; position: relative;
background: white; background: white;
border: 1px solid ${cssManager.bdTheme('hsl(214 31% 88%)', 'hsl(217 25% 30%)')}; border: 1px solid ${cssManager.bdTheme('hsl(214 31% 88%)', 'hsl(217 25% 30%)')};
border-radius: 4px; border-radius: 4px;
display: block; display: block;
max-width: 100%;
max-height: 100%;
object-fit: contain;
image-rendering: -webkit-optimize-contrast; image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges; 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 { .preview-info {
@@ -153,6 +160,35 @@ export const previewStyles = [
font-size: 32px; 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 */ /* Responsive sizes */
:host([size="small"]) .preview-container { :host([size="small"]) .preview-container {
width: 150px; width: 150px;

View File

@@ -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 { keyed } from 'lit/directives/keyed.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { DeesInputBase } from '../dees-input-base.js';
import { PdfManager } from '../dees-pdf-shared/PdfManager.js'; import { PdfManager } from '../dees-pdf-shared/PdfManager.js';
import { viewerStyles } from './styles.js'; import { viewerStyles } from './styles.js';
import { demo as demoFunc } from './demo.js'; import { demo as demoFunc } from './demo.js';
import { DeesContextmenu } from '../dees-contextmenu.js';
import '../dees-icon.js'; import '../dees-icon.js';
declare global { declare global {
@@ -68,7 +66,6 @@ export class DeesPdfViewer extends DeesElement {
private resizeObserver?: ResizeObserver; private resizeObserver?: ResizeObserver;
private viewportDimensions = { width: 0, height: 0 }; private viewportDimensions = { width: 0, height: 0 };
private viewportMode: 'auto' | 'page-fit' | 'page-width' | 'custom' = 'auto'; private viewportMode: 'auto' | 'page-fit' | 'page-width' | 'custom' = 'auto';
private loadedPdfUrl: string | null = null;
private readonly MANUAL_MIN_ZOOM = 0.5; private readonly MANUAL_MIN_ZOOM = 0.5;
private readonly MANUAL_MAX_ZOOM = 3; private readonly MANUAL_MAX_ZOOM = 3;
private readonly ABSOLUTE_MIN_ZOOM = 0.1; private readonly ABSOLUTE_MIN_ZOOM = 0.1;
@@ -96,7 +93,7 @@ export class DeesPdfViewer extends DeesElement {
type="number" type="number"
min="1" min="1"
max="${this.totalPages}" max="${this.totalPages}"
.value=${this.currentPage} .value=${String(this.currentPage)}
@change=${this.handlePageInput} @change=${this.handlePageInput}
class="page-input" class="page-input"
/> />
@@ -334,7 +331,6 @@ export class DeesPdfViewer extends DeesElement {
} }
this.renderState = 'rendered'; this.renderState = 'rendered';
this.loadedPdfUrl = this.pdfUrl;
} catch (error) { } catch (error) {
console.error('Error loading PDF:', error); console.error('Error loading PDF:', error);
this.loading = false; 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 // Finally null the document reference
this.pdfDocument = null; this.pdfDocument = null;