import { DeesElement, property, html, customElement, type TemplateResult, css, domtools, } from "@design.estate/dees-element"; import * as plugins from "../plugins.js"; export const defaultDocumentSettings: plugins.shared.interfaces.IDocumentSettings = { enableTopDraftText: true, enableDefaultHeader: true, enableDefaultFooter: true, enableFoldMarks: true, enableInvoiceContractRefSection: true, languageCode: "EN", vatGroupPositions: true, dateStyle: "short", }; import { DePage } from "./page.js"; import { DeContentInvoice } from "./contentinvoice.js"; import { demoFunc } from "./document.demo.js"; export interface IPagePaginationInfo { pageNumber: number; startOffset: number; contentLength: number; } declare global { interface HTMLElementTagNameMap { "dedocument-dedocument": DeDocument; } } @customElement("dedocument-dedocument") export class DeDocument extends DeesElement { public static demo = demoFunc; @property({ type: String, reflect: true, }) accessor format: "a4" = "a4"; @property({ type: Number, reflect: true, }) accessor viewWidth: number = null; @property({ type: Number, reflect: true, }) accessor viewHeight: number = null; @property({ type: Boolean, reflect: true, }) accessor printMode = false; @property({ type: Object, reflect: true, converter: (valueArg) => { if (typeof valueArg === "string") { return plugins.smartjson.parseBase64(valueArg); } else { return valueArg; } }, }) accessor letterData: plugins.tsclass.business.TLetter; @property({ type: Object, reflect: true, converter: (valueArg) => { if (typeof valueArg === "string") { return plugins.smartjson.parseBase64(valueArg); } else { return valueArg; } }, }) accessor documentSettings: plugins.shared.interfaces.IDocumentSettings = defaultDocumentSettings; @property({ type: Number }) accessor zoomLevel: number = null; // null = auto-fit, otherwise percentage (e.g., 100 = 100%) @property({ type: Number }) accessor pageGap: number = 16; // pixels between pages constructor() { super(); domtools.DomTools.setupDomTools(); } public static styles = [ domtools.elementBasic.staticStyles, css` :host { display: block; } .betweenPagesSpacer { height: 16px; } `, ]; public render(): TemplateResult { return html`
`; } public async firstUpdated( _changedProperties: Map ) { domtools.plugins.smartdelay.delayFor(0).then(async () => { this.documentSettings = { ...defaultDocumentSettings, ...this.documentSettings, }; }); const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const width = entry.contentRect.width; const height = entry.contentRect.height; // Handle the new dimensions here this.adjustDePageScaling(); } }); resizeObserver.observe(this); this.registerGarbageFunction(() => { resizeObserver.disconnect(); }); } public latestDocumentSettings: plugins.shared.interfaces.IDocumentSettings = null; public latestRenderedLetterData: plugins.tsclass.business.TLetter = null; public cleanupStore: any[] = []; public paginationInfo: IPagePaginationInfo[] = []; public async renderDocument() { this.latestDocumentSettings = this.documentSettings; this.latestRenderedLetterData = this.letterData; this.paginationInfo = []; const cleanUpStoreCurrentRender = []; const cleanUpStoreNextRender = []; const domtools = await this.domtoolsPromise; const documentBuildContainer = document.createElement("div"); cleanUpStoreCurrentRender.push(documentBuildContainer); document.body.appendChild(documentBuildContainer); let pages: DePage[] = []; let pageCounter = 0; let complete = false; // lets append the content const content: DeContentInvoice = new DeContentInvoice(); cleanUpStoreCurrentRender.push(content); content.letterData = this .letterData as unknown as plugins.tsclass.finance.TInvoice; content.documentSettings = this.documentSettings; document.body.appendChild(content); await domtools.convenience.smartdelay.delayFor(0); let overallContentOffset: number = 0; let currentContentOffset: number; let trimmed: number; while (!complete) { pageCounter++; const currentContent = content.cloneNode(true) as DeContentInvoice; cleanUpStoreNextRender.push(currentContent); const newPage = new DePage(); newPage.printMode = this.printMode; newPage.letterData = this.letterData; newPage.documentSettings = this.documentSettings; pages.push(newPage); newPage.pageNumber = pageCounter; newPage.append(currentContent); newPage.pageTotalNumber = pageCounter; // store current page cleanUpStoreNextRender.push(newPage); documentBuildContainer.append(newPage); await currentContent.elementDomReady; await currentContent.trimStartToOffset(overallContentOffset); let newPageOverflows = await newPage.checkOverflow(); trimmed = 0; while (newPageOverflows) { await currentContent.trimEndByOne(); trimmed++; newPageOverflows = await newPage.checkOverflow(); } currentContentOffset = await currentContent.getContentLength(); const pageStartOffset = overallContentOffset; overallContentOffset = overallContentOffset + currentContentOffset; // Track pagination info for this page this.paginationInfo.push({ pageNumber: pageCounter, startOffset: pageStartOffset, contentLength: currentContentOffset, }); if (trimmed === 0) { complete = true; } } for (const cleanUp of this.cleanupStore) { cleanUp.remove(); } this.cleanupStore = cleanUpStoreNextRender; cleanUpStoreCurrentRender.forEach((cleanUp) => { cleanUp.remove(); }); const documentContainer = this.shadowRoot.querySelector(".documentContainer"); if (documentContainer) { const children = Array.from(documentContainer.children); children.forEach((child) => { documentContainer.removeChild(child); child.remove(); }); } for (const page of pages) { page.pageTotalNumber = pageCounter; documentContainer.append(page); // betweenPagesSpacer if (!this.printMode) { const betweenPagesSpacerDiv = document.createElement("div"); betweenPagesSpacerDiv.classList.add("betweenPagesSpacer"); betweenPagesSpacerDiv.style.height = `${this.pageGap}px`; documentContainer.appendChild(betweenPagesSpacerDiv); } } this.adjustDePageScaling(); // Emit event with pagination info for thumbnails this.dispatchEvent(new CustomEvent('pagination-complete', { detail: { pageCount: pageCounter, paginationInfo: this.paginationInfo, }, bubbles: true, composed: true, })); } async updated( changedProperties: Map ): Promise { super.updated(changedProperties); const domtools = await this.domtoolsPromise; let renderedDocIsUpToDate = domtools.convenience.smartjson.deepEqualObjects( this.letterData, this.latestRenderedLetterData ) && domtools.convenience.smartjson.deepEqualObjects( this.documentSettings, this.latestDocumentSettings ); if (!renderedDocIsUpToDate) { this.renderDocument(); } if ( changedProperties.has("viewHeight") || changedProperties.has("viewWidth") || changedProperties.has("zoomLevel") ) { this.adjustDePageScaling(); } if (changedProperties.has("pageGap")) { this.updatePageGaps(); } } /** * Update page gap spacing without re-rendering document */ private updatePageGaps(): void { const spacers = this.shadowRoot.querySelectorAll(".betweenPagesSpacer"); spacers.forEach((spacer: HTMLElement) => { spacer.style.height = `${this.pageGap}px`; }); } private adjustDePageScaling() { if (this.printMode) { return; } this.viewWidth = this.clientWidth; // Find all DePage instances within this DeDocument const pages = this.shadowRoot.querySelectorAll("dedocument-page"); // Update each DePage instance's viewHeight, viewWidth, and zoomLevel pages.forEach((page: DePage) => { // Pass manual zoom level if set page.manualZoomLevel = this.zoomLevel; if (this.viewHeight) { page.viewHeight = this.viewHeight; } if (this.viewWidth) { page.viewWidth = this.viewWidth; } }); } /** * Set zoom level manually. Pass null to return to auto-fit mode. * @param level - Zoom percentage (e.g., 100 for 100%) or null for auto-fit */ public setZoomLevel(level: number | null): void { this.zoomLevel = level; this.adjustDePageScaling(); } /** * Get the current effective zoom percentage */ public getEffectiveZoom(): number { if (this.zoomLevel !== null) { return this.zoomLevel; } // Calculate auto-fit zoom percentage const scale = this.viewWidth / plugins.shared.A4_WIDTH; return Math.round(scale * 100); } }