ngx-word-viewer
Version:
Angular component for viewing Word documents (.docx) with page navigation, zoom.
649 lines (611 loc) • 36 kB
JavaScript
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>
{ 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