import { DeesElement, property, html, customElement, type TemplateResult, cssManager, css, unsafeCSS, state, type PropertyValues, } from '@design.estate/dees-element'; // Import design tokens import { bdTheme } from './00colors.js'; import { spacing, radius, shadows } from './00tokens.js'; import { fontFamilies } from './00fonts.js'; declare global { interface HTMLElementTagNameMap { 'sio-pdf-viewer': SioPdfViewer; } interface Window { pdfjsLib?: any; } } @customElement('sio-pdf-viewer') export class SioPdfViewer extends DeesElement { public static demo = () => html` `; @property({ type: String }) public url: string = ''; @property({ type: String }) public fileName: string = 'document.pdf'; @state() private isLoading: boolean = true; @state() private hasError: boolean = false; @state() private pdfDocument: any = null; @state() private currentPage: number = 1; @state() private totalPages: number = 0; @state() private scale: number = 1; private static pdfJsLoaded: boolean = false; private static pdfJsLoading: Promise | null = null; private renderTask: any = null; private resizeObserver: ResizeObserver | null = null; public static styles = [ cssManager.defaultStyles, css` :host { display: block; width: 100%; height: 100%; position: relative; background: ${bdTheme('background')}; font-family: ${unsafeCSS(fontFamilies.sans)}; } .container { width: 100%; height: 100%; position: relative; display: flex; flex-direction: column; } .pdf-container { width: 100%; height: 100%; overflow: auto; display: flex; flex-direction: column; align-items: center; background: ${bdTheme('muted')}; } .pdf-canvas-wrapper { position: relative; margin: ${unsafeCSS(spacing["4"])} auto; box-shadow: ${unsafeCSS(shadows.lg)}; background: white; } canvas { display: block; max-width: 100%; height: auto; } .pdf-controls { position: sticky; top: 0; z-index: 10; width: 100%; padding: ${unsafeCSS(spacing["3"])}; background: ${bdTheme('background')}; border-bottom: 1px solid ${bdTheme('border')}; display: flex; align-items: center; justify-content: center; gap: ${unsafeCSS(spacing["3"])}; box-shadow: ${unsafeCSS(shadows.sm)}; } .pdf-controls-group { display: flex; align-items: center; gap: ${unsafeCSS(spacing["2"])}; } .page-info { font-size: 0.875rem; color: ${bdTheme('mutedForeground')}; min-width: 100px; text-align: center; } .loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: ${bdTheme('mutedForeground')}; } .spinner { animation: spin 1s linear infinite; margin-bottom: ${unsafeCSS(spacing["2"])}; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .error-container { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; padding: ${unsafeCSS(spacing["6"])}; background: ${bdTheme('card')}; border: 1px solid ${bdTheme('border')}; border-radius: ${unsafeCSS(radius.lg)}; box-shadow: ${unsafeCSS(shadows.md)}; max-width: 400px; } .error-icon { color: ${bdTheme('destructive')}; margin-bottom: ${unsafeCSS(spacing["3"])}; } .error-title { font-size: 1.125rem; font-weight: 600; color: ${bdTheme('foreground')}; margin-bottom: ${unsafeCSS(spacing["2"])}; } .error-message { color: ${bdTheme('mutedForeground')}; margin-bottom: ${unsafeCSS(spacing["4"])}; font-size: 0.875rem; } .error-actions { display: flex; gap: ${unsafeCSS(spacing["2"])}; justify-content: center; } .fallback-viewer { width: 100%; height: 100%; display: flex; flex-direction: column; background: ${bdTheme('background')}; } .fallback-header { padding: ${unsafeCSS(spacing["4"])}; border-bottom: 1px solid ${bdTheme('border')}; display: flex; align-items: center; justify-content: space-between; background: ${bdTheme('card')}; } .fallback-title { font-weight: 500; color: ${bdTheme('foreground')}; display: flex; align-items: center; gap: ${unsafeCSS(spacing["2"])}; } .fallback-content { flex: 1; display: flex; align-items: center; justify-content: center; padding: ${unsafeCSS(spacing["8"])}; } .fallback-message { text-align: center; color: ${bdTheme('mutedForeground')}; } .fallback-icon { font-size: 48px; margin-bottom: ${unsafeCSS(spacing["4"])}; opacity: 0.5; } .fallback-text { margin-bottom: ${unsafeCSS(spacing["4"])}; } /* Scrollbar styling */ .pdf-container::-webkit-scrollbar { width: 8px; height: 8px; } .pdf-container::-webkit-scrollbar-track { background: ${bdTheme('muted')}; } .pdf-container::-webkit-scrollbar-thumb { background: ${bdTheme('border')}; border-radius: 4px; } .pdf-container::-webkit-scrollbar-thumb:hover { background: ${bdTheme('mutedForeground')}; } /* Responsive */ @media (max-width: 600px) { .pdf-controls { flex-wrap: wrap; gap: ${unsafeCSS(spacing["2"])}; } .pdf-controls-group { flex-wrap: nowrap; } } `, ]; public render(): TemplateResult { if (this.hasError) { return this.renderError(); } return html`
${this.isLoading ? html`
Loading PDF...
` : ''} ${this.pdfDocument ? html`
Page ${this.currentPage} of ${this.totalPages}
= this.totalPages} >
` : ''}
`; } private renderError(): TemplateResult { return html`
Unable to display PDF
The PDF viewer couldn't load this document. This might be due to browser restrictions or an invalid PDF file.
Download PDF Open in New Tab
`; } public async connectedCallback() { await super.connectedCallback(); if (this.url) { await this.loadPdf(); } } public async firstUpdated(_changedProperties: PropertyValues) { super.firstUpdated(_changedProperties); // Set up resize observer for responsive rendering after first render const container = this.shadowRoot?.querySelector('.pdf-container'); if (container) { this.resizeObserver = new ResizeObserver(() => { if (this.pdfDocument && !this.isLoading) { this.renderPage(); } }); this.resizeObserver.observe(container); } } public async updated(changedProperties: Map) { super.updated(changedProperties); if (changedProperties.has('url') && this.url) { await this.loadPdf(); } // Re-render when scale changes if (changedProperties.has('scale') && this.pdfDocument && !this.isLoading) { await this.renderPage(); } } private static async loadPdfJs(): Promise { if (SioPdfViewer.pdfJsLoaded) return; if (SioPdfViewer.pdfJsLoading) { return SioPdfViewer.pdfJsLoading; } SioPdfViewer.pdfJsLoading = new Promise(async (resolve, reject) => { try { // Load PDF.js from jsDelivr const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js'; script.onload = () => { if (window.pdfjsLib) { // Configure worker window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js'; SioPdfViewer.pdfJsLoaded = true; resolve(); } else { reject(new Error('PDF.js failed to load')); } }; script.onerror = () => reject(new Error('Failed to load PDF.js script')); document.head.appendChild(script); } catch (error) { reject(error); } }); return SioPdfViewer.pdfJsLoading; } private async loadPdf() { this.isLoading = true; this.hasError = false; this.pdfDocument = null; try { // Load PDF.js if not already loaded await SioPdfViewer.loadPdfJs(); // Load the PDF document const loadingTask = window.pdfjsLib.getDocument({ url: this.url, // Enable range requests for better performance disableRange: false, // Enable streaming for large PDFs disableStream: false, }); this.pdfDocument = await loadingTask.promise; this.totalPages = this.pdfDocument.numPages; this.currentPage = 1; this.isLoading = false; // Render the first page await this.renderPage(); } catch (error) { console.error('Failed to load PDF:', error); this.hasError = true; this.isLoading = false; } } private async renderPage() { if (!this.pdfDocument) return; // Cancel any ongoing render task if (this.renderTask) { this.renderTask.cancel(); } try { const page = await this.pdfDocument.getPage(this.currentPage); const canvas = this.shadowRoot?.querySelector('canvas') as HTMLCanvasElement; if (!canvas) return; const context = canvas.getContext('2d'); const viewport = page.getViewport({ scale: this.scale }); // Set canvas dimensions canvas.height = viewport.height; canvas.width = viewport.width; // Render PDF page into canvas context const renderContext = { canvasContext: context, viewport: viewport }; this.renderTask = page.render(renderContext); await this.renderTask.promise; } catch (error) { if (error.name !== 'RenderingCancelledException') { console.error('Error rendering page:', error); } } } private async previousPage() { if (this.currentPage > 1) { this.currentPage--; await this.renderPage(); } } private async nextPage() { if (this.currentPage < this.totalPages) { this.currentPage++; await this.renderPage(); } } private async zoomIn() { this.scale = Math.min(this.scale * 1.2, 3); await this.renderPage(); } private async zoomOut() { this.scale = Math.max(this.scale / 1.2, 0.5); await this.renderPage(); } private async resetZoom() { this.scale = 1; await this.renderPage(); } private downloadPdf() { const a = document.createElement('a'); a.href = this.url; a.download = this.fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); } private openInNewTab() { window.open(this.url, '_blank'); } public async disconnectedCallback() { await super.disconnectedCallback(); // Clean up resize observer if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; } // Cancel any ongoing render task if (this.renderTask) { this.renderTask.cancel(); } // Destroy PDF document to free memory if (this.pdfDocument) { this.pdfDocument.destroy(); } } }