3 * Global helpers: clearable inputs, back-to-top, app/web mode, skin/theme.
5(function (window, document) {
8 const LIB = "utilities";
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"]
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 };
24 return !!(window.dbx && window.dbx.feature);
27 function onReady(fn) {
28 if (document.readyState === "loading") {
29 document.addEventListener("DOMContentLoaded", fn, { once: true });
35 function storeGet(key, def) {
36 return hasDbx() && typeof dbx.uiGet === "function" ? dbx.uiGet(LIB, ID, key, def) : def;
39 function storeSet(key, value) {
40 if (hasDbx() && typeof dbx.uiSet === "function") {
41 dbx.uiSet(LIB, ID, key, value);
45 function currentMode() {
46 return document.body.classList.contains("dbx-web") ? "dbx-web" : "dbx-app";
49 function currentTheme() {
50 return document.body.classList.contains("theme-dark") || document.body.classList.contains("dark")
55 function skinIdsForDesign() {
56 const design = String(getDesign() || "").toLowerCase();
57 return DESIGN_SKINS[design] || ALL_SKINS;
60 function defaultSkinForDesign() {
61 return String(getDesign() || "").toLowerCase() === "fleurop" ? "hell" : "blau";
64 function isValidSkin(skin) {
65 return skinIdsForDesign().indexOf(String(skin || "")) !== -1;
68 function getDesign() {
69 if (document.body && document.body.getAttribute("data-dbx-design")) {
70 return document.body.getAttribute("data-dbx-design");
72 if (window.dbx && dbx.config && dbx.config.design) {
73 return dbx.config.design;
78 function getServerSkin() {
79 if (document.body && document.body.getAttribute("data-dbx-skin")) {
80 return document.body.getAttribute("data-dbx-skin");
85 function parseSkinFromBody() {
86 if (!document.body || !document.body.className) {
87 return getServerSkin();
90 const match = document.body.className.match(/\bskin-([a-z]+)\b/);
91 return match ? match[1] : getServerSkin();
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";
100 function findSkinStylesheet() {
101 return document.querySelector('link[rel="stylesheet"][href*="skin-"]');
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");
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");
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);
124 item.classList.toggle("is-active", active);
127 let mark = opt.querySelector(".dbx-skin-check");
130 mark = document.createElement("i");
131 mark.className = "bi bi-check2 dbx-skin-check";
132 opt.appendChild(mark);
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();
146 function cssEscape(value) {
147 if (window.CSS && typeof window.CSS.escape === "function") {
148 return window.CSS.escape(value);
151 return String(value || "").replace(/["\\]/g, "\\$&");
154 function escapeHtml(value) {
155 const div = document.createElement("div");
156 div.textContent = value == null ? "" : String(value);
157 return div.innerHTML;
160 let tooltipEl = null;
161 let tooltipTarget = null;
162 let tooltipBound = false;
163 const TOOLTIP_SELECTOR = "[data-dbx-tooltip],[data-dbx-errormsg]";
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 {
174 max-width: min(460px, calc(100vw - 24px));
175 padding: .55rem .7rem;
178 border: 1px solid #d2a800;
179 border-radius: .45rem;
180 box-shadow: 0 .55rem 1.25rem rgba(15, 23, 42, .22);
183 pointer-events: none;
186.dbx-utility-tooltip::before,
187.dbx-utility-tooltip::after {
190 left: var(--dbx-tooltip-arrow-left, 22px);
193 border-left: 8px solid transparent;
194 border-right: 8px solid transparent;
196.dbx-utility-tooltip::before {
198 border-top: 9px solid #d2a800;
200.dbx-utility-tooltip::after {
202 border-top: 8px solid #fff1a8;
204.dbx-utility-tooltip[data-placement="bottom"]::before {
208 border-bottom: 9px solid #d2a800;
210.dbx-utility-tooltip[data-placement="bottom"]::after {
214 border-bottom: 8px solid #fff1a8;
216.dbx-utility-tooltip[data-kind="error"] {
219 border-color: #dc3545;
221.dbx-utility-tooltip[data-kind="error"]::before {
222 border-top-color: #dc3545;
224.dbx-utility-tooltip[data-kind="error"]::after {
225 border-top-color: #ffe4e6;
227.dbx-utility-tooltip[data-kind="error"][data-placement="bottom"]::before {
228 border-top-color: transparent;
229 border-bottom-color: #dc3545;
231.dbx-utility-tooltip[data-kind="error"][data-placement="bottom"]::after {
232 border-top-color: transparent;
233 border-bottom-color: #ffe4e6;
235.dbx-utility-tooltip-title {
237 margin-bottom: .35rem;
240.dbx-utility-tooltip-list {
244.dbx-utility-tooltip-row {
246 grid-template-columns: max-content minmax(4.5rem, max-content) minmax(8rem, 1fr);
250.dbx-utility-tooltip-id {
254.dbx-utility-tooltip-folder {
258.dbx-utility-tooltip-text,
259.dbx-utility-tooltip-title-text {
262.dbx-utility-tooltip-empty {
266 document.head.appendChild(style);
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 "";
274 html = escapeHtml(html);
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 "";
285 function tooltipHtmlList(rows, options) {
286 options = options || {};
287 rows = Array.isArray(rows) ? rows : [];
289 return `<div class="dbx-utility-tooltip-empty">${escapeHtml(options.empty || "Keine Verwendung")}</div>`;
291 const title = options.title
292 ? `<span class="dbx-utility-tooltip-title">${escapeHtml(options.title)}</span>`
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>`
303 }).join("") + `</div>`;
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) {
312 html: normalizedErrorText,
316 const text = target.getAttribute("data-dbx-tooltip") || "";
317 const normalizedText = normalizeTooltipHtml(text, true);
318 if (normalizedText) {
320 html: normalizedText,
327 function positionTooltip(target) {
328 if (!tooltipEl || !target || !target.getBoundingClientRect) return;
329 const rect = target.getBoundingClientRect();
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";
339 top = rect.bottom + gap;
340 placement = "bottom";
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));
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`);
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");
358 ensureTooltipStyle();
360 tooltipEl = document.createElement("div");
361 tooltipEl.className = "dbx-utility-tooltip";
362 tooltipEl.setAttribute("role", "tooltip");
363 document.body.appendChild(tooltipEl);
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);
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");
381 tooltipTarget = null;
383 tooltipEl.hidden = true;
387 function initHtmlTooltips(root) {
388 if (tooltipBound) return;
391 document.addEventListener("mouseover", function (e) {
392 const target = e.target && e.target.closest
393 ? e.target.closest(TOOLTIP_SELECTOR)
395 if (!target || target.contains(e.relatedTarget)) return;
399 document.addEventListener("mouseout", function (e) {
400 let target = e.target && e.target.closest
401 ? e.target.closest(TOOLTIP_SELECTOR)
403 if (!target && tooltipTarget && (e.target === tooltipTarget || (tooltipTarget.contains && tooltipTarget.contains(e.target)))) {
404 target = tooltipTarget;
406 if (!target || target.contains(e.relatedTarget)) return;
410 document.addEventListener("focusin", function (e) {
411 const target = e.target && e.target.closest
412 ? e.target.closest(TOOLTIP_SELECTOR)
414 if (target) showTooltip(target);
417 document.addEventListener("focusout", function (e) {
418 let target = e.target && e.target.closest
419 ? e.target.closest(TOOLTIP_SELECTOR)
421 if (!target && tooltipTarget && (e.target === tooltipTarget || (tooltipTarget.contains && tooltipTarget.contains(e.target)))) {
422 target = tooltipTarget;
424 if (target) hideTooltip(target);
427 document.addEventListener("scroll", function () {
430 window.addEventListener("resize", function () {
435 function getCollapseState(key) {
436 if (!key || !window.dbx || typeof dbx.uiGet !== "function") return "";
437 return dbx.uiGet(COLLAPSE_LIB, key, "state", "");
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");
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)
455 function setCollapseUi(button, panel, collapsed) {
456 panel.classList.toggle("is-collapsed", collapsed);
457 button.setAttribute("aria-expanded", collapsed ? "false" : "true");
459 const label = button.querySelector("[data-collapse-label]") || button.querySelector("span");
461 label.textContent = collapsed ? "Aufklappen" : "Zuklappen";
465 function initCollapsible(root) {
466 const ctx = root && root.querySelectorAll ? root : document;
469 if (ctx.nodeType === 1 && ctx.matches && ctx.matches("[data-collapse-toggle],[data-admin-collapse-toggle]")) {
473 ctx.querySelectorAll("[data-collapse-toggle],[data-admin-collapse-toggle]").forEach(button => {
474 buttons.push(button);
477 buttons.forEach(button => {
478 if (button.__dbxUtilityCollapse) return;
479 button.__dbxUtilityCollapse = true;
481 const target = attr(button, "data-collapse-toggle", attr(button, "data-admin-collapse-toggle", ""));
484 const safeTarget = cssEscape(target);
486 `[data-collapse-panel="${safeTarget}"]`,
487 `[data-admin-collapsible-panel="${safeTarget}"]`
489 const panel = button.closest(selector) || ctx.querySelector(selector);
492 const stateKey = collapseStateKey(button, panel, target);
493 const storedState = getCollapseState(stateKey);
494 if (storedState === "collapsed" || storedState === "open") {
495 setCollapseUi(button, panel, storedState === "collapsed");
498 button.addEventListener("click", () => {
499 const collapsed = !panel.classList.contains("is-collapsed");
500 setCollapseUi(button, panel, collapsed);
501 setCollapseState(stateKey, collapsed);
503 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
504 dbx.event.emit("ui:collapse", {
516 function syncSkinToServer(skin) {
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);
524 if (dbx.ajax && typeof dbx.ajax.request === "function") {
525 const syncUrl = new URL(window.location.href);
526 syncUrl.searchParams.set("dbx_color", skin);
528 url: syncUrl.toString(),
531 }).catch(function () {});
536 function applySkin(skin, persist) {
537 if (!isValidSkin(skin)) {
538 skin = defaultSkinForDesign();
541 const link = findSkinStylesheet();
543 link.href = skinCssHref(skin);
546 ALL_SKINS.forEach(function (id) {
547 document.body.classList.remove("skin-" + id);
549 document.body.classList.add("skin-" + skin);
550 document.body.setAttribute("data-dbx-skin", skin);
552 document.body.classList.remove("light", "dark", "theme-light", "theme-dark");
553 if (skin === "dunkel") {
554 document.body.classList.add("theme-dark");
556 document.body.classList.add("theme-light");
559 updateSkinMenuState(skin);
560 updateThemeIcons(skin === "dunkel" ? "dark" : "light");
563 storeSet(SKIN_KEY, skin);
564 storeSet(THEME_KEY, skin === "dunkel" ? "dark" : "light");
565 syncSkinToServer(skin);
568 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
569 dbx.event.emit("utilities:skin", { id: ID, skin: skin });
573 function applyMode(mode, persist) {
574 if (mode !== "dbx-web" && mode !== "dbx-app") {
575 mode = currentMode();
578 document.body.classList.remove("dbx-web", "dbx-app");
579 document.body.classList.add(mode);
580 updateModeIcons(mode);
583 storeSet(MODE_KEY, mode);
586 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
587 dbx.event.emit("utilities:mode", { id: ID, mode: mode });
590 updateBackToTopState();
593 function applyTheme(theme, persist) {
594 if (theme !== "light" && theme !== "dark") {
595 theme = currentTheme();
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);
603 storeSet(THEME_KEY, theme);
606 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
607 dbx.event.emit("utilities:theme", { id: ID, theme: theme });
611 function appScrollElement() {
612 return document.querySelector(".dbx-content") || document.scrollingElement || document.documentElement;
615 function currentScrollTop() {
616 if (document.body.classList.contains("dbx-app")) {
617 const el = appScrollElement();
618 return el ? el.scrollTop : 0;
621 return window.scrollY || document.documentElement.scrollTop || 0;
624 function updateBackToTopState() {
625 const btn = document.getElementById("dbxBackToTop");
627 btn.classList.toggle("show", currentScrollTop() > 200);
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" });
639 window.scrollTo({ top: 0, behavior: "smooth" });
642 function initClearableInputs() {
643 if (document.__dbxUtilitiesClearableBound) {
644 document.querySelectorAll(".dbx-clearable").forEach(bindClearableInput);
647 document.__dbxUtilitiesClearableBound = true;
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);
655 document.addEventListener("click", function (e) {
656 const btn = e.target && e.target.closest ? e.target.closest(".dbx-clear-btn") : null;
658 const wrap = btn.closest(".dbx-input-clearable");
659 const input = wrap ? wrap.querySelector(".dbx-clearable") : null;
663 input.dispatchEvent(new Event("input", { bubbles: true }));
664 input.dispatchEvent(new Event("change", { bubbles: true }));
666 syncClearableButton(input, btn);
669 document.querySelectorAll(".dbx-clearable").forEach(bindClearableInput);
672 function syncClearableButton(input, btn) {
673 if (!input || !btn) return;
674 if (String(input.value || "").length > 0) {
675 btn.removeAttribute("hidden");
677 btn.setAttribute("hidden", "");
681 function bindClearableInput(input) {
682 if (!input || !input.classList || !input.classList.contains("dbx-clearable")) return;
684 const wrap = input.closest(".dbx-input-clearable");
687 let btn = wrap.querySelector(".dbx-clear-btn");
689 btn = document.createElement("button");
691 btn.className = "dbx-clear-btn";
692 btn.setAttribute("aria-label", "Eingabe loeschen");
693 btn.setAttribute("tabindex", "-1");
694 btn.innerHTML = "×";
695 wrap.appendChild(btn);
698 syncClearableButton(input, btn);
701 function initBackToTop() {
702 const btn = document.getElementById("dbxBackToTop");
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();
715 if (!btn.__dbxUtilitiesBackToTopBound) {
716 btn.__dbxUtilitiesBackToTopBound = true;
717 btn.addEventListener("click", function (e) {
723 updateBackToTopState();
726 function initSkin() {
727 const storedSkin = storeGet(SKIN_KEY, null);
728 const bodySkin = parseSkinFromBody();
729 const skin = isValidSkin(storedSkin)
731 : (isValidSkin(bodySkin) ? bodySkin : defaultSkinForDesign());
733 applySkin(skin, false);
735 if (storedSkin !== skin) {
736 storeSet(SKIN_KEY, skin);
739 if (bodySkin !== skin) {
740 syncSkinToServer(skin);
744 function initModeTheme() {
745 const storedMode = storeGet(MODE_KEY, null);
746 applyMode(storedMode === "dbx-web" || storedMode === "dbx-app" ? storedMode : currentMode(), false);
750 if (document.__dbxUtilitiesModeThemeBound) return;
751 document.__dbxUtilitiesModeThemeBound = true;
753 document.addEventListener("click", function (e) {
754 const modeBtn = e.target && e.target.closest ? e.target.closest(".dbxModeToggle") : null;
757 applyMode(currentMode() === "dbx-app" ? "dbx-web" : "dbx-app", true);
761 const skinOpt = e.target && e.target.closest ? e.target.closest(".dbx-skin-opt") : null;
764 const skin = skinOpt.getAttribute("data-skin");
766 applySkin(skin, true);
772 function getConsent() {
773 const stored = storeGet(CONSENT_KEY, null);
774 if (stored && typeof stored === "object") {
775 return Object.assign({}, DEFAULT_CONSENT, stored);
777 return Object.assign({}, DEFAULT_CONSENT);
780 function setConsent(patch) {
781 const next = Object.assign({}, getConsent(), patch || {}, { ts: Date.now() });
782 storeSet(CONSENT_KEY, next);
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);
793 function isAdminEditorContext() {
795 const url = new URL(window.location.href);
796 if (url.searchParams.get("dbx_modul") === "dbxContent_admin") {
800 if (document.body && document.body.classList.contains("dbx-cms-admin")) {
803 return !!document.querySelector(".dbx-cms-editor, .cms-admin, [data-dbx-cms-editor]");
806 function openConsentHelpWindow(url, title) {
812 position: "center-top",
817 if (window.dbx && dbx.openWin && typeof dbx.openWin.open === "function") {
818 dbx.openWin.open(cfg);
821 window.location.href = url;
824 function openConsentSettings() {
825 openConsentHelpWindow(CONSENT_PRIVACY_URL, "Datenschutz");
828 function openImpressum() {
829 openConsentHelpWindow(CONSENT_IMPRESSUM_URL, "Impressum");
832 function hideConsentBanner() {
833 const banner = document.getElementById("dbxConsentBanner");
837 document.body.classList.remove("dbx-consent-open");
840 function showConsentBanner() {
841 if (document.getElementById("dbxConsentBanner")) {
844 if (isAdminEditorContext()) {
847 if (getConsent().decided) {
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 & Cookies</h5>'
862 + '<p class="dbx-consent-modal-text">'
863 + 'Auf dieser Website setzen wir technisch notwendige Cookies ein, damit dbXapp '
864 + 'funktioniert (z. B. Anmeldung, Sprache, Ihre Einstellungen). '
865 + 'Externe Medien wie YouTube-Videos laden wir erst nach Ihrer Zustimmung.'
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>'
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>'
880 document.body.classList.add("dbx-consent-open");
881 document.body.appendChild(banner);
884 function syncConsentPanel(panel) {
888 const consent = getConsent();
889 const youtube = panel.querySelector("[data-dbx-consent-youtube]");
891 youtube.checked = !!consent.youtube;
893 const cookies = panel.querySelector("[data-dbx-consent-cookies]");
895 cookies.checked = true;
896 cookies.disabled = true;
900 function initConsentPanels(root) {
901 const scope = root && root.querySelectorAll ? root : document;
902 scope.querySelectorAll(".dbx-consent-panel").forEach(syncConsentPanel);
905 function youtubeVideoIdFromUrl(url) {
906 const value = String(url || "");
908 /(?:embed\/|v=|youtu\.be\/)([A-Za-z0-9_-]{11})/,
909 /[?&]v=([A-Za-z0-9_-]{11})/
911 for (let i = 0; i < patterns.length; i++) {
912 const match = value.match(patterns[i]);
913 if (match && match[1]) {
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">'
928 + '<button type="button" class="dbx-youtube-consent-play" aria-label="Video abspielen">'
929 + '<i class="bi bi-play-fill"></i></button>';
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);
940 function activateYoutubeEmbed(el) {
941 if (!el || !el.getAttribute || el.classList.contains("dbx-youtube-activated")) {
944 if (!getConsent().youtube) {
948 const url = el.getAttribute("data-youtube-embed-url");
953 const iframe = document.createElement("iframe");
954 iframe.className = "dbx-content-video-player";
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;
961 el.classList.add("dbx-youtube-activated");
963 el.appendChild(iframe);
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);
971 function closeConsentHostWindow(fromEl) {
972 if (!fromEl || typeof fromEl.closest !== "function") {
975 const winEl = fromEl.closest(".dbx-window");
976 if (!winEl || !winEl.id) {
979 if (window.dbx && dbx.openWin && typeof dbx.openWin.close === "function") {
980 dbx.openWin.close(winEl.id);
984 function acceptAllConsent(fromEl) {
985 const consent = setConsent({ cookies: true, youtube: true, decided: true });
987 dispatchConsentChanged(consent);
988 activateYoutubeEmbeds(document);
989 closeConsentHostWindow(fromEl);
992 function acceptNecessaryConsent(fromEl) {
993 const consent = setConsent({ cookies: true, youtube: false, decided: true });
995 dispatchConsentChanged(consent);
996 closeConsentHostWindow(fromEl);
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();
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({
1013 youtube: youtube ? !!youtube.checked : false,
1016 hideConsentBanner();
1017 dispatchConsentChanged(consent);
1018 activateYoutubeEmbeds(document);
1019 closeConsentHostWindow(fromEl || panel);
1022 function initConsentDelegation() {
1023 if (document.__dbxUtilitiesConsentBound) {
1026 document.__dbxUtilitiesConsentBound = true;
1028 document.addEventListener("click", function (e) {
1029 const linkBtn = e.target && e.target.closest ? e.target.closest("[data-dbx-consent-link]") : null;
1032 const link = linkBtn.getAttribute("data-dbx-consent-link");
1033 if (link === "privacy") {
1034 openConsentSettings();
1035 } else if (link === "impressum") {
1041 const actionBtn = e.target && e.target.closest ? e.target.closest("[data-dbx-consent-action]") : null;
1043 const action = actionBtn.getAttribute("data-dbx-consent-action");
1044 const panel = actionBtn.closest(".dbx-consent-panel");
1045 if (action === "accept-all") {
1047 acceptAllConsent(actionBtn);
1048 } else if (action === "necessary" || action === "accept-necessary") {
1050 acceptNecessaryConsent(actionBtn);
1051 } else if (action === "save") {
1053 saveConsentFromPanel(panel, actionBtn);
1054 } else if (action === "reject") {
1056 rejectAllConsent(actionBtn);
1057 } else if (action === "settings") {
1059 openConsentSettings();
1064 const playBtn = e.target && e.target.closest ? e.target.closest(".dbx-youtube-consent-play") : null;
1068 const placeholder = playBtn.closest("[data-youtube-embed-url]");
1069 if (!placeholder || placeholder.classList.contains("dbx-youtube-activated")) {
1073 if (!getConsent().youtube) {
1074 openConsentSettings();
1077 activateYoutubeEmbed(placeholder);
1080 document.addEventListener("change", function (e) {
1081 const input = e.target;
1082 if (!input || !input.matches || !input.matches("[data-dbx-consent-youtube]")) {
1085 const panel = input.closest(".dbx-consent-panel");
1087 syncConsentPanel(panel);
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);
1100 function initConsent(root) {
1101 initConsentDelegation();
1102 initConsentPanels(root || document);
1103 const consent = getConsent();
1104 if (!consent.decided && (!root || root === document)) {
1105 showConsentBanner();
1107 if (consent.youtube) {
1108 activateYoutubeEmbeds(root || document);
1112 function init(root) {
1113 if (!document.body) return;
1115 initClearableInputs();
1119 initCollapsible(root);
1120 initHtmlTooltips(root);
1122 if (window.dbx && typeof dbx.log === "function") {
1123 dbx.log("[utilities] init");
1127 const consentApi = {
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
1150 currentSkin: parseSkinFromBody,
1151 skins: ALL_SKINS.slice(),
1152 designSkins: skinIdsForDesign(),
1154 init: initCollapsible
1157 init: initHtmlTooltips,
1158 htmlList: tooltipHtmlList,
1166 dbx.utilities = api;
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);
1177 if (dbx.feature && typeof dbx.feature.register === "function") {
1178 dbx.feature.register(LIB, {
1180 priority: "verylast",
1189})(window, document);