const MODULE_SELECTOR = '[data-pdf-flipbook-element="1"]'; const PDF_MODULE_URL = '/bundles/flipbook/assets/vendor/pdf.min.mjs'; const PDF_WORKER_URL = '/bundles/flipbook/assets/vendor/pdf.worker.min.mjs'; const FLIPBOOK_MODULE_URL = '/bundles/flipbook/assets/vendor/flipbook.esm.min.js'; const INIT_MARKER = 'pdfFlipbookInitialized'; const BOOTSTRAP_MARKER = '__mummertPdfFlipbookBootstrapBound'; let dependenciesPromise; let moduleCounter = 0; const loadDependencies = async () => { if (dependenciesPromise) { return dependenciesPromise; } dependenciesPromise = Promise.all([ import(PDF_MODULE_URL), import(FLIPBOOK_MODULE_URL), ]).then(([pdfjsLib, flipbookModule]) => { pdfjsLib.GlobalWorkerOptions.workerSrc = PDF_WORKER_URL; return { pdfjsLib, FlipBook: flipbookModule.default, }; }); return dependenciesPromise; }; class PdfFlipbookModule { constructor(root, dependencies) { const parsedInitialPages = Number.parseInt(root.dataset.initialPages || '4', 10); this.root = root; this.dependencies = dependencies; this.pdfUrl = (root.dataset.pdfUrl || '').trim(); this.initialPages = Number.isFinite(parsedInitialPages) ? Math.min(4, Math.max(2, parsedInitialPages)) : 4; this.showNavigation = root.dataset.showNavigation === '1'; this.startMode = (root.dataset.startMode === 'spread' || root.dataset.startMode === 'cover') ? 'spread' : 'center'; this.loader = root.querySelector('[data-flipbook-loader="1"]'); this.stage = root.querySelector('[data-flipbook-stage="1"]'); this.bookElement = root.querySelector('[data-flipbook-book="1"]'); this.nextButton = this.showNavigation ? root.querySelector('[data-flipbook-next="1"]') : null; this.prevButton = this.showNavigation ? root.querySelector('[data-flipbook-prev="1"]') : null; this.pdf = null; this.flipbook = null; this.pageElements = new Map(); this.renderedPages = new Set(); this.pendingPages = new Set(); this.renderQueue = []; this.renderInProgress = false; this.resizeTimer = null; this.touchStart = null; this.aspectRatio = 1.4142; this.pageWidth = 0; this.pageHeight = 0; this.pageGap = 2; this.totalPages = 0; this.instanceId = `pdf-flipbook-${++moduleCounter}`; } async init() { if (!this.bookElement || !this.stage) { return; } if (!this.pdfUrl) { this.setStatus('Keine PDF-Datei gefunden.', true); return; } this.bookElement.id = this.instanceId; try { this.setStatus('PDF wird geladen ...'); await this.loadPdf(); await this.resolveInitialAspectRatio(); this.buildPageSkeleton(); this.updateLayout(); await this.renderInitialPages(); this.initializeFlipbook(); this.bindKeyboard(); this.bindTouchSwipe(); this.bindResize(); this.queuePages(this.getLazyCandidates()); this.setStatus(''); } catch { this.setStatus('PDF konnte nicht geladen werden.', true); } } async loadPdf() { const loadingTask = this.dependencies.pdfjsLib.getDocument({ url: this.pdfUrl, useWorkerFetch: true, isEvalSupported: false, }); this.pdf = await loadingTask.promise; this.totalPages = Number(this.pdf.numPages || 0); if (this.totalPages <= 0) { throw new Error('The selected PDF has no pages.'); } } async resolveInitialAspectRatio() { const firstPage = await this.pdf.getPage(1); const viewport = firstPage.getViewport({ scale: 1 }); if (viewport.width > 0 && viewport.height > 0) { this.aspectRatio = viewport.height / viewport.width; } } buildPageSkeleton() { const visualPageCount = this.totalPages % 2 === 0 ? this.totalPages : this.totalPages + 1; this.bookElement.innerHTML = ''; this.pageElements.clear(); for (let pageNumber = 1; pageNumber <= visualPageCount; pageNumber += 1) { const page = document.createElement('div'); const loader = document.createElement('div'); page.className = 'c-flipbook__page'; page.dataset.pageNumber = String(pageNumber); loader.className = 'mod-pdf-flipbook__page-loader'; loader.textContent = pageNumber <= this.totalPages ? `Seite ${pageNumber} wird gerendert ...` : 'Leere Seite'; page.appendChild(loader); if (pageNumber > this.totalPages) { page.dataset.empty = '1'; } this.bookElement.appendChild(page); if (pageNumber <= this.totalPages) { this.pageElements.set(pageNumber, page); } } } updateLayout() { const stageWidth = Math.max(this.stage.clientWidth || 0, 282); this.pageWidth = Math.max(Math.floor((stageWidth - this.pageGap) / 2), 140); this.pageHeight = Math.max(Math.floor(this.pageWidth * this.aspectRatio), 180); this.stage.style.height = `${this.pageHeight}px`; this.bookElement.style.height = `${this.pageHeight}px`; this.pageElements.forEach((pageElement) => { pageElement.style.width = `${this.pageWidth}px`; pageElement.style.height = `${this.pageHeight}px`; }); } async renderInitialPages() { const limit = Math.min(this.initialPages, this.totalPages); for (let pageNumber = 1; pageNumber <= limit; pageNumber += 1) { await this.renderPage(pageNumber); } } initializeFlipbook() { const startsWithDoubleSpread = this.startMode === 'spread'; this.flipbook = new this.dependencies.FlipBook(this.bookElement, { nextButton: this.nextButton, previousButton: this.prevButton, canClose: !startsWithDoubleSpread, arrowKeys: false, initialActivePage: 0, initialCall: false, width: '100%', height: `${this.pageHeight}px`, onPageTurn: () => { this.queuePages(this.getLazyCandidates()); }, }); } bindKeyboard() { this.root.addEventListener('pointerdown', () => { this.root.focus(); }); this.root.addEventListener('keydown', (event) => { if (!this.flipbook) { return; } if (event.key === 'ArrowRight') { this.flipbook.turnPage('forward'); event.preventDefault(); } if (event.key === 'ArrowLeft') { this.flipbook.turnPage('back'); event.preventDefault(); } }); } bindTouchSwipe() { this.bookElement.addEventListener('touchstart', (event) => { const touch = event.changedTouches && event.changedTouches[0]; if (!touch) { return; } this.touchStart = { x: touch.clientX, y: touch.clientY, }; }, { passive: true }); this.bookElement.addEventListener('touchend', (event) => { if (!this.flipbook || !this.touchStart) { this.touchStart = null; return; } const touch = event.changedTouches && event.changedTouches[0]; if (!touch) { this.touchStart = null; return; } const deltaX = touch.clientX - this.touchStart.x; const deltaY = touch.clientY - this.touchStart.y; const horizontalThreshold = 40; const verticalLimit = 60; if (Math.abs(deltaX) >= horizontalThreshold && Math.abs(deltaY) < verticalLimit) { if (deltaX < 0) { this.flipbook.turnPage('forward'); } else { this.flipbook.turnPage('back'); } } this.touchStart = null; }, { passive: true }); } bindResize() { const onResize = () => { window.clearTimeout(this.resizeTimer); this.resizeTimer = window.setTimeout(() => { if (!this.pdf) { return; } const previousHeight = this.pageHeight; this.updateLayout(); if (Math.abs(previousHeight - this.pageHeight) < 2) { return; } this.bookElement.style.height = `${this.pageHeight}px`; const rerender = Array.from(this.renderedPages); this.renderedPages.clear(); rerender.forEach((pageNumber) => { const page = this.pageElements.get(pageNumber); if (page) { const oldCanvas = page.querySelector('canvas'); if (oldCanvas) { oldCanvas.remove(); } } }); this.queuePages(rerender, true); }, 120); }; if ('ResizeObserver' in window) { const observer = new ResizeObserver(onResize); observer.observe(this.stage); } else { window.addEventListener('resize', onResize, { passive: true }); } } getLazyCandidates() { const activePages = Array.from(this.bookElement.querySelectorAll('.c-flipbook__page.is-active')); const candidates = new Set(); if (activePages.length === 0) { const fallbackFrom = Math.min(this.initialPages + 1, this.totalPages); const fallbackTo = Math.min(fallbackFrom + 2, this.totalPages); for (let pageNumber = fallbackFrom; pageNumber <= fallbackTo; pageNumber += 1) { candidates.add(pageNumber); } return Array.from(candidates); } activePages.forEach((activePage) => { const currentPage = Number(activePage.dataset.pageNumber || 0); for (let offset = -1; offset <= 3; offset += 1) { const pageNumber = currentPage + offset; if (pageNumber >= 1 && pageNumber <= this.totalPages) { candidates.add(pageNumber); } } }); return Array.from(candidates); } queuePages(pageNumbers, prioritize = false) { for (const pageNumber of pageNumbers) { if (!Number.isInteger(pageNumber) || pageNumber < 1 || pageNumber > this.totalPages) { continue; } if (this.renderedPages.has(pageNumber) || this.pendingPages.has(pageNumber)) { continue; } this.pendingPages.add(pageNumber); if (prioritize) { this.renderQueue.unshift(pageNumber); } else { this.renderQueue.push(pageNumber); } } this.processRenderQueue(); } async processRenderQueue() { if (this.renderInProgress) { return; } this.renderInProgress = true; while (this.renderQueue.length > 0) { const pageNumber = this.renderQueue.shift(); if (!pageNumber) { continue; } this.pendingPages.delete(pageNumber); try { await this.renderPage(pageNumber); } catch { continue; } } this.renderInProgress = false; } async renderPage(pageNumber) { if (this.renderedPages.has(pageNumber)) { return; } const pageElement = this.pageElements.get(pageNumber); if (!pageElement) { return; } const page = await this.pdf.getPage(pageNumber); const viewportAtScale1 = page.getViewport({ scale: 1 }); const scale = this.pageWidth / viewportAtScale1.width; const viewport = page.getViewport({ scale }); const devicePixelRatio = window.devicePixelRatio || 1; const outputScale = Math.max(1, devicePixelRatio); const existingCanvas = pageElement.querySelector('canvas'); if (existingCanvas) { existingCanvas.remove(); } const canvas = document.createElement('canvas'); const context = canvas.getContext('2d', { alpha: false }); if (!context) { throw new Error('Could not create canvas 2D context.'); } canvas.width = Math.floor(viewport.width * outputScale); canvas.height = Math.floor(viewport.height * outputScale); canvas.style.width = `${Math.floor(viewport.width)}px`; canvas.style.height = `${Math.floor(viewport.height)}px`; const renderTask = page.render({ canvasContext: context, viewport, transform: outputScale === 1 ? null : [outputScale, 0, 0, outputScale, 0, 0], intent: 'display', }); await renderTask.promise; const loader = pageElement.querySelector('.mod-pdf-flipbook__page-loader'); if (loader) { loader.remove(); } pageElement.appendChild(canvas); this.renderedPages.add(pageNumber); } setStatus(message, isError = false) { if (!this.loader) { return; } if (!message) { this.loader.classList.add('is-hidden'); this.loader.classList.remove('is-error'); this.loader.textContent = ''; return; } this.loader.classList.remove('is-hidden'); this.loader.classList.toggle('is-error', isError); this.loader.textContent = message; } } const run = async () => { const modules = Array.from(document.querySelectorAll(MODULE_SELECTOR)) .filter((moduleElement) => moduleElement.dataset[INIT_MARKER] !== '1'); if (modules.length === 0) { return; } const dependencies = await loadDependencies(); await Promise.all(modules.map(async (moduleElement) => { moduleElement.dataset[INIT_MARKER] = '1'; const module = new PdfFlipbookModule(moduleElement, dependencies); await module.init(); })); }; if (window[BOOTSTRAP_MARKER]) { run().catch(() => { }); } else { window[BOOTSTRAP_MARKER] = true; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { run().catch(() => { }); }, { once: true }); } else { run().catch(() => { }); } }