feat: refine flipbook start modes and UI behavior

This commit is contained in:
Juergen
2026-04-13 21:34:29 +02:00
commit 9f9b1c9935
23 changed files with 1007 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
.DS_Store
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Mummert
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+43
View File
@@ -0,0 +1,43 @@
# Flipbook Bundle (Contao 5.7)
Contao Inhaltselement zum Darstellen von PDFs als blaetterbares Flipbook.
## Features
- Reines JavaScript (ES Modules)
- `pdfjs-dist` fuer PDF-Rendering in `<canvas>`
- `flipbook-js` fuer den Blaettereffekt
- Lazy Rendering: 2-4 Seiten sofort, Rest on-demand
- Optional Vor-/Zurueck-Navigation im Modul
- Tastatursteuerung (Pfeiltasten) und Touch-Swipe
- Responsive Breite mit beibehaltenem Seitenverhaeltnis
## Installation (VCS oder Paket)
```bash
composer require mummert/flipbook-bundle
```
## Inhaltselement im Backend
Elementtyp: `Blätterbares PDF`
Felder:
- `PDF-Datei` (singleSRC / UUID)
- `Initial geladene Seiten` (2, 3 oder 4)
- `Startmodus` (zentriert oder als Doppelseite)
- `Navigation anzeigen` (optional)
## Hinweise
- Das Inhaltselement rendert PDF-Seiten als `<canvas>` innerhalb von `.c-flipbook__page`.
- Die benoetigten Open-Source-Dateien sind lokal im Bundle enthalten.
- Siehe `THIRD_PARTY_LICENSES.md` fuer Abhaengigkeiten.
## Release
```bash
git tag v1.0.0
git push origin v1.0.0
```
+17
View File
@@ -0,0 +1,17 @@
# Third-Party Licenses
This bundle contains the following third-party open source libraries in `public/assets/vendor/`:
## pdfjs-dist
- Project: https://github.com/mozilla/pdf.js
- Package: `pdfjs-dist`
- Version: `4.9.155`
- License: Apache-2.0
## flipbook-js
- Project: https://github.com/taufiqelrahman/flipbook-js
- Package: `flipbook-js`
- Version: `1.1.1`
- License: MIT
+32
View File
@@ -0,0 +1,32 @@
{
"name": "mummert/flipbook-bundle",
"description": "PDF flipbook content element for Contao 5.7 using pdfjs-dist and flipbook-js.",
"type": "contao-bundle",
"license": "MIT",
"keywords": [
"contao",
"contao-bundle",
"pdf",
"flipbook"
],
"require": {
"php": "^8.3",
"contao/core-bundle": "^5.7",
"contao/manager-plugin": "^2.0"
},
"autoload": {
"psr-4": {
"Mummert\\FlipbookBundle\\": "src/"
}
},
"extra": {
"contao-manager-plugin": "Mummert\\FlipbookBundle\\Contao\\Manager\\Plugin"
},
"prefer-stable": true,
"config": {
"allow-plugins": {
"contao-components/installer": true,
"contao/manager-plugin": true
}
}
}
+11
View File
@@ -0,0 +1,11 @@
services:
_defaults:
autowire: true
autoconfigure: true
Mummert\FlipbookBundle\:
resource: ../src/
exclude:
- ../src/DependencyInjection/
- ../src/Contao/Manager/
- ../src/FlipbookBundle.php
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
$GLOBALS['TL_DCA']['tl_content']['palettes']['blatterbares_pdf'] = '{type_legend},type,headline;{flipbook_legend},flipbookPdfSrc,flipbookInitialPages,flipbookStartMode,flipbookShowNavigation;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},cssID;{invisible_legend:hide},invisible,start,stop';
$GLOBALS['TL_DCA']['tl_content']['fields']['flipbookPdfSrc'] = [
'label' => &$GLOBALS['TL_LANG']['tl_content']['flipbookPdfSrc'],
'exclude' => true,
'inputType' => 'fileTree',
'eval' => ['fieldType' => 'radio', 'files' => true, 'filesOnly' => true, 'extensions' => 'pdf', 'mandatory' => true, 'tl_class' => 'w50'],
'sql' => ['type' => 'binary', 'length' => 16, 'notnull' => false],
];
$GLOBALS['TL_DCA']['tl_content']['fields']['flipbookInitialPages'] = [
'label' => &$GLOBALS['TL_LANG']['tl_content']['flipbookInitialPages'],
'exclude' => true,
'inputType' => 'select',
'options' => ['2', '3', '4'],
'reference' => &$GLOBALS['TL_LANG']['tl_content']['flipbookInitialPagesOptions'],
'eval' => ['mandatory' => true, 'includeBlankOption' => false, 'tl_class' => 'w50'],
'sql' => ['type' => 'string', 'length' => 1, 'default' => '4'],
];
$GLOBALS['TL_DCA']['tl_content']['fields']['flipbookStartMode'] = [
'label' => &$GLOBALS['TL_LANG']['tl_content']['flipbookStartMode'],
'exclude' => true,
'inputType' => 'select',
'options' => ['center', 'spread'],
'reference' => &$GLOBALS['TL_LANG']['tl_content']['flipbookStartModeOptions'],
'eval' => ['mandatory' => true, 'includeBlankOption' => false, 'tl_class' => 'w50'],
'sql' => ['type' => 'string', 'length' => 16, 'default' => 'center'],
];
$GLOBALS['TL_DCA']['tl_content']['fields']['flipbookShowNavigation'] = [
'label' => &$GLOBALS['TL_LANG']['tl_content']['flipbookShowNavigation'],
'exclude' => true,
'inputType' => 'checkbox',
'eval' => ['tl_class' => 'w50 m12'],
'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => '1'],
];
+5
View File
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
$GLOBALS['TL_LANG']['CTE']['blatterbares_pdf'] = ['Blätterbares PDF', 'Zeigt ein PDF als blätterbares Flipbook mit Lazy-Rendering an.'];
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
$GLOBALS['TL_LANG']['tl_content']['flipbook_legend'] = 'Blätterbares PDF';
$GLOBALS['TL_LANG']['tl_content']['flipbookPdfSrc'] = ['PDF-Datei', 'Bitte waehlen Sie die PDF-Datei aus, die als Flipbook angezeigt werden soll.'];
$GLOBALS['TL_LANG']['tl_content']['flipbookInitialPages'] = ['Initial geladene Seiten', 'Anzahl der Seiten, die beim Laden direkt gerendert werden (Rest lazy).'];
$GLOBALS['TL_LANG']['tl_content']['flipbookInitialPagesOptions'] = [
'2' => '2 Seiten',
'3' => '3 Seiten',
'4' => '4 Seiten',
];
$GLOBALS['TL_LANG']['tl_content']['flipbookStartMode'] = ['Startmodus', 'Legt fest, ob das Flipbook zentriert oder mit Doppelseite startet.'];
$GLOBALS['TL_LANG']['tl_content']['flipbookStartModeOptions'] = [
'center' => 'Zentriert starten',
'spread' => 'als Doppelseite starten',
];
$GLOBALS['TL_LANG']['tl_content']['flipbookShowNavigation'] = ['Navigation anzeigen', 'Zeigt Vor-/Zurueck-Buttons unter dem Flipbook an.'];
+5
View File
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
$GLOBALS['TL_LANG']['CTE']['blatterbares_pdf'] = ['Flippable PDF', 'Displays a PDF as a page-turning flipbook with lazy rendering.'];
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
$GLOBALS['TL_LANG']['tl_content']['flipbook_legend'] = 'Flippable PDF';
$GLOBALS['TL_LANG']['tl_content']['flipbookPdfSrc'] = ['PDF file', 'Please select the PDF file to display as a flipbook.'];
$GLOBALS['TL_LANG']['tl_content']['flipbookInitialPages'] = ['Initially rendered pages', 'Number of pages rendered immediately on load (remaining pages are lazy-rendered).'];
$GLOBALS['TL_LANG']['tl_content']['flipbookInitialPagesOptions'] = [
'2' => '2 pages',
'3' => '3 pages',
'4' => '4 pages',
];
$GLOBALS['TL_LANG']['tl_content']['flipbookStartMode'] = ['Start mode', 'Defines whether the flipbook starts centered or as a double-page spread.'];
$GLOBALS['TL_LANG']['tl_content']['flipbookStartModeOptions'] = [
'center' => 'Start centered',
'spread' => 'Start with double-page spread',
];
$GLOBALS['TL_LANG']['tl_content']['flipbookShowNavigation'] = ['Show navigation', 'Displays previous/next buttons below the flipbook.'];
View File
@@ -0,0 +1,32 @@
{% set hasPdf = pdfUrl|default('') is not empty %}
<div
class="ce-blatterbares-pdf mod-pdf-flipbook"
data-pdf-flipbook-element="1"
data-pdf-url="{{ pdfUrl|default('')|e('html_attr') }}"
data-initial-pages="{{ initialRenderPages|default(4)|e('html_attr') }}"
data-start-mode="{{ startMode|default('center')|e('html_attr') }}"
data-show-navigation="{{ showNavigation ? '1' : '0' }}"
tabindex="0"
>
<div class="mod-pdf-flipbook__status" data-flipbook-loader="1" aria-live="polite">PDF wird geladen ...</div>
<div class="mod-pdf-flipbook__stage" data-flipbook-stage="1">
<div id="flipbook-{{ data.id|default(random()) }}" class="c-flipbook" data-flipbook-book="1"></div>
</div>
{% if showNavigation %}
<div class="mod-pdf-flipbook__controls" aria-label="Flipbook Navigation">
<button type="button" data-flipbook-prev="1">Zurück</button>
<button type="button" data-flipbook-next="1">Weiter</button>
</div>
{% endif %}
{% if not hasPdf %}
<p class="mod-pdf-flipbook__error">Keine PDF-Datei konfiguriert.</p>
{% endif %}
</div>
<link rel="stylesheet" href="/bundles/flipbook/assets/vendor/flipbook.min.css?v=20260414d">
<link rel="stylesheet" href="/bundles/flipbook/assets/flipbook-module.css?v=20260414d">
<script type="module" src="/bundles/flipbook/assets/flipbook-module.js?v=20260414d"></script>
+84
View File
@@ -0,0 +1,84 @@
.mod-pdf-flipbook {
--flipbook-bg: #e0e0e0;
--flipbook-text: #302d29;
--flipbook-accent: #6f4f28;
position: relative;
padding: 3rem;
background: var(--flipbook-bg);
}
.mod-pdf-flipbook:focus-visible {
outline: 2px solid var(--flipbook-accent);
outline-offset: 3px;
}
.mod-pdf-flipbook__status {
margin-bottom: 0.8rem;
font-size: 0.92rem;
color: var(--flipbook-text);
}
.mod-pdf-flipbook__status.is-hidden {
display: none;
}
.mod-pdf-flipbook__status.is-error {
color: #9f1f1f;
}
.mod-pdf-flipbook__stage {
position: relative;
width: 100%;
margin: 0 auto;
background: #e0e0e0;
}
.mod-pdf-flipbook .c-flipbook {
max-width: 100%;
}
.mod-pdf-flipbook .c-flipbook__page {
background: #fefdf9;
overflow: hidden;
border: 1px solid #e2dbcc;
}
.mod-pdf-flipbook .c-flipbook__page canvas {
display: block;
width: 100%;
height: 100%;
}
.mod-pdf-flipbook__page-loader {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
color: #6f675a;
background: repeating-linear-gradient(
-45deg,
rgba(184, 176, 160, 0.18),
rgba(184, 176, 160, 0.18) 14px,
rgba(239, 235, 227, 0.28) 14px,
rgba(239, 235, 227, 0.28) 28px
);
}
.mod-pdf-flipbook__controls {
display: flex;
gap: 0.6rem;
margin-top: 0.9rem;
}
.mod-pdf-flipbook__error {
margin-top: 0.8rem;
color: #9f1f1f;
}
@media (max-width: 767px) {
.mod-pdf-flipbook__controls {
justify-content: space-between;
}
}
+490
View File
@@ -0,0 +1,490 @@
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(() => {
});
}
}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.c-flipbook{perspective:2200px;transform-style:preserve-3d;opacity:1;height:200px;position:absolute;left:0;transition:left .7s;top:0}.c-flipbook.at-front-cover{left:-25%}.c-flipbook.at-rear-cover{left:25%}.c-flipbook:not(.is-ready) *{transition:none!important}.c-flipbook:after{content:'';display:table;clear:both}.c-flipbook[data-useragent*='MSIE 10.0'] .c-flipbook__page{opacity:0}.c-flipbook[data-useragent*='MSIE 10.0'] .c-flipbook__page.is-active{transition:opacity .9s ease,transform .9s ease;opacity:1}.c-flipbook[data-useragent*='MSIE 10.0'] .c-flipbook__page.was-active{transition-delay:2s;transition:opacity .9s ease,transform .9s ease;opacity:0}.is-calling{transform:rotateY(-20deg)!important}.c-flipbook__page{cursor:pointer;overflow:hidden;position:absolute;width:50%;background:#efeef4;backface-visibility:hidden;transform:rotateY(0);user-select:none;transition:transform .9s ease}.c-flipbook__page.is-active{z-index:2}.c-flipbook__page.was-active{z-index:1}.c-flipbook__page.is-animating:nth-child(odd){z-index:4}.c-flipbook__page.is-animating:nth-child(odd)~.c-flipbook__page.is-animating{z-index:3}.c-flipbook__page.is-animating+.c-flipbook__page:not(.is-animating):nth-child(odd){z-index:1}.c-flipbook__page:nth-child(2n){transform-origin:100%;left:0;border-radius:6px 0 0 6px}.c-flipbook__page:nth-child(2n).is-active{transform:rotateY(10deg)}.c-flipbook__page:nth-child(2n).is-active:hover{transform:rotateY(15deg)}.c-flipbook__page:nth-child(2n):not(.last-page){border-right:none}.c-flipbook__page:nth-child(2n).is-active:hover{transform:rotateY(5deg)}.c-flipbook__page:nth-child(odd){transform-origin:0;right:0;transform:rotateY(-180deg);border-radius:0 6px 6px 0}.c-flipbook__page:nth-child(odd).is-active{transform:rotateY(-10deg)}.c-flipbook__page:nth-child(odd).is-active:hover{transform:rotateY(-15deg)}.c-flipbook__page:nth-child(odd):not(.first-page){border-left:none}.c-flipbook__page:nth-child(odd).is-active~.c-flipbook__page:nth-child(2n){transform:rotateY(180deg)}.c-flipbook__page:nth-child(odd).is-active~.c-flipbook__page:nth-child(odd){transform:rotateY(0)}.c-flipbook__page:nth-child(odd).is-active:hover{transform:rotateY(-5deg)}.c-flipbook__page.is-active:not(:hover){transform:rotateY(0)}.c-flipbook__page:before{content:'';position:absolute;z-index:3;right:0;width:100%;height:100%;background-size:100% 100%}.no-csstransforms3d .c-flipbook__page{display:none}.no-csstransforms3d .c-flipbook__page.is-active{display:block;position:relative;float:left}.c-flipbook-image{position:relative;z-index:2;height:auto;width:100%;display:block;pointer-events:none}.c-flipbook__page .ss-loading{font-size:2rem;position:absolute;z-index:1;top:0;bottom:0;width:100%;display:flex}.c-flipbook__page .ss-loading:before{display:flex;align-items:center;justify-content:center;width:100%}@supports (transition:transform 0.9s ease) and (not (-ms-ime-align:auto)){.c-flipbook__page{transition:transform .9s ease}}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+22
View File
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Mummert\FlipbookBundle\Contao\Manager;
use Contao\CoreBundle\ContaoCoreBundle;
use Contao\ManagerPlugin\Bundle\BundlePluginInterface;
use Contao\ManagerPlugin\Bundle\Config\BundleConfig;
use Contao\ManagerPlugin\Bundle\Parser\ParserInterface;
use Mummert\FlipbookBundle\FlipbookBundle;
class Plugin implements BundlePluginInterface
{
public function getBundles(ParserInterface $parser): iterable
{
return [
BundleConfig::create(FlipbookBundle::class)
->setLoadAfter([ContaoCoreBundle::class]),
];
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Mummert\FlipbookBundle\Controller\ContentElement;
use Contao\ContentModel;
use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController;
use Contao\CoreBundle\DependencyInjection\Attribute\AsContentElement;
use Contao\CoreBundle\Twig\FragmentTemplate;
use Contao\FilesModel;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
#[AsContentElement(type: 'blatterbares_pdf', category: 'media', template: 'content_element/blatterbares_pdf')]
class BlatterbaresPdfController extends AbstractContentElementController
{
protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
{
$template->set('pdfUrl', $this->resolvePdfUrl($model));
$template->set('showNavigation', '1' === (string) ($model->flipbookShowNavigation ?? '1'));
$template->set('initialRenderPages', $this->normalizeInitialRenderPages((string) ($model->flipbookInitialPages ?? '4')));
$template->set('startMode', $this->normalizeStartMode((string) ($model->flipbookStartMode ?? 'center')));
return $template->getResponse();
}
private function resolvePdfUrl(ContentModel $model): string
{
$uuid = (string) ($model->flipbookPdfSrc ?? '');
if ('' === $uuid) {
return '';
}
$fileModel = FilesModel::findByUuid($uuid);
if (null === $fileModel) {
return '';
}
$path = trim((string) $fileModel->path, '/');
if ('' === $path || !str_ends_with(strtolower($path), '.pdf')) {
return '';
}
return '/'.implode('/', array_map('rawurlencode', explode('/', $path)));
}
private function normalizeInitialRenderPages(string $value): int
{
$count = (int) $value;
if ($count < 2) {
return 2;
}
if ($count > 4) {
return 4;
}
return $count;
}
private function normalizeStartMode(string $value): string
{
if ('spread' === $value || 'cover' === $value) {
return 'spread';
}
return 'center';
}
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Mummert\FlipbookBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
class FlipbookExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../../config'));
$loader->load('services.yaml');
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Mummert\FlipbookBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class FlipbookBundle extends Bundle
{
public function getPath(): string
{
return dirname(__DIR__);
}
}