import { DeesElement, property, html, customElement, domtools, type TemplateResult, type CSSResult, css, cssManager } from '@design.estate/dees-element'; 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 { interface HTMLElementTagNameMap { 'dees-pdf-viewer': DeesPdfViewer; } } @customElement('dees-pdf-viewer') export class DeesPdfViewer extends DeesElement { public static demo = demoFunc; public static styles = viewerStyles; @property({ type: String }) public pdfUrl: string = ''; @property({ type: Number }) public initialPage: number = 1; @property({ type: String }) public initialZoom: 'auto' | 'page-fit' | 'page-width' | number = 'auto'; @property({ type: Boolean }) public showToolbar: boolean = true; @property({ type: Boolean }) public showSidebar: boolean = false; @property({ type: Number }) private currentPage: number = 1; @property({ type: Number }) private totalPages: number = 1; @property({ type: Number }) private currentZoom: number = 1; @property({ type: Boolean }) private loading: boolean = false; private pdfDocument: any; private pageRendering: boolean = false; private pageNumPending: number | null = null; private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; constructor() { super(); } public render(): TemplateResult { return html`
${this.showToolbar ? html`
/ ${this.totalPages}
` : ''}
${this.showSidebar ? html` ` : ''}
${this.loading ? html`
Loading PDF...
` : html`
`}
`; } public async connectedCallback() { super.connectedCallback(); await this.updateComplete; if (this.pdfUrl) { await this.loadPdf(); } } public async updated(changedProperties: Map) { super.updated(changedProperties); if (changedProperties.has('pdfUrl') && this.pdfUrl) { await this.loadPdf(); } } private async loadPdf() { this.loading = true; try { this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl); this.totalPages = this.pdfDocument.numPages; this.currentPage = this.initialPage; await this.updateComplete; this.canvas = this.shadowRoot?.querySelector('#pdf-canvas') as HTMLCanvasElement; this.ctx = this.canvas?.getContext('2d') as CanvasRenderingContext2D; await this.renderPage(this.currentPage); if (this.showSidebar) { this.renderThumbnails(); } } catch (error) { console.error('Error loading PDF:', error); } finally { this.loading = false; } } private async renderPage(pageNum: number) { if (!this.pdfDocument || !this.canvas || !this.ctx) return; this.pageRendering = true; try { const page = await this.pdfDocument.getPage(pageNum); let viewport; if (this.initialZoom === 'auto' || this.initialZoom === 'page-fit') { const tempViewport = page.getViewport({ scale: 1 }); const containerWidth = this.canvas.parentElement?.clientWidth || 800; const containerHeight = this.canvas.parentElement?.clientHeight || 600; const scaleX = containerWidth / tempViewport.width; const scaleY = containerHeight / tempViewport.height; this.currentZoom = Math.min(scaleX, scaleY); viewport = page.getViewport({ scale: this.currentZoom }); } else if (this.initialZoom === 'page-width') { const tempViewport = page.getViewport({ scale: 1 }); const containerWidth = this.canvas.parentElement?.clientWidth || 800; this.currentZoom = containerWidth / tempViewport.width; viewport = page.getViewport({ scale: this.currentZoom }); } else { this.currentZoom = typeof this.initialZoom === 'number' ? this.initialZoom : 1; viewport = page.getViewport({ scale: this.currentZoom }); } this.canvas.height = viewport.height; this.canvas.width = viewport.width; const renderContext = { canvasContext: this.ctx, viewport: viewport, }; await page.render(renderContext).promise; this.pageRendering = false; if (this.pageNumPending !== null) { await this.renderPage(this.pageNumPending); this.pageNumPending = null; } } catch (error) { console.error('Error rendering page:', error); this.pageRendering = false; } } private queueRenderPage(pageNum: number) { if (this.pageRendering) { this.pageNumPending = pageNum; } else { this.renderPage(pageNum); } } private async renderThumbnails() { await this.updateComplete; const thumbnails = this.shadowRoot?.querySelectorAll('.thumbnail-canvas') as NodeListOf; const thumbnailWidth = 176; // Fixed width for thumbnails (200px container - 24px padding) for (const canvas of Array.from(thumbnails)) { const pageNum = parseInt(canvas.dataset.page || '1'); const page = await this.pdfDocument.getPage(pageNum); // Calculate scale to fit thumbnail width while maintaining aspect ratio const initialViewport = page.getViewport({ scale: 1 }); const scale = thumbnailWidth / 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 canvas.style.width = `${viewport.width}px`; canvas.style.height = `${viewport.height}px`; const context = canvas.getContext('2d'); const renderContext = { canvasContext: context, viewport: viewport, }; await page.render(renderContext).promise; } } private previousPage() { if (this.currentPage > 1) { this.currentPage--; this.queueRenderPage(this.currentPage); } } private nextPage() { if (this.currentPage < this.totalPages) { this.currentPage++; this.queueRenderPage(this.currentPage); } } private goToPage(pageNum: number) { if (pageNum >= 1 && pageNum <= this.totalPages) { this.currentPage = pageNum; this.queueRenderPage(this.currentPage); } } private handlePageInput(e: Event) { const input = e.target as HTMLInputElement; const pageNum = parseInt(input.value); this.goToPage(pageNum); } private zoomIn() { if (this.currentZoom < 3) { this.currentZoom = Math.min(3, this.currentZoom * 1.2); this.queueRenderPage(this.currentPage); } } private zoomOut() { if (this.currentZoom > 0.5) { this.currentZoom = Math.max(0.5, this.currentZoom / 1.2); this.queueRenderPage(this.currentPage); } } private resetZoom() { this.currentZoom = 1; this.queueRenderPage(this.currentPage); } private fitToPage() { this.initialZoom = 'page-fit'; this.queueRenderPage(this.currentPage); } private fitToWidth() { this.initialZoom = 'page-width'; this.queueRenderPage(this.currentPage); } private downloadPdf() { const link = document.createElement('a'); link.href = this.pdfUrl; link.download = this.pdfUrl.split('/').pop() || 'document.pdf'; link.click(); } private printPdf() { window.open(this.pdfUrl, '_blank')?.print(); } /** * Provide context menu items for right-click functionality */ public getContextMenuItems() { return [ { name: 'Open PDF in New Tab', iconName: 'lucide:ExternalLink', action: async () => { window.open(this.pdfUrl, '_blank'); } }, { divider: true }, { name: 'Copy PDF URL', iconName: 'lucide:Copy', action: async () => { await navigator.clipboard.writeText(this.pdfUrl); } }, { name: 'Download PDF', iconName: 'lucide:Download', action: async () => { this.downloadPdf(); } }, { name: 'Print PDF', iconName: 'lucide:Printer', action: async () => { this.printPdf(); } } ]; } }