1(function (window, document) { 2 "use strict"; 3 4 const CONFIG = { 5 pageWidth: 1000, 6 pageHeight: 1404, 7 minZoom: 0.75, 8 /* 9 * Layout-Zoom nutzt die Vollauflösung (~2082px). 10 * Darüber hinaus wäre nur noch leere VergröÃerung. 11 */ 12 maxZoom: 3, 13 zoomStep: 0.25, 14 /* Obere Grenze für StPageFlip-Seitenbreite â Quell-JPG */ 15 maxPageRenderWidth: 2100, 16 showCover: false, 17 flippingTime: 650 18 }; 19 20 const ICON_CLOSE = ` 21 <svg viewBox="0 0 24 24" aria-hidden="true"> 22 <path fill="currentColor" d="M6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12 19 6.4 17.6 5 12 10.6 6.4 5Z"/> 23 </svg> 24 `; 25 26 function clamp(value, minimum, maximum) { 27 return Math.min(Math.max(value, minimum), maximum); 28 } 29 30 function createViewerMarkup(options = {}) { 31 const showArchive = Boolean(options.showArchiveSlot); 32 const archiveSlot = showArchive 33 ? `<div class="toolbar__group" data-pageflip-archive></div>` 34 : ""; 35 const pageInputId = `pageflip-page-${Math.random().toString(36).slice(2, 9)}`; 36 37 return ` 38 <div class="viewer" data-pageflip-root> 39 <div class="toolbar" aria-label="Zeitungsnavigation"> 40 ${archiveSlot} 41 <div class="toolbar__group"> 42 <button type="button" class="toolbar__button" data-action="toggleSidebar" title="Seitenübersicht" aria-label="Seitenübersicht öffnen oder schlieÃen" aria-expanded="true"> 43 <svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M4 5h4v4H4V5Zm6 0h10v4H10V5ZM4 10h4v4H4v-4Zm6 0h10v4H10v-4ZM4 15h4v4H4v-4Zm6 0h10v4H10v-4Z"/></svg> 44 <span class="toolbar__label">Seiten</span> 45 </button> 46 </div> 47 <span class="toolbar__separator"></span> 48 <div class="toolbar__group"> 49 <button type="button" class="toolbar__button toolbar__desktop-only" data-action="firstPage" title="Erste Seite" aria-label="Erste Seite"> 50 <svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M6 5h2v14H6V5Zm11.5 1.5L12 12l5.5 5.5-1.4 1.4L9.2 12l6.9-6.9 1.4 1.4Z"/></svg> 51 </button> 52 <button type="button" class="toolbar__button" data-action="previousPage" title="Vorherige Seite" aria-label="Vorherige Seite"> 53 <svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M15.4 5.4 8.8 12l6.6 6.6L14 20l-8-8 8-8 1.4 1.4Z"/></svg> 54 </button> 55 </div> 56 <form class="page-control" data-page-form> 57 <label class="page-control__label" for="${pageInputId}">Seite</label> 58 <input class="page-number" id="${pageInputId}" data-page-number type="number" inputmode="numeric" min="1" value="1" autocomplete="off" aria-label="Zielseite eingeben"> 59 <span class="page-count" data-page-count>/ â</span> 60 </form> 61 <div class="toolbar__group"> 62 <button type="button" class="toolbar__button" data-action="nextPage" title="Nächste Seite" aria-label="Nächste Seite"> 63 <svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="m8.6 18.6 6.6-6.6-6.6-6.6L10 4l8 8-8 8-1.4-1.4Z"/></svg> 64 </button> 65 <button type="button" class="toolbar__button toolbar__desktop-only" data-action="lastPage" title="Letzte Seite" aria-label="Letzte Seite"> 66 <svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M16 5h2v14h-2V5ZM6.5 6.5 12 12l-5.5 5.5 1.4 1.4 6.9-6.9-6.9-6.9-1.4 1.4Z"/></svg> 67 </button> 68 </div> 69 <span class="toolbar__separator"></span> 70 <div class="toolbar__group toolbar__desktop-only"> 71 <button type="button" class="toolbar__button" data-action="zoomOut" title="Verkleinern" aria-label="Verkleinern"> 72 <svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M9.5 4a5.5 5.5 0 1 0 3.45 9.78L18.17 19 19.6 17.6l-5.22-5.22A5.5 5.5 0 0 0 9.5 4Zm-3 5h6v2h-6V9Z"/></svg> 73 </button> 74 <button type="button" class="toolbar__button" data-action="resetZoom" title="Zoom zurücksetzen" aria-label="Zoom zurücksetzen"> 75 <span data-zoom-value>100 %</span> 76 </button> 77 <button type="button" class="toolbar__button" data-action="zoomIn" title="VergröÃern" aria-label="VergröÃern"> 78 <svg viewBox="0 0 24 24" aria-hidden="true">
78<path fill="currentColor" d="M9.5 4a5.5 5.5 0 1 0 3.45 9.78L18.17 19 19.6 17.6l-5.22-5.22A5.5 5.5 0 0 0 9.5 4Zm-1 2h2v3h3v2h-3v3h-2v-3h-3V9h3V6Z"/></svg> 79 </button> 80 </div> 81 <div class="toolbar__spacer"></div> 82 <button type="button" class="toolbar__button" data-action="fullscreen" title="Vollbild" aria-label="Vollbild ein- oder ausschalten"> 83 <svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M4 4h6v2H6v4H4V4Zm10 0h6v6h-2V6h-4V4ZM4 14h2v4h4v2H4v-6Zm14 0h2v6h-6v-2h4v-4Z"/></svg> 84 <span class="toolbar__label">Vollbild</span> 85 </button> 86 </div> 87 <div class="viewer__body"> 88 <aside class="sidebar" data-sidebar aria-label="Seitenübersicht"> 89 <div class="sidebar__header"> 90 <h2 class="sidebar__title">Seitenübersicht</h2> 91 </div> 92 <div class="thumbnail-list" data-thumbnail-list></div> 93 </aside> 94 <div class="book-area" data-book-area> 95 <div class="book-zoom" data-book-zoom> 96 <div data-book></div> 97 </div> 98 <div class="status-message" data-status-message role="status" aria-live="polite"></div> 99 </div> 100 </div> 101 <div class="loading" data-loading> 102 <div class="loading__box">Ausgabe wird geladen â¦</div> 103 </div> 104 </div> 105 `; 106 } 107 108 class MeranerPageFlipInstance { 109 constructor(root, options = {}) { 110 this.root = root; 111 this.options = options; 112 this.mode = options.mode || "page"; 113 this.updateHash = options.updateHash !== false && this.mode === "page"; 114 this.fitToViewport = options.fitToViewport !== false && this.mode === "page"; 115 116 this.pageFlip = null; 117 this.pages = []; 118 this.pageWidth = CONFIG.pageWidth; 119 this.pageHeight = CONFIG.pageHeight; 120 this.currentZoom = 1; 121 this.selectedPageNumber = null; 122 this.pendingPageNumber = null; 123 this.statusTimer = null; 124 this.resizeFrame = null; 125 this.resizeTimer = null; 126 this.toolbarHeavyFrame = null; 127 this.backgroundPrimeStarted = false; 128 this.backgroundPrimeIdle = null; 129 this.backgroundPrimeTimer = null; 130 131 this.panX = 0; 132 this.panY = 0; 133 this.zoomOriginX = 50; 134 this.zoomOriginY = 50; 135 136 this.activePointers = new Map(); 137 this.pinchStartDistance = 0; 138 this.pinchStartZoom = 1; 139 this.isPinching = false; 140 this.isPanning = false; 141 this.panPointerId = null; 142 this.lastPanX = 0; 143 this.lastPanY = 0; 144 this.zoomBoundFrame = null; 145 this.pendingZoomBound = 1; 146 147 this.boundHandlers = {}; 148 this.cacheElements(); 149 } 150 151 cacheElements() { 152 const root = this.root; 153 154 this.elements = { 155 viewer: root, 156 loading: 157 root.querySelector("[data-loading]") || 158 root.querySelector("#loading"), 159 book: 160 root.querySelector("[data-book]") || 161 root.querySelector("#book"), 162 bookArea: 163 root.querySelector("[data-book-area]") || 164 root.querySelector("#bookArea"), 165 bookZoom: 166 root.querySelector("[data-book-zoom]") || 167 root.querySelector("#bookZoom"), 168 sidebar: 169 root.querySelector("[data-sidebar]") || 170 root.querySelector("#sidebar"), 171 toggleSidebar: 172 root.querySelector('[data-action="toggleSidebar"]') || 173 root.querySelector("#toggleSidebar"), 174 thumbnailList: 175 root.querySelector("[data-thumbnail-list]") || 176 root.querySelector("#thumbnailList"), 177 firstPage: 178 root.querySelector('[data-action="firstPage"]') || 179 root.querySelector("#firstPage"), 180 previousPage: 181 root.querySelector('[data-action="previousPage"]') || 182 root.querySelector("#previousPage"), 183 nextPage: 184 root.querySelector('[data-action="nextPage"]') || 185 root.querySelector("#nextPage"), 186 lastPage: 187 root.querySelector('[data-action="lastPage"]') || 188 root.querySelector("#lastPage"), 189 pageForm: 190 root.querySelector("[data-page-form]") || 191 root.querySelector("#pageForm"), 192 pageNumber: 193 root.querySelector("[data-page-number]") || 194 root.querySelector("#pageNumber"), 195 pageCount: 196 root.querySelector("[data-page-count]") || 197 root.querySelector("#pageCount"), 198 zoomOut: 199 root.querySelector('[data-action="zoomOut"]') || 200 root.querySelector("#zoomOut"), 201 zoomIn: 202 root.querySelector('[data-action="zoomIn"]') || 203 root.querySelector("#zoomIn"), 204 resetZoom: 205 root.querySelector('[data-action="resetZoom"]') || 206 root.querySelector("#resetZoom"), 207 zoomValue: 208 root.querySelector("[data-zoom-value]") || 209 root.querySelector("#zoomValue"), 210 fullscreen: 211 root.querySelector('[data-action="fullscreen"]') || 212 root.querySelector("#fullscreen"), 213 statusMessage: 214 root.querySelector("[data-status-message]") || 215 root.querySelector("#statusMessage") 216 }; 217 218 if (this.elements.book && !this.elements.book.id) { 219 this.elements.book.id = "book"; 220 } 221 } 222
223 async init(pageSources = null) { 224 if ( 225 typeof St === "undefined" || 226 typeof St.PageFlip !== "function" 227 ) { 228 throw new Error("StPageFlip wurde nicht geladen."); 229 } 230 231 if (pageSources) { 232 this.renderPages(pageSources); 233 } 234 235 this.pages = Array.from( 236 this.elements.book.querySelectorAll(".book-page") 237 ); 238 239 if (!this.pages.length) { 240 throw new Error("Keine Seiten gefunden."); 241 } 242 243 this.createThumbnails(); 244 this.bindEvents(); 245 this.refreshBookLayout(); 246 247 await this.resolvePageSizeFromImages(); 248 this.applyPageAspectCss(); 249 this.initializePageFlip(); 250 251 if (window.matchMedia("(max-width: 900px)").matches) { 252 this.setSidebar(false); 253 } 254 255 this.resetZoom(false); 256 257 if (this.elements.loading) { 258 this.elements.loading.hidden = true; 259 } 260 261 await this.waitForNearbyImages(); 262 this.startBackgroundImagePrime(); 263 this.refreshBookLayout(); 264 return this; 265 } 266 267 getPageAspect() { 268 return this.pageHeight / this.pageWidth; 269 } 270 271 applyPageAspectCss() { 272 if (!this.elements.viewer) { 273 return; 274 } 275 276 this.elements.viewer.style.setProperty( 277 "--page-aspect", 278 `${this.pageWidth} / ${this.pageHeight}` 279 ); 280 } 281 282 async resolvePageSizeFromImages() { 283 const samplePage = this.pages.find( 284 page => 285 page && 286 !page.classList.contains("book-page--blank") && 287 page.querySelector("img") 288 ); 289 const image = samplePage?.querySelector("img"); 290 291 if (!image) { 292 return; 293 } 294 295 await this.primeImage(image); 296 297 if (!(image.naturalWidth > 0) || !(image.naturalHeight > 0)) { 298 return; 299 } 300 301 this.pageWidth = CONFIG.pageWidth; 302 this.pageHeight = Math.max( 303 1, 304 Math.round( 305 (this.pageWidth * image.naturalHeight) / image.naturalWidth 306 ) 307 ); 308 } 309 310 renderPages(pageSources) { 311 const fragment = document.createDocumentFragment(); 312 313 const blank = document.createElement("div"); 314 blank.className = "book-page book-page--blank"; 315 blank.dataset.density = "soft"; 316 blank.setAttribute("aria-hidden", "true"); 317 fragment.appendChild(blank); 318 319 pageSources.forEach((source, index) => { 320 const src = typeof source === "string" ? source : source.src; 321 const pageNumber = 322 typeof source === "object" && source.page 323 ? source.page 324 : index + 1; 325 326 const page = document.createElement("div"); 327 page.className = "book-page"; 328 page.dataset.page = String(pageNumber); 329 330 const image = document.createElement("img"); 331 image.alt = `Seite ${pageNumber}`; 332 image.draggable = false; 333 image.decoding = "async"; 334 image.loading = "eager"; 335 image.src = src; 336 337 page.appendChild(image); 338 fragment.appendChild(page); 339 }); 340 341 this.elements.book.replaceChildren(fragment); 342 } 343 344 pageIndexToNumber(pageIndex) { 345 const page = this.pages[pageIndex]; 346 347 if (!page) { 348 return null; 349 } 350 351 const pageNumber = Number.parseInt(page.dataset.page, 10); 352 return Number.isFinite(pageNumber) ? pageNumber : null; 353 } 354 355 pageNumberToIndex(pageNumber) { 356 const requested = Number.parseInt(pageNumber, 10); 357 358 if (!Number.isFinite(requested)) { 359 return -1; 360 } 361 362 return this.pages.findIndex(page => { 363 const current = Number.parseInt(page.dataset.page, 10); 364 return Number.isFinite(current) && current === requested; 365 }); 366 } 367 368 getVisiblePageCount() { 369 return this.pages.filter(page => 370 Number.isFinite(Number.parseInt(page.dataset.page, 10)) 371 ).length; 372 } 373 374 getCurrentPageIndex() { 375 return this.pageFlip ? this.pageFlip.getCurrentPageIndex() : 0; 376 } 377 378 getCurrentPageNumber() { 379 return this.pageIndexToNumber(this.getCurrentPageIndex()); 380 } 381 382 showStatus(message) { 383 if (!this.elements.statusMessage) { 384 return; 385 } 386 387 window.clearTimeout(this.statusTimer); 388 this.elements.statusMessage.textContent = message; 389 this.elements.statusMessage.classList.add("status-message--visible"); 390 391 this.statusTimer = window.setTimeout(() => { 392 this.elements.statusMessage.classList.remove( 393 "status-message--visible" 394 ); 395 }, 1600); 396 } 397 398 createThumbnails() { 399 const fragment = document.createDocumentFragment(); 400
401 this.pages.forEach((page, index) => { 402 const originalImage = page.querySelector("img"); 403 404 if (!originalImage) { 405 return; 406 } 407 408 const pageNumber = 409 page.dataset.page || 410 originalImage.alt?.replace(/\D+/g, "") || 411 String(index + 1); 412 413 const button = document.createElement("button"); 414 button.type = "button"; 415 button.className = "thumbnail"; 416 417 if (String(index) === "1") { 418 button.classList.add("thumbnail--cover"); 419 } 420 421 button.dataset.pageIndex = String(index); 422 button.title = `Seite ${pageNumber} öffnen`; 423 button.setAttribute( 424 "aria-label", 425 `Seite ${pageNumber} öffnen` 426 ); 427 428 const image = document.createElement("img"); 429 image.className = "thumbnail__image"; 430 image.src = originalImage.currentSrc || originalImage.src; 431 image.alt = ""; 432 image.loading = "lazy"; 433 image.decoding = "async"; 434 435 const number = document.createElement("span"); 436 number.className = "thumbnail__number"; 437 number.textContent = `Seite ${pageNumber}`; 438 439 button.append(image, number); 440 441 button.addEventListener("click", () => { 442 this.goToPageNumber(pageNumber); 443 444 if (window.matchMedia("(max-width: 900px)").matches) { 445 this.setSidebar(false); 446 } 447 }); 448 449 fragment.appendChild(button); 450 }); 451 452 this.elements.thumbnailList.replaceChildren(fragment); 453 } 454 455 updateActiveThumbnail(pageIndex) { 456 const oldActive = this.elements.thumbnailList.querySelector( 457 ".thumbnail--active" 458 ); 459 460 if (oldActive) { 461 oldActive.classList.remove("thumbnail--active"); 462 oldActive.removeAttribute("aria-current"); 463 } 464 465 const newActive = this.elements.thumbnailList.querySelector( 466 `[data-page-index="${pageIndex}"]` 467 ); 468 469 if (!newActive) { 470 return; 471 } 472 473 newActive.classList.add("thumbnail--active"); 474 newActive.setAttribute("aria-current", "page"); 475 476 const list = this.elements.thumbnailList; 477 const listRect = list.getBoundingClientRect(); 478 const thumbRect = newActive.getBoundingClientRect(); 479 const outOfView = 480 thumbRect.top < listRect.top || 481 thumbRect.bottom > listRect.bottom; 482 483 if (outOfView) { 484 newActive.scrollIntoView({ 485 block: "nearest", 486 behavior: "auto" 487 }); 488 } 489 } 490 491 goToPageNumber(requestedPageNumber, animate = true) { 492 const pageCount = this.getVisiblePageCount(); 493 let pageNumber = Number.parseInt(requestedPageNumber, 10); 494 495 if (!Number.isFinite(pageNumber)) { 496 pageNumber = this.getCurrentPageNumber() || 1; 497 } 498 499 pageNumber = clamp(pageNumber, 1, pageCount); 500 const pageIndex = this.pageNumberToIndex(pageNumber); 501 502 if (pageIndex < 0 || !this.pageFlip) { 503 return; 504 } 505 506 /* 507 * Enter im Zahlenfeld löst oft change + submit aus. 508 * StPageFlip.flip() würde dann zweimal flipNext/flipPrev 509 * ausführen und eine Seite zu weit springen. 510 */ 511 if (this.pendingPageNumber === pageNumber) { 512 return; 513 } 514 515 this.pendingPageNumber = pageNumber; 516 this.selectedPageNumber = pageNumber; 517 this.elements.pageNumber.value = String(pageNumber); 518 this.ensureNearbyImages(pageIndex); 519 520 if (animate) { 521 this.pageFlip.flip(pageIndex, "top"); 522 } else { 523 this.pageFlip.turnToPage(pageIndex); 524 this.updateToolbar(this.pageFlip.getCurrentPageIndex()); 525 } 526 527 window.setTimeout(() => { 528 if (this.pendingPageNumber === pageNumber) { 529 this.pendingPageNumber = null; 530 } 531 }, CONFIG.flippingTime + 50); 532 } 533 534 goToFirstPage() { 535 this.goToPageNumber(1); 536 } 537 538 goToLastPage() { 539 const lastPage = this.pages 540 .map(page => Number.parseInt(page.dataset.page, 10)) 541 .filter(Number.isFinite) 542 .pop(); 543 544 this.goToPageNumber(lastPage); 545 } 546 547 goToPreviousPage() { 548 this.selectedPageNumber = null; 549 this.pageFlip?.flipPrev("top"); 550 } 551 552 goToNextPage() { 553 this.selectedPageNumber = null; 554 this.pageFlip?.flipNext("top"); 555 } 556 557 getPageNumberFromHash() { 558 const match = window.location.hash.match(/^#seite\/(\d+)$/); 559 560 if (!match) { 561 return 1; 562 } 563 564 return clamp( 565 Number.parseInt(match[1], 10), 566 1, 567 this.getVisiblePageCount() 568 ); 569 } 570 571 writeHash(pageNumber) { 572 if (!this.updateHash) { 573 return; 574 } 575 576 const newHash = `#seite/${pageNumber}`; 577 578 if (window.location.hash === newHash) { 579 return; 580 } 581 582 history.replaceState( 583 null, 584 "", 585 `${window.location.pathname}${window.location.search}${newHash}` 586 ); 587 } 588 589 resolveToolbarState(pageIndex) { 590 const pageCount = this.getVisiblePageCount(); 591 let effectivePageIndex = pageIndex; 592 let pageNumber = this.pageIndexToNumber(effectivePageIndex); 593 594 if (pageNumber === null) { 595 const nextPageIndex = this.pages.findIndex( 596 (page, index) => 597 index > pageIndex && 598 Number.isFinite(Number.parseInt(page.dataset.page, 10)) 599 ); 600 601 if (nextPageIndex >= 0) { 602 effectivePageIndex = nextPageIndex; 603 pageNumber = this.pageIndexToNumber(effectivePageIndex); 604 } 605 } 606 607 const visiblePageNumbers = [ 608 this.pageIndexToNumber(effectivePageIndex), 609 this.pageIndexToNumber(effectivePageIndex + 1) 610 ].filter(number => number !== null); 611 612 if ( 613 this.selectedPageNumber !== null && 614 visiblePageNumbers.includes(this.selectedPageNumber) 615 ) { 616 pageNumber = this.selectedPageNumber; 617 effectivePageIndex = this.pageNumberToIndex(pageNumber); 618 } 619 620 return { 621 pageCount, 622 effectivePageIndex, 623 pageNumber 624 }; 625 } 626 627 updateToolbar(pageIndex, options = {}) { 628 const deferHeavy = options.deferHeavy === true; 629 const { 630 pageCount, 631 effectivePageIndex, 632 pageNumber 633 } = this.resolveToolbarState(pageIndex); 634 635 this.elements.pageNumber.min = "1"; 636 this.elements.pageNumber.max = String(pageCount); 637 this.elements.pageCount.textContent = `/ ${pageCount}`; 638 639 if (pageNumber === null) { 640 return; 641 } 642
643 this.elements.pageNumber.value = String(pageNumber); 644 645 const firstPageIndex = this.pageNumberToIndex(1); 646 const lastPageIndex = this.pageNumberToIndex(pageCount); 647 648 this.elements.firstPage.disabled = 649 effectivePageIndex <= firstPageIndex; 650 this.elements.previousPage.disabled = 651 effectivePageIndex <= firstPageIndex; 652 this.elements.nextPage.disabled = 653 effectivePageIndex >= lastPageIndex; 654 this.elements.lastPage.disabled = 655 effectivePageIndex >= lastPageIndex; 656 657 this.writeHash(pageNumber); 658 659 const runHeavy = () => { 660 this.updateActiveThumbnail(effectivePageIndex); 661 this.ensureNearbyImages(effectivePageIndex); 662 663 if (this.mode === "page") { 664 document.title = 665 `Seite ${pageNumber} â Meraner Blätterzeitung`; 666 } 667 }; 668 669 /* 670 * Chrome flasht, wenn am Animationsende gleichzeitig 671 * StPageFlip die Styles zurücksetzt und wir scrollen/decoden. 672 */ 673 if (deferHeavy) { 674 window.cancelAnimationFrame(this.toolbarHeavyFrame); 675 this.toolbarHeavyFrame = window.requestAnimationFrame(() => { 676 this.toolbarHeavyFrame = window.requestAnimationFrame(runHeavy); 677 }); 678 return; 679 } 680 681 runHeavy(); 682 } 683 684 setSidebar(open) { 685 if (!this.elements.sidebar || !this.elements.toggleSidebar) { 686 return; 687 } 688 689 this.elements.sidebar.hidden = !open; 690 this.elements.toggleSidebar.setAttribute( 691 "aria-expanded", 692 String(open) 693 ); 694 695 window.setTimeout(() => this.refreshBookLayout(), 220); 696 } 697 698 toggleSidebar() { 699 this.setSidebar(this.elements.sidebar.hidden); 700 } 701 702 applyZoom(newZoom, showMessage = true) { 703 this.currentZoom = clamp( 704 newZoom, 705 CONFIG.minZoom, 706 CONFIG.maxZoom 707 ); 708 709 if (this.currentZoom <= 1) { 710 this.panX = 0; 711 this.panY = 0; 712 this.zoomOriginX = 50; 713 this.zoomOriginY = 50; 714 } 715 716 this.updateBookTransform({ resizeBook: true }); 717 718 this.elements.zoomValue.textContent = 719 this.currentZoom === 1 720 ? "Auto" 721 : `${Math.round(this.currentZoom * 100)} %`; 722 723 this.elements.zoomOut.disabled = 724 this.currentZoom <= CONFIG.minZoom; 725 this.elements.zoomIn.disabled = 726 this.currentZoom >= CONFIG.maxZoom; 727 728 if (showMessage) { 729 this.showStatus( 730 this.currentZoom === 1 731 ? "Automatisch eingepasst" 732 : `Zoom: ${Math.round(this.currentZoom * 100)} %` 733 ); 734 } 735 } 736 737 clampPan(areaWidth, areaHeight, zoomWidth, zoomHeight) { 738 const maxPanX = Math.max(0, (zoomWidth - areaWidth) / 2); 739 const maxPanY = Math.max(0, (zoomHeight - areaHeight) / 2); 740 741 this.panX = clamp(this.panX, -maxPanX, maxPanX); 742 this.panY = clamp(this.panY, -maxPanY, maxPanY); 743 } 744 745 syncPageFlipZoomBounds(zoom) { 746 this.pendingZoomBound = zoom; 747 748 if (this.zoomBoundFrame !== null) { 749 return; 750 } 751 752 this.zoomBoundFrame = requestAnimationFrame(() => { 753 this.zoomBoundFrame = null; 754 755 if ( 756 !this.pageFlip || 757 typeof this.pageFlip.getSettings !== "function" 758 ) { 759 return; 760 } 761 762 const settings = this.pageFlip.getSettings(); 763 const scale = Math.max(1, this.pendingZoomBound); 764 const maxWidth = Math.min( 765 Math.round(this.pageWidth * scale), 766 CONFIG.maxPageRenderWidth 767 ); 768 const maxHeight = Math.round(maxWidth * this.getPageAspect()); 769 770 settings.maxWidth = maxWidth; 771 settings.maxHeight = maxHeight; 772 this.pageFlip.update(); 773 }); 774 } 775 776 updateBookTransform({ resizeBook = true } = {}) { 777 const bookZoom = this.elements.bookZoom; 778 const bookArea = this.elements.bookArea; 779 const magnified = this.currentZoom > 1; 780 const atFit = this.currentZoom === 1; 781 782 bookZoom.classList.toggle("is-zoomed", magnified); 783 784 bookArea.style.cursor = magnified ? "grab" : "default"; 785 786 /* 787 * Bei Zoom übernimmt bookArea das Panning. 788 * StPageFlip darf dann keine Flip-Gesten mehr empfangen. 789 */ 790 this.elements.book.style.pointerEvents = magnified ? "none" : ""; 791 792 if (atFit) { 793 bookZoom.style.width = ""; 794 bookZoom.style.height = ""; 795 bookZoom.style.minWidth = ""; 796 bookZoom.style.minHeight = ""; 797 bookZoom.style.flexShrink = ""; 798 bookZoom.style.transform = ""; 799 800 if (resizeBook) { 801 this.syncPageFlipZoomBounds(1); 802 } 803 804 return; 805 } 806 807 const areaWidth = bookArea.clientWidth; 808 const areaHeight = bookArea.clientHeight; 809 const zoomWidth = areaWidth * this.currentZoom;
810 const zoomHeight = areaHeight * this.currentZoom; 811 812 if (resizeBook) { 813 bookZoom.style.flexShrink = "0"; 814 bookZoom.style.width = `${zoomWidth}px`; 815 bookZoom.style.height = `${zoomHeight}px`; 816 bookZoom.style.minWidth = `${zoomWidth}px`; 817 bookZoom.style.minHeight = `${zoomHeight}px`; 818 /* 819 * Verkleinern: maxWidth bleibt bei Basis. 820 * VergröÃern: maxWidth steigt mit Zoom bis zur Quellauflösung. 821 */ 822 this.syncPageFlipZoomBounds(magnified ? this.currentZoom : 1); 823 } 824 825 if (!magnified) { 826 this.panX = 0; 827 this.panY = 0; 828 bookZoom.style.transform = ""; 829 return; 830 } 831 832 const currentWidth = resizeBook 833 ? zoomWidth 834 : bookZoom.offsetWidth || zoomWidth; 835 const currentHeight = resizeBook 836 ? zoomHeight 837 : bookZoom.offsetHeight || zoomHeight; 838 839 this.clampPan(areaWidth, areaHeight, currentWidth, currentHeight); 840 841 /* 842 * Nur translate â kein scale(). Die Seite wird real gröÃer, 843 * damit der Browser aus dem Vollbild-JPG resamplet. 844 */ 845 bookZoom.style.transform = 846 `translate(${this.panX}px, ${this.panY}px)`; 847 } 848 849 zoomIn() { 850 this.applyZoom(this.currentZoom + CONFIG.zoomStep); 851 } 852 853 zoomOut() { 854 this.applyZoom(this.currentZoom - CONFIG.zoomStep); 855 } 856 857 resetZoom(showMessage = true) { 858 this.panX = 0; 859 this.panY = 0; 860 this.zoomOriginX = 50; 861 this.zoomOriginY = 50; 862 this.applyZoom(1, showMessage); 863 } 864 865 handleWheelZoom(event) { 866 if (!(event.ctrlKey || event.metaKey)) { 867 return; 868 } 869 870 event.preventDefault(); 871 872 const rect = this.elements.bookArea.getBoundingClientRect(); 873 const oldZoom = this.currentZoom; 874 const direction = event.deltaY < 0 ? 1 : -1;
875 const newZoom = clamp( 876 oldZoom + direction * 0.1, 877 CONFIG.minZoom, 878 CONFIG.maxZoom 879 ); 880 881 const localX = event.clientX - rect.left - rect.width / 2; 882 const localY = event.clientY - rect.top - rect.height / 2; 883 884 if (oldZoom > 0) { 885 const zoomRatio = newZoom / oldZoom; 886 this.panX = localX - (localX - this.panX) * zoomRatio; 887 this.panY = localY - (localY - this.panY) * zoomRatio; 888 } 889 890 this.applyZoom(newZoom); 891 } 892 893 getPointerDistance() { 894 const pointers = [...this.activePointers.values()]; 895 896 if (pointers.length < 2) { 897 return 0; 898 } 899 900 return Math.hypot( 901 pointers[1].clientX - pointers[0].clientX, 902 pointers[1].clientY - pointers[0].clientY 903 ); 904 } 905 906 shouldHandlePointer(event) { 907 if (event.pointerType === "mouse") { 908 return this.currentZoom > 1 && event.buttons === 1; 909 } 910 911 return this.activePointers.size >= 2 || this.currentZoom > 1; 912 } 913 914 handlePointerDown(event) { 915 if (event.pointerType === "mouse") { 916 if (this.currentZoom <= 1 || event.button !== 0) { 917 return; 918 } 919 920 this.isPanning = true; 921 this.panPointerId = event.pointerId; 922 this.lastPanX = event.clientX; 923 this.lastPanY = event.clientY; 924 this.elements.bookArea.setPointerCapture?.(event.pointerId); 925 this.elements.bookArea.style.cursor = "grabbing"; 926 event.preventDefault(); 927 return; 928 } 929 930 this.activePointers.set(event.pointerId, { 931 clientX: event.clientX, 932 clientY: event.clientY 933 }); 934 935 if (this.activePointers.size === 2) { 936 this.isPinching = true; 937 this.pinchStartDistance = this.getPointerDistance(); 938 this.pinchStartZoom = this.currentZoom; 939 event.preventDefault(); 940 return; 941 } 942 943 if (this.currentZoom > 1 && this.activePointers.size === 1) { 944 this.isPanning = true; 945 this.panPointerId = event.pointerId; 946 this.lastPanX = event.clientX; 947 this.lastPanY = event.clientY; 948 this.elements.bookArea.setPointerCapture?.(event.pointerId); 949 event.preventDefault(); 950 } 951 } 952 953 handlePointerMove(event) { 954 if (event.pointerType !== "mouse" && this.activePointers.has(event.pointerId)) { 955 this.activePointers.set(event.pointerId, { 956 clientX: event.clientX, 957 clientY: event.clientY 958 }); 959 } 960 961 if (this.isPinching && this.activePointers.size === 2) { 962 event.preventDefault(); 963 964 const currentDistance = this.getPointerDistance(); 965 966 if (this.pinchStartDistance <= 0 || currentDistance <= 0) { 967 return; 968 } 969 970 const pointers = [...this.activePointers.values()]; 971 const centerX = 972 (pointers[0].clientX + pointers[1].clientX) / 2; 973 const centerY = 974 (pointers[0].clientY + pointers[1].clientY) / 2; 975 const rect = this.elements.bookArea.getBoundingClientRect(); 976 const oldZoom = this.currentZoom;
977 const newZoom = clamp( 978 this.pinchStartZoom * 979 (currentDistance / this.pinchStartDistance), 980 CONFIG.minZoom, 981 CONFIG.maxZoom 982 ); 983 984 const localX = centerX - rect.left - rect.width / 2; 985 const localY = centerY - rect.top - rect.height / 2; 986 987 if (oldZoom > 0) { 988 const zoomRatio = newZoom / oldZoom; 989 this.panX = localX - (localX - this.panX) * zoomRatio; 990 this.panY = localY - (localY - this.panY) * zoomRatio; 991 } 992 993 this.applyZoom(newZoom, false); 994 return; 995 } 996 997 if ( 998 this.isPanning && 999 this.panPointerId === event.pointerId && 1000 this.currentZoom > 1 1001 ) { 1002 event.preventDefault(); 1003 1004 this.panX += event.clientX - this.lastPanX; 1005 this.panY += event.clientY - this.lastPanY; 1006 this.lastPanX = event.clientX; 1007 this.lastPanY = event.clientY; 1008 this.updateBookTransform({ resizeBook: false }); 1009 } 1010 } 1011 1012 handlePointerEnd(event) { 1013 if (event.pointerType !== "mouse") { 1014 this.activePointers.delete(event.pointerId); 1015 } 1016 1017 try { 1018 this.elements.bookArea.releasePointerCapture?.( 1019 event.pointerId 1020 ); 1021 } catch { 1022 // already released 1023 } 1024 1025 if (this.activePointers.size < 2) { 1026 this.isPinching = false; 1027 this.pinchStartDistance = 0; 1028 this.pinchStartZoom = this.currentZoom; 1029 } 1030 1031 if (this.panPointerId === event.pointerId) { 1032 this.isPanning = false; 1033 this.panPointerId = null; 1034 this.elements.bookArea.style.cursor = 1035 this.currentZoom > 1 ? "grab" : "default"; 1036 } 1037 } 1038 1039 1040 async toggleFullscreen() { 1041 try { 1042 if (!document.fullscreenElement) { 1043 await this.elements.viewer.requestFullscreen(); 1044 } else { 1045 await document.exitFullscreen(); 1046 } 1047 } catch (error) { 1048 console.error(error); 1049 this.showStatus("Vollbild konnte nicht aktiviert werden."); 1050 } 1051 } 1052 1053 updateFullscreenButton() { 1054 const isFullscreen = Boolean(document.fullscreenElement); 1055 1056 this.elements.fullscreen.title = isFullscreen 1057 ? "Vollbild verlassen" 1058 : "Vollbild"; 1059 1060 this.elements.fullscreen.setAttribute( 1061 "aria-label", 1062 isFullscreen 1063 ? "Vollbild verlassen" 1064 : "Vollbild einschalten" 1065 ); 1066 1067 window.setTimeout(() => this.refreshBookLayout(), 100); 1068 } 1069 1070 refreshBookLayout() { 1071 cancelAnimationFrame(this.resizeFrame); 1072 1073 if (this.fitToViewport) { 1074 const isFullscreen = 1075 document.fullscreenElement === this.elements.viewer; 1076 const viewerTop = isFullscreen 1077 ? 0 1078 : Math.max( 1079 0, 1080 this.elements.viewer.getBoundingClientRect().top 1081 ); 1082 const availableHeight = isFullscreen 1083 ? window.innerHeight 1084 : Math.max(280, window.innerHeight - viewerTop - 16); 1085 1086 this.elements.viewer.style.height = 1087 `${Math.floor(availableHeight)}px`; 1088 } else { 1089 this.elements.viewer.style.height = "100%"; 1090 } 1091 1092 this.resizeFrame = requestAnimationFrame(() => { 1093 /* 1094 * Bei Layout-Zoom GröÃe neu aus dem Viewport ableiten, 1095 * sonst nur StPageFlip neu vermessen. 1096 */ 1097 if (this.currentZoom !== 1) { 1098 this.updateBookTransform({ resizeBook: true }); 1099 } else { 1100 this.pageFlip?.update(); 1101 } 1102 }); 1103 } 1104 1105 scheduleRefreshBookLayout() { 1106 window.clearTimeout(this.resizeTimer); 1107 this.resizeTimer = window.setTimeout(() => { 1108 this.refreshBookLayout(); 1109 }, 80); 1110 } 1111 1112 isInputElement(element) { 1113 return ( 1114 element instanceof HTMLInputElement || 1115 element instanceof HTMLTextAreaElement || 1116 element instanceof HTMLSelectElement 1117 ); 1118 } 1119 1120 handleKeyboard(event) { 1121 if (this.isInputElement(event.target)) { 1122 return; 1123 } 1124 1125 if ( 1126 this.mode === "lightbox" && 1127 (event.key === "Escape" || event.key === "Esc") 1128 ) { 1129 event.preventDefault(); 1130 MeranerPageFlip.closeLightbox(); 1131 return; 1132 } 1133 1134 switch (event.key) { 1135 case "ArrowLeft": 1136 case "PageUp": 1137 event.preventDefault(); 1138 this.goToPreviousPage(); 1139 break; 1140 case "ArrowRight": 1141 case "PageDown": 1142 event.preventDefault(); 1143 this.goToNextPage(); 1144 break; 1145 case "Home": 1146 event.preventDefault(); 1147 this.goToFirstPage(); 1148 break; 1149 case "End": 1150 event.preventDefault(); 1151 this.goToLastPage(); 1152 break; 1153 case "+": 1154 event.preventDefault(); 1155 this.zoomIn(); 1156 break; 1157 case "-": 1158 event.preventDefault(); 1159 this.zoomOut(); 1160 break; 1161 case "0": 1162 event.preventDefault(); 1163 this.resetZoom(); 1164 break; 1165 case "f": 1166 case "F": 1167 event.preventDefault(); 1168 this.toggleFullscreen(); 1169 break; 1170 } 1171 } 1172 1173 primeImage(image) { 1174 if (!image || image.dataset.primed === "1") { 1175 return Promise.resolve(); 1176 } 1177 1178 image.loading = "eager"; 1179 1180 if (image.dataset.src && !image.getAttribute("src")) { 1181 image.src = image.dataset.src; 1182 } 1183 1184 const markPrimed = () => { 1185 image.dataset.primed = "1"; 1186 }; 1187 1188 const decodeImage = () => { 1189 if (typeof image.decode !== "function") { 1190 markPrimed(); 1191 return Promise.resolve(); 1192 } 1193 1194 return image.decode().then(markPrimed).catch(markPrimed); 1195 }; 1196
1197 if (image.complete && image.naturalWidth > 0) { 1198 return decodeImage(); 1199 } 1200 1201 return new Promise(resolve => { 1202 const finish = () => { 1203 decodeImage().then(resolve); 1204 }; 1205 1206 image.addEventListener("load", finish, { once: true }); 1207 image.addEventListener( 1208 "error", 1209 () => { 1210 markPrimed(); 1211 resolve(); 1212 }, 1213 { once: true } 1214 ); 1215 }); 1216 } 1217 1218 ensureNearbyImages(centerIndex, ahead = 6) { 1219 const start = Math.max(0, centerIndex - 2); 1220 const end = Math.min(this.pages.length - 1, centerIndex + ahead); 1221 1222 for (let index = start; index <= end; index += 1) { 1223 const image = this.pages[index]?.querySelector("img"); 1224 this.primeImage(image); 1225 } 1226 } 1227 1228 startBackgroundImagePrime() { 1229 if (this.backgroundPrimeStarted) { 1230 return; 1231 } 1232 1233 this.backgroundPrimeStarted = true; 1234 1235 let index = 0; 1236 1237 const step = () => { 1238 if (!this.pages.length || index >= this.pages.length) { 1239 return; 1240 } 1241 1242 const image = this.pages[index]?.querySelector("img"); 1243 index += 1; 1244 1245 const scheduleNext = () => { 1246 if (typeof window.requestIdleCallback === "function") { 1247 this.backgroundPrimeIdle = window.requestIdleCallback( 1248 step, 1249 { timeout: 500 } 1250 ); 1251 } else { 1252 this.backgroundPrimeTimer = window.setTimeout(step, 80); 1253 } 1254 }; 1255 1256 this.primeImage(image).finally(scheduleNext); 1257 }; 1258 1259 step(); 1260 } 1261 1262 async waitForNearbyImages() { 1263 const startIndex = Math.max( 1264 0, 1265 this.pageNumberToIndex(this.getPageNumberFromHash()) 1266 ); 1267 1268 const indexes = [ 1269 startIndex, 1270 startIndex + 1, 1271 startIndex + 2, 1272 startIndex + 3, 1273 startIndex + 4 1274 ].filter(index => index >= 0 && index < this.pages.length); 1275 1276 await Promise.allSettled( 1277 indexes.map(index => { 1278 const image = this.pages[index]?.querySelector("img"); 1279 return this.primeImage(image); 1280 }) 1281 ); 1282 } 1283 1284 initializePageFlip() { 1285 const minWidth = 280; 1286 const minHeight = Math.max( 1287 1, 1288 Math.round(minWidth * this.getPageAspect()) 1289 ); 1290 1291 this.pageFlip = new St.PageFlip(this.elements.book, { 1292 width: this.pageWidth, 1293 height: this.pageHeight, 1294 size: "stretch", 1295 minWidth, 1296 maxWidth: this.pageWidth, 1297 minHeight, 1298 maxHeight: this.pageHeight, 1299 maxShadowOpacity: 0.35, 1300 showCover: CONFIG.showCover, 1301 mobileScrollSupport: false, 1302 disableFlipByClick: false, 1303 usePortrait: true, 1304 flippingTime: CONFIG.flippingTime, 1305 drawShadow: true, 1306 autoSize: false, 1307 startZIndex: 0, 1308 useMouseEvents: true, 1309 swipeDistance: 30 1310 }); 1311 1312 this.pageFlip.loadFromHTML(this.pages); 1313 1314 this.pageFlip.on("flip", event => { 1315 this.updateToolbar(event.data, { deferHeavy: true }); 1316 }); 1317 1318 this.pageFlip.on("changeState", event => { 1319 if (!this.pageFlip) { 1320 return; 1321 } 1322 1323 const currentIndex = this.pageFlip.getCurrentPageIndex(); 1324 1325 /* 1326 * Schon beim Anfassen/Blättern die nächsten Seiten decodieren, 1327 * bevor Chrome sie das erste Mal einblendet. 1328 */ 1329 if ( 1330 event.data === "user_fold" || 1331 event.data === "fold_corner" || 1332 event.data === "flipping" 1333 ) { 1334 this.ensureNearbyImages(currentIndex, 8); 1335 return; 1336 } 1337 1338 if (event.data === "read") { 1339 this.updateToolbar(currentIndex, { 1340 deferHeavy: true 1341 }); 1342 } 1343 }); 1344 1345 this.pageFlip.on("changeOrientation", () => { 1346 window.setTimeout(() => { 1347 this.updateToolbar(this.pageFlip.getCurrentPageIndex()); 1348 this.refreshBookLayout(); 1349 }, 50); 1350 }); 1351 1352 const startPage = this.updateHash 1353 ? this.getPageNumberFromHash() 1354 : 1; 1355 1356 this.pageFlip.turnToPage(this.pageNumberToIndex(startPage)); 1357 this.updateToolbar(this.pageFlip.getCurrentPageIndex()); 1358 } 1359 1360 bindEvents() { 1361 const on = (element, type, handler, options) => { 1362 if (!element) { 1363 return; 1364 } 1365 1366 element.addEventListener(type, handler, options); 1367 }; 1368 1369 on(this.elements.toggleSidebar, "click", () => this.toggleSidebar()); 1370 on(this.elements.firstPage, "click", () => this.goToFirstPage()); 1371 on(this.elements.previousPage, "click", () => this.goToPreviousPage()); 1372 on(this.elements.nextPage, "click", () => this.goToNextPage()); 1373 on(this.elements.lastPage, "click", () => this.goToLastPage()); 1374 on(this.elements.zoomIn, "click", () => this.zoomIn()); 1375 on(this.elements.zoomOut, "click", () => this.zoomOut()); 1376 on(this.elements.resetZoom, "click", () => this.resetZoom()); 1377 on(this.elements.fullscreen, "click", () => this.toggleFullscreen()); 1378 1379 on(this.elements.pageForm, "submit", event => { 1380 event.preventDefault(); 1381 /* 1382 * Direkt springen (ohne Flip-Animation): zuverlässiger bei 1383 * Seitensprüngen und vermeidet Doppel-flip durch change+submit. 1384 */ 1385 this.goToPageNumber(this.elements.pageNumber.value, false); 1386 this.elements.pageNumber.select(); 1387 }); 1388 1389 on(this.elements.pageNumber, "change", () => { 1390 this.goToPageNumber(this.elements.pageNumber.value, false); 1391 }); 1392
1393 on(this.elements.pageNumber, "focus", () => { 1394 this.elements.pageNumber.select(); 1395 }); 1396 1397 this.boundHandlers.pointerDown = event => this.handlePointerDown(event); 1398 this.boundHandlers.pointerMove = event => this.handlePointerMove(event); 1399 this.boundHandlers.pointerEnd = event => this.handlePointerEnd(event); 1400 this.boundHandlers.wheel = event => this.handleWheelZoom(event); 1401 this.boundHandlers.keyboard = event => this.handleKeyboard(event); 1402 this.boundHandlers.fullscreen = () => this.updateFullscreenButton(); 1403 this.boundHandlers.resize = () => this.scheduleRefreshBookLayout(); 1404 this.boundHandlers.hashchange = () => { 1405 if (!this.updateHash) { 1406 return; 1407 } 1408 1409 const pageNumber = this.getPageNumberFromHash(); 1410 1411 if (pageNumber !== this.getCurrentPageNumber()) { 1412 this.goToPageNumber(pageNumber, false); 1413 } 1414 }; 1415 1416 on( 1417 this.elements.bookArea, 1418 "pointerdown", 1419 this.boundHandlers.pointerDown 1420 ); 1421 on( 1422 this.elements.bookArea, 1423 "pointermove", 1424 this.boundHandlers.pointerMove, 1425 { passive: false } 1426 ); 1427 on( 1428 this.elements.bookArea, 1429 "pointerup", 1430 this.boundHandlers.pointerEnd 1431 ); 1432 on( 1433 this.elements.bookArea, 1434 "pointercancel", 1435 this.boundHandlers.pointerEnd 1436 ); 1437 on( 1438 this.elements.bookArea, 1439 "wheel", 1440 this.boundHandlers.wheel, 1441 { passive: false } 1442 ); 1443 1444 document.addEventListener( 1445 "fullscreenchange", 1446 this.boundHandlers.fullscreen 1447 ); 1448 document.addEventListener( 1449 "keydown", 1450 this.boundHandlers.keyboard 1451 ); 1452 window.addEventListener("resize", this.boundHandlers.resize); 1453 window.addEventListener( 1454 "orientationchange", 1455 this.boundHandlers.resize 1456 ); 1457 window.addEventListener( 1458 "hashchange", 1459 this.boundHandlers.hashchange 1460 ); 1461 } 1462 1463 destroy() { 1464 window.clearTimeout(this.statusTimer); 1465 window.clearTimeout(this.resizeTimer); 1466 window.clearTimeout(this.backgroundPrimeTimer); 1467 1468 if ( 1469 this.backgroundPrimeIdle !== null && 1470 typeof window.cancelIdleCallback === "function" 1471 ) { 1472 window.cancelIdleCallback(this.backgroundPrimeIdle); 1473 } 1474 1475 cancelAnimationFrame(this.resizeFrame); 1476 cancelAnimationFrame(this.toolbarHeavyFrame); 1477 cancelAnimationFrame(this.zoomBoundFrame); 1478 this.zoomBoundFrame = null; 1479 1480 document.removeEventListener( 1481 "fullscreenchange", 1482 this.boundHandlers.fullscreen 1483 ); 1484 document.removeEventListener( 1485 "keydown", 1486 this.boundHandlers.keyboard 1487 ); 1488 window.removeEventListener("resize", this.boundHandlers.resize); 1489 window.removeEventListener( 1490 "orientationchange", 1491 this.boundHandlers.resize 1492 ); 1493 window.removeEventListener( 1494 "hashchange", 1495 this.boundHandlers.hashchange 1496 ); 1497 1498 if (this.pageFlip) { 1499 try { 1500 this.pageFlip.destroy(); 1501 } catch { 1502 // ignore library cleanup errors 1503 } 1504 1505 this.pageFlip = null; 1506 } 1507 } 1508 } 1509 1510 const MeranerPageFlip = { 1511 instance: null, 1512 lightboxHost: null, 1513 lightboxInstance: null, 1514 assetsPromise: null, 1515 1516 async ensureAssets() { 1517 if (typeof St !== "undefined" && typeof St.PageFlip === "function") { 1518 return; 1519 } 1520 1521 if (this.assetsPromise) { 1522 return this.assetsPromise; 1523 } 1524 1525 this.assetsPromise = new Promise((resolve, reject) => { 1526 const existing = document.querySelector( 1527 'script[src="/js/page-flip.browser.js"]' 1528 ); 1529 1530 if (existing) { 1531 existing.addEventListener("load", resolve, { once: true }); 1532 existing.addEventListener( 1533 "error", 1534 () => reject(new Error("StPageFlip konnte nicht geladen werden.")), 1535 { once: true } 1536 ); 1537 return; 1538 } 1539 1540 const script = document.createElement("script"); 1541 script.src = "/js/page-flip.browser.js"; 1542 script.onload = resolve; 1543 script.onerror = () => 1544 reject(new Error("StPageFlip konnte nicht geladen werden.")); 1545 document.head.appendChild(script); 1546 }); 1547 1548 return this.assetsPromise; 1549 }, 1550 1551 ensureStyles() { 1552 if (document.querySelector('link[href="/css/page-flip.meraner.css"]')) { 1553 return; 1554 } 1555 1556 const link = document.createElement("link"); 1557 link.rel = "stylesheet"; 1558 link.href = "/css/page-flip.meraner.css"; 1559 document.head.appendChild(link); 1560 }, 1561
1562 async init(root = document.getElementById("viewer"), options = {}) { 1563 if (!root) { 1564 throw new Error("Viewer-Root nicht gefunden."); 1565 } 1566 1567 this.ensureStyles(); 1568 await this.ensureAssets(); 1569 1570 if (this.instance) { 1571 this.instance.destroy(); 1572 this.instance = null; 1573 } 1574 1575 this.instance = new MeranerPageFlipInstance(root, { 1576 mode: "page", 1577 updateHash: true, 1578 fitToViewport: true, 1579 ...options 1580 }); 1581 1582 await this.instance.init(); 1583 return this.instance; 1584 }, 1585 1586 collectPagesFromTrigger(trigger) { 1587 const source = trigger.querySelector(".pageflip-source, #book1, [data-pageflip-source]"); 1588 1589 if (!source) { 1590 return []; 1591 } 1592 1593 return Array.from( 1594 source.querySelectorAll("[data-src], [data-image], img[src]") 1595 ).map((node, index) => { 1596 const src = 1597 node.getAttribute("data-src") || 1598 node.getAttribute("data-image") || 1599 node.getAttribute("src"); 1600 1601 return { 1602 src: src.startsWith("//") 1603 ? `${window.location.protocol}${src}` 1604 : src, 1605 page: Number.parseInt( 1606 node.getAttribute("data-page") || String(index + 1), 1607 10 1608 ) 1609 }; 1610 }).filter(page => Boolean(page.src)); 1611 }, 1612 1613 buildLightboxHost() { 1614 if (this.lightboxHost) { 1615 this.lightboxHost.remove(); 1616 this.lightboxHost = null; 1617 } 1618 1619 const host = document.createElement("div"); 1620 host.className = "pageflip-lightbox"; 1621 host.id = "pageflip-lightbox"; 1622 host.innerHTML = ` 1623 <div class="pageflip-lightbox__overlay" data-pageflip-dismiss></div> 1624 <div class="pageflip-lightbox__panel"> 1625 <button type="button" class="pageflip-lightbox__close" data-pageflip-dismiss title="SchlieÃen" aria-label="Blätterzeitung schlieÃen"> 1626 ${ICON_CLOSE} 1627 </button> 1628 ${createViewerMarkup()} 1629 </div> 1630 `; 1631 1632 document.body.appendChild(host); 1633 this.lightboxHost = host; 1634 1635 host.querySelectorAll("[data-pageflip-dismiss]").forEach(node => { 1636 node.addEventListener("click", () => this.closeLightbox()); 1637 }); 1638 1639 return host; 1640 }, 1641 1642 async openLightbox(pagesOrTrigger) { 1643 this.ensureStyles(); 1644 await this.ensureAssets(); 1645 1646 const pages = Array.isArray(pagesOrTrigger) 1647 ? pagesOrTrigger 1648 : this.collectPagesFromTrigger(pagesOrTrigger); 1649 1650 if (!pages.length) { 1651 console.error("Keine Seiten für die Lightbox gefunden."); 1652 return null; 1653 } 1654 1655 if (this.lightboxInstance) { 1656 this.lightboxInstance.destroy(); 1657 this.lightboxInstance = null; 1658 } 1659 1660 const host = this.buildLightboxHost(); 1661 const root = host.querySelector("[data-pageflip-root]"); 1662 1663 host.classList.add("is-open"); 1664 document.body.classList.add("pageflip-lightbox-on"); 1665 1666 this.lightboxInstance = new MeranerPageFlipInstance(root, { 1667 mode: "lightbox", 1668 updateHash: false, 1669 fitToViewport: false 1670 }); 1671 1672 await this.lightboxInstance.init(pages); 1673 this.lightboxInstance.refreshBookLayout(); 1674 return this.lightboxInstance; 1675 }, 1676 1677 closeLightbox() { 1678 if (this.lightboxInstance) { 1679 this.lightboxInstance.destroy(); 1680 this.lightboxInstance = null; 1681 } 1682 1683 if (this.lightboxHost) { 1684 this.lightboxHost.remove(); 1685 this.lightboxHost = null; 1686 } 1687 1688 document.body.classList.remove("pageflip-lightbox-on"); 1689 1690 if (document.fullscreenElement) { 1691 document.exitFullscreen().catch(() => {}); 1692 } 1693 }, 1694 1695 bindTriggers(selector = "[data-pageflip-trigger], #book1-trigger, #book2-trigger, #book3-trigger") { 1696 document.querySelectorAll(selector).forEach(trigger => { 1697 if (trigger.dataset.pageflipBound === "1") { 1698 return; 1699 } 1700 1701 const pages = this.collectPagesFromTrigger(trigger); 1702 1703 if (!pages.length) { 1704 return; 1705 } 1706 1707 trigger.dataset.pageflipBound = "1"; 1708 trigger.classList.add("pageflip-trigger"); 1709 1710 trigger.addEventListener("click", event => { 1711 event.preventDefault(); 1712 this.openLightbox(pages); 1713 }); 1714 }); 1715 } 1716 }; 1717 1718 window.MeranerPageFlip = MeranerPageFlip; 1719 1720 async function bootPageMode() { 1721 const root = document.getElementById("viewer"); 1722 1723 if (!root || root.dataset.pageflipMode === "lightbox-only") { 1724 return; 1725 } 1726 1727 const book = root.querySelector("#book, [data-book]"); 1728 1729 if (!book || !book.querySelector(".book-page")) { 1730 return; 1731 } 1732 1733 try { 1734 await MeranerPageFlip.init(root); 1735 } catch (error) { 1736 console.error(error); 1737 1738 const loading = root.querySelector("#loading, [data-loading]"); 1739 1740 if (loading) { 1741 loading.hidden = false;
1742 loading.innerHTML = ` 1743 <div class="loading__box"> 1744 Die Ausgabe konnte nicht geladen werden.<br> 1745 ${error.message} 1746 </div> 1747 `; 1748 } 1749 } 1750 } 1751 1752 document.addEventListener("DOMContentLoaded", () => { 1753 MeranerPageFlip.bindTriggers(); 1754 bootPageMode(); 1755 }); 1756 1757 window.addEventListener("load", () => { 1758 MeranerPageFlip.instance?.refreshBookLayout(); 1759 MeranerPageFlip.lightboxInstance?.refreshBookLayout(); 1760 }); 1761})(window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.