diff --git a/test/test.browser.ts b/test/test.browser.ts index 7573551..0704518 100644 --- a/test/test.browser.ts +++ b/test/test.browser.ts @@ -71,7 +71,21 @@ tap.test('render image lightbox component', async () => { await lightbox.updateComplete; expect(lightbox.isOpen).toEqual(true); - console.log('Image lightbox component rendered successfully'); + // Test opening with a PDF + await lightbox.open({ + url: 'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G', + name: 'test.pdf', + type: 'application/pdf', + size: 565 + }); + + await lightbox.updateComplete; + + // Check that PDF viewer is rendered + const pdfViewer = lightbox.shadowRoot.querySelector('sio-pdf-viewer'); + expect(pdfViewer).toBeTruthy(); + + console.log('Image lightbox component rendered successfully with both image and PDF support'); document.body.removeChild(lightbox); }); @@ -106,4 +120,30 @@ tap.test('render dropdown menu component', async () => { document.body.removeChild(dropdown); }); +tap.test('render pdf viewer component', async () => { + // Create and add PDF viewer + const pdfViewer = new socialioCatalog.SioPdfViewer(); + pdfViewer.style.width = '600px'; + pdfViewer.style.height = '400px'; + pdfViewer.url = 'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G'; + pdfViewer.fileName = 'test.pdf'; + document.body.appendChild(pdfViewer); + + await pdfViewer.updateComplete; + expect(pdfViewer).toBeInstanceOf(socialioCatalog.SioPdfViewer); + + // Check main elements + const container = pdfViewer.shadowRoot.querySelector('.container'); + expect(container).toBeTruthy(); + + // PDF viewer uses canvas after loading, not iframe + // Just verify the component rendered correctly + expect(pdfViewer.url).toEqual('data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G'); + expect(pdfViewer.fileName).toEqual('test.pdf'); + + console.log('PDF viewer component rendered successfully'); + + document.body.removeChild(pdfViewer); +}); + tap.start(); \ No newline at end of file diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts index 4bd6071..8d75400 100644 --- a/ts_web/elements/index.ts +++ b/ts_web/elements/index.ts @@ -12,3 +12,4 @@ export * from './sio-combox.js'; export * from './sio-fab.js'; export * from './sio-recorder.js'; export * from './sio-image-lightbox.js'; +export * from './sio-pdf-viewer.js'; diff --git a/ts_web/elements/sio-image-lightbox.ts b/ts_web/elements/sio-image-lightbox.ts index b4f5c4f..d5fbc27 100644 --- a/ts_web/elements/sio-image-lightbox.ts +++ b/ts_web/elements/sio-image-lightbox.ts @@ -15,6 +15,10 @@ import { colors, bdTheme } from './00colors.js'; import { spacing, radius, shadows, transitions } from './00tokens.js'; import { fontFamilies } from './00fonts.js'; +// Import components +import { SioPdfViewer } from './sio-pdf-viewer.js'; +SioPdfViewer; + export interface ILightboxFile { url: string; name: string; @@ -360,14 +364,12 @@ export class SioImageLightbox extends DeesElement { ` : ''} ${isPDF ? html` - + > ` : html` 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(); + + // Set up resize observer for responsive rendering + const container = this.shadowRoot?.querySelector('.pdf-container'); + if (container) { + this.resizeObserver = new ResizeObserver(() => { + if (this.pdfDocument && !this.isLoading) { + this.renderPage(); + } + }); + this.resizeObserver.observe(container); + } + + if (this.url) { + await this.loadPdf(); + } + } + + public async updated(changedProperties: Map) { + await 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(); + } + } +} \ No newline at end of file