dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
utilities.js
Go to the documentation of this file.
1/*!
2 * dbxapp utilities.js
3 * Global helpers: clearable inputs, back-to-top, app/web mode, skin/theme.
4 */
5(function (window, document) {
6 "use strict";
7
8 const LIB = "utilities";
9 const ID = "global";
10 const MODE_KEY = "mode";
11 const THEME_KEY = "theme";
12 const SKIN_KEY = "skin";
13 const COLLAPSE_LIB = "collapse";
14 const ALL_SKINS = ["hell", "gelb", "rot", "gruen", "blau", "dunkel"];
15 const DESIGN_SKINS = {
16 fleurop: ["hell", "dunkel"]
17 };
18 const CONSENT_KEY = "consent";
19 const CONSENT_PRIVACY_URL = "datenschutz";
20 const CONSENT_IMPRESSUM_URL = "impressum";
21 const DEFAULT_CONSENT = { cookies: true, youtube: false, decided: false, ts: 0 };
22
23 function hasDbx() {
24 return !!(window.dbx && window.dbx.feature);
25 }
26
27 function onReady(fn) {
28 if (document.readyState === "loading") {
29 document.addEventListener("DOMContentLoaded", fn, { once: true });
30 } else {
31 fn();
32 }
33 }
34
35 function storeGet(key, def) {
36 return hasDbx() && typeof dbx.uiGet === "function" ? dbx.uiGet(LIB, ID, key, def) : def;
37 }
38
39 function storeSet(key, value) {
40 if (hasDbx() && typeof dbx.uiSet === "function") {
41 dbx.uiSet(LIB, ID, key, value);
42 }
43 }
44
45 function currentMode() {
46 return document.body.classList.contains("dbx-web") ? "dbx-web" : "dbx-app";
47 }
48
49 function currentTheme() {
50 return document.body.classList.contains("theme-dark") || document.body.classList.contains("dark")
51 ? "dark"
52 : "light";
53 }
54
55 function skinIdsForDesign() {
56 const design = String(getDesign() || "").toLowerCase();
57 return DESIGN_SKINS[design] || ALL_SKINS;
58 }
59
60 function defaultSkinForDesign() {
61 return String(getDesign() || "").toLowerCase() === "fleurop" ? "hell" : "blau";
62 }
63
64 function isValidSkin(skin) {
65 return skinIdsForDesign().indexOf(String(skin || "")) !== -1;
66 }
67
68 function getDesign() {
69 if (document.body && document.body.getAttribute("data-dbx-design")) {
70 return document.body.getAttribute("data-dbx-design");
71 }
72 if (window.dbx && dbx.config && dbx.config.design) {
73 return dbx.config.design;
74 }
75 return "dbxapp";
76 }
77
78 function getServerSkin() {
79 if (document.body && document.body.getAttribute("data-dbx-skin")) {
80 return document.body.getAttribute("data-dbx-skin");
81 }
82 return "blau";
83 }
84
85 function parseSkinFromBody() {
86 if (!document.body || !document.body.className) {
87 return getServerSkin();
88 }
89
90 const match = document.body.className.match(/\bskin-([a-z]+)\b/);
91 return match ? match[1] : getServerSkin();
92 }
93
94 function skinCssHref(skin) {
95 const design = getDesign();
96 const root = (window.dbx && dbx.config && dbx.config.rootPath) ? dbx.config.rootPath : "";
97 return root + "design/" + design + "/css/skin-" + skin + ".css";
98 }
99
100 function findSkinStylesheet() {
101 return document.querySelector('link[rel="stylesheet"][href*="skin-"]');
102 }
103
104 function updateModeIcons(mode) {
105 document.querySelectorAll(".dbxModeIcon").forEach(icon => {
106 icon.classList.toggle("bi-window", mode === "dbx-app");
107 icon.classList.toggle("bi-globe", mode !== "dbx-app");
108 });
109 }
110
111 function updateThemeIcons(theme) {
112 document.querySelectorAll(".dbxThemeIcon").forEach(icon => {
113 icon.classList.toggle("bi-moon", theme === "dark");
114 icon.classList.toggle("bi-sun", theme !== "dark");
115 });
116 }
117
118 function updateSkinMenuState(skin) {
119 document.querySelectorAll(".dbx-skin-opt").forEach(opt => {
120 const item = opt.closest("li");
121 const active = opt.getAttribute("data-skin") === skin;
122 opt.classList.toggle("is-active", active);
123 if (item) {
124 item.classList.toggle("is-active", active);
125 }
126
127 let mark = opt.querySelector(".dbx-skin-check");
128 if (active) {
129 if (!mark) {
130 mark = document.createElement("i");
131 mark.className = "bi bi-check2 dbx-skin-check";
132 opt.appendChild(mark);
133 }
134 } else if (mark) {
135 mark.remove();
136 }
137 });
138 }
139
140 function attr(el, name, def = "") {
141 if (!el || !el.getAttribute) return def;
142 const value = el.getAttribute(name);
143 return value == null ? def : String(value).trim();
144 }
145
146 function cssEscape(value) {
147 if (window.CSS && typeof window.CSS.escape === "function") {
148 return window.CSS.escape(value);
149 }
150
151 return String(value || "").replace(/["\\‍]/g, "\\$&");
152 }
153
154 function escapeHtml(value) {
155 const div = document.createElement("div");
156 div.textContent = value == null ? "" : String(value);
157 return div.innerHTML;
158 }
159
160 let tooltipEl = null;
161 let tooltipTarget = null;
162 let tooltipBound = false;
163 const TOOLTIP_SELECTOR = "[data-dbx-tooltip],[data-dbx-errormsg]";
164
165 function ensureTooltipStyle() {
166 if (document.getElementById("dbx-utility-tooltip-style")) return;
167 const style = document.createElement("style");
168 style.id = "dbx-utility-tooltip-style";
169 style.textContent = `
170.dbx-utility-tooltip {
171 position: fixed;
172 z-index: 2147483000;
173 min-width: 9rem;
174 max-width: min(460px, calc(100vw - 24px));
175 padding: .55rem .7rem;
176 color: #111827;
177 background: #fff1a8;
178 border: 1px solid #d2a800;
179 border-radius: .45rem;
180 box-shadow: 0 .55rem 1.25rem rgba(15, 23, 42, .22);
181 font-size: .82rem;
182 line-height: 1.35;
183 pointer-events: none;
184 white-space: normal;
185}
186.dbx-utility-tooltip::before,
187.dbx-utility-tooltip::after {
188 content: "";
189 position: absolute;
190 left: var(--dbx-tooltip-arrow-left, 22px);
191 width: 0;
192 height: 0;
193 border-left: 8px solid transparent;
194 border-right: 8px solid transparent;
195}
196.dbx-utility-tooltip::before {
197 bottom: -9px;
198 border-top: 9px solid #d2a800;
199}
200.dbx-utility-tooltip::after {
201 bottom: -8px;
202 border-top: 8px solid #fff1a8;
203}
204.dbx-utility-tooltip[data-placement="bottom"]::before {
205 top: -9px;
206 bottom: auto;
207 border-top: 0;
208 border-bottom: 9px solid #d2a800;
209}
210.dbx-utility-tooltip[data-placement="bottom"]::after {
211 top: -8px;
212 bottom: auto;
213 border-top: 0;
214 border-bottom: 8px solid #fff1a8;
215}
216.dbx-utility-tooltip[data-kind="error"] {
217 color: #4a0306;
218 background: #ffe4e6;
219 border-color: #dc3545;
220}
221.dbx-utility-tooltip[data-kind="error"]::before {
222 border-top-color: #dc3545;
223}
224.dbx-utility-tooltip[data-kind="error"]::after {
225 border-top-color: #ffe4e6;
226}
227.dbx-utility-tooltip[data-kind="error"][data-placement="bottom"]::before {
228 border-top-color: transparent;
229 border-bottom-color: #dc3545;
230}
231.dbx-utility-tooltip[data-kind="error"][data-placement="bottom"]::after {
232 border-top-color: transparent;
233 border-bottom-color: #ffe4e6;
234}
235.dbx-utility-tooltip-title {
236 display: block;
237 margin-bottom: .35rem;
238 font-weight: 700;
239}
240.dbx-utility-tooltip-list {
241 display: grid;
242 gap: .28rem;
243}
244.dbx-utility-tooltip-row {
245 display: grid;
246 grid-template-columns: max-content minmax(4.5rem, max-content) minmax(8rem, 1fr);
247 gap: .35rem .55rem;
248 align-items: start;
249}
250.dbx-utility-tooltip-id {
251 font-weight: 700;
252 white-space: nowrap;
253}
254.dbx-utility-tooltip-folder {
255 white-space: nowrap;
256 color: #1f2937;
257}
258.dbx-utility-tooltip-text,
259.dbx-utility-tooltip-title-text {
260 min-width: 0;
261}
262.dbx-utility-tooltip-empty {
263 font-weight: 600;
264}
265`;
266 document.head.appendChild(style);
267 }
268
269 function normalizeTooltipHtml(value, asHtml) {
270 let html = String(value == null ? "" : value).trim();
271 if (!html || html === "{tooltip}" || html === "#tooltip#") return "";
272 if (/^(?: |\u00a0|\s)+$/i.test(html)) return "";
273 if (!asHtml) {
274 html = escapeHtml(html);
275 }
276
277 const tpl = document.createElement("template");
278 tpl.innerHTML = html;
279 const text = (tpl.content.textContent || "").replace(/\u00a0/g, " ").trim();
280 const hasVisualNode = !!tpl.content.querySelector("img,svg,canvas,table,ul,ol,li");
281 if (!text && !hasVisualNode) return "";
282 return html;
283 }
284
285 function tooltipHtmlList(rows, options) {
286 options = options || {};
287 rows = Array.isArray(rows) ? rows : [];
288 if (!rows.length) {
289 return `<div class="dbx-utility-tooltip-empty">${escapeHtml(options.empty || "Keine Verwendung")}</div>`;
290 }
291 const title = options.title
292 ? `<span class="dbx-utility-tooltip-title">${escapeHtml(options.title)}</span>`
293 : "";
294 return title + `<div class="dbx-utility-tooltip-list">` + rows.map(row => {
295 const id = row && row.id != null ? row.id : "";
296 const folder = row && row.folder != null ? row.folder : "";
297 const text = row && row.title != null ? row.title : "";
298 return `<div class="dbx-utility-tooltip-row">`
299 + `<span class="dbx-utility-tooltip-id">${escapeHtml(id)}</span>`
300 + `<span class="dbx-utility-tooltip-folder">${escapeHtml(folder)}</span>`
301 + `<span class="dbx-utility-tooltip-title-text">${escapeHtml(text)}</span>`
302 + `</div>`;
303 }).join("") + `</div>`;
304 }
305
306 function tooltipData(target) {
307 if (!target) return "";
308 const errorText = target.getAttribute("data-dbx-errormsg") || "";
309 const normalizedErrorText = normalizeTooltipHtml(errorText, true);
310 if (normalizedErrorText) {
311 return {
312 html: normalizedErrorText,
313 kind: "error"
314 };
315 }
316 const text = target.getAttribute("data-dbx-tooltip") || "";
317 const normalizedText = normalizeTooltipHtml(text, true);
318 if (normalizedText) {
319 return {
320 html: normalizedText,
321 kind: "tooltip"
322 };
323 }
324 return null;
325 }
326
327 function positionTooltip(target) {
328 if (!tooltipEl || !target || !target.getBoundingClientRect) return;
329 const rect = target.getBoundingClientRect();
330 const gap = 10;
331 const margin = 8;
332 const width = tooltipEl.offsetWidth || 0;
333 const height = tooltipEl.offsetHeight || 0;
334 let left = rect.left + Math.min(rect.width / 2, 28) - 22;
335 let top = rect.top - height - gap;
336 let placement = "top";
337
338 if (top < margin) {
339 top = rect.bottom + gap;
340 placement = "bottom";
341 }
342 left = Math.max(margin, Math.min(left, window.innerWidth - width - margin));
343 const arrowLeft = Math.max(14, Math.min(rect.left + rect.width / 2 - left - 8, width - 22));
344
345 tooltipEl.dataset.placement = placement;
346 tooltipEl.style.left = `${Math.round(left)}px`;
347 tooltipEl.style.top = `${Math.round(top)}px`;
348 tooltipEl.style.setProperty("--dbx-tooltip-arrow-left", `${Math.round(arrowLeft)}px`);
349 }
350
351 function showTooltip(target) {
352 const data = tooltipData(target);
353 if (!data || !data.html) return;
354 if (target.hasAttribute("title") && !target.hasAttribute("data-dbx-tooltip-title-cache")) {
355 target.setAttribute("data-dbx-tooltip-title-cache", target.getAttribute("title") || "");
356 target.removeAttribute("title");
357 }
358 ensureTooltipStyle();
359 if (!tooltipEl) {
360 tooltipEl = document.createElement("div");
361 tooltipEl.className = "dbx-utility-tooltip";
362 tooltipEl.setAttribute("role", "tooltip");
363 document.body.appendChild(tooltipEl);
364 }
365 tooltipTarget = target;
366 tooltipEl.dataset.kind = data.kind || "tooltip";
367 tooltipEl.innerHTML = data.html;
368 tooltipEl.style.left = "-9999px";
369 tooltipEl.style.top = "-9999px";
370 tooltipEl.hidden = false;
371 positionTooltip(target);
372 }
373
374 function hideTooltip(target) {
375 if (target && tooltipTarget && target !== tooltipTarget) return;
376 const restoreTarget = target || tooltipTarget;
377 if (restoreTarget && restoreTarget.hasAttribute("data-dbx-tooltip-title-cache")) {
378 restoreTarget.setAttribute("title", restoreTarget.getAttribute("data-dbx-tooltip-title-cache") || "");
379 restoreTarget.removeAttribute("data-dbx-tooltip-title-cache");
380 }
381 tooltipTarget = null;
382 if (tooltipEl) {
383 tooltipEl.hidden = true;
384 }
385 }
386
387 function initHtmlTooltips(root) {
388 if (tooltipBound) return;
389 tooltipBound = true;
390
391 document.addEventListener("mouseover", function (e) {
392 const target = e.target && e.target.closest
393 ? e.target.closest(TOOLTIP_SELECTOR)
394 : null;
395 if (!target || target.contains(e.relatedTarget)) return;
396 showTooltip(target);
397 }, true);
398
399 document.addEventListener("mouseout", function (e) {
400 let target = e.target && e.target.closest
401 ? e.target.closest(TOOLTIP_SELECTOR)
402 : null;
403 if (!target && tooltipTarget && (e.target === tooltipTarget || (tooltipTarget.contains && tooltipTarget.contains(e.target)))) {
404 target = tooltipTarget;
405 }
406 if (!target || target.contains(e.relatedTarget)) return;
407 hideTooltip(target);
408 }, true);
409
410 document.addEventListener("focusin", function (e) {
411 const target = e.target && e.target.closest
412 ? e.target.closest(TOOLTIP_SELECTOR)
413 : null;
414 if (target) showTooltip(target);
415 }, true);
416
417 document.addEventListener("focusout", function (e) {
418 let target = e.target && e.target.closest
419 ? e.target.closest(TOOLTIP_SELECTOR)
420 : null;
421 if (!target && tooltipTarget && (e.target === tooltipTarget || (tooltipTarget.contains && tooltipTarget.contains(e.target)))) {
422 target = tooltipTarget;
423 }
424 if (target) hideTooltip(target);
425 }, true);
426
427 document.addEventListener("scroll", function () {
428 hideTooltip();
429 }, true);
430 window.addEventListener("resize", function () {
431 hideTooltip();
432 });
433 }
434
435 function getCollapseState(key) {
436 if (!key || !window.dbx || typeof dbx.uiGet !== "function") return "";
437 return dbx.uiGet(COLLAPSE_LIB, key, "state", "");
438 }
439
440 function setCollapseState(key, collapsed) {
441 if (!key || !window.dbx || typeof dbx.uiSet !== "function") return;
442 dbx.uiSet(COLLAPSE_LIB, key, "state", collapsed ? "collapsed" : "open");
443 }
444
445 function collapseStateKey(button, panel, target) {
446 return attr(button, "data-collapse-state-key",
447 attr(panel, "data-collapse-state-key",
448 attr(button, "data-ui-state-key",
449 attr(panel, "data-ui-state-key", target)
450 )
451 )
452 );
453 }
454
455 function setCollapseUi(button, panel, collapsed) {
456 panel.classList.toggle("is-collapsed", collapsed);
457 button.setAttribute("aria-expanded", collapsed ? "false" : "true");
458
459 const label = button.querySelector("[data-collapse-label]") || button.querySelector("span");
460 if (label) {
461 label.textContent = collapsed ? "Aufklappen" : "Zuklappen";
462 }
463 }
464
465 function initCollapsible(root) {
466 const ctx = root && root.querySelectorAll ? root : document;
467 const buttons = [];
468
469 if (ctx.nodeType === 1 && ctx.matches && ctx.matches("[data-collapse-toggle],[data-admin-collapse-toggle]")) {
470 buttons.push(ctx);
471 }
472
473 ctx.querySelectorAll("[data-collapse-toggle],[data-admin-collapse-toggle]").forEach(button => {
474 buttons.push(button);
475 });
476
477 buttons.forEach(button => {
478 if (button.__dbxUtilityCollapse) return;
479 button.__dbxUtilityCollapse = true;
480
481 const target = attr(button, "data-collapse-toggle", attr(button, "data-admin-collapse-toggle", ""));
482 if (!target) return;
483
484 const safeTarget = cssEscape(target);
485 const selector = [
486 `[data-collapse-panel="${safeTarget}"]`,
487 `[data-admin-collapsible-panel="${safeTarget}"]`
488 ].join(",");
489 const panel = button.closest(selector) || ctx.querySelector(selector);
490 if (!panel) return;
491
492 const stateKey = collapseStateKey(button, panel, target);
493 const storedState = getCollapseState(stateKey);
494 if (storedState === "collapsed" || storedState === "open") {
495 setCollapseUi(button, panel, storedState === "collapsed");
496 }
497
498 button.addEventListener("click", () => {
499 const collapsed = !panel.classList.contains("is-collapsed");
500 setCollapseUi(button, panel, collapsed);
501 setCollapseState(stateKey, collapsed);
502
503 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
504 dbx.event.emit("ui:collapse", {
505 key: stateKey,
506 target,
507 collapsed,
508 panel,
509 button
510 });
511 }
512 });
513 });
514 }
515
516 function syncSkinToServer(skin) {
517 try {
518 const url = new URL(window.location.href);
519 url.searchParams.set("dbx_color", skin);
520 window.history.replaceState({}, "", url.pathname + url.search + url.hash);
521 } catch (_) {}
522
523 try {
524 if (dbx.ajax && typeof dbx.ajax.request === "function") {
525 const syncUrl = new URL(window.location.href);
526 syncUrl.searchParams.set("dbx_color", skin);
527 dbx.ajax.request({
528 url: syncUrl.toString(),
529 method: "GET",
530 mode: "html"
531 }).catch(function () {});
532 }
533 } catch (_) {}
534 }
535
536 function applySkin(skin, persist) {
537 if (!isValidSkin(skin)) {
538 skin = defaultSkinForDesign();
539 }
540
541 const link = findSkinStylesheet();
542 if (link) {
543 link.href = skinCssHref(skin);
544 }
545
546 ALL_SKINS.forEach(function (id) {
547 document.body.classList.remove("skin-" + id);
548 });
549 document.body.classList.add("skin-" + skin);
550 document.body.setAttribute("data-dbx-skin", skin);
551
552 document.body.classList.remove("light", "dark", "theme-light", "theme-dark");
553 if (skin === "dunkel") {
554 document.body.classList.add("theme-dark");
555 } else {
556 document.body.classList.add("theme-light");
557 }
558
559 updateSkinMenuState(skin);
560 updateThemeIcons(skin === "dunkel" ? "dark" : "light");
561
562 if (persist) {
563 storeSet(SKIN_KEY, skin);
564 storeSet(THEME_KEY, skin === "dunkel" ? "dark" : "light");
565 syncSkinToServer(skin);
566 }
567
568 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
569 dbx.event.emit("utilities:skin", { id: ID, skin: skin });
570 }
571 }
572
573 function applyMode(mode, persist) {
574 if (mode !== "dbx-web" && mode !== "dbx-app") {
575 mode = currentMode();
576 }
577
578 document.body.classList.remove("dbx-web", "dbx-app");
579 document.body.classList.add(mode);
580 updateModeIcons(mode);
581
582 if (persist) {
583 storeSet(MODE_KEY, mode);
584 }
585
586 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
587 dbx.event.emit("utilities:mode", { id: ID, mode: mode });
588 }
589
590 updateBackToTopState();
591 }
592
593 function applyTheme(theme, persist) {
594 if (theme !== "light" && theme !== "dark") {
595 theme = currentTheme();
596 }
597
598 document.body.classList.remove("light", "dark", "theme-light", "theme-dark");
599 document.body.classList.add(theme === "dark" ? "theme-dark" : "theme-light");
600 updateThemeIcons(theme);
601
602 if (persist) {
603 storeSet(THEME_KEY, theme);
604 }
605
606 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
607 dbx.event.emit("utilities:theme", { id: ID, theme: theme });
608 }
609 }
610
611 function appScrollElement() {
612 return document.querySelector(".dbx-content") || document.scrollingElement || document.documentElement;
613 }
614
615 function currentScrollTop() {
616 if (document.body.classList.contains("dbx-app")) {
617 const el = appScrollElement();
618 return el ? el.scrollTop : 0;
619 }
620
621 return window.scrollY || document.documentElement.scrollTop || 0;
622 }
623
624 function updateBackToTopState() {
625 const btn = document.getElementById("dbxBackToTop");
626 if (!btn) return;
627 btn.classList.toggle("show", currentScrollTop() > 200);
628 }
629
630 function scrollBackToTop() {
631 if (document.body.classList.contains("dbx-app")) {
632 const el = appScrollElement();
633 if (el && typeof el.scrollTo === "function") {
634 el.scrollTo({ top: 0, behavior: "smooth" });
635 }
636 return;
637 }
638
639 window.scrollTo({ top: 0, behavior: "smooth" });
640 }
641
642 function initClearableInputs() {
643 if (document.__dbxUtilitiesClearableBound) {
644 document.querySelectorAll(".dbx-clearable").forEach(bindClearableInput);
645 return;
646 }
647 document.__dbxUtilitiesClearableBound = true;
648
649 document.addEventListener("input", function (e) {
650 const input = e.target;
651 if (!input || !input.classList || !input.classList.contains("dbx-clearable")) return;
652 bindClearableInput(input);
653 }, true);
654
655 document.addEventListener("click", function (e) {
656 const btn = e.target && e.target.closest ? e.target.closest(".dbx-clear-btn") : null;
657 if (!btn) return;
658 const wrap = btn.closest(".dbx-input-clearable");
659 const input = wrap ? wrap.querySelector(".dbx-clearable") : null;
660 if (!input) return;
661 e.preventDefault();
662 input.value = "";
663 input.dispatchEvent(new Event("input", { bubbles: true }));
664 input.dispatchEvent(new Event("change", { bubbles: true }));
665 input.focus();
666 syncClearableButton(input, btn);
667 }, true);
668
669 document.querySelectorAll(".dbx-clearable").forEach(bindClearableInput);
670 }
671
672 function syncClearableButton(input, btn) {
673 if (!input || !btn) return;
674 if (String(input.value || "").length > 0) {
675 btn.removeAttribute("hidden");
676 } else {
677 btn.setAttribute("hidden", "");
678 }
679 }
680
681 function bindClearableInput(input) {
682 if (!input || !input.classList || !input.classList.contains("dbx-clearable")) return;
683
684 const wrap = input.closest(".dbx-input-clearable");
685 if (!wrap) return;
686
687 let btn = wrap.querySelector(".dbx-clear-btn");
688 if (!btn) {
689 btn = document.createElement("button");
690 btn.type = "button";
691 btn.className = "dbx-clear-btn";
692 btn.setAttribute("aria-label", "Eingabe loeschen");
693 btn.setAttribute("tabindex", "-1");
694 btn.innerHTML = "&times;";
695 wrap.appendChild(btn);
696 }
697
698 syncClearableButton(input, btn);
699 }
700
701 function initBackToTop() {
702 const btn = document.getElementById("dbxBackToTop");
703 if (!btn) return;
704
705 if (!document.__dbxUtilitiesBackToTopBound) {
706 document.__dbxUtilitiesBackToTopBound = true;
707 window.addEventListener("scroll", updateBackToTopState, { passive: true });
708 document.addEventListener("scroll", function (e) {
709 if (e.target && e.target.classList && e.target.classList.contains("dbx-content")) {
710 updateBackToTopState();
711 }
712 }, true);
713 }
714
715 if (!btn.__dbxUtilitiesBackToTopBound) {
716 btn.__dbxUtilitiesBackToTopBound = true;
717 btn.addEventListener("click", function (e) {
718 e.preventDefault();
719 scrollBackToTop();
720 });
721 }
722
723 updateBackToTopState();
724 }
725
726 function initSkin() {
727 const storedSkin = storeGet(SKIN_KEY, null);
728 const bodySkin = parseSkinFromBody();
729 const skin = isValidSkin(storedSkin)
730 ? storedSkin
731 : (isValidSkin(bodySkin) ? bodySkin : defaultSkinForDesign());
732
733 applySkin(skin, false);
734
735 if (storedSkin !== skin) {
736 storeSet(SKIN_KEY, skin);
737 }
738
739 if (bodySkin !== skin) {
740 syncSkinToServer(skin);
741 }
742 }
743
744 function initModeTheme() {
745 const storedMode = storeGet(MODE_KEY, null);
746 applyMode(storedMode === "dbx-web" || storedMode === "dbx-app" ? storedMode : currentMode(), false);
747
748 initSkin();
749
750 if (document.__dbxUtilitiesModeThemeBound) return;
751 document.__dbxUtilitiesModeThemeBound = true;
752
753 document.addEventListener("click", function (e) {
754 const modeBtn = e.target && e.target.closest ? e.target.closest(".dbxModeToggle") : null;
755 if (modeBtn) {
756 e.preventDefault();
757 applyMode(currentMode() === "dbx-app" ? "dbx-web" : "dbx-app", true);
758 return;
759 }
760
761 const skinOpt = e.target && e.target.closest ? e.target.closest(".dbx-skin-opt") : null;
762 if (skinOpt) {
763 e.preventDefault();
764 const skin = skinOpt.getAttribute("data-skin");
765 if (skin) {
766 applySkin(skin, true);
767 }
768 }
769 }, true);
770 }
771
772 function getConsent() {
773 const stored = storeGet(CONSENT_KEY, null);
774 if (stored && typeof stored === "object") {
775 return Object.assign({}, DEFAULT_CONSENT, stored);
776 }
777 return Object.assign({}, DEFAULT_CONSENT);
778 }
779
780 function setConsent(patch) {
781 const next = Object.assign({}, getConsent(), patch || {}, { ts: Date.now() });
782 storeSet(CONSENT_KEY, next);
783 return next;
784 }
785
786 function dispatchConsentChanged(consent) {
787 document.dispatchEvent(new CustomEvent("dbx:consent-changed", { detail: consent }));
788 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
789 dbx.event.emit("utilities:consent", consent);
790 }
791 }
792
793 function isAdminEditorContext() {
794 try {
795 const url = new URL(window.location.href);
796 if (url.searchParams.get("dbx_modul") === "dbxContent_admin") {
797 return true;
798 }
799 } catch (_) {}
800 if (document.body && document.body.classList.contains("dbx-cms-admin")) {
801 return true;
802 }
803 return !!document.querySelector(".dbx-cms-editor, .cms-admin, [data-dbx-cms-editor]");
804 }
805
806 function openConsentHelpWindow(url, title) {
807 const cfg = {
808 url: url,
809 title: title,
810 width: "900",
811 height: "85%",
812 position: "center-top",
813 reload: "1",
814 minimizable: "1",
815 maximizable: "1"
816 };
817 if (window.dbx && dbx.openWin && typeof dbx.openWin.open === "function") {
818 dbx.openWin.open(cfg);
819 return;
820 }
821 window.location.href = url;
822 }
823
824 function openConsentSettings() {
825 openConsentHelpWindow(CONSENT_PRIVACY_URL, "Datenschutz");
826 }
827
828 function openImpressum() {
829 openConsentHelpWindow(CONSENT_IMPRESSUM_URL, "Impressum");
830 }
831
832 function hideConsentBanner() {
833 const banner = document.getElementById("dbxConsentBanner");
834 if (banner) {
835 banner.remove();
836 }
837 document.body.classList.remove("dbx-consent-open");
838 }
839
840 function showConsentBanner() {
841 if (document.getElementById("dbxConsentBanner")) {
842 return;
843 }
844 if (isAdminEditorContext()) {
845 return;
846 }
847 if (getConsent().decided) {
848 return;
849 }
850
851 const banner = document.createElement("div");
852 banner.id = "dbxConsentBanner";
853 banner.className = "dbx-consent-overlay";
854 banner.setAttribute("role", "dialog");
855 banner.setAttribute("aria-modal", "true");
856 banner.setAttribute("aria-label", "Datenschutz und Cookies");
857 banner.innerHTML = ''
858 + '<div class="dbx-consent-backdrop" aria-hidden="true"></div>'
859 + '<div class="dbx-consent-modal card shadow">'
860 + '<div class="card-body dbx-consent-modal-body">'
861 + '<h5 class="dbx-consent-modal-title">Datenschutz &amp; Cookies</h5>'
862 + '<p class="dbx-consent-modal-text">'
863 + 'Auf dieser Website setzen wir technisch notwendige Cookies ein, damit dbXapp '
864 + 'funktioniert (z.&nbsp;B. Anmeldung, Sprache, Ihre Einstellungen). '
865 + 'Externe Medien wie YouTube-Videos laden wir erst nach Ihrer Zustimmung.'
866 + '</p>'
867 + '<p class="dbx-consent-modal-links">'
868 + '<button type="button" class="btn btn-link btn-sm p-0 align-baseline" data-dbx-consent-link="privacy">Datenschutz</button>'
869 + '<span class="dbx-consent-modal-links-sep" aria-hidden="true">ยท</span>'
870 + '<button type="button" class="btn btn-link btn-sm p-0 align-baseline" data-dbx-consent-link="impressum">Impressum</button>'
871 + '</p>'
872 + '<div class="dbx-consent-banner-actions">'
873 + '<button type="button" class="btn btn-outline-secondary btn-sm" data-dbx-consent-action="settings">Einstellungen</button>'
874 + '<button type="button" class="btn btn-outline-secondary btn-sm" data-dbx-consent-action="necessary">Nur notwendige</button>'
875 + '<button type="button" class="btn btn-primary btn-sm" data-dbx-consent-action="accept-all">Alle akzeptieren</button>'
876 + '</div>'
877 + '</div>'
878 + '</div>';
879
880 document.body.classList.add("dbx-consent-open");
881 document.body.appendChild(banner);
882 }
883
884 function syncConsentPanel(panel) {
885 if (!panel) {
886 return;
887 }
888 const consent = getConsent();
889 const youtube = panel.querySelector("[data-dbx-consent-youtube]");
890 if (youtube) {
891 youtube.checked = !!consent.youtube;
892 }
893 const cookies = panel.querySelector("[data-dbx-consent-cookies]");
894 if (cookies) {
895 cookies.checked = true;
896 cookies.disabled = true;
897 }
898 }
899
900 function initConsentPanels(root) {
901 const scope = root && root.querySelectorAll ? root : document;
902 scope.querySelectorAll(".dbx-consent-panel").forEach(syncConsentPanel);
903 }
904
905 function youtubeVideoIdFromUrl(url) {
906 const value = String(url || "");
907 const patterns = [
908 /(?:embed\/|v=|youtu\.be\/)([A-Za-z0-9_-]{11})/,
909 /[?&]v=([A-Za-z0-9_-]{11})/
910 ];
911 for (let i = 0; i < patterns.length; i++) {
912 const match = value.match(patterns[i]);
913 if (match && match[1]) {
914 return match[1];
915 }
916 }
917 return "";
918 }
919
920 function buildYoutubeConsentPlaceholder(el) {
921 const url = el.getAttribute("data-youtube-embed-url") || "";
922 const videoId = youtubeVideoIdFromUrl(url);
923 const thumb = videoId
924 ? '<img class="dbx-youtube-consent-thumb" src="https://img.youtube.com/vi/'
925 + videoId + '/hqdefault.jpg" alt="" loading="lazy">'
926 : "";
927 return thumb
928 + '<button type="button" class="dbx-youtube-consent-play" aria-label="Video abspielen">'
929 + '<i class="bi bi-play-fill"></i></button>';
930 }
931
932 function deactivateYoutubeEmbeds(root) {
933 const scope = root && root.querySelectorAll ? root : document;
934 scope.querySelectorAll("[data-youtube-embed-url].dbx-youtube-activated").forEach(function (el) {
935 el.classList.remove("dbx-youtube-activated");
936 el.innerHTML = buildYoutubeConsentPlaceholder(el);
937 });
938 }
939
940 function activateYoutubeEmbed(el) {
941 if (!el || !el.getAttribute || el.classList.contains("dbx-youtube-activated")) {
942 return;
943 }
944 if (!getConsent().youtube) {
945 return;
946 }
947
948 const url = el.getAttribute("data-youtube-embed-url");
949 if (!url) {
950 return;
951 }
952
953 const iframe = document.createElement("iframe");
954 iframe.className = "dbx-content-video-player";
955 iframe.src = url;
956 iframe.title = el.getAttribute("data-youtube-title") || "YouTube Video";
957 iframe.loading = el.getAttribute("data-youtube-loading") || "lazy";
958 iframe.setAttribute("allowfullscreen", "");
959 iframe.allowFullscreen = true;
960
961 el.classList.add("dbx-youtube-activated");
962 el.innerHTML = "";
963 el.appendChild(iframe);
964 }
965
966 function activateYoutubeEmbeds(root) {
967 const scope = root && root.querySelectorAll ? root : document;
968 scope.querySelectorAll("[data-youtube-embed-url]:not(.dbx-youtube-activated)").forEach(activateYoutubeEmbed);
969 }
970
971 function closeConsentHostWindow(fromEl) {
972 if (!fromEl || typeof fromEl.closest !== "function") {
973 return;
974 }
975 const winEl = fromEl.closest(".dbx-window");
976 if (!winEl || !winEl.id) {
977 return;
978 }
979 if (window.dbx && dbx.openWin && typeof dbx.openWin.close === "function") {
980 dbx.openWin.close(winEl.id);
981 }
982 }
983
984 function acceptAllConsent(fromEl) {
985 const consent = setConsent({ cookies: true, youtube: true, decided: true });
986 hideConsentBanner();
987 dispatchConsentChanged(consent);
988 activateYoutubeEmbeds(document);
989 closeConsentHostWindow(fromEl);
990 }
991
992 function acceptNecessaryConsent(fromEl) {
993 const consent = setConsent({ cookies: true, youtube: false, decided: true });
994 hideConsentBanner();
995 dispatchConsentChanged(consent);
996 closeConsentHostWindow(fromEl);
997 }
998
999 function rejectAllConsent(fromEl) {
1000 const consent = setConsent(Object.assign({}, DEFAULT_CONSENT, { ts: Date.now() }));
1001 deactivateYoutubeEmbeds(document);
1002 initConsentPanels(document);
1003 dispatchConsentChanged(consent);
1004 closeConsentHostWindow(fromEl);
1005 showConsentBanner();
1006 }
1007
1008 function saveConsentFromPanel(panel, fromEl) {
1009 const scope = panel && panel.querySelector ? panel : document;
1010 const youtube = scope.querySelector("[data-dbx-consent-youtube]");
1011 const consent = setConsent({
1012 cookies: true,
1013 youtube: youtube ? !!youtube.checked : false,
1014 decided: true
1015 });
1016 hideConsentBanner();
1017 dispatchConsentChanged(consent);
1018 activateYoutubeEmbeds(document);
1019 closeConsentHostWindow(fromEl || panel);
1020 }
1021
1022 function initConsentDelegation() {
1023 if (document.__dbxUtilitiesConsentBound) {
1024 return;
1025 }
1026 document.__dbxUtilitiesConsentBound = true;
1027
1028 document.addEventListener("click", function (e) {
1029 const linkBtn = e.target && e.target.closest ? e.target.closest("[data-dbx-consent-link]") : null;
1030 if (linkBtn) {
1031 e.preventDefault();
1032 const link = linkBtn.getAttribute("data-dbx-consent-link");
1033 if (link === "privacy") {
1034 openConsentSettings();
1035 } else if (link === "impressum") {
1036 openImpressum();
1037 }
1038 return;
1039 }
1040
1041 const actionBtn = e.target && e.target.closest ? e.target.closest("[data-dbx-consent-action]") : null;
1042 if (actionBtn) {
1043 const action = actionBtn.getAttribute("data-dbx-consent-action");
1044 const panel = actionBtn.closest(".dbx-consent-panel");
1045 if (action === "accept-all") {
1046 e.preventDefault();
1047 acceptAllConsent(actionBtn);
1048 } else if (action === "necessary" || action === "accept-necessary") {
1049 e.preventDefault();
1050 acceptNecessaryConsent(actionBtn);
1051 } else if (action === "save") {
1052 e.preventDefault();
1053 saveConsentFromPanel(panel, actionBtn);
1054 } else if (action === "reject") {
1055 e.preventDefault();
1056 rejectAllConsent(actionBtn);
1057 } else if (action === "settings") {
1058 e.preventDefault();
1059 openConsentSettings();
1060 }
1061 return;
1062 }
1063
1064 const playBtn = e.target && e.target.closest ? e.target.closest(".dbx-youtube-consent-play") : null;
1065 if (!playBtn) {
1066 return;
1067 }
1068 const placeholder = playBtn.closest("[data-youtube-embed-url]");
1069 if (!placeholder || placeholder.classList.contains("dbx-youtube-activated")) {
1070 return;
1071 }
1072 e.preventDefault();
1073 if (!getConsent().youtube) {
1074 openConsentSettings();
1075 return;
1076 }
1077 activateYoutubeEmbed(placeholder);
1078 }, true);
1079
1080 document.addEventListener("change", function (e) {
1081 const input = e.target;
1082 if (!input || !input.matches || !input.matches("[data-dbx-consent-youtube]")) {
1083 return;
1084 }
1085 const panel = input.closest(".dbx-consent-panel");
1086 if (panel) {
1087 syncConsentPanel(panel);
1088 }
1089 }, true);
1090
1091 document.addEventListener("dbx:consent-changed", function (ev) {
1092 if (ev.detail && ev.detail.youtube) {
1093 activateYoutubeEmbeds(document);
1094 } else if (ev.detail && !ev.detail.youtube) {
1095 deactivateYoutubeEmbeds(document);
1096 }
1097 });
1098 }
1099
1100 function initConsent(root) {
1101 initConsentDelegation();
1102 initConsentPanels(root || document);
1103 const consent = getConsent();
1104 if (!consent.decided && (!root || root === document)) {
1105 showConsentBanner();
1106 }
1107 if (consent.youtube) {
1108 activateYoutubeEmbeds(root || document);
1109 }
1110 }
1111
1112 function init(root) {
1113 if (!document.body) return;
1114
1115 initClearableInputs();
1116 initBackToTop();
1117 initModeTheme();
1118 initConsent(root);
1119 initCollapsible(root);
1120 initHtmlTooltips(root);
1121
1122 if (window.dbx && typeof dbx.log === "function") {
1123 dbx.log("[utilities] init");
1124 }
1125 }
1126
1127 const consentApi = {
1128 get: getConsent,
1129 set: setConsent,
1130 acceptAll: acceptAllConsent,
1131 acceptNecessary: acceptNecessaryConsent,
1132 rejectAll: rejectAllConsent,
1133 savePanel: saveConsentFromPanel,
1134 openSettings: openConsentSettings,
1135 openImpressum: openImpressum,
1136 activateYoutube: activateYoutubeEmbeds,
1137 syncPanels: initConsentPanels,
1138 showBanner: showConsentBanner,
1139 hideBanner: hideConsentBanner
1140 };
1141
1142 const api = {
1143 init,
1144 rescan: init,
1145 applyMode,
1146 applyTheme,
1147 applySkin,
1148 currentMode,
1149 currentTheme,
1150 currentSkin: parseSkinFromBody,
1151 skins: ALL_SKINS.slice(),
1152 designSkins: skinIdsForDesign(),
1153 collapsible: {
1154 init: initCollapsible
1155 },
1156 tooltip: {
1157 init: initHtmlTooltips,
1158 htmlList: tooltipHtmlList,
1159 show: showTooltip,
1160 hide: hideTooltip
1161 },
1162 consent: consentApi
1163 };
1164
1165 if (window.dbx) {
1166 dbx.utilities = api;
1167
1168 if (dbx.event && typeof dbx.event.on === "function" && !dbx.utilities.__collapseAjaxAfterBound) {
1169 dbx.utilities.__collapseAjaxAfterBound = true;
1170 dbx.event.on("ajax:after", data => {
1171 const ajaxRoot = data && (data.targetElement || data.root) ? (data.targetElement || data.root) : document;
1172 initCollapsible(ajaxRoot);
1173 initHtmlTooltips(ajaxRoot);
1174 });
1175 }
1176
1177 if (dbx.feature && typeof dbx.feature.register === "function") {
1178 dbx.feature.register(LIB, {
1179 scope: "global",
1180 priority: "verylast",
1181 init,
1182 rescan: init
1183 });
1184 }
1185 }
1186
1187 onReady(init);
1188
1189})(window, document);