Files
flipbook-bundle/public/assets/flipbook-module.js
T

491 lines
15 KiB
JavaScript

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(() => {
});
}
}