dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
gallery.js
Go to the documentation of this file.
1/*!
2 * dbxapp gallery.js
3 * Lightweight image gallery and lightbox for rendered CMS content.
4 */
5(function (window, document) {
6 "use strict";
7
8 if (!window.dbx || !window.dbx.feature) {
9 console.error("[dbx][gallery] dbx core missing");
10 return;
11 }
12
13 const dbx = window.dbx;
14 const LIB = "gallery";
15 let lightbox = null;
16
17 const VENDOR_PACKS = {
18 swiper: [
19 ["css", "root", "vendor/npm/node_modules/swiper/swiper-bundle.min.css"],
20 ["js", "root", "vendor/npm/node_modules/swiper/swiper-bundle.min.js"]
21 ],
22 viewerjs: [
23 ["css", "root", "vendor/npm/node_modules/viewerjs/dist/viewer.min.css"],
24 ["js", "root", "vendor/npm/node_modules/viewerjs/dist/viewer.min.js"]
25 ],
26 blueimp: [
27 ["css", "root", "vendor/npm/node_modules/blueimp-gallery/css/blueimp-gallery.min.css"],
28 ["js", "root", "vendor/npm/node_modules/blueimp-gallery/js/blueimp-gallery.min.js"],
29 ["js", "root", "vendor/npm/node_modules/blueimp-gallery/js/blueimp-gallery-video.js"]
30 ],
31 photoswipe: [
32 ["css", "root", "vendor/npm/node_modules/photoswipe/dist/photoswipe.css"],
33 ["js", "root", "vendor/npm/node_modules/photoswipe/dist/umd/photoswipe.umd.min.js"]
34 ],
35 openseadragon: [
36 ["js", "root", "vendor/npm/node_modules/openseadragon/build/openseadragon/openseadragon.min.js"]
37 ]
38 };
39
40 const vendorWaiters = {};
41
42 function vendorKeyForMode(mode) {
43 const value = String(mode || "").toLowerCase();
44 if (value.indexOf("swiper-") === 0) return "swiper";
45 if (value === "viewerjs") return "viewerjs";
46 if (value === "blueimp") return "blueimp";
47 if (value === "photoswipe") return "photoswipe";
48 if (value === "deepzoom") return "openseadragon";
49 return "";
50 }
51
52 function ensureVendor(mode, done) {
53 const key = vendorKeyForMode(mode);
54 if (!key || !VENDOR_PACKS[key]) {
55 if (done) done();
56 return;
57 }
58
59 if (vendorWaiters[key]) {
60 if (done) vendorWaiters[key].push(done);
61 return;
62 }
63
64 const queue = done ? [done] : [];
65 vendorWaiters[key] = queue;
66
67 dbx.load(VENDOR_PACKS[key], function () {
68 const callbacks = vendorWaiters[key] || [];
69 delete vendorWaiters[key];
70 callbacks.forEach(fn => fn && fn());
71 });
72 }
73
74 function qsa(root, sel) {
75 return root ? Array.from(root.querySelectorAll(sel)) : [];
76 }
77
78 function closestElement(target, selector) {
79 if (!target) return null;
80 const el = target.nodeType === 1 ? target : target.parentElement;
81 return el && el.closest ? el.closest(selector) : null;
82 }
83
84 function ensureLightbox() {
85 if (lightbox && lightbox.isConnected) return lightbox;
86 let box = document.querySelector("[data-dbx-gallery-lightbox]");
87 if (box) {
88 lightbox = box;
89 normalizeLightboxMarkup(box);
90 cacheLightboxRefs(box);
91 return box;
92 }
93
94 box = document.createElement("div");
95 box.className = "dbx-gallery-lightbox";
96 box.setAttribute("data-dbx-gallery-lightbox", "1");
97 box.hidden = true;
98 box.innerHTML = `
99 <figure class="dbx-gallery-lightbox-figure">
100 <div class="dbx-gallery-lightbox-stage">
101 <button type="button" class="dbx-gallery-lightbox-nav dbx-gallery-lightbox-prev" data-dbx-gallery-prev title="Vorheriges Bild"><i class="bi bi-chevron-left"></i></button>
102 <button type="button" class="dbx-gallery-lightbox-close" data-dbx-gallery-close title="Schliessen"><i class="bi bi-x-lg"></i></button>
103 <img src="" alt="" data-dbx-gallery-image>
104 <video controls playsinline preload="metadata" data-dbx-gallery-video hidden></video>
105 <iframe src="" title="" loading="lazy" allowfullscreen data-dbx-gallery-frame hidden></iframe>
106 <button type="button" class="dbx-gallery-lightbox-nav dbx-gallery-lightbox-next" data-dbx-gallery-next title="Naechstes Bild"><i class="bi bi-chevron-right"></i></button>
107 </div>
108 <figcaption data-dbx-gallery-caption></figcaption>
109 </figure>`;
110 document.body.appendChild(box);
111 normalizeLightboxMarkup(box);
112 cacheLightboxRefs(box);
113 bindLightboxGestures(box);
114 lightbox = box;
115 return box;
116 }
117
118 function normalizeLightboxMarkup(box) {
119 if (!box) return;
120 const figure = box.querySelector(".dbx-gallery-lightbox-figure");
121 if (!figure) return;
122 let stage = figure.querySelector(".dbx-gallery-lightbox-stage");
123 if (!stage) {
124 stage = document.createElement("div");
125 stage.className = "dbx-gallery-lightbox-stage";
126 const caption = figure.querySelector("[data-dbx-gallery-caption]");
127 figure.insertBefore(stage, caption || figure.firstChild);
128 }
129 qsa(box, "[data-dbx-gallery-prev], [data-dbx-gallery-next], [data-dbx-gallery-close], [data-dbx-gallery-image], [data-dbx-gallery-video], [data-dbx-gallery-frame]").forEach(el => {
130 if (el.parentElement !== stage) stage.appendChild(el);
131 });
132 }
133
134 function cacheLightboxRefs(box) {
135 if (!box || box.__dbxGalleryRefs) return;
136 box.__dbxGalleryRefs = {
137 stage: box.querySelector(".dbx-gallery-lightbox-stage"),
138 image: box.querySelector("[data-dbx-gallery-image]"),
139 video: box.querySelector("[data-dbx-gallery-video]"),
140 frame: box.querySelector("[data-dbx-gallery-frame]"),
141 caption: box.querySelector("[data-dbx-gallery-caption]")
142 };
143 }
144
145 function resetZoom(box) {
146 if (!box) return;
147 box.__dbxGalleryZoom = 1;
148 box.__dbxGalleryPanX = 0;
149 box.__dbxGalleryPanY = 0;
150 applyZoom(box);
151 }
152
153 function activeMedia(box) {
154 const refs = box && box.__dbxGalleryRefs || {};
155 return [refs.image, refs.video].find(el => el && !el.hidden) || null;
156 }
157
158 function applyZoom(box) {
159 const media = activeMedia(box);
160 if (!media) return;
161 const zoom = Number(box.__dbxGalleryZoom || 1);
162 const panX = Number(box.__dbxGalleryPanX || 0);
163 const panY = Number(box.__dbxGalleryPanY || 0);
164 media.style.transform = zoom > 1 ? `translate3d(${panX}px, ${panY}px, 0) scale(${zoom})` : "";
165 media.style.cursor = zoom > 1 ? "grab" : "";
166 }
167
168 function setZoom(box, zoom, origin) {
169 if (!box) return;
170 const previous = Number(box.__dbxGalleryZoom || 1);
171 const next = Math.max(1, Math.min(6, zoom));
172 box.__dbxGalleryZoom = next;
173 if (next === 1) {
174 box.__dbxGalleryPanX = 0;
175 box.__dbxGalleryPanY = 0;
176 } else if (origin && previous > 0) {
177 box.__dbxGalleryPanX = Number(box.__dbxGalleryPanX || 0) * (next / previous);
178 box.__dbxGalleryPanY = Number(box.__dbxGalleryPanY || 0) * (next / previous);
179 }
180 applyZoom(box);
181 }
182
183 function overlayMode(settings) {
184 return cleanToken(settings && settings.click || "lightbox", "lightbox");
185 }
186
187 function bindLightboxGestures(box) {
188 if (!box || box.__dbxGalleryGesturesBound) return;
189 box.__dbxGalleryGesturesBound = true;
190 let startX = 0;
191 let startY = 0;
192 let lastX = 0;
193 let lastY = 0;
194 let tracking = false;
195 let panning = false;
196 let pinchStartDistance = 0;
197 let pinchStartZoom = 1;
198 const pointers = new Map();
199
200 box.addEventListener("pointerdown", e => {
201 if (closestElement(e.target, "button")) return;
202 pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
203 if (pointers.size === 2) {
204 const pts = Array.from(pointers.values());
205 pinchStartDistance = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
206 pinchStartZoom = Number(box.__dbxGalleryZoom || 1);
207 tracking = false;
208 return;
209 }
210 tracking = true;
211 panning = Number(box.__dbxGalleryZoom || 1) > 1;
212 startX = e.clientX;
213 startY = e.clientY;
214 lastX = e.clientX;
215 lastY = e.clientY;
216 }, { passive: true });
217
218 box.addEventListener("pointermove", e => {
219 if (pointers.has(e.pointerId)) pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
220 if (pointers.size === 2 && pinchStartDistance > 0) {
221 const pts = Array.from(pointers.values());
222 const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
223 setZoom(box, pinchStartZoom * (dist / pinchStartDistance), true);
224 return;
225 }
226 if (!panning) return;
227 box.__dbxGalleryPanX = Number(box.__dbxGalleryPanX || 0) + (e.clientX - lastX);
228 box.__dbxGalleryPanY = Number(box.__dbxGalleryPanY || 0) + (e.clientY - lastY);
229 lastX = e.clientX;
230 lastY = e.clientY;
231 applyZoom(box);
232 }, { passive: true });
233
234 box.addEventListener("pointerup", e => {
235 pointers.delete(e.pointerId);
236 if (panning) {
237 panning = false;
238 tracking = false;
239 return;
240 }
241 if (!tracking) return;
242 tracking = false;
243 const dx = e.clientX - startX;
244 const dy = e.clientY - startY;
245 if (Math.abs(dx) < 48 || Math.abs(dx) < Math.abs(dy) * 1.35) return;
246 move(dx < 0 ? 1 : -1);
247 }, { passive: true });
248
249 box.addEventListener("pointercancel", e => {
250 pointers.delete(e.pointerId);
251 tracking = false;
252 panning = false;
253 }, { passive: true });
254
255 box.addEventListener("wheel", e => {
256 const media = activeMedia(box);
257 if (!media || (media.tagName || "").toLowerCase() === "video") return;
258 e.preventDefault();
259 const current = Number(box.__dbxGalleryZoom || 1);
260 const factor = e.deltaY < 0 ? 1.16 : 0.86;
261 setZoom(box, current * factor, true);
262 }, { passive: false });
263
264 box.addEventListener("dblclick", e => {
265 if (closestElement(e.target, "button")) return;
266 e.preventDefault();
267 setZoom(box, Number(box.__dbxGalleryZoom || 1) > 1 ? 1 : 2.4, true);
268 });
269 }
270
271 function itemData(item) {
272 if (item.__dbxGalleryData) return item.__dbxGalleryData;
273 const img = item.querySelector("img");
274 const video = item.querySelector("video");
275 const frame = item.querySelector("iframe");
276 const type = item.getAttribute("data-media-type") || (frame ? "external_video" : (video ? "video" : "image"));
277 item.__dbxGalleryData = {
278 href: item.getAttribute("href") || (frame ? frame.getAttribute("src") : "") || (video ? video.getAttribute("src") : "") || (img ? (img.getAttribute("data-full-src") || img.getAttribute("src")) : ""),
279 poster: item.getAttribute("data-poster") || (img ? img.getAttribute("src") : "") || (video ? video.getAttribute("poster") : ""),
280 type,
281 title: item.getAttribute("data-title") || (img ? img.getAttribute("alt") : ""),
282 caption: item.getAttribute("data-caption") || "",
283 width: Number(item.getAttribute("data-width") || (img ? (img.naturalWidth || img.getAttribute("width")) : 0) || 0),
284 height: Number(item.getAttribute("data-height") || (img ? (img.naturalHeight || img.getAttribute("height")) : 0) || 0)
285 };
286 return item.__dbxGalleryData;
287 }
288
289 function collectItems(root) {
290 return qsa(root, "[data-dbx-gallery-item]").filter(item => item.getAttribute("href") || item.querySelector("img,video,iframe"));
291 }
292
293 function itemsFor(root) {
294 if (!root) return [];
295 if (!root.__dbxGalleryItems) root.__dbxGalleryItems = collectItems(root);
296 return root.__dbxGalleryItems;
297 }
298
299 function preloadNext(root, index) {
300 const items = itemsFor(root);
301 if (items.length < 2 || !window.Image) return;
302 const next = items[(index + 1) % items.length];
303 const data = itemData(next);
304 if (!data.href || data.type === "video") return;
305 const img = new Image();
306 img.decoding = "async";
307 img.src = data.href;
308 }
309
310 function cleanToken(value, fallback) {
311 const token = String(value || fallback || "")
312 .toLowerCase()
313 .replace(/[^a-z0-9_-]+/g, "-")
314 .replace(/^-+|-+$/g, "");
315 return token || fallback || "default";
316 }
317
318 function cssLength(value, fallback) {
319 const raw = String(value || fallback || "").trim();
320 if (!raw || raw.toLowerCase() === "parent") return fallback;
321 if (/^[0-9]+(?:\.[0-9]+)?$/.test(raw)) return raw === "0" ? "0" : raw + "px";
322 if (/^[a-z0-9\s#.,%()+\-\/_]+$/i.test(raw) && !/[;"<>]/.test(raw)) return raw;
323 return fallback;
324 }
325
326 function optionToken(value, fallback, allowed) {
327 const token = cleanToken(value || fallback, fallback);
328 return allowed.includes(token) ? token : fallback;
329 }
330
331 function imageSizeToken(value) {
332 const token = cleanToken(value || "800x600", "800x600");
333 if (/^[0-9]+x[0-9]+$/.test(token) || token === "square" || token === "original") {
334 return token;
335 }
336 return "800x600";
337 }
338
339 function containerWidth(root) {
340 if (!root) return 0;
341 const rect = root.getBoundingClientRect ? root.getBoundingClientRect() : null;
342 if (rect && rect.width > 0) return rect.width;
343 const parent = root.parentElement;
344 const parentRect = parent && parent.getBoundingClientRect ? parent.getBoundingClientRect() : null;
345 if (parentRect && parentRect.width > 0) return parentRect.width;
346 return document.documentElement ? document.documentElement.clientWidth : 0;
347 }
348
349 function galleryConfig(cfg) {
350 cfg = cfg || {};
351 const visible = Math.max(1, Math.min(12, parseInt(cfg["img-count"] || cfg.img_count || 3, 10) || 3));
352 const overflow = optionToken(cfg.overflow, "grid", ["grid", "scroll", "slider", "laufband", "tutorial"]);
353 const click = optionToken(cfg.click, "lightbox", ["lightbox", "link", "newtab", "none", "swiper-coverflow", "swiper-cube", "swiper-cards", "swiper-3d", "viewerjs", "blueimp", "photoswipe", "deepzoom"]);
354 const imgSize = imageSizeToken(cfg["img-size"] || cfg.img_size);
355 const lightboxWidth = cssLength(cfg["lightbox-width"] || cfg.lightbox_width || "100vw", "100vw");
356 return { visible, overflow, click, imgSize, lightboxWidth };
357 }
358
359 function applyConfig(root, cfg) {
360 const settings = galleryConfig(cfg);
361 root.classList.add("dbx-gallery");
362 root.classList.add("dbx-gallery-overflow-" + settings.overflow);
363 root.classList.add("dbx-gallery-click-" + settings.click);
364 root.classList.add("dbx-gallery-size-" + settings.imgSize);
365 root.style.setProperty("--dbx-gallery-visible-count", String(settings.visible));
366 root.style.setProperty("--dbx-gallery-item-width", `calc((100% - ${(settings.visible - 1) * 16}px) / ${settings.visible})`);
367 root.style.setProperty("--dbx-gallery-lightbox-width", settings.lightboxWidth);
368
369 const size = /^([0-9]+)x([0-9]+)$/i.exec(settings.imgSize);
370 if (size) {
371 root.style.setProperty("--dbx-gallery-aspect-ratio", `${size[1]} / ${size[2]}`);
372 }
373 return settings;
374 }
375
376 function clearOverflowBehavior(root) {
377 if (!root) return;
378 if (root.__dbxGalleryFrame) {
379 window.cancelAnimationFrame(root.__dbxGalleryFrame);
380 root.__dbxGalleryFrame = null;
381 }
382 if (root.__dbxGalleryTimer) {
383 window.clearInterval(root.__dbxGalleryTimer);
384 root.__dbxGalleryTimer = null;
385 }
386 qsa(root, "[data-dbx-gallery-clone]").forEach(clone => clone.remove());
387 if (root.__dbxGalleryTutorial) {
388 if (root.__dbxGalleryTutorial.onKeydown) {
389 root.removeEventListener("keydown", root.__dbxGalleryTutorial.onKeydown);
390 }
391 if (root.__dbxGalleryTutorial.ui) root.__dbxGalleryTutorial.ui.remove();
392 root.__dbxGalleryTutorial = null;
393 }
394 qsa(root, ".dbx-media-gallery-item").forEach(item => {
395 item.hidden = false;
396 item.classList.remove("is-active");
397 item.removeAttribute("aria-hidden");
398 });
399 root.classList.remove("dbx-gallery-tutorial-ready");
400 root.scrollLeft = 0;
401 }
402
403 function bindOverflowPause(root) {
404 if (!root || root.__dbxGalleryPauseBound) return;
405 root.__dbxGalleryPauseBound = true;
406 root.__dbxGalleryPaused = false;
407 root.addEventListener("pointerenter", () => { root.__dbxGalleryPaused = true; });
408 root.addEventListener("pointerleave", () => { root.__dbxGalleryPaused = false; });
409 root.addEventListener("focusin", () => { root.__dbxGalleryPaused = true; });
410 root.addEventListener("focusout", () => { root.__dbxGalleryPaused = false; });
411 }
412
413 function itemLeft(root, item) {
414 const rootRect = root.getBoundingClientRect();
415 const itemRect = item.getBoundingClientRect();
416 return root.scrollLeft + itemRect.left - rootRect.left;
417 }
418
419 function startSlider(root) {
420 const items = itemsFor(root);
421 if (!items.length || root.scrollWidth <= root.clientWidth) return;
422 bindOverflowPause(root);
423 let index = 0;
424 root.__dbxGalleryTimer = window.setInterval(() => {
425 if (root.__dbxGalleryPaused) return;
426 const currentItems = itemsFor(root);
427 if (!currentItems.length) return;
428 index = (index + 1) % currentItems.length;
429 root.scrollTo({ left: itemLeft(root, currentItems[index]), behavior: "smooth" });
430 }, 4200);
431 }
432
433 function cloneForMarquee(item) {
434 const clone = item.cloneNode(true);
435 clone.setAttribute("data-dbx-gallery-clone", "1");
436 clone.setAttribute("aria-hidden", "true");
437 qsa(clone, "[data-dbx-gallery-item]").forEach(el => {
438 el.removeAttribute("data-dbx-gallery-item");
439 el.removeAttribute("href");
440 el.setAttribute("tabindex", "-1");
441 });
442 return clone;
443 }
444
445 function startMarquee(root) {
446 const items = itemsFor(root);
447 if (items.length < 2) return;
448 items.forEach(item => root.appendChild(cloneForMarquee(item.closest(".dbx-media-gallery-item") || item)));
449 if (root.scrollWidth <= root.clientWidth) {
450 qsa(root, "[data-dbx-gallery-clone]").forEach(clone => clone.remove());
451 return;
452 }
453 bindOverflowPause(root);
454 const step = function () {
455 if (!root.isConnected) return;
456 if (!root.__dbxGalleryPaused) {
457 const resetAt = Math.max(1, (root.scrollWidth - root.clientWidth) / 2);
458 root.scrollLeft += 0.45;
459 if (root.scrollLeft >= resetAt) root.scrollLeft = 0;
460 }
461 root.__dbxGalleryFrame = window.requestAnimationFrame(step);
462 };
463 root.__dbxGalleryFrame = window.requestAnimationFrame(step);
464 }
465
466 function tutorialFigures(root) {
467 return itemsFor(root)
468 .map(item => item.closest(".dbx-media-gallery-item"))
469 .filter((item, index, list) => item && list.indexOf(item) === index);
470 }
471
472 function prepareTutorialCaption(figure) {
473 const link = figure.querySelector("[data-dbx-gallery-item]");
474 const data = itemData(link || figure);
475 const title = String(data.title || "").trim();
476 let text = String((data.caption || "")).trim();
477 let caption = figure.querySelector("figcaption");
478 if (!caption) {
479 caption = document.createElement("figcaption");
480 figure.appendChild(caption);
481 }
482 if (!text) text = String(caption.textContent || "").trim();
483 if (!title && !text) return;
484 caption.textContent = "";
485 if (title && title !== text) {
486 const headline = document.createElement("strong");
487 headline.className = "dbx-gallery-tutorial-title";
488 headline.textContent = title;
489 caption.appendChild(headline);
490 }
491 if (text) {
492 const body = document.createElement("span");
493 body.className = "dbx-gallery-tutorial-text";
494 body.textContent = text;
495 caption.appendChild(body);
496 } else if (title) {
497 caption.textContent = title;
498 }
499 }
500
501 function setTutorialSlide(root, index) {
502 const state = root.__dbxGalleryTutorial;
503 if (!state || !state.figures.length) return;
504 const count = state.figures.length;
505 state.index = (index + count) % count;
506 state.figures.forEach((figure, itemIndex) => {
507 const active = itemIndex === state.index;
508 figure.hidden = !active;
509 figure.classList.toggle("is-active", active);
510 figure.setAttribute("aria-hidden", active ? "false" : "true");
511 });
512 if (state.copy) {
513 const caption = state.figures[state.index].querySelector("figcaption");
514 state.copy.innerHTML = caption ? caption.innerHTML : "";
515 state.copy.hidden = !state.copy.innerHTML.trim();
516 }
517 if (state.status) state.status.textContent = `${state.index + 1} / ${count}`;
518 state.dots.forEach((dot, dotIndex) => {
519 const active = dotIndex === state.index;
520 dot.classList.toggle("is-active", active);
521 dot.setAttribute("aria-current", active ? "true" : "false");
522 });
523 }
524
525 function startTutorialSlideshow(root) {
526 const figures = tutorialFigures(root);
527 if (!figures.length) return;
528 figures.forEach(figure => {
529 const image = figure.querySelector("img[data-full-src]");
530 if (image && image.dataset.fullSrc) {
531 image.src = image.dataset.fullSrc;
532 }
533 });
534 figures.forEach(prepareTutorialCaption);
535
536 root.classList.add("dbx-gallery-tutorial-ready");
537 root.setAttribute("role", "region");
538 root.setAttribute("aria-roledescription", "carousel");
539 root.setAttribute("aria-label", root.getAttribute("aria-label") || "Tutorial Slideshow");
540 if (!root.hasAttribute("tabindex")) root.setAttribute("tabindex", "0");
541
542 const ui = document.createElement("div");
543 ui.className = "dbx-gallery-tutorial-ui";
544 ui.setAttribute("data-dbx-gallery-tutorial-ui", "1");
545
546 const copy = document.createElement("div");
547 copy.className = "dbx-gallery-tutorial-copy";
548
549 const prev = document.createElement("button");
550 prev.type = "button";
551 prev.className = "dbx-gallery-tutorial-prev";
552 prev.setAttribute("aria-label", "Vorherige Folie");
553 prev.innerHTML = '<i class="bi bi-chevron-left"></i><span>Zurück</span>';
554
555 const status = document.createElement("div");
556 status.className = "dbx-gallery-tutorial-status";
557 status.setAttribute("aria-live", "polite");
558
559 const next = document.createElement("button");
560 next.type = "button";
561 next.className = "dbx-gallery-tutorial-next";
562 next.setAttribute("aria-label", "Naechste Folie");
563 next.innerHTML = '<span>Weiter</span><i class="bi bi-chevron-right"></i>';
564
565 const controls = document.createElement("div");
566 controls.className = "dbx-gallery-tutorial-controls";
567
568 const dotsWrap = document.createElement("div");
569 dotsWrap.className = "dbx-gallery-tutorial-dots";
570 const dots = figures.map((figure, index) => {
571 const data = itemData(figure.querySelector("[data-dbx-gallery-item]") || figure);
572 const dot = document.createElement("button");
573 dot.type = "button";
574 dot.className = "dbx-gallery-tutorial-dot";
575 dot.setAttribute("aria-label", data.title ? `Folie ${index + 1}: ${data.title}` : `Folie ${index + 1}`);
576 dot.textContent = String(index + 1);
577 dot.addEventListener("click", () => setTutorialSlide(root, index));
578 dotsWrap.appendChild(dot);
579 return dot;
580 });
581
582 prev.addEventListener("click", () => setTutorialSlide(root, (root.__dbxGalleryTutorial.index || 0) - 1));
583 next.addEventListener("click", () => setTutorialSlide(root, (root.__dbxGalleryTutorial.index || 0) + 1));
584
585 controls.appendChild(prev);
586 controls.appendChild(status);
587 controls.appendChild(next);
588 ui.appendChild(copy);
589 ui.appendChild(controls);
590 ui.appendChild(dotsWrap);
591 root.appendChild(ui);
592
593 const onKeydown = function (e) {
594 if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
595 if (closestElement(e.target, "input, textarea, select")) return;
596 e.preventDefault();
597 setTutorialSlide(root, (root.__dbxGalleryTutorial.index || 0) + (e.key === "ArrowRight" ? 1 : -1));
598 };
599 root.addEventListener("keydown", onKeydown);
600 root.__dbxGalleryTutorial = { figures, ui, copy, status, dots, index: 0, onKeydown };
601 setTutorialSlide(root, 0);
602 }
603
604 function startOverflowBehavior(root, settings) {
605 clearOverflowBehavior(root);
606 if (settings.overflow === "slider") {
607 startSlider(root);
608 } else if (settings.overflow === "laufband") {
609 startMarquee(root);
610 } else if (settings.overflow === "tutorial") {
611 startTutorialSlideshow(root);
612 }
613 }
614
615 function cleanupOverlay(box) {
616 if (!box) return;
617 if (box.__dbxGalleryViewer && typeof box.__dbxGalleryViewer.destroy === "function") {
618 box.__dbxGalleryViewer.destroy();
619 box.__dbxGalleryViewer = null;
620 }
621 if (box.__dbxGallerySwiper && typeof box.__dbxGallerySwiper.destroy === "function") {
622 box.__dbxGallerySwiper.destroy(true, true);
623 box.__dbxGallerySwiper = null;
624 }
625 if (box.__dbxGalleryOpenSeadragon && typeof box.__dbxGalleryOpenSeadragon.destroy === "function") {
626 box.__dbxGalleryOpenSeadragon.destroy();
627 box.__dbxGalleryOpenSeadragon = null;
628 }
629 qsa(box, "[data-dbx-gallery-thirdparty]").forEach(el => el.remove());
630 }
631
632 function showThirdPartyOverlay(box, root, index, mode) {
633 const items = itemsFor(root);
634 const links = items.map(itemData);
635 const current = links[index] || {};
636 if (!current.href) return false;
637
638 if (mode === "viewerjs" && window.Viewer && current.type === "image") {
639 const host = document.createElement("div");
640 host.hidden = true;
641 host.setAttribute("data-dbx-gallery-thirdparty", "viewerjs");
642 host.innerHTML = links.filter(item => item.type === "image").map(item => `<img src="${String(item.href).replace(/"/g, "&quot;")}" alt="${String(item.title || "").replace(/"/g, "&quot;")}">`).join("");
643 document.body.appendChild(host);
644 const imageIndex = links.slice(0, index + 1).filter(item => item.type === "image").length - 1;
645 box.__dbxGalleryViewer = new window.Viewer(host, {
646 initialViewIndex: Math.max(0, imageIndex),
647 inline: false,
648 navbar: true,
649 toolbar: true,
650 title: true,
651 hidden() {
652 if (box.__dbxGalleryViewer) box.__dbxGalleryViewer.destroy();
653 box.__dbxGalleryViewer = null;
654 host.remove();
655 }
656 });
657 box.__dbxGalleryViewer.show();
658 return true;
659 }
660
661 if (mode === "blueimp" && window.blueimp && typeof window.blueimp.Gallery === "function") {
662 window.blueimp.Gallery(links.map(item => ({
663 href: item.href,
664 title: item.caption || item.title || "",
665 type: item.type === "video" ? (item.mime || "video/mp4") : undefined,
666 poster: item.poster || undefined
667 })), {
668 index,
669 fullscreen: false,
670 stretchImages: false,
671 toggleControlsOnSlideClick: true,
672 closeOnEscape: true,
673 closeOnSlideClick: true
674 });
675 return true;
676 }
677
678 if (mode === "photoswipe" && window.PhotoSwipe && current.type === "image") {
679 const dataSource = links.filter(item => item.type === "image").map(item => ({
680 src: item.href,
681 width: item.width || 1600,
682 height: item.height || 1000,
683 alt: item.title || ""
684 }));
685 const imageIndex = links.slice(0, index + 1).filter(item => item.type === "image").length - 1;
686 const pswp = new window.PhotoSwipe({
687 dataSource,
688 index: Math.max(0, imageIndex),
689 bgOpacity: 0.92,
690 showHideAnimationType: "zoom"
691 });
692 pswp.init();
693 return true;
694 }
695
696 return false;
697 }
698
699 function openSwiperOverlay(box, root, index, mode) {
700 if (!window.Swiper) return false;
701 const refs = box.__dbxGalleryRefs || {};
702 const stage = refs.stage;
703 if (!stage) return false;
704 const items = itemsFor(root).map(itemData);
705 if (!items.length) return false;
706
707 cleanupOverlay(box);
708 [refs.image, refs.video, refs.frame].forEach(el => {
709 if (!el) return;
710 el.hidden = true;
711 if (el.tagName === "VIDEO") {
712 el.pause();
713 el.removeAttribute("src");
714 el.load();
715 } else {
716 el.removeAttribute("src");
717 }
718 });
719
720 const swiper = document.createElement("div");
721 swiper.className = "swiper dbx-gallery-swiper";
722 swiper.setAttribute("data-dbx-gallery-thirdparty", "swiper");
723 swiper.setAttribute("data-dbx-gallery-swiper-mode", mode);
724 swiper.innerHTML = '<div class="swiper-wrapper"></div><div class="swiper-pagination"></div>';
725 const wrapper = swiper.querySelector(".swiper-wrapper");
726 items.forEach(item => {
727 const slide = document.createElement("div");
728 slide.className = "swiper-slide";
729 if (item.type === "video") {
730 slide.innerHTML = `<video src="${String(item.href).replace(/"/g, "&quot;")}" poster="${String(item.poster || "").replace(/"/g, "&quot;")}" controls playsinline preload="metadata"></video>`;
731 } else if (item.type === "external_video") {
732 slide.innerHTML = `<iframe src="${String(item.href).replace(/"/g, "&quot;")}" title="${String(item.title || "").replace(/"/g, "&quot;")}" allowfullscreen></iframe>`;
733 } else {
734 slide.innerHTML = `<img src="${String(item.href).replace(/"/g, "&quot;")}" alt="${String(item.title || "").replace(/"/g, "&quot;")}">`;
735 }
736 wrapper.appendChild(slide);
737 });
738 stage.appendChild(swiper);
739
740 const effect = mode === "swiper-cube" ? "cube" : (mode === "swiper-cards" ? "cards" : "coverflow");
741 const isCube = mode === "swiper-cube";
742 box.__dbxGallerySwiper = new window.Swiper(swiper, {
743 effect,
744 initialSlide: index,
745 grabCursor: true,
746 loop: !isCube && items.length > 2,
747 centeredSlides: !isCube,
748 slidesPerView: isCube ? 1 : (mode === "swiper-3d" || mode === "swiper-coverflow" ? "auto" : 1),
749 watchSlidesProgress: true,
750 pagination: { el: swiper.querySelector(".swiper-pagination"), clickable: true },
751 cubeEffect: { shadow: true, slideShadows: true, shadowOffset: 24, shadowScale: 0.82 },
752 cardsEffect: { perSlideOffset: 10, perSlideRotate: 2 },
753 coverflowEffect: { rotate: 42, stretch: 0, depth: 180, modifier: 1, slideShadows: true },
754 on: {
755 slideChange(sw) {
756 box.__dbxGalleryIndex = sw.realIndex;
757 const data = items[sw.realIndex] || {};
758 if (refs.caption) refs.caption.textContent = data.caption || data.title || "";
759 }
760 }
761 });
762 return true;
763 }
764
765 function openDeepZoomOverlay(box, data) {
766 if (!window.OpenSeadragon || data.type !== "image") return false;
767 const refs = box.__dbxGalleryRefs || {};
768 const stage = refs.stage;
769 if (!stage) return false;
770 cleanupOverlay(box);
771 [refs.image, refs.video, refs.frame].forEach(el => {
772 if (!el) return;
773 el.hidden = true;
774 el.removeAttribute("src");
775 });
776 const viewer = document.createElement("div");
777 viewer.className = "dbx-gallery-deepzoom";
778 viewer.setAttribute("data-dbx-gallery-thirdparty", "openseadragon");
779 stage.appendChild(viewer);
780 box.__dbxGalleryOpenSeadragon = window.OpenSeadragon({
781 element: viewer,
782 prefixUrl: "dbx/vendor/npm/node_modules/openseadragon/build/openseadragon/images/",
783 tileSources: { type: "image", url: data.href },
784 showNavigator: true,
785 gestureSettingsMouse: { clickToZoom: true, dblClickToZoom: true, scrollToZoom: true },
786 gestureSettingsTouch: { pinchToZoom: true, flickEnabled: true }
787 });
788 return true;
789 }
790
791 function open(root, index) {
792 const items = itemsFor(root);
793 if (!items.length) return;
794 index = Math.max(0, Math.min(items.length - 1, Number(index || 0)));
795 const settings = root.__dbxGalleryCfg || {};
796 const mode = overlayMode(settings);
797
798 ensureVendor(mode, function () {
799 openWithMode(root, index, mode);
800 });
801 }
802
803 function openWithMode(root, index, mode) {
804 const items = itemsFor(root);
805 if (!items.length) return;
806
807 const box = ensureLightbox();
808 const data = itemData(items[index]);
809 cleanupOverlay(box);
810 if (showThirdPartyOverlay(box, root, index, mode)) return;
811 box.setAttribute("data-dbx-gallery-mode", mode);
812 box.style.setProperty("--dbx-gallery-lightbox-width", "calc(100vw - (var(--dbx-gallery-lightbox-margin) * 2))");
813 resetZoom(box);
814 const refs = box.__dbxGalleryRefs || {};
815 const image = refs.image;
816 const video = refs.video;
817 const frame = refs.frame;
818 const caption = refs.caption;
819 if (video) {
820 video.pause();
821 video.removeAttribute("src");
822 video.removeAttribute("poster");
823 video.load();
824 video.hidden = true;
825 }
826 if (frame) {
827 frame.hidden = true;
828 frame.removeAttribute("src");
829 frame.title = "";
830 }
831 if (image) {
832 image.style.transform = "";
833 image.style.cursor = "";
834 image.alt = data.title || "";
835 if (data.type === "video" || data.type === "external_video") {
836 image.hidden = true;
837 image.removeAttribute("src");
838 } else {
839 image.hidden = false;
840 if (image.getAttribute("src") !== data.href) image.src = data.href;
841 if (image.decode) image.decode().catch(() => {});
842 }
843 }
844 if (data.type === "video" && video) {
845 video.style.transform = "";
846 video.style.cursor = "";
847 video.src = data.href;
848 if (data.poster) video.poster = data.poster;
849 video.hidden = false;
850 }
851 if (data.type === "external_video" && frame) {
852 frame.src = data.href;
853 frame.title = data.title || "";
854 frame.hidden = false;
855 }
856 if (caption) caption.textContent = data.caption || data.title || "";
857 box.__dbxGalleryRoot = root;
858 box.__dbxGalleryIndex = index;
859 box.hidden = false;
860 document.documentElement.classList.add("dbx-gallery-open");
861 if (mode.indexOf("swiper-") === 0) openSwiperOverlay(box, root, index, mode);
862 if (mode === "deepzoom") openDeepZoomOverlay(box, data);
863 preloadNext(root, index);
864 }
865
866 function move(delta) {
867 const box = ensureLightbox();
868 const root = box.__dbxGalleryRoot;
869 if (box.__dbxGallerySwiper) {
870 delta > 0 ? box.__dbxGallerySwiper.slideNext() : box.__dbxGallerySwiper.slidePrev();
871 return;
872 }
873 const items = itemsFor(root);
874 if (!root || !items.length) return;
875 const next = (Number(box.__dbxGalleryIndex || 0) + delta + items.length) % items.length;
876 open(root, next);
877 }
878
879 function close() {
880 const box = ensureLightbox();
881 const refs = box.__dbxGalleryRefs || {};
882 const video = refs.video;
883 const image = refs.image;
884 const frame = refs.frame;
885 cleanupOverlay(box);
886 if (video) {
887 video.pause();
888 video.removeAttribute("src");
889 video.load();
890 }
891 if (image) image.removeAttribute("src");
892 if (frame) frame.removeAttribute("src");
893 box.hidden = true;
894 box.removeAttribute("data-dbx-gallery-mode");
895 document.documentElement.classList.remove("dbx-gallery-open");
896 }
897
898 function init(root, cfg) {
899 if (!root || root.__dbxGalleryReady) return;
900 root.__dbxGalleryReady = true;
901 root.__dbxGalleryCfg = applyConfig(root, cfg);
902 root.__dbxGalleryItems = collectItems(root);
903 if (root.__dbxGalleryCfg.click === "newtab") {
904 root.__dbxGalleryItems.forEach(item => {
905 item.setAttribute("target", "_blank");
906 item.setAttribute("rel", "noopener");
907 });
908 }
909 startOverflowBehavior(root, root.__dbxGalleryCfg);
910
911 root.addEventListener("click", e => {
912 const item = closestElement(e.target, "[data-dbx-gallery-item]");
913 if (!item || !root.contains(item)) return;
914 if ((root.__dbxGalleryCfg || {}).click === "none") {
915 e.preventDefault();
916 return;
917 }
918 if ((root.__dbxGalleryCfg || {}).click === "link" || (root.__dbxGalleryCfg || {}).click === "newtab") {
919 return;
920 }
921 e.preventDefault();
922 const items = itemsFor(root);
923 open(root, items.indexOf(item));
924 });
925 }
926
927 document.addEventListener("click", e => {
928 if (closestElement(e.target, "[data-dbx-gallery-close]")) {
929 e.preventDefault();
930 close();
931 return;
932 }
933 if (closestElement(e.target, "[data-dbx-gallery-prev]")) {
934 e.preventDefault();
935 move(-1);
936 return;
937 }
938 if (closestElement(e.target, "[data-dbx-gallery-next]")) {
939 e.preventDefault();
940 move(1);
941 }
942 });
943
944 document.addEventListener("keydown", e => {
945 const box = lightbox || document.querySelector("[data-dbx-gallery-lightbox]");
946 if (!box || box.hidden) return;
947 if (e.key === "Escape") close();
948 if (e.key === "ArrowLeft") move(-1);
949 if (e.key === "ArrowRight") move(1);
950 });
951
952 dbx.gallery = {
953 init,
954 rescan(ctx) {
955 qsa(ctx || document, "[data-dbx]").forEach(el => {
956 if (el.__dbxGalleryReady) return;
957 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
958 const cfg = cfgList.find(item => item.lib === LIB);
959 if (cfg) init(el, cfg);
960 });
961 }
962 };
963
964 dbx.feature.register(LIB, {
965 scope: "element",
966 priority: "last",
967 css: [
968 ["css", "design", "c-content.css"]
969 ],
970 js: [],
971 init,
972 rescan(ctx) {
973 dbx.gallery.rescan(ctx);
974 }
975 });
976
977})(window, document);