UNPKG

ngx-word-viewer

Version:

Angular component for viewing Word documents (.docx) with page navigation, zoom.

649 lines (611 loc) 36 kB
import * as i0 from '@angular/core'; import { EventEmitter, PLATFORM_ID, Output, Input, ViewChild, Inject, ViewEncapsulation, Component, NgModule } from '@angular/core'; import * as i1 from '@angular/common'; import { isPlatformBrowser, CommonModule } from '@angular/common'; import * as i2 from '@angular/forms'; import { FormsModule } from '@angular/forms'; // Mammoth ko any type se import karo let mammothConverter; class WordViewerComponent { constructor(platformId) { this.platformId = platformId; this.src = null; this.showToolbar = true; this.showPageHeader = false; this.showPageFooter = true; this.fileName = 'Document'; this.initialZoom = 1; this.pageHeight = 1000; this.onDocumentLoad = new EventEmitter(); this.onError = new EventEmitter(); this.pageChange = new EventEmitter(); this.fullContent = ''; this.pages = []; this.currentPage = 1; this.totalPages = 0; this.currentPageContent = ''; this.zoom = 1; this.isLoading = false; this.errorMessage = ''; this.pendingSrc = null; this.A4_WIDTH_PX = 794; this.A4_HEIGHT_PX = 1123; this.MAX_IMAGE_WIDTH = 650; // Page content width minus padding if (isPlatformBrowser(this.platformId)) { this.initializeMammoth(); } } async initializeMammoth() { if (window.mammoth) { mammothConverter = window.mammoth; return; } try { const mammothModule = await import('mammoth'); mammothConverter = mammothModule.default || mammothModule; } catch (e) { console.log('Mammoth not found in node_modules, will load from CDN when needed'); } } ngOnChanges(changes) { if (changes['src'] && this.src) { this.loadDocument(); } if (changes['initialZoom']) { this.zoom = this.initialZoom; } } async loadMammothAndRetry() { this.errorMessage = ''; this.isLoading = true; try { await this.loadMammothFromCDN(); if (this.pendingSrc || this.src) { await this.loadDocument(); } } catch (error) { this.errorMessage = 'Failed to load converter library from CDN'; this.isLoading = false; } } loadMammothFromCDN() { return new Promise((resolve, reject) => { if (window.mammoth) { mammothConverter = window.mammoth; resolve(); return; } const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/mammoth@1.6.0/mammoth.browser.min.js'; script.onload = () => { mammothConverter = window.mammoth; resolve(); }; script.onerror = () => reject(new Error('Failed to load mammoth from CDN')); document.head.appendChild(script); }); } async loadDocument() { if (!isPlatformBrowser(this.platformId)) { return; } this.isLoading = true; this.errorMessage = ''; this.pages = []; this.currentPageContent = ''; this.pendingSrc = this.src; try { if (!mammothConverter) { await this.loadMammothFromCDN(); } if (!mammothConverter) { throw new Error('Mammoth library could not be loaded. Click retry to load from CDN.'); } let arrayBuffer; if (this.src instanceof File) { this.fileName = this.src.name.replace('.docx', ''); arrayBuffer = await this.fileToArrayBuffer(this.src); } else if (this.src instanceof ArrayBuffer) { arrayBuffer = this.src; } else if (typeof this.src === 'string') { if (this.src.startsWith('data:')) { arrayBuffer = this.base64ToArrayBuffer(this.src); } else { const urlParts = this.src.split('/'); this.fileName = urlParts[urlParts.length - 1].replace('.docx', ''); arrayBuffer = await this.fetchDocument(this.src); } } else { throw new Error('Invalid source type'); } const options = { arrayBuffer: arrayBuffer, // Add custom style map for better formatting styleMap: [ "p[style-name='Title'] => h1:fresh", "p[style-name='Heading 1'] => h1:fresh", "p[style-name='Heading 2'] => h2:fresh", "p[style-name='Heading 3'] => h3:fresh" ], includeDefaultStyleMap: true }; // Handle images with better conversion if (mammothConverter.images && mammothConverter.images.imgElement) { options.convertImage = mammothConverter.images.imgElement((image) => { return image.read("base64").then((imageBuffer) => { // Get image dimensions if available const width = image.width || 'auto'; const height = image.height || 'auto'; return { src: "data:" + image.contentType + ";base64," + imageBuffer, style: `max-width: 100%; width: ${width}px; height: auto;` }; }); }); } const result = await mammothConverter.convertToHtml(options); // Process and fix images in the HTML this.fullContent = this.processImages(this.enhanceHtmlContent(result.value)); this.processContentIntoPages(); this.onDocumentLoad.emit({ content: this.fullContent, messages: result.messages, totalPages: this.totalPages }); this.pendingSrc = null; } catch (error) { this.errorMessage = error.message || 'Failed to load document'; this.onError.emit(this.errorMessage); } finally { this.isLoading = false; } } processImages(html) { // Process images to ensure they fit within page bounds const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; // Find all images and set max dimensions const images = tempDiv.querySelectorAll('img'); images.forEach((img) => { // Remove any width/height attributes that might cause overflow img.removeAttribute('width'); img.removeAttribute('height'); // Add style to ensure image fits const currentStyle = img.getAttribute('style') || ''; img.setAttribute('style', `${currentStyle}; max-width: 100% !important; height: auto !important; display: block; margin: 10px auto;`); // Add a wrapper div for better control const wrapper = document.createElement('div'); wrapper.style.cssText = 'text-align: center; margin: 15px 0; page-break-inside: avoid;'; img.parentNode?.insertBefore(wrapper, img); wrapper.appendChild(img); }); return tempDiv.innerHTML; } enhanceHtmlContent(html) { let enhancedHtml = html; // Convert strong paragraphs to headings enhancedHtml = enhancedHtml.replace(/<p><strong>([^<]{1,100})<\/strong><\/p>/gi, (match, p1) => { if (p1.length < 100 && (p1 === p1.toUpperCase() || /^[A-Z]/.test(p1))) { return `<h2>${p1}</h2>`; } return match; }); // Fix table formatting enhancedHtml = enhancedHtml.replace(/<table>/g, '<table style="width: 100%; margin: 15px 0;">'); return enhancedHtml; } processContentIntoPages() { const tempContainer = document.createElement('div'); tempContainer.style.position = 'absolute'; tempContainer.style.visibility = 'hidden'; tempContainer.style.width = `${this.A4_WIDTH_PX - 120}px`; tempContainer.style.padding = '40px 60px'; tempContainer.style.fontSize = '12pt'; tempContainer.style.lineHeight = '1.6'; tempContainer.innerHTML = this.fullContent; document.body.appendChild(tempContainer); const elements = Array.from(tempContainer.children); this.pages = []; let currentPageHtml = ''; let currentHeight = 0; const effectivePageHeight = this.pageHeight - 100; // Leave some margin elements.forEach((element) => { const elementHeight = element.getBoundingClientRect().height; // Special handling for images if (element.tagName === 'IMG' || element.querySelector('img')) { const img = element.tagName === 'IMG' ? element : element.querySelector('img'); if (img) { // If image is too tall for remaining space, start new page if (currentHeight + elementHeight > effectivePageHeight && currentPageHtml) { this.pages.push(currentPageHtml); currentPageHtml = element.outerHTML; currentHeight = elementHeight; } else { currentPageHtml += element.outerHTML; currentHeight += elementHeight; } } } else { // Regular content if (currentHeight + elementHeight > effectivePageHeight && currentPageHtml) { this.pages.push(currentPageHtml); currentPageHtml = element.outerHTML; currentHeight = elementHeight; } else { currentPageHtml += element.outerHTML; currentHeight += elementHeight; } } }); if (currentPageHtml) { this.pages.push(currentPageHtml); } document.body.removeChild(tempContainer); if (this.pages.length === 0) { this.pages = [this.fullContent || '<p>Empty document</p>']; } this.totalPages = this.pages.length; this.currentPage = 1; this.displayPage(1); } displayPage(pageNumber) { if (pageNumber >= 1 && pageNumber <= this.totalPages) { this.currentPage = pageNumber; this.currentPageContent = this.pages[pageNumber - 1]; this.pageChange.emit(this.currentPage); } } goToPage(event) { const pageNumber = parseInt(event.target.value, 10); if (!isNaN(pageNumber) && pageNumber >= 1 && pageNumber <= this.totalPages) { this.displayPage(pageNumber); } } nextPage() { if (this.currentPage < this.totalPages) { this.displayPage(this.currentPage + 1); } } previousPage() { if (this.currentPage > 1) { this.displayPage(this.currentPage - 1); } } onZoomChange() { } zoomIn() { this.zoom = Math.min(this.zoom + 0.25, 3); } zoomOut() { this.zoom = Math.max(this.zoom - 0.25, 0.5); } fitToWidth() { const viewerArea = document.querySelector('.document-viewer-area'); if (viewerArea) { const viewerWidth = viewerArea.clientWidth - 40; this.zoom = viewerWidth / this.A4_WIDTH_PX; this.zoom = Math.min(Math.max(this.zoom, 0.5), 2); } } print() { const printWindow = window.open('', '_blank'); if (printWindow) { printWindow.document.write(` <!DOCTYPE html> <html> <head> <title>${this.fileName}</title> <style> @page { size: A4; margin: 20mm; } body { font-family: Arial, sans-serif; font-size: 12pt; line-height: 1.6; } img { max-width: 100% !important; height: auto !important; page-break-inside: avoid; } .page { page-break-after: always; } </style> </head> <body> ${this.pages.map(page => `<div class="page">${page}</div>`).join('')} </body> </html> `); printWindow.document.close(); setTimeout(() => printWindow.print(), 250); } } download() { const htmlContent = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>${this.fileName}</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } img { max-width: 100%; height: auto; display: block; margin: 15px auto; } table { width: 100%; border-collapse: collapse; margin: 15px 0; } th, td { border: 1px solid #ddd; padding: 8px; } </style> </head> <body>${this.fullContent}</body> </html> `; const blob = new Blob([htmlContent], { type: 'text/html' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${this.fileName}.html`; link.click(); window.URL.revokeObjectURL(url); } fileToArrayBuffer(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target?.result); reader.onerror = reject; reader.readAsArrayBuffer(file); }); } base64ToArrayBuffer(base64) { const base64Content = base64.split(',')[1]; const binaryString = window.atob(base64Content); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } async fetchDocument(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch document: ${response.statusText}`); } return await response.arrayBuffer(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: WordViewerComponent, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: WordViewerComponent, isStandalone: true, selector: "ngx-word-viewer", inputs: { src: "src", showToolbar: "showToolbar", showPageHeader: "showPageHeader", showPageFooter: "showPageFooter", fileName: "fileName", initialZoom: "initialZoom", pageHeight: "pageHeight" }, outputs: { onDocumentLoad: "onDocumentLoad", onError: "onError", pageChange: "pageChange" }, viewQueries: [{ propertyName: "documentContainer", first: true, predicate: ["documentContainer"], descendants: true }], usesOnChanges: true, ngImport: i0, template: ` <div class="word-viewer-container"> <!-- Toolbar --> <div class="word-viewer-toolbar" *ngIf="showToolbar"> <button (click)="previousPage()" [disabled]="currentPage === 1" class="toolbar-btn"> <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/> </svg> Previous </button> <div class="page-navigation"> <input type="number" [value]="currentPage" (change)="goToPage($event)" [min]="1" [max]="totalPages" class="page-input" > <span class="page-info">/ {{ totalPages }}</span> </div> <button (click)="nextPage()" [disabled]="currentPage === totalPages" class="toolbar-btn"> Next <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/> </svg> </button> <div class="separator"></div> <button (click)="zoomOut()" class="toolbar-btn zoom-btn" title="Zoom Out"> <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <path d="M19 13H5v-2h14v2z"/> </svg> </button> <select [(ngModel)]="zoom" (change)="onZoomChange()" class="zoom-select"> <option [value]="0.5">50%</option> <option [value]="0.75">75%</option> <option [value]="1">100%</option> <option [value]="1.25">125%</option> <option [value]="1.5">150%</option> <option [value]="2">200%</option> </select> <button (click)="zoomIn()" class="toolbar-btn zoom-btn" title="Zoom In"> <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> </svg> </button> <button (click)="fitToWidth()" class="toolbar-btn" title="Fit to Width"> <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <path d="M4 5v14h16V5H4zm14 12H6V7h12v10z"/> </svg> Fit Width </button> </div> <!-- Loading Spinner --> <div *ngIf="isLoading" class="loading-container"> <div class="loading-spinner"> <div class="spinner"></div> <p>Loading document...</p> </div> </div> <!-- Error Message --> <div *ngIf="errorMessage && !isLoading" class="error-container"> <div class="error-message"> <svg width="48" height="48" viewBox="0 0 24 24" fill="#dc3545"> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/> </svg> <h3>Error Loading Document</h3> <p>{{ errorMessage }}</p> <button *ngIf="errorMessage.includes('Mammoth')" (click)="loadMammothAndRetry()" class="retry-btn"> Load from CDN and Retry </button> </div> </div> <!-- Document Viewer Area --> <div class="document-viewer-area" *ngIf="!isLoading && !errorMessage"> <div class="pages-container" [style.transform]="'scale(' + zoom + ')'"> <!-- A4 Page --> <div class="a4-page" *ngIf="currentPageContent"> <div class="page-header" *ngIf="showPageHeader"> <span>{{ fileName }}</span> <span>Page {{ currentPage }} of {{ totalPages }}</span> </div> <div class="page-content" [innerHTML]="currentPageContent"> </div> <div class="page-footer" *ngIf="showPageFooter"> <span>{{ currentPage }}</span> </div> </div> </div> </div> </div> `, isInline: true, styles: ["*{box-sizing:border-box}.word-viewer-container{width:100%;height:100%;display:flex;flex-direction:column;background:#e5e5e5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;position:relative}.word-viewer-toolbar{display:flex;align-items:center;gap:8px;padding:8px 16px;background:#2c3e50;border-bottom:1px solid #1a252f;box-shadow:0 2px 4px #0000001a;z-index:100}.toolbar-btn{display:flex;align-items:center;gap:4px;padding:6px 12px;background:transparent;color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s;font-size:13px;font-weight:500}.toolbar-btn:hover:not(:disabled){background:#ffffff1a;border-color:#ffffff4d}.toolbar-btn:disabled{opacity:.5;cursor:not-allowed}.zoom-btn{padding:6px 8px}.page-navigation{display:flex;align-items:center;gap:8px;padding:0 12px}.page-input{width:50px;padding:4px 8px;border:1px solid rgba(255,255,255,.2);border-radius:4px;background:#ffffff1a;color:#fff;text-align:center;font-size:13px}.page-info{color:#fffc;font-size:13px}.zoom-select{padding:4px 8px;border:1px solid rgba(255,255,255,.2);border-radius:4px;background:#ffffff1a;color:#fff;font-size:13px;cursor:pointer}.zoom-select option{background:#2c3e50}.separator{width:1px;height:24px;background:#fff3;margin:0 8px}.loading-container{flex:1;display:flex;align-items:center;justify-content:center}.loading-spinner{display:flex;flex-direction:column;align-items:center;gap:16px}.spinner{border:3px solid #f3f3f3;border-top:3px solid #2c3e50;border-radius:50%;width:40px;height:40px;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.error-container{flex:1;display:flex;align-items:center;justify-content:center;padding:20px}.error-message{text-align:center;padding:40px;background:#fff;border-radius:8px;box-shadow:0 2px 10px #0000001a;max-width:400px}.error-message h3{color:#dc3545;margin:16px 0 8px}.retry-btn{margin-top:20px;padding:10px 20px;background:#007bff;color:#fff;border:none;border-radius:4px;cursor:pointer}.retry-btn:hover{background:#0056b3}.document-viewer-area{flex:1;overflow:auto;display:flex;justify-content:center;padding:20px;background:#e5e5e5}.pages-container{transform-origin:top center;transition:transform .2s ease}.a4-page{width:794px;min-height:1123px;background:#fff;box-shadow:0 0 20px #0000001a;margin:0 auto;position:relative;display:flex;flex-direction:column}.page-header{display:flex;justify-content:space-between;padding:20px 40px 10px;font-size:11px;color:#666;border-bottom:1px solid #e0e0e0}.page-content{flex:1;padding:40px 60px;font-size:12pt;line-height:1.6;color:#333;overflow:hidden;word-wrap:break-word;overflow-wrap:break-word}.page-footer{padding:10px 40px 20px;text-align:center;font-size:11px;color:#666;border-top:1px solid #e0e0e0}.page-content h1{font-size:24pt;margin:0 0 12pt;color:#2c3e50;font-weight:700;page-break-after:avoid}.page-content h2{font-size:18pt;margin:12pt 0 10pt;color:#34495e;font-weight:700;page-break-after:avoid}.page-content h3{font-size:14pt;margin:10pt 0 8pt;color:#34495e;font-weight:700;page-break-after:avoid}.page-content p{margin:0 0 10pt;text-align:justify;page-break-inside:avoid}.page-content ul,.page-content ol{margin:10pt 0;padding-left:30pt;page-break-inside:avoid}.page-content li{margin:5pt 0}.page-content table{border-collapse:collapse;width:100%;margin:12pt 0;page-break-inside:avoid}.page-content th,.page-content td{border:1px solid #ddd;padding:8pt;text-align:left}.page-content th{background-color:#f8f9fa;font-weight:700}.page-content img{max-width:100%!important;width:auto!important;height:auto!important;max-height:600px!important;display:block;margin:12pt auto;object-fit:contain;page-break-inside:avoid;box-shadow:0 2px 8px #0000001a;border-radius:4px}.page-content p img{display:inline-block;vertical-align:middle;margin:4pt;max-height:400px!important}.page-content>*{max-width:100%;overflow:hidden}.page-content blockquote{border-left:4px solid #ddd;padding-left:16pt;margin:12pt 0;color:#666;font-style:italic;page-break-inside:avoid}@media (max-width: 900px){.a4-page{width:100%;min-height:auto}.page-content{padding:20px}.page-content img{max-width:100%!important;max-height:400px!important}}@media print{.word-viewer-toolbar{display:none!important}.document-viewer-area{padding:0;background:#fff}.a4-page{box-shadow:none;page-break-after:always;margin:0}.page-header,.page-footer{display:none}.page-content img{max-width:100%!important;page-break-inside:avoid!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: WordViewerComponent, decorators: [{ type: Component, args: [{ selector: 'ngx-word-viewer', standalone: true, imports: [CommonModule, FormsModule], template: ` <div class="word-viewer-container"> <!-- Toolbar --> <div class="word-viewer-toolbar" *ngIf="showToolbar"> <button (click)="previousPage()" [disabled]="currentPage === 1" class="toolbar-btn"> <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/> </svg> Previous </button> <div class="page-navigation"> <input type="number" [value]="currentPage" (change)="goToPage($event)" [min]="1" [max]="totalPages" class="page-input" > <span class="page-info">/ {{ totalPages }}</span> </div> <button (click)="nextPage()" [disabled]="currentPage === totalPages" class="toolbar-btn"> Next <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/> </svg> </button> <div class="separator"></div> <button (click)="zoomOut()" class="toolbar-btn zoom-btn" title="Zoom Out"> <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <path d="M19 13H5v-2h14v2z"/> </svg> </button> <select [(ngModel)]="zoom" (change)="onZoomChange()" class="zoom-select"> <option [value]="0.5">50%</option> <option [value]="0.75">75%</option> <option [value]="1">100%</option> <option [value]="1.25">125%</option> <option [value]="1.5">150%</option> <option [value]="2">200%</option> </select> <button (click)="zoomIn()" class="toolbar-btn zoom-btn" title="Zoom In"> <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> </svg> </button> <button (click)="fitToWidth()" class="toolbar-btn" title="Fit to Width"> <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <path d="M4 5v14h16V5H4zm14 12H6V7h12v10z"/> </svg> Fit Width </button> </div> <!-- Loading Spinner --> <div *ngIf="isLoading" class="loading-container"> <div class="loading-spinner"> <div class="spinner"></div> <p>Loading document...</p> </div> </div> <!-- Error Message --> <div *ngIf="errorMessage && !isLoading" class="error-container"> <div class="error-message"> <svg width="48" height="48" viewBox="0 0 24 24" fill="#dc3545"> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/> </svg> <h3>Error Loading Document</h3> <p>{{ errorMessage }}</p> <button *ngIf="errorMessage.includes('Mammoth')" (click)="loadMammothAndRetry()" class="retry-btn"> Load from CDN and Retry </button> </div> </div> <!-- Document Viewer Area --> <div class="document-viewer-area" *ngIf="!isLoading && !errorMessage"> <div class="pages-container" [style.transform]="'scale(' + zoom + ')'"> <!-- A4 Page --> <div class="a4-page" *ngIf="currentPageContent"> <div class="page-header" *ngIf="showPageHeader"> <span>{{ fileName }}</span> <span>Page {{ currentPage }} of {{ totalPages }}</span> </div> <div class="page-content" [innerHTML]="currentPageContent"> </div> <div class="page-footer" *ngIf="showPageFooter"> <span>{{ currentPage }}</span> </div> </div> </div> </div> </div> `, encapsulation: ViewEncapsulation.None, styles: ["*{box-sizing:border-box}.word-viewer-container{width:100%;height:100%;display:flex;flex-direction:column;background:#e5e5e5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;position:relative}.word-viewer-toolbar{display:flex;align-items:center;gap:8px;padding:8px 16px;background:#2c3e50;border-bottom:1px solid #1a252f;box-shadow:0 2px 4px #0000001a;z-index:100}.toolbar-btn{display:flex;align-items:center;gap:4px;padding:6px 12px;background:transparent;color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s;font-size:13px;font-weight:500}.toolbar-btn:hover:not(:disabled){background:#ffffff1a;border-color:#ffffff4d}.toolbar-btn:disabled{opacity:.5;cursor:not-allowed}.zoom-btn{padding:6px 8px}.page-navigation{display:flex;align-items:center;gap:8px;padding:0 12px}.page-input{width:50px;padding:4px 8px;border:1px solid rgba(255,255,255,.2);border-radius:4px;background:#ffffff1a;color:#fff;text-align:center;font-size:13px}.page-info{color:#fffc;font-size:13px}.zoom-select{padding:4px 8px;border:1px solid rgba(255,255,255,.2);border-radius:4px;background:#ffffff1a;color:#fff;font-size:13px;cursor:pointer}.zoom-select option{background:#2c3e50}.separator{width:1px;height:24px;background:#fff3;margin:0 8px}.loading-container{flex:1;display:flex;align-items:center;justify-content:center}.loading-spinner{display:flex;flex-direction:column;align-items:center;gap:16px}.spinner{border:3px solid #f3f3f3;border-top:3px solid #2c3e50;border-radius:50%;width:40px;height:40px;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.error-container{flex:1;display:flex;align-items:center;justify-content:center;padding:20px}.error-message{text-align:center;padding:40px;background:#fff;border-radius:8px;box-shadow:0 2px 10px #0000001a;max-width:400px}.error-message h3{color:#dc3545;margin:16px 0 8px}.retry-btn{margin-top:20px;padding:10px 20px;background:#007bff;color:#fff;border:none;border-radius:4px;cursor:pointer}.retry-btn:hover{background:#0056b3}.document-viewer-area{flex:1;overflow:auto;display:flex;justify-content:center;padding:20px;background:#e5e5e5}.pages-container{transform-origin:top center;transition:transform .2s ease}.a4-page{width:794px;min-height:1123px;background:#fff;box-shadow:0 0 20px #0000001a;margin:0 auto;position:relative;display:flex;flex-direction:column}.page-header{display:flex;justify-content:space-between;padding:20px 40px 10px;font-size:11px;color:#666;border-bottom:1px solid #e0e0e0}.page-content{flex:1;padding:40px 60px;font-size:12pt;line-height:1.6;color:#333;overflow:hidden;word-wrap:break-word;overflow-wrap:break-word}.page-footer{padding:10px 40px 20px;text-align:center;font-size:11px;color:#666;border-top:1px solid #e0e0e0}.page-content h1{font-size:24pt;margin:0 0 12pt;color:#2c3e50;font-weight:700;page-break-after:avoid}.page-content h2{font-size:18pt;margin:12pt 0 10pt;color:#34495e;font-weight:700;page-break-after:avoid}.page-content h3{font-size:14pt;margin:10pt 0 8pt;color:#34495e;font-weight:700;page-break-after:avoid}.page-content p{margin:0 0 10pt;text-align:justify;page-break-inside:avoid}.page-content ul,.page-content ol{margin:10pt 0;padding-left:30pt;page-break-inside:avoid}.page-content li{margin:5pt 0}.page-content table{border-collapse:collapse;width:100%;margin:12pt 0;page-break-inside:avoid}.page-content th,.page-content td{border:1px solid #ddd;padding:8pt;text-align:left}.page-content th{background-color:#f8f9fa;font-weight:700}.page-content img{max-width:100%!important;width:auto!important;height:auto!important;max-height:600px!important;display:block;margin:12pt auto;object-fit:contain;page-break-inside:avoid;box-shadow:0 2px 8px #0000001a;border-radius:4px}.page-content p img{display:inline-block;vertical-align:middle;margin:4pt;max-height:400px!important}.page-content>*{max-width:100%;overflow:hidden}.page-content blockquote{border-left:4px solid #ddd;padding-left:16pt;margin:12pt 0;color:#666;font-style:italic;page-break-inside:avoid}@media (max-width: 900px){.a4-page{width:100%;min-height:auto}.page-content{padding:20px}.page-content img{max-width:100%!important;max-height:400px!important}}@media print{.word-viewer-toolbar{display:none!important}.document-viewer-area{padding:0;background:#fff}.a4-page{box-shadow:none;page-break-after:always;margin:0}.page-header,.page-footer{display:none}.page-content img{max-width:100%!important;page-break-inside:avoid!important}}\n"] }] }], ctorParameters: () => [{ type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }], propDecorators: { documentContainer: [{ type: ViewChild, args: ['documentContainer'] }], src: [{ type: Input }], showToolbar: [{ type: Input }], showPageHeader: [{ type: Input }], showPageFooter: [{ type: Input }], fileName: [{ type: Input }], initialZoom: [{ type: Input }], pageHeight: [{ type: Input }], onDocumentLoad: [{ type: Output }], onError: [{ type: Output }], pageChange: [{ type: Output }] } }); class NgxWordViewerModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NgxWordViewerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: NgxWordViewerModule, imports: [WordViewerComponent], exports: [WordViewerComponent] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NgxWordViewerModule, imports: [WordViewerComponent] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NgxWordViewerModule, decorators: [{ type: NgModule, args: [{ imports: [WordViewerComponent], exports: [WordViewerComponent] }] }] }); /* * Public API Surface of ngx-word-viewer */ /** * Generated bundle index. Do not edit. */ export { NgxWordViewerModule, WordViewerComponent }; //# sourceMappingURL=ngx-word-viewer.mjs.map