4 Commits

Author SHA1 Message Date
Jürgen Mummert be27ce1340 Fix shared-hosting MIME issue for PDF.js assets 2026-04-14 12:02:17 +02:00
Juergen 97d5f447e1 chore: switch package license to proprietary 2026-04-13 22:26:13 +02:00
Juergen 1564d96b01 fix: polish initial flipbook reveal and update third-party metadata 2026-04-13 22:18:45 +02:00
Juergen 781f050dfc feat: add optional page-turn sound toggle and refine flipbook UI 2026-04-13 22:00:39 +02:00
14 changed files with 191 additions and 27 deletions
+14 -16
View File
@@ -1,21 +1,19 @@
MIT License All rights reserved.
Copyright (c) 2026 Mummert Copyright (c) 2026 Mummert
Permission is hereby granted, free of charge, to any person obtaining a copy This source code is publicly visible for informational and deployment purposes only.
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 Permission is NOT granted to:
copies or substantial portions of the Software. - use this code for any commercial or non-commercial purpose
- copy, modify, merge, publish, distribute, sublicense, or sell copies of the Software
- use this code in any other project
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR Any use of this code without explicit written permission from the author is strictly prohibited.
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
SOFTWARE. OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+1
View File
@@ -33,6 +33,7 @@ Felder:
- Das Inhaltselement rendert PDF-Seiten als `<canvas>` innerhalb von `.c-flipbook__page`. - Das Inhaltselement rendert PDF-Seiten als `<canvas>` innerhalb von `.c-flipbook__page`.
- Die benoetigten Open-Source-Dateien sind lokal im Bundle enthalten. - Die benoetigten Open-Source-Dateien sind lokal im Bundle enthalten.
- Fuer Shared-Hosting ohne konfigurierbare MIME-Types werden PDF.js-Module als `.js` ausgeliefert (statt `.mjs`).
- Siehe `THIRD_PARTY_LICENSES.md` fuer Abhaengigkeiten. - Siehe `THIRD_PARTY_LICENSES.md` fuer Abhaengigkeiten.
## Release ## Release
+1 -1
View File
@@ -6,7 +6,7 @@ This bundle contains the following third-party open source libraries in `public/
- Project: https://github.com/mozilla/pdf.js - Project: https://github.com/mozilla/pdf.js
- Package: `pdfjs-dist` - Package: `pdfjs-dist`
- Version: `4.9.155` - Version: `5.6.205`
- License: Apache-2.0 - License: Apache-2.0
## flipbook-js ## flipbook-js
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "mummert/flipbook-bundle", "name": "mummert/flipbook-bundle",
"description": "PDF flipbook content element for Contao 5.7 using pdfjs-dist and flipbook-js.", "description": "PDF flipbook content element for Contao 5.7 using pdfjs-dist and flipbook-js.",
"type": "contao-bundle", "type": "contao-bundle",
"license": "MIT", "license": "proprietary",
"keywords": [ "keywords": [
"contao", "contao",
"contao-bundle", "contao-bundle",
+9 -1
View File
@@ -2,7 +2,7 @@
declare(strict_types=1); 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']['palettes']['blatterbares_pdf'] = '{type_legend},type,headline;{flipbook_legend},flipbookPdfSrc,flipbookInitialPages,flipbookStartMode,flipbookShowNavigation,flipbookPlaySound;{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'] = [ $GLOBALS['TL_DCA']['tl_content']['fields']['flipbookPdfSrc'] = [
'label' => &$GLOBALS['TL_LANG']['tl_content']['flipbookPdfSrc'], 'label' => &$GLOBALS['TL_LANG']['tl_content']['flipbookPdfSrc'],
@@ -39,3 +39,11 @@ $GLOBALS['TL_DCA']['tl_content']['fields']['flipbookShowNavigation'] = [
'eval' => ['tl_class' => 'w50 m12'], 'eval' => ['tl_class' => 'w50 m12'],
'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => '1'], 'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => '1'],
]; ];
$GLOBALS['TL_DCA']['tl_content']['fields']['flipbookPlaySound'] = [
'label' => &$GLOBALS['TL_LANG']['tl_content']['flipbookPlaySound'],
'exclude' => true,
'inputType' => 'checkbox',
'eval' => ['tl_class' => 'w50 m12'],
'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => '1'],
];
+1
View File
@@ -16,3 +16,4 @@ $GLOBALS['TL_LANG']['tl_content']['flipbookStartModeOptions'] = [
'spread' => 'als Doppelseite starten', 'spread' => 'als Doppelseite starten',
]; ];
$GLOBALS['TL_LANG']['tl_content']['flipbookShowNavigation'] = ['Navigation anzeigen', 'Zeigt Vor-/Zurueck-Buttons unter dem Flipbook an.']; $GLOBALS['TL_LANG']['tl_content']['flipbookShowNavigation'] = ['Navigation anzeigen', 'Zeigt Vor-/Zurueck-Buttons unter dem Flipbook an.'];
$GLOBALS['TL_LANG']['tl_content']['flipbookPlaySound'] = ['Blättersound abspielen', 'Spielt beim Blättern einen Sound ab.'];
+1
View File
@@ -16,3 +16,4 @@ $GLOBALS['TL_LANG']['tl_content']['flipbookStartModeOptions'] = [
'spread' => 'Start with double-page spread', 'spread' => 'Start with double-page spread',
]; ];
$GLOBALS['TL_LANG']['tl_content']['flipbookShowNavigation'] = ['Show navigation', 'Displays previous/next buttons below the flipbook.']; $GLOBALS['TL_LANG']['tl_content']['flipbookShowNavigation'] = ['Show navigation', 'Displays previous/next buttons below the flipbook.'];
$GLOBALS['TL_LANG']['tl_content']['flipbookPlaySound'] = ['Play page-turn sound', 'Plays a sound effect while turning pages.'];
@@ -1,22 +1,23 @@
{% set hasPdf = pdfUrl|default('') is not empty %} {% set hasPdf = pdfUrl|default('') is not empty %}
<div <div
class="ce-blatterbares-pdf mod-pdf-flipbook" class="ce-blatterbares-pdf mod-pdf-flipbook is-loading"
data-pdf-flipbook-element="1" data-pdf-flipbook-element="1"
data-pdf-url="{{ pdfUrl|default('')|e('html_attr') }}" data-pdf-url="{{ pdfUrl|default('')|e('html_attr') }}"
data-initial-pages="{{ initialRenderPages|default(4)|e('html_attr') }}" data-initial-pages="{{ initialRenderPages|default(4)|e('html_attr') }}"
data-start-mode="{{ startMode|default('center')|e('html_attr') }}" data-start-mode="{{ startMode|default('center')|e('html_attr') }}"
data-show-navigation="{{ showNavigation ? '1' : '0' }}" data-show-navigation="{{ showNavigation ? '1' : '0' }}"
data-play-turn-sound="{{ ((playTurnSound is defined) ? playTurnSound : true) ? '1' : '0' }}"
tabindex="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__status" data-flipbook-loader="1" aria-live="polite">PDF wird geladen ...</div>
<div class="mod-pdf-flipbook__stage" data-flipbook-stage="1"> <div class="mod-pdf-flipbook__stage" data-flipbook-stage="1" style="visibility:hidden;opacity:0">
<div id="flipbook-{{ data.id|default(random()) }}" class="c-flipbook" data-flipbook-book="1"></div> <div id="flipbook-{{ data.id|default(random()) }}" class="c-flipbook" data-flipbook-book="1"></div>
</div> </div>
{% if showNavigation %} {% if showNavigation %}
<div class="mod-pdf-flipbook__controls" aria-label="Flipbook Navigation"> <div class="mod-pdf-flipbook__controls" data-flipbook-controls="1" aria-label="Flipbook Navigation" style="visibility:hidden;opacity:0">
<button type="button" data-flipbook-prev="1">Zurück</button> <button type="button" data-flipbook-prev="1">Zurück</button>
<button type="button" data-flipbook-next="1">Weiter</button> <button type="button" data-flipbook-next="1">Weiter</button>
</div> </div>
@@ -27,6 +28,6 @@
{% endif %} {% endif %}
</div> </div>
<link rel="stylesheet" href="/bundles/flipbook/assets/vendor/flipbook.min.css?v=20260414d"> <link rel="stylesheet" href="/bundles/flipbook/assets/vendor/flipbook.min.css?v=20260414j">
<link rel="stylesheet" href="/bundles/flipbook/assets/flipbook-module.css?v=20260414d"> <link rel="stylesheet" href="/bundles/flipbook/assets/flipbook-module.css?v=20260414j">
<script type="module" src="/bundles/flipbook/assets/flipbook-module.js?v=20260414d"></script> <script type="module" src="/bundles/flipbook/assets/flipbook-module.js?v=20260414j"></script>
Binary file not shown.
+34
View File
@@ -13,11 +13,24 @@
} }
.mod-pdf-flipbook__status { .mod-pdf-flipbook__status {
display: inline-flex;
align-items: center;
gap: 0.55rem;
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
font-size: 0.92rem; font-size: 0.92rem;
color: var(--flipbook-text); color: var(--flipbook-text);
} }
.mod-pdf-flipbook__status::before {
content: '';
width: 0.9rem;
height: 0.9rem;
border: 2px solid rgba(48, 45, 41, 0.25);
border-top-color: rgba(48, 45, 41, 0.85);
border-radius: 50%;
animation: mod-pdf-flipbook-spin 0.75s linear infinite;
}
.mod-pdf-flipbook__status.is-hidden { .mod-pdf-flipbook__status.is-hidden {
display: none; display: none;
} }
@@ -26,17 +39,29 @@
color: #9f1f1f; color: #9f1f1f;
} }
.mod-pdf-flipbook__status.is-error::before {
display: none;
}
.mod-pdf-flipbook__stage { .mod-pdf-flipbook__stage {
position: relative; position: relative;
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
background: #e0e0e0; background: #e0e0e0;
z-index: 1;
transition: opacity 1.3s ease 0.5s;
} }
.mod-pdf-flipbook .c-flipbook { .mod-pdf-flipbook .c-flipbook {
max-width: 100%; max-width: 100%;
} }
.mod-pdf-flipbook .c-flipbook.is-booting,
.mod-pdf-flipbook .c-flipbook.is-booting .c-flipbook__page,
.mod-pdf-flipbook .c-flipbook.is-booting .c-flipbook__page::before {
transition: none !important;
}
.mod-pdf-flipbook .c-flipbook__page { .mod-pdf-flipbook .c-flipbook__page {
background: #fefdf9; background: #fefdf9;
overflow: hidden; overflow: hidden;
@@ -67,9 +92,12 @@
} }
.mod-pdf-flipbook__controls { .mod-pdf-flipbook__controls {
position: relative;
z-index: 20;
display: flex; display: flex;
gap: 0.6rem; gap: 0.6rem;
margin-top: 0.9rem; margin-top: 0.9rem;
transition: opacity 1.3s ease 0.5s;
} }
.mod-pdf-flipbook__error { .mod-pdf-flipbook__error {
@@ -82,3 +110,9 @@
justify-content: space-between; justify-content: space-between;
} }
} }
@keyframes mod-pdf-flipbook-spin {
to {
transform: rotate(360deg);
}
}
+65 -2
View File
@@ -1,7 +1,8 @@
const MODULE_SELECTOR = '[data-pdf-flipbook-element="1"]'; const MODULE_SELECTOR = '[data-pdf-flipbook-element="1"]';
const PDF_MODULE_URL = '/bundles/flipbook/assets/vendor/pdf.min.mjs'; const PDF_MODULE_URL = '/bundles/flipbook/assets/vendor/pdf.min.js';
const PDF_WORKER_URL = '/bundles/flipbook/assets/vendor/pdf.worker.min.mjs'; const PDF_WORKER_URL = '/bundles/flipbook/assets/vendor/pdf.worker.min.js';
const FLIPBOOK_MODULE_URL = '/bundles/flipbook/assets/vendor/flipbook.esm.min.js'; const FLIPBOOK_MODULE_URL = '/bundles/flipbook/assets/vendor/flipbook.esm.min.js';
const TURN_SOUND_URL = '/bundles/flipbook/assets/audio/turn.mp3';
const INIT_MARKER = 'pdfFlipbookInitialized'; const INIT_MARKER = 'pdfFlipbookInitialized';
const BOOTSTRAP_MARKER = '__mummertPdfFlipbookBootstrapBound'; const BOOTSTRAP_MARKER = '__mummertPdfFlipbookBootstrapBound';
@@ -35,16 +36,20 @@ class PdfFlipbookModule {
this.root = root; this.root = root;
this.dependencies = dependencies; this.dependencies = dependencies;
this.pdfUrl = (root.dataset.pdfUrl || '').trim(); this.pdfUrl = (root.dataset.pdfUrl || '').trim();
this.root.classList.add('is-loading');
this.root.classList.remove('is-ready');
this.initialPages = Number.isFinite(parsedInitialPages) this.initialPages = Number.isFinite(parsedInitialPages)
? Math.min(4, Math.max(2, parsedInitialPages)) ? Math.min(4, Math.max(2, parsedInitialPages))
: 4; : 4;
this.showNavigation = root.dataset.showNavigation === '1'; this.showNavigation = root.dataset.showNavigation === '1';
this.playTurnSoundEnabled = root.dataset.playTurnSound !== '0';
this.startMode = (root.dataset.startMode === 'spread' || root.dataset.startMode === 'cover') this.startMode = (root.dataset.startMode === 'spread' || root.dataset.startMode === 'cover')
? 'spread' ? 'spread'
: 'center'; : 'center';
this.loader = root.querySelector('[data-flipbook-loader="1"]'); this.loader = root.querySelector('[data-flipbook-loader="1"]');
this.stage = root.querySelector('[data-flipbook-stage="1"]'); this.stage = root.querySelector('[data-flipbook-stage="1"]');
this.bookElement = root.querySelector('[data-flipbook-book="1"]'); this.bookElement = root.querySelector('[data-flipbook-book="1"]');
this.controlsElement = root.querySelector('[data-flipbook-controls="1"]');
this.nextButton = this.showNavigation ? root.querySelector('[data-flipbook-next="1"]') : null; this.nextButton = this.showNavigation ? root.querySelector('[data-flipbook-next="1"]') : null;
this.prevButton = this.showNavigation ? root.querySelector('[data-flipbook-prev="1"]') : null; this.prevButton = this.showNavigation ? root.querySelector('[data-flipbook-prev="1"]') : null;
@@ -57,6 +62,7 @@ class PdfFlipbookModule {
this.renderInProgress = false; this.renderInProgress = false;
this.resizeTimer = null; this.resizeTimer = null;
this.touchStart = null; this.touchStart = null;
this.turnSound = this.playTurnSoundEnabled ? this.createTurnSound() : null;
this.aspectRatio = 1.4142; this.aspectRatio = 1.4142;
this.pageWidth = 0; this.pageWidth = 0;
this.pageHeight = 0; this.pageHeight = 0;
@@ -73,6 +79,7 @@ class PdfFlipbookModule {
if (!this.pdfUrl) { if (!this.pdfUrl) {
this.setStatus('Keine PDF-Datei gefunden.', true); this.setStatus('Keine PDF-Datei gefunden.', true);
this.root.classList.remove('is-loading');
return; return;
} }
@@ -91,12 +98,47 @@ class PdfFlipbookModule {
this.bindTouchSwipe(); this.bindTouchSwipe();
this.bindResize(); this.bindResize();
this.queuePages(this.getLazyCandidates()); this.queuePages(this.getLazyCandidates());
await this.showReadyState();
this.setStatus(''); this.setStatus('');
} catch { } catch {
this.setStatus('PDF konnte nicht geladen werden.', true); this.setStatus('PDF konnte nicht geladen werden.', true);
this.root.classList.remove('is-loading');
} }
} }
async showReadyState() {
await new Promise((resolve) => {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(resolve);
});
});
if (this.bookElement) {
this.bookElement.classList.remove('is-booting');
}
this.root.classList.remove('is-loading');
this.root.classList.add('is-ready');
if (this.stage) {
this.stage.style.visibility = 'visible';
}
if (this.controlsElement) {
this.controlsElement.style.visibility = 'visible';
}
window.requestAnimationFrame(() => {
if (this.stage) {
this.stage.style.opacity = '1';
}
if (this.controlsElement) {
this.controlsElement.style.opacity = '1';
}
});
}
async loadPdf() { async loadPdf() {
const loadingTask = this.dependencies.pdfjsLib.getDocument({ const loadingTask = this.dependencies.pdfjsLib.getDocument({
url: this.pdfUrl, url: this.pdfUrl,
@@ -175,6 +217,8 @@ class PdfFlipbookModule {
initializeFlipbook() { initializeFlipbook() {
const startsWithDoubleSpread = this.startMode === 'spread'; const startsWithDoubleSpread = this.startMode === 'spread';
this.bookElement.classList.add('is-booting');
this.flipbook = new this.dependencies.FlipBook(this.bookElement, { this.flipbook = new this.dependencies.FlipBook(this.bookElement, {
nextButton: this.nextButton, nextButton: this.nextButton,
previousButton: this.prevButton, previousButton: this.prevButton,
@@ -185,11 +229,30 @@ class PdfFlipbookModule {
width: '100%', width: '100%',
height: `${this.pageHeight}px`, height: `${this.pageHeight}px`,
onPageTurn: () => { onPageTurn: () => {
this.playTurnSound();
this.queuePages(this.getLazyCandidates()); this.queuePages(this.getLazyCandidates());
}, },
}); });
} }
createTurnSound() {
const audio = new Audio(TURN_SOUND_URL);
audio.preload = 'auto';
return audio;
}
playTurnSound() {
if (!this.playTurnSoundEnabled || !this.turnSound) {
return;
}
this.turnSound.currentTime = 0;
this.turnSound.play().catch(() => {
});
}
bindKeyboard() { bindKeyboard() {
this.root.addEventListener('pointerdown', () => { this.root.addEventListener('pointerdown', () => {
this.root.focus(); this.root.focus();
+28
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -19,6 +19,7 @@ class BlatterbaresPdfController extends AbstractContentElementController
{ {
$template->set('pdfUrl', $this->resolvePdfUrl($model)); $template->set('pdfUrl', $this->resolvePdfUrl($model));
$template->set('showNavigation', '1' === (string) ($model->flipbookShowNavigation ?? '1')); $template->set('showNavigation', '1' === (string) ($model->flipbookShowNavigation ?? '1'));
$template->set('playTurnSound', '1' === (string) ($model->flipbookPlaySound ?? '1'));
$template->set('initialRenderPages', $this->normalizeInitialRenderPages((string) ($model->flipbookInitialPages ?? '4'))); $template->set('initialRenderPages', $this->normalizeInitialRenderPages((string) ($model->flipbookInitialPages ?? '4')));
$template->set('startMode', $this->normalizeStartMode((string) ($model->flipbookStartMode ?? 'center'))); $template->set('startMode', $this->normalizeStartMode((string) ($model->flipbookStartMode ?? 'center')));