feat: support PDF spread splitting and nav button states

This commit is contained in:
Jürgen Mummert
2026-04-14 15:04:57 +02:00
parent be27ce1340
commit a6c9aef952
6 changed files with 179 additions and 19 deletions
+166 -18
View File
@@ -41,6 +41,7 @@ class PdfFlipbookModule {
this.initialPages = Number.isFinite(parsedInitialPages)
? Math.min(4, Math.max(2, parsedInitialPages))
: 4;
this.splitSpreads = root.dataset.splitSpreads === '1';
this.showNavigation = root.dataset.showNavigation === '1';
this.playTurnSoundEnabled = root.dataset.playTurnSound !== '0';
this.startMode = (root.dataset.startMode === 'spread' || root.dataset.startMode === 'cover')
@@ -67,7 +68,9 @@ class PdfFlipbookModule {
this.pageWidth = 0;
this.pageHeight = 0;
this.pageGap = 2;
this.sourcePageCount = 0;
this.totalPages = 0;
this.pageDescriptors = [];
this.instanceId = `pdf-flipbook-${++moduleCounter}`;
}
@@ -136,6 +139,8 @@ class PdfFlipbookModule {
if (this.controlsElement) {
this.controlsElement.style.opacity = '1';
}
this.updateNavigationState();
});
}
@@ -147,19 +152,84 @@ class PdfFlipbookModule {
});
this.pdf = await loadingTask.promise;
this.totalPages = Number(this.pdf.numPages || 0);
this.sourcePageCount = Number(this.pdf.numPages || 0);
if (this.totalPages <= 0) {
if (this.sourcePageCount <= 0) {
throw new Error('The selected PDF has no pages.');
}
await this.buildPageDescriptors();
this.totalPages = this.pageDescriptors.length;
}
async buildPageDescriptors() {
const descriptors = [];
for (let sourcePageNumber = 1; sourcePageNumber <= this.sourcePageCount; sourcePageNumber += 1) {
const page = await this.pdf.getPage(sourcePageNumber);
const viewport = page.getViewport({ scale: 1 });
if (this.shouldSplitSpread(sourcePageNumber, viewport)) {
descriptors.push({
sourcePageNumber,
segment: 'left',
});
descriptors.push({
sourcePageNumber,
segment: 'right',
});
} else {
descriptors.push({
sourcePageNumber,
segment: 'full',
});
}
}
this.pageDescriptors = descriptors;
}
shouldSplitSpread(sourcePageNumber, viewport) {
if (!this.splitSpreads) {
return false;
}
if (sourcePageNumber <= 1) {
return false;
}
const width = Number(viewport.width || 0);
const height = Number(viewport.height || 0);
if (width <= 0 || height <= 0) {
return false;
}
return width >= (height * 1.2);
}
getDescriptor(pageNumber) {
if (!Number.isInteger(pageNumber) || pageNumber < 1) {
return null;
}
return this.pageDescriptors[pageNumber - 1] || null;
}
async resolveInitialAspectRatio() {
const firstPage = await this.pdf.getPage(1);
const viewport = firstPage.getViewport({ scale: 1 });
const firstDescriptor = this.getDescriptor(1);
if (viewport.width > 0 && viewport.height > 0) {
this.aspectRatio = viewport.height / viewport.width;
if (!firstDescriptor) {
return;
}
const firstPage = await this.pdf.getPage(firstDescriptor.sourcePageNumber);
const viewport = firstPage.getViewport({ scale: 1 });
const divisor = firstDescriptor.segment === 'full' ? 1 : 2;
const width = viewport.width / divisor;
if (width > 0 && viewport.height > 0) {
this.aspectRatio = viewport.height / width;
}
}
@@ -231,8 +301,42 @@ class PdfFlipbookModule {
onPageTurn: () => {
this.playTurnSound();
this.queuePages(this.getLazyCandidates());
this.updateNavigationState();
},
});
window.requestAnimationFrame(() => {
this.updateNavigationState();
});
}
updateNavigationState() {
if (!this.showNavigation || !this.prevButton || !this.nextButton) {
return;
}
const activePages = Array.from(this.bookElement.querySelectorAll('.c-flipbook__page.is-active'))
.map((element) => Number(element.dataset.pageNumber || 0))
.filter((pageNumber) => Number.isInteger(pageNumber) && pageNumber > 0);
let disablePrev = false;
let disableNext = false;
if (activePages.length === 0) {
disablePrev = true;
disableNext = this.totalPages <= 1;
} else {
const minActivePage = Math.min(...activePages);
const maxActivePage = Math.max(...activePages);
disablePrev = minActivePage <= 1;
disableNext = maxActivePage >= this.totalPages;
}
this.prevButton.disabled = disablePrev;
this.nextButton.disabled = disableNext;
this.prevButton.setAttribute('aria-disabled', disablePrev ? 'true' : 'false');
this.nextButton.setAttribute('aria-disabled', disableNext ? 'true' : 'false');
}
createTurnSound() {
@@ -449,14 +553,18 @@ class PdfFlipbookModule {
}
const pageElement = this.pageElements.get(pageNumber);
const descriptor = this.getDescriptor(pageNumber);
if (!pageElement) {
if (!pageElement || !descriptor) {
return;
}
const page = await this.pdf.getPage(pageNumber);
const page = await this.pdf.getPage(descriptor.sourcePageNumber);
const viewportAtScale1 = page.getViewport({ scale: 1 });
const scale = this.pageWidth / viewportAtScale1.width;
const renderWidth = descriptor.segment === 'full'
? viewportAtScale1.width
: viewportAtScale1.width / 2;
const scale = this.pageWidth / renderWidth;
const viewport = page.getViewport({ scale });
const devicePixelRatio = window.devicePixelRatio || 1;
const outputScale = Math.max(1, devicePixelRatio);
@@ -473,19 +581,59 @@ class PdfFlipbookModule {
throw new Error('Could not create canvas 2D context.');
}
canvas.width = Math.floor(viewport.width * outputScale);
const canvasCssWidth = descriptor.segment === 'full'
? Math.floor(viewport.width)
: Math.floor(viewport.width / 2);
canvas.width = Math.floor(canvasCssWidth * outputScale);
canvas.height = Math.floor(viewport.height * outputScale);
canvas.style.width = `${Math.floor(viewport.width)}px`;
canvas.style.width = `${canvasCssWidth}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',
});
if (descriptor.segment === 'full') {
const renderTask = page.render({
canvasContext: context,
viewport,
transform: outputScale === 1 ? null : [outputScale, 0, 0, outputScale, 0, 0],
intent: 'display',
});
await renderTask.promise;
await renderTask.promise;
} else {
const spreadCanvas = document.createElement('canvas');
const spreadContext = spreadCanvas.getContext('2d', { alpha: false });
if (!spreadContext) {
throw new Error('Could not create split canvas context.');
}
spreadCanvas.width = Math.floor(viewport.width * outputScale);
spreadCanvas.height = Math.floor(viewport.height * outputScale);
const spreadRenderTask = page.render({
canvasContext: spreadContext,
viewport,
transform: outputScale === 1 ? null : [outputScale, 0, 0, outputScale, 0, 0],
intent: 'display',
});
await spreadRenderTask.promise;
const halfWidth = Math.floor(spreadCanvas.width / 2);
const sourceX = descriptor.segment === 'left' ? 0 : spreadCanvas.width - halfWidth;
context.drawImage(
spreadCanvas,
sourceX,
0,
halfWidth,
spreadCanvas.height,
0,
0,
canvas.width,
canvas.height,
);
}
const loader = pageElement.querySelector('.mod-pdf-flipbook__page-loader');