feat: refine flipbook start modes and UI behavior
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
_defaults:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
|
||||||
|
Mummert\FlipbookBundle\:
|
||||||
|
resource: ../src/
|
||||||
|
exclude:
|
||||||
|
- ../src/DependencyInjection/
|
||||||
|
- ../src/Contao/Manager/
|
||||||
|
- ../src/FlipbookBundle.php
|
||||||
@@ -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'],
|
||||||
|
];
|
||||||
@@ -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.'];
|
||||||
@@ -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.'];
|
||||||
@@ -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.'];
|
||||||
@@ -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.'];
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
File diff suppressed because one or more lines are too long
+1
@@ -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}}
|
||||||
Vendored
+28
File diff suppressed because one or more lines are too long
+28
File diff suppressed because one or more lines are too long
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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__);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user