update
This commit is contained in:
@@ -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();
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
</div>
|
||||
` : ''}
|
||||
${isPDF ? html`
|
||||
<iframe
|
||||
<sio-pdf-viewer
|
||||
class="pdf-viewer ${this.fileLoaded ? 'loaded' : ''}"
|
||||
src="${this.file.url}"
|
||||
title="${this.file.name}"
|
||||
.url="${this.file.url}"
|
||||
.fileName="${this.file.name}"
|
||||
@load=${() => this.fileLoaded = true}
|
||||
@error=${() => this.fileLoaded = false}
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
></iframe>
|
||||
></sio-pdf-viewer>
|
||||
` : html`
|
||||
<img
|
||||
class="image ${this.fileLoaded ? 'loaded' : ''}"
|
||||
@@ -404,6 +406,12 @@ export class SioImageLightbox extends DeesElement {
|
||||
this.resetZoom();
|
||||
this.isOpen = true;
|
||||
|
||||
// For PDFs, we'll handle loading state differently since it's in a separate component
|
||||
if (this.isPDF()) {
|
||||
// PDFs are handled by sio-pdf-viewer which manages its own loading state
|
||||
this.fileLoaded = true;
|
||||
}
|
||||
|
||||
// Add keyboard listener
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
589
ts_web/elements/sio-pdf-viewer.ts
Normal file
589
ts_web/elements/sio-pdf-viewer.ts
Normal file
@@ -0,0 +1,589 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
cssManager,
|
||||
css,
|
||||
unsafeCSS,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
// Import design tokens
|
||||
import { colors, bdTheme } from './00colors.js';
|
||||
import { spacing, radius, shadows, transitions } 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`
|
||||
<sio-pdf-viewer
|
||||
.url=${'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G'}
|
||||
.fileName=${'demo.pdf'}
|
||||
style="width: 600px; height: 800px;"
|
||||
></sio-pdf-viewer>
|
||||
`;
|
||||
|
||||
@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<void> | 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`
|
||||
<div class="container">
|
||||
${this.isLoading ? html`
|
||||
<div class="loading">
|
||||
<sio-icon class="spinner" icon="loader" size="24"></sio-icon>
|
||||
<div>Loading PDF...</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.pdfDocument ? html`
|
||||
<div class="pdf-controls">
|
||||
<div class="pdf-controls-group">
|
||||
<sio-button
|
||||
type="ghost"
|
||||
size="sm"
|
||||
@click=${this.previousPage}
|
||||
?disabled=${this.currentPage <= 1}
|
||||
>
|
||||
<sio-icon icon="chevron-left" size="16"></sio-icon>
|
||||
</sio-button>
|
||||
<div class="page-info">
|
||||
Page ${this.currentPage} of ${this.totalPages}
|
||||
</div>
|
||||
<sio-button
|
||||
type="ghost"
|
||||
size="sm"
|
||||
@click=${this.nextPage}
|
||||
?disabled=${this.currentPage >= this.totalPages}
|
||||
>
|
||||
<sio-icon icon="chevron-right" size="16"></sio-icon>
|
||||
</sio-button>
|
||||
</div>
|
||||
|
||||
<div class="pdf-controls-group">
|
||||
<sio-button
|
||||
type="ghost"
|
||||
size="sm"
|
||||
@click=${this.zoomOut}
|
||||
>
|
||||
<sio-icon icon="zoom-out" size="16"></sio-icon>
|
||||
</sio-button>
|
||||
<sio-button
|
||||
type="ghost"
|
||||
size="sm"
|
||||
@click=${this.resetZoom}
|
||||
>
|
||||
<sio-icon icon="maximize-2" size="16"></sio-icon>
|
||||
</sio-button>
|
||||
<sio-button
|
||||
type="ghost"
|
||||
size="sm"
|
||||
@click=${this.zoomIn}
|
||||
>
|
||||
<sio-icon icon="zoom-in" size="16"></sio-icon>
|
||||
</sio-button>
|
||||
</div>
|
||||
|
||||
<div class="pdf-controls-group">
|
||||
<sio-button
|
||||
type="ghost"
|
||||
size="sm"
|
||||
@click=${this.downloadPdf}
|
||||
>
|
||||
<sio-icon icon="download" size="16"></sio-icon>
|
||||
</sio-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pdf-container">
|
||||
<div class="pdf-canvas-wrapper">
|
||||
<canvas></canvas>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderError(): TemplateResult {
|
||||
return html`
|
||||
<div class="error-container">
|
||||
<sio-icon class="error-icon" icon="alert-circle" size="48"></sio-icon>
|
||||
<div class="error-title">Unable to display PDF</div>
|
||||
<div class="error-message">
|
||||
The PDF viewer couldn't load this document. This might be due to browser restrictions or an invalid PDF file.
|
||||
</div>
|
||||
<div class="error-actions">
|
||||
<sio-button
|
||||
type="primary"
|
||||
size="sm"
|
||||
@click=${this.downloadPdf}
|
||||
>
|
||||
<sio-icon icon="download" size="16"></sio-icon>
|
||||
Download PDF
|
||||
</sio-button>
|
||||
<sio-button
|
||||
type="outline"
|
||||
size="sm"
|
||||
@click=${this.openInNewTab}
|
||||
>
|
||||
<sio-icon icon="external-link" size="16"></sio-icon>
|
||||
Open in New Tab
|
||||
</sio-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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<string | number | symbol, unknown>) {
|
||||
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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user