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 CONSENT_KEY = "consent";
15 const CONSENT_PRIVACY_URL = "datenschutz";
16 const CONSENT_IMPRESSUM_URL = "impressum";
17 const DEFAULT_CONSENT = { cookies: true, youtube: false, decided: false, ts: 0 };
18 let leaveGuardEnabled = true;
19 let leaveGuardAllowUntil = 0;
22 return !!(window.dbx && window.dbx.feature);
25 function onReady(fn) {
26 if (document.readyState === "loading") {
27 document.addEventListener("DOMContentLoaded", fn, { once: true });
33 function allowLeaveGuardNavigation(durationMs = 2000) {
34 leaveGuardAllowUntil = Date.now() + Math.max(250, Number(durationMs) || 2000);
37 function isSameWebsiteNavigation(url) {
39 const target = new URL(url, window.location.href);
40 return target.origin === window.location.origin;
46 function allowIfInternalNavigation(url, durationMs = 2000) {
47 if (!isSameWebsiteNavigation(url)) {
50 allowLeaveGuardNavigation(durationMs);
54 function initLeaveGuard() {
55 if (document.__dbxUtilitiesLeaveGuardBound) {
58 document.__dbxUtilitiesLeaveGuardBound = true;
60 document.addEventListener("click", function (event) {
61 const explicit = event.target && event.target.closest
62 ? event.target.closest("[data-dbx-leave-allow]")
65 allowLeaveGuardNavigation();
69 const link = event.target && event.target.closest
70 ? event.target.closest("a[href]")
75 const target = String(link.getAttribute("target") || "").toLowerCase();
76 if (target && target !== "_self") {
79 if (isSameWebsiteNavigation(link.href)) {
80 allowLeaveGuardNavigation();
84 document.addEventListener("submit", function (event) {
85 const form = event.target;
86 if (!form || !form.action) {
89 if (isSameWebsiteNavigation(form.action)) {
90 allowLeaveGuardNavigation();
94 // Ein Reload verlaesst fachlich weder Datensatz noch Anwendung. Die
95 // native Browserwarnung ist dabei nur stoerend. Tastatur-Reloads
96 // werden auch in Browsern ohne Navigation API erkannt; Chromium und
97 // andere moderne Browser melden zusaetzlich Toolbar-/API-Reloads hier.
98 document.addEventListener("keydown", function (event) {
99 const key = String(event.key || "").toLowerCase();
100 if (key === "f5" || ((event.ctrlKey || event.metaKey) && key === "r")) {
101 allowLeaveGuardNavigation(10000);
105 if (window.navigation && typeof window.navigation.addEventListener === "function") {
106 window.navigation.addEventListener("navigate", function (event) {
107 if (event && event.navigationType === "reload") {
108 allowLeaveGuardNavigation(10000);
113 window.addEventListener("beforeunload", function (event) {
114 if (!leaveGuardEnabled || Date.now() <= leaveGuardAllowUntil) {
117 event.preventDefault();
118 event.returnValue = "";
122 function storeGet(key, def) {
123 return hasDbx() && typeof dbx.uiGet === "function" ? dbx.uiGet(LIB, ID, key, def) : def;
126 function storeSet(key, value) {
127 if (hasDbx() && typeof dbx.uiSet === "function") {
128 dbx.uiSet(LIB, ID, key, value);
132 function currentMode() {
133 return document.body.classList.contains("dbx-web") ? "dbx-web" : "dbx-app";
136 function currentTheme() {
137 return document.body.classList.contains("theme-dark") || document.body.classList.contains("dark")
142 function skinIdsForDesign() {
143 const design = String(getDesign() || "").toLowerCase();
146 document.querySelectorAll(".dbx-design-skin-opt[data-design][data-skin]").forEach(opt => {
147 if (String(opt.getAttribute("data-design") || "").toLowerCase() !== design) {
150 const skin = String(opt.getAttribute("data-skin") || "").toLowerCase();
151 if (/^[a-z0-9][a-z0-9_-]*$/.test(skin) && found.indexOf(skin) === -1) {
157 const serverSkin = String(getServerSkin() || "").toLowerCase();
158 if (/^[a-z0-9][a-z0-9_-]*$/.test(serverSkin)) {
159 found.push(serverSkin);
166 function defaultSkinForDesign() {
167 const skins = skinIdsForDesign();
168 const serverSkin = String(getServerSkin() || "").toLowerCase();
169 if (skins.indexOf(serverSkin) !== -1) {
172 if (skins.indexOf("blau") !== -1) {
175 if (skins.indexOf("hell") !== -1) {
178 return skins[0] || "blau";
181 function isValidSkin(skin) {
182 return skinIdsForDesign().indexOf(String(skin || "")) !== -1;
185 function getDesign() {
186 if (document.body && document.body.getAttribute("data-dbx-design")) {
187 return document.body.getAttribute("data-dbx-design");
189 if (window.dbx && dbx.config && dbx.config.design) {
190 return dbx.config.design;
195 function getServerSkin() {
196 if (document.body && document.body.getAttribute("data-dbx-skin")) {
197 return document.body.getAttribute("data-dbx-skin");
202 function skinStoreKey() {
203 const design = String(getDesign() || "dbxapp").toLowerCase().replace(/[^a-z0-9_-]/g, "");
204 return SKIN_KEY + ":" + (design || "dbxapp");
207 function requestDefinesSkin() {
209 return new URL(window.location.href).searchParams.has("dbx_color");
215 function parseSkinFromBody() {
216 if (!document.body || !document.body.className) {
217 return getServerSkin();
220 const match = document.body.className.match(/\bskin-([a-z0-9][a-z0-9_-]*)\b/);
221 return match ? match[1] : getServerSkin();
224 function skinCssHref(skin) {
225 const design = getDesign();
226 const root = (window.dbx && dbx.config && dbx.config.rootPath) ? dbx.config.rootPath : "";
227 return root + "design/" + design + "/css/skin-" + skin + ".css";
230 function findSkinStylesheet() {
231 return document.querySelector('link[rel="stylesheet"][href*="skin-"]');
234 function updateModeIcons(mode) {
235 document.querySelectorAll(".dbxModeIcon").forEach(icon => {
236 icon.classList.toggle("bi-window", mode === "dbx-app");
237 icon.classList.toggle("bi-globe", mode !== "dbx-app");
241 function updateThemeIcons(theme) {
242 document.querySelectorAll(".dbxThemeIcon").forEach(icon => {
243 icon.classList.toggle("bi-moon", theme === "dark");
244 icon.classList.toggle("bi-sun", theme !== "dark");
248 function updateSkinMenuState(skin) {
249 document.querySelectorAll(".dbx-skin-opt").forEach(opt => {
250 const item = opt.closest("li");
251 const active = opt.getAttribute("data-skin") === skin;
252 opt.classList.toggle("is-active", active);
254 item.classList.toggle("is-active", active);
257 let mark = opt.querySelector(".dbx-skin-check");
260 mark = document.createElement("i");
261 mark.className = "bi bi-check2 dbx-skin-check";
262 opt.appendChild(mark);
270 function attr(el, name, def = "") {
271 if (!el || !el.getAttribute) return def;
272 const value = el.getAttribute(name);
273 return value == null ? def : String(value).trim();
276 function cssEscape(value) {
277 if (window.CSS && typeof window.CSS.escape === "function") {
278 return window.CSS.escape(value);
281 return String(value || "").replace(/["\\]/g, "\\$&");
284 function escapeHtml(value) {
285 const div = document.createElement("div");
286 div.textContent = value == null ? "" : String(value);
287 return div.innerHTML;
290 let tooltipEl = null;
291 let tooltipTarget = null;
292 let tooltipBound = false;
293 const TOOLTIP_SELECTOR = "[data-dbx-tooltip],[data-dbx-errormsg]";
295 function ensureTooltipStyle() {
296 if (document.getElementById("dbx-utility-tooltip-style")) return;
297 const style = document.createElement("style");
298 style.id = "dbx-utility-tooltip-style";
299 style.textContent = `
300.dbx-utility-tooltip {
304 max-width: min(460px, calc(100vw - 24px));
305 padding: .55rem .7rem;
308 border: 1px solid #d2a800;
309 border-radius: .45rem;
310 box-shadow: 0 .55rem 1.25rem rgba(15, 23, 42, .22);
313 pointer-events: none;
316.dbx-utility-tooltip::before,
317.dbx-utility-tooltip::after {
320 left: var(--dbx-tooltip-arrow-left, 22px);
323 border-left: 8px solid transparent;
324 border-right: 8px solid transparent;
326.dbx-utility-tooltip::before {
328 border-top: 9px solid #d2a800;
330.dbx-utility-tooltip::after {
332 border-top: 8px solid #fff1a8;
334.dbx-utility-tooltip[data-placement="bottom"]::before {
338 border-bottom: 9px solid #d2a800;
340.dbx-utility-tooltip[data-placement="bottom"]::after {
344 border-bottom: 8px solid #fff1a8;
346.dbx-utility-tooltip[data-kind="error"] {
349 border-color: #dc3545;
351.dbx-utility-tooltip[data-kind="error"]::before {
352 border-top-color: #dc3545;
354.dbx-utility-tooltip[data-kind="error"]::after {
355 border-top-color: #ffe4e6;
357.dbx-utility-tooltip[data-kind="error"][data-placement="bottom"]::before {
358 border-top-color: transparent;
359 border-bottom-color: #dc3545;
361.dbx-utility-tooltip[data-kind="error"][data-placement="bottom"]::after {
362 border-top-color: transparent;
363 border-bottom-color: #ffe4e6;
365.dbx-utility-tooltip-title {
367 margin-bottom: .35rem;
370.dbx-utility-tooltip-list {
374.dbx-utility-tooltip-row {
376 grid-template-columns: max-content minmax(4.5rem, max-content) minmax(8rem, 1fr);
380.dbx-utility-tooltip-id {
384.dbx-utility-tooltip-folder {
388.dbx-utility-tooltip-text,
389.dbx-utility-tooltip-title-text {
392.dbx-utility-tooltip-empty {
396 document.head.appendChild(style);
399 function normalizeTooltipHtml(value, asHtml) {
400 let html = String(value == null ? "" : value).trim();
401 if (!html || html === "{tooltip}" || html === "#tooltip#") return "";
402 if (/^(?: |\u00a0|\s)+$/i.test(html)) return "";
404 html = escapeHtml(html);
407 const tpl = document.createElement("template");
408 tpl.innerHTML = html;
409 const text = (tpl.content.textContent || "").replace(/\u00a0/g, " ").trim();
410 const hasVisualNode = !!tpl.content.querySelector("img,svg,canvas,table,ul,ol,li");
411 if (!text && !hasVisualNode) return "";
415 function tooltipHtmlList(rows, options) {
416 options = options || {};
417 rows = Array.isArray(rows) ? rows : [];
419 return `<div class="dbx-utility-tooltip-empty">${escapeHtml(options.empty || "Keine Verwendung")}</div>`;
421 const title = options.title
422 ? `<span class="dbx-utility-tooltip-title">${escapeHtml(options.title)}</span>`
424 return title + `<div class="dbx-utility-tooltip-list">` + rows.map(row => {
425 const id = row && row.id != null ? row.id : "";
426 const folder = row && row.folder != null ? row.folder : "";
427 const text = row && row.title != null ? row.title : "";
428 return `<div class="dbx-utility-tooltip-row">`
429 + `<span class="dbx-utility-tooltip-id">${escapeHtml(id)}</span>`
430 + `<span class="dbx-utility-tooltip-folder">${escapeHtml(folder)}</span>`
431 + `<span class="dbx-utility-tooltip-title-text">${escapeHtml(text)}</span>`
433 }).join("") + `</div>`;
436 function tooltipData(target) {
437 if (!target) return "";
438 const errorText = target.getAttribute("data-dbx-errormsg") || "";
439 const normalizedErrorText = normalizeTooltipHtml(errorText, true);
440 if (normalizedErrorText) {
442 html: normalizedErrorText,
446 const text = target.getAttribute("data-dbx-tooltip") || "";
447 const normalizedText = normalizeTooltipHtml(text, true);
448 if (normalizedText) {
450 html: normalizedText,
457 function positionTooltip(target) {
458 if (!tooltipEl || !target || !target.getBoundingClientRect) return;
459 const rect = target.getBoundingClientRect();
462 const width = tooltipEl.offsetWidth || 0;
463 const height = tooltipEl.offsetHeight || 0;
464 let left = rect.left + Math.min(rect.width / 2, 28) - 22;
465 let top = rect.top - height - gap;
466 let placement = "top";
469 top = rect.bottom + gap;
470 placement = "bottom";
472 left = Math.max(margin, Math.min(left, window.innerWidth - width - margin));
473 const arrowLeft = Math.max(14, Math.min(rect.left + rect.width / 2 - left - 8, width - 22));
475 tooltipEl.dataset.placement = placement;
476 tooltipEl.style.left = `${Math.round(left)}px`;
477 tooltipEl.style.top = `${Math.round(top)}px`;
478 tooltipEl.style.setProperty("--dbx-tooltip-arrow-left", `${Math.round(arrowLeft)}px`);
481 function showTooltip(target) {
482 const data = tooltipData(target);
483 if (!data || !data.html) return;
484 if (target.hasAttribute("title") && !target.hasAttribute("data-dbx-tooltip-title-cache")) {
485 target.setAttribute("data-dbx-tooltip-title-cache", target.getAttribute("title") || "");
486 target.removeAttribute("title");
488 ensureTooltipStyle();
490 tooltipEl = document.createElement("div");
491 tooltipEl.className = "dbx-utility-tooltip";
492 tooltipEl.setAttribute("role", "tooltip");
493 document.body.appendChild(tooltipEl);
495 tooltipTarget = target;
496 tooltipEl.dataset.kind = data.kind || "tooltip";
497 tooltipEl.innerHTML = data.html;
498 tooltipEl.style.left = "-9999px";
499 tooltipEl.style.top = "-9999px";
500 tooltipEl.hidden = false;
501 positionTooltip(target);
504 function hideTooltip(target) {
505 if (target && tooltipTarget && target !== tooltipTarget) return;
506 const restoreTarget = target || tooltipTarget;
507 if (restoreTarget && restoreTarget.hasAttribute("data-dbx-tooltip-title-cache")) {
508 restoreTarget.setAttribute("title", restoreTarget.getAttribute("data-dbx-tooltip-title-cache") || "");
509 restoreTarget.removeAttribute("data-dbx-tooltip-title-cache");
511 tooltipTarget = null;
513 tooltipEl.hidden = true;
517 function initHtmlTooltips(root) {
518 if (tooltipBound) return;
521 document.addEventListener("mouseover", function (e) {
522 const target = e.target && e.target.closest
523 ? e.target.closest(TOOLTIP_SELECTOR)
525 if (!target || target.contains(e.relatedTarget)) return;
529 document.addEventListener("mouseout", function (e) {
530 let target = e.target && e.target.closest
531 ? e.target.closest(TOOLTIP_SELECTOR)
533 if (!target && tooltipTarget && (e.target === tooltipTarget || (tooltipTarget.contains && tooltipTarget.contains(e.target)))) {
534 target = tooltipTarget;
536 if (!target || target.contains(e.relatedTarget)) return;
540 document.addEventListener("focusin", function (e) {
541 const target = e.target && e.target.closest
542 ? e.target.closest(TOOLTIP_SELECTOR)
544 if (target) showTooltip(target);
547 document.addEventListener("focusout", function (e) {
548 let target = e.target && e.target.closest
549 ? e.target.closest(TOOLTIP_SELECTOR)
551 if (!target && tooltipTarget && (e.target === tooltipTarget || (tooltipTarget.contains && tooltipTarget.contains(e.target)))) {
552 target = tooltipTarget;
554 if (target) hideTooltip(target);
557 document.addEventListener("scroll", function () {
560 window.addEventListener("resize", function () {
565 function initPasswordCriteria(root) {
566 root = root || document;
567 const rulesList = [];
568 if (root.matches && root.matches("[data-dbx-password-rules]")) {
569 rulesList.push(root);
571 if (root.querySelectorAll) {
572 root.querySelectorAll("[data-dbx-password-rules]").forEach(
573 rules => rulesList.push(rules)
577 rulesList.forEach(function (rules) {
578 if (rules.dataset.dbxPasswordRulesBound === "1") {
581 const form = rules.closest("form") || document;
582 const passwordName = rules.dataset.passwordInput || "";
583 const repeatName = rules.dataset.passwordRepeat || "";
584 const password = passwordName && form.elements
585 ? form.elements.namedItem(passwordName)
587 const repeat = repeatName && form.elements
588 ? form.elements.namedItem(repeatName)
590 if (!password || !repeat) {
593 rules.dataset.dbxPasswordRulesBound = "1";
595 function setRule(name, valid, active) {
596 const item = rules.querySelector(
597 "[data-password-rule='" + name + "']"
600 const icon = item.querySelector("i");
601 item.classList.toggle("is-valid", active && valid);
602 item.classList.toggle("is-missing", active && !valid);
604 icon.className = "bi " + (!active
607 ? "bi-check-circle-fill"
608 : "bi-x-circle-fill"));
612 function updateRules() {
613 const value = password.value || "";
614 const repeated = repeat.value || "";
615 const active = value !== "" || repeated !== "";
616 const minimumLength = Math.max(
618 Math.min(128, Number(rules.dataset.passwordMin) || 6)
620 const minimumLabel = rules.querySelector(
621 "[data-password-min-label]"
624 minimumLabel.textContent = String(minimumLength);
628 Array.from(value).length >= minimumLength,
633 /[A-Z]/.test(value) && /[a-z]/.test(value),
636 setRule("number", /[0-9]/.test(value), active);
637 setRule("special", /[^A-Za-z0-9]/.test(value), active);
640 value !== "" && value === repeated,
645 password.addEventListener("input", updateRules);
646 repeat.addEventListener("input", updateRules);
651 function getCollapseState(key) {
652 if (!key || !window.dbx || typeof dbx.uiGet !== "function") return "";
653 return dbx.uiGet(COLLAPSE_LIB, key, "state", "");
656 function setCollapseState(key, collapsed) {
657 if (!key || !window.dbx || typeof dbx.uiSet !== "function") return;
658 dbx.uiSet(COLLAPSE_LIB, key, "state", collapsed ? "collapsed" : "open");
661 function collapseStateKey(button, panel, target) {
662 return attr(button, "data-collapse-state-key",
663 attr(panel, "data-collapse-state-key",
664 attr(button, "data-ui-state-key",
665 attr(panel, "data-ui-state-key", target)
671 function setCollapseUi(button, panel, collapsed) {
672 panel.classList.toggle("is-collapsed", collapsed);
673 button.setAttribute("aria-expanded", collapsed ? "false" : "true");
675 const label = button.querySelector("[data-collapse-label]") || button.querySelector("span");
677 label.textContent = collapsed ? "Aufklappen" : "Zuklappen";
681 function initCollapsible(root) {
682 const ctx = root && root.querySelectorAll ? root : document;
685 if (ctx.nodeType === 1 && ctx.matches && ctx.matches("[data-collapse-toggle],[data-admin-collapse-toggle]")) {
689 ctx.querySelectorAll("[data-collapse-toggle],[data-admin-collapse-toggle]").forEach(button => {
690 buttons.push(button);
693 buttons.forEach(button => {
694 if (button.__dbxUtilityCollapse) return;
695 button.__dbxUtilityCollapse = true;
697 const target = attr(button, "data-collapse-toggle", attr(button, "data-admin-collapse-toggle", ""));
700 const safeTarget = cssEscape(target);
702 `[data-collapse-panel="${safeTarget}"]`,
703 `[data-admin-collapsible-panel="${safeTarget}"]`
705 const panel = button.closest(selector) || ctx.querySelector(selector);
708 const stateKey = collapseStateKey(button, panel, target);
709 const storedState = getCollapseState(stateKey);
710 if (storedState === "collapsed" || storedState === "open") {
711 setCollapseUi(button, panel, storedState === "collapsed");
714 button.addEventListener("click", () => {
715 const collapsed = !panel.classList.contains("is-collapsed");
716 setCollapseUi(button, panel, collapsed);
717 setCollapseState(stateKey, collapsed);
719 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
720 dbx.event.emit("ui:collapse", {
732 function syncSkinToServer(skin) {
734 const url = new URL(window.location.href);
735 url.searchParams.set("dbx_color", skin);
736 window.history.replaceState({}, "", url.pathname + url.search + url.hash);
740 if (dbx.ajax && typeof dbx.ajax.request === "function") {
741 const syncUrl = new URL(window.location.href);
742 syncUrl.searchParams.set("dbx_color", skin);
744 url: syncUrl.toString(),
747 }).catch(function () {});
752 function applySkin(skin, persist) {
753 if (!isValidSkin(skin)) {
754 skin = defaultSkinForDesign();
757 const link = findSkinStylesheet();
759 link.href = skinCssHref(skin);
762 Array.from(document.body.classList).forEach(function (name) {
763 if (/^skin-[a-z0-9][a-z0-9_-]*$/.test(name)) {
764 document.body.classList.remove(name);
767 document.body.classList.add("skin-" + skin);
768 document.body.setAttribute("data-dbx-skin", skin);
770 document.body.classList.remove("light", "dark", "theme-light", "theme-dark");
771 if (skin === "dunkel") {
772 document.body.classList.add("theme-dark");
774 document.body.classList.add("theme-light");
777 updateSkinMenuState(skin);
778 updateThemeIcons(skin === "dunkel" ? "dark" : "light");
781 storeSet(skinStoreKey(), skin);
782 storeSet(THEME_KEY, skin === "dunkel" ? "dark" : "light");
783 syncSkinToServer(skin);
786 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
787 dbx.event.emit("utilities:skin", { id: ID, skin: skin });
791 function applyMode(mode, persist) {
792 if (mode !== "dbx-web" && mode !== "dbx-app") {
793 mode = currentMode();
796 document.body.classList.remove("dbx-web", "dbx-app");
797 document.body.classList.add(mode);
798 updateModeIcons(mode);
801 storeSet(MODE_KEY, mode);
804 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
805 dbx.event.emit("utilities:mode", { id: ID, mode: mode });
808 updateBackToTopState();
811 function applyTheme(theme, persist) {
812 if (theme !== "light" && theme !== "dark") {
813 theme = currentTheme();
816 document.body.classList.remove("light", "dark", "theme-light", "theme-dark");
817 document.body.classList.add(theme === "dark" ? "theme-dark" : "theme-light");
818 updateThemeIcons(theme);
821 storeSet(THEME_KEY, theme);
824 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
825 dbx.event.emit("utilities:theme", { id: ID, theme: theme });
829 function appScrollElement() {
830 return document.querySelector(".dbx-content") || document.scrollingElement || document.documentElement;
833 function currentScrollTop() {
834 if (document.body.classList.contains("dbx-app")) {
835 const el = appScrollElement();
836 return el ? el.scrollTop : 0;
839 return window.scrollY || document.documentElement.scrollTop || 0;
842 function updateBackToTopState() {
843 const btn = document.getElementById("dbxBackToTop");
845 btn.classList.toggle("show", currentScrollTop() > 200);
848 function scrollBackToTop() {
849 if (document.body.classList.contains("dbx-app")) {
850 const el = appScrollElement();
851 if (el && typeof el.scrollTo === "function") {
852 el.scrollTo({ top: 0, behavior: "smooth" });
857 window.scrollTo({ top: 0, behavior: "smooth" });
860 function initClearableInputs() {
861 if (document.__dbxUtilitiesClearableBound) {
862 document.querySelectorAll(".dbx-clearable").forEach(bindClearableInput);
865 document.__dbxUtilitiesClearableBound = true;
867 document.addEventListener("input", function (e) {
868 const input = e.target;
869 if (!input || !input.classList || !input.classList.contains("dbx-clearable")) return;
870 bindClearableInput(input);
873 document.addEventListener("click", function (e) {
874 const btn = e.target && e.target.closest ? e.target.closest(".dbx-clear-btn") : null;
876 const wrap = btn.closest(".dbx-input-clearable");
877 const input = wrap ? wrap.querySelector(".dbx-clearable") : null;
881 input.dispatchEvent(new Event("input", { bubbles: true }));
882 input.dispatchEvent(new Event("change", { bubbles: true }));
884 syncClearableButton(input, btn);
885 if (input.hasAttribute("data-dbx-clear-submit") && input.form) {
886 if (typeof input.form.requestSubmit === "function") {
887 input.form.requestSubmit();
894 document.querySelectorAll(".dbx-clearable").forEach(bindClearableInput);
897 function syncClearableButton(input, btn) {
898 if (!input || !btn) return;
899 if (String(input.value || "").length > 0) {
900 btn.removeAttribute("hidden");
902 btn.setAttribute("hidden", "");
906 function bindClearableInput(input) {
907 if (!input || !input.classList || !input.classList.contains("dbx-clearable")) return;
909 const wrap = input.closest(".dbx-input-clearable");
912 let btn = wrap.querySelector(".dbx-clear-btn");
914 btn = document.createElement("button");
916 btn.className = "dbx-clear-btn";
917 btn.setAttribute("aria-label", "Suche zurücksetzen");
918 btn.setAttribute("title", "Suche zurücksetzen");
919 btn.innerHTML = '<i class="bi bi-x-lg" aria-hidden="true"></i>';
920 wrap.appendChild(btn);
923 syncClearableButton(input, btn);
926 function initBackToTop() {
927 const btn = document.getElementById("dbxBackToTop");
930 if (!document.__dbxUtilitiesBackToTopBound) {
931 document.__dbxUtilitiesBackToTopBound = true;
932 window.addEventListener("scroll", updateBackToTopState, { passive: true });
933 document.addEventListener("scroll", function (e) {
934 if (e.target && e.target.classList && e.target.classList.contains("dbx-content")) {
935 updateBackToTopState();
940 if (!btn.__dbxUtilitiesBackToTopBound) {
941 btn.__dbxUtilitiesBackToTopBound = true;
942 btn.addEventListener("click", function (e) {
948 updateBackToTopState();
951 function initSkin() {
952 let storedSkin = storeGet(skinStoreKey(), null);
953 if (storedSkin === null && String(getDesign() || "").toLowerCase() === "dbxapp") {
954 storedSkin = storeGet(SKIN_KEY, null);
956 const bodySkin = parseSkinFromBody();
957 const skin = requestDefinesSkin() && isValidSkin(bodySkin)
959 : (isValidSkin(storedSkin)
961 : (isValidSkin(bodySkin) ? bodySkin : defaultSkinForDesign()));
963 applySkin(skin, false);
965 if (storedSkin !== skin) {
966 storeSet(skinStoreKey(), skin);
969 if (bodySkin !== skin) {
970 syncSkinToServer(skin);
974 function initModeTheme() {
975 const storedMode = storeGet(MODE_KEY, null);
976 applyMode(storedMode === "dbx-web" || storedMode === "dbx-app" ? storedMode : currentMode(), false);
980 if (document.__dbxUtilitiesModeThemeBound) return;
981 document.__dbxUtilitiesModeThemeBound = true;
983 document.addEventListener("click", function (e) {
984 const modeBtn = e.target && e.target.closest ? e.target.closest(".dbxModeToggle") : null;
987 applyMode(currentMode() === "dbx-app" ? "dbx-web" : "dbx-app", true);
991 const skinOpt = e.target && e.target.closest ? e.target.closest(".dbx-skin-opt") : null;
993 const href = String(skinOpt.getAttribute("href") || "").trim();
994 if (href && href !== "#") {
998 const skin = skinOpt.getAttribute("data-skin");
1000 applySkin(skin, true);
1006 function getConsent() {
1007 const stored = storeGet(CONSENT_KEY, null);
1008 if (stored && typeof stored === "object") {
1009 return Object.assign({}, DEFAULT_CONSENT, stored);
1011 return Object.assign({}, DEFAULT_CONSENT);
1014 function setConsent(patch) {
1015 const next = Object.assign({}, getConsent(), patch || {}, { ts: Date.now() });
1016 storeSet(CONSENT_KEY, next);
1020 function dispatchConsentChanged(consent) {
1021 document.dispatchEvent(new CustomEvent("dbx:consent-changed", { detail: consent }));
1022 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
1023 dbx.event.emit("utilities:consent", consent);
1027 function isAdminEditorContext() {
1029 const url = new URL(window.location.href);
1030 if (url.searchParams.get("dbx_modul") === "dbxContent_admin") {
1034 if (document.body && document.body.classList.contains("dbx-cms-admin")) {
1037 return !!document.querySelector(".dbx-cms-editor, .cms-admin, [data-dbx-cms-editor]");
1040 function openConsentHelpWindow(url, title) {
1046 position: "center-top",
1051 if (window.dbx && dbx.openWin && typeof dbx.openWin.open === "function") {
1052 dbx.openWin.open(cfg);
1055 allowIfInternalNavigation(url);
1056 window.location.href = url;
1059 function openConsentSettings() {
1060 openConsentHelpWindow(CONSENT_PRIVACY_URL, "Datenschutz");
1063 function openImpressum() {
1064 openConsentHelpWindow(CONSENT_IMPRESSUM_URL, "Impressum");
1067 function hideConsentBanner() {
1068 const banner = document.getElementById("dbxConsentBanner");
1072 document.body.classList.remove("dbx-consent-open");
1075 function showConsentBanner() {
1076 if (document.getElementById("dbxConsentBanner")) {
1079 if (isAdminEditorContext()) {
1082 if (getConsent().decided) {
1086 const banner = document.createElement("div");
1087 banner.id = "dbxConsentBanner";
1088 banner.className = "dbx-consent-overlay";
1089 banner.setAttribute("role", "dialog");
1090 banner.setAttribute("aria-modal", "true");
1091 banner.setAttribute("aria-label", "Datenschutz und Cookies");
1092 banner.innerHTML = ''
1093 + '<div class="dbx-consent-backdrop" aria-hidden="true"></div>'
1094 + '<div class="dbx-consent-modal card shadow">'
1095 + '<div class="card-body dbx-consent-modal-body">'
1096 + '<h5 class="dbx-consent-modal-title">Datenschutz & Cookies</h5>'
1097 + '<p class="dbx-consent-modal-text">'
1098 + 'Auf dieser Website setzen wir technisch notwendige Cookies ein, damit dbXapp '
1099 + 'funktioniert (z. B. Anmeldung, Sprache, Ihre Einstellungen). '
1100 + 'Externe Medien wie YouTube-Videos laden wir erst nach Ihrer Zustimmung.'
1102 + '<p class="dbx-consent-modal-links">'
1103 + '<button type="button" class="btn btn-link btn-sm p-0 align-baseline" data-dbx-consent-link="privacy">Datenschutz</button>'
1104 + '<span class="dbx-consent-modal-links-sep" aria-hidden="true">·</span>'
1105 + '<button type="button" class="btn btn-link btn-sm p-0 align-baseline" data-dbx-consent-link="impressum">Impressum</button>'
1107 + '<div class="dbx-consent-banner-actions">'
1108 + '<button type="button" class="btn btn-outline-secondary btn-sm" data-dbx-consent-action="settings">Einstellungen</button>'
1109 + '<button type="button" class="btn btn-outline-secondary btn-sm" data-dbx-consent-action="necessary">Nur notwendige</button>'
1110 + '<button type="button" class="btn btn-primary btn-sm" data-dbx-consent-action="accept-all">Alle akzeptieren</button>'
1115 document.body.classList.add("dbx-consent-open");
1116 document.body.appendChild(banner);
1119 function syncConsentPanel(panel) {
1123 const consent = getConsent();
1124 const youtube = panel.querySelector("[data-dbx-consent-youtube]");
1126 youtube.checked = !!consent.youtube;
1128 const cookies = panel.querySelector("[data-dbx-consent-cookies]");
1130 cookies.checked = true;
1131 cookies.disabled = true;
1135 function initConsentPanels(root) {
1136 const scope = root && root.querySelectorAll ? root : document;
1137 scope.querySelectorAll(".dbx-consent-panel").forEach(syncConsentPanel);
1140 function youtubeVideoIdFromUrl(url) {
1141 const value = String(url || "");
1143 /(?:embed\/|v=|youtu\.be\/)([A-Za-z0-9_-]{11})/,
1144 /[?&]v=([A-Za-z0-9_-]{11})/
1146 for (let i = 0; i < patterns.length; i++) {
1147 const match = value.match(patterns[i]);
1148 if (match && match[1]) {
1155 function buildYoutubeConsentPlaceholder(el) {
1156 const url = el.getAttribute("data-youtube-embed-url") || "";
1157 const videoId = youtubeVideoIdFromUrl(url);
1158 const thumb = videoId
1159 ? '<img class="dbx-youtube-consent-thumb" src="https://img.youtube.com/vi/'
1160 + videoId + '/hqdefault.jpg" alt="" loading="lazy">'
1163 + '<button type="button" class="dbx-youtube-consent-play" aria-label="Video abspielen">'
1164 + '<i class="bi bi-play-fill"></i></button>';
1167 function deactivateYoutubeEmbeds(root) {
1168 const scope = root && root.querySelectorAll ? root : document;
1169 scope.querySelectorAll("[data-youtube-embed-url].dbx-youtube-activated").forEach(function (el) {
1170 el.classList.remove("dbx-youtube-activated");
1171 el.innerHTML = buildYoutubeConsentPlaceholder(el);
1175 function activateYoutubeEmbed(el) {
1176 if (!el || !el.getAttribute || el.classList.contains("dbx-youtube-activated")) {
1179 if (!getConsent().youtube) {
1183 const url = el.getAttribute("data-youtube-embed-url");
1188 const iframe = document.createElement("iframe");
1189 iframe.className = "dbx-content-video-player";
1191 iframe.title = el.getAttribute("data-youtube-title") || "YouTube Video";
1192 iframe.loading = el.getAttribute("data-youtube-loading") || "lazy";
1193 iframe.setAttribute("allowfullscreen", "");
1194 iframe.allowFullscreen = true;
1196 el.classList.add("dbx-youtube-activated");
1198 el.appendChild(iframe);
1201 function activateYoutubeEmbeds(root) {
1202 const scope = root && root.querySelectorAll ? root : document;
1203 scope.querySelectorAll("[data-youtube-embed-url]:not(.dbx-youtube-activated)").forEach(activateYoutubeEmbed);
1206 function closeConsentHostWindow(fromEl) {
1207 if (!fromEl || typeof fromEl.closest !== "function") {
1210 const winEl = fromEl.closest(".dbx-window");
1211 if (!winEl || !winEl.id) {
1214 if (window.dbx && dbx.openWin && typeof dbx.openWin.close === "function") {
1215 dbx.openWin.close(winEl.id);
1219 function acceptAllConsent(fromEl) {
1220 const consent = setConsent({ cookies: true, youtube: true, decided: true });
1221 hideConsentBanner();
1222 dispatchConsentChanged(consent);
1223 activateYoutubeEmbeds(document);
1224 closeConsentHostWindow(fromEl);
1227 function acceptNecessaryConsent(fromEl) {
1228 const consent = setConsent({ cookies: true, youtube: false, decided: true });
1229 hideConsentBanner();
1230 dispatchConsentChanged(consent);
1231 closeConsentHostWindow(fromEl);
1234 function rejectAllConsent(fromEl) {
1235 const consent = setConsent(Object.assign({}, DEFAULT_CONSENT, { ts: Date.now() }));
1236 deactivateYoutubeEmbeds(document);
1237 initConsentPanels(document);
1238 dispatchConsentChanged(consent);
1239 closeConsentHostWindow(fromEl);
1240 showConsentBanner();
1243 function saveConsentFromPanel(panel, fromEl) {
1244 const scope = panel && panel.querySelector ? panel : document;
1245 const youtube = scope.querySelector("[data-dbx-consent-youtube]");
1246 const consent = setConsent({
1248 youtube: youtube ? !!youtube.checked : false,
1251 hideConsentBanner();
1252 dispatchConsentChanged(consent);
1253 activateYoutubeEmbeds(document);
1254 closeConsentHostWindow(fromEl || panel);
1257 function initConsentDelegation() {
1258 if (document.__dbxUtilitiesConsentBound) {
1261 document.__dbxUtilitiesConsentBound = true;
1263 document.addEventListener("click", function (e) {
1264 const linkBtn = e.target && e.target.closest ? e.target.closest("[data-dbx-consent-link]") : null;
1267 const link = linkBtn.getAttribute("data-dbx-consent-link");
1268 if (link === "privacy") {
1269 openConsentSettings();
1270 } else if (link === "impressum") {
1276 const actionBtn = e.target && e.target.closest ? e.target.closest("[data-dbx-consent-action]") : null;
1278 const action = actionBtn.getAttribute("data-dbx-consent-action");
1279 const panel = actionBtn.closest(".dbx-consent-panel");
1280 if (action === "accept-all") {
1282 acceptAllConsent(actionBtn);
1283 } else if (action === "necessary" || action === "accept-necessary") {
1285 acceptNecessaryConsent(actionBtn);
1286 } else if (action === "save") {
1288 saveConsentFromPanel(panel, actionBtn);
1289 } else if (action === "reject") {
1291 rejectAllConsent(actionBtn);
1292 } else if (action === "settings") {
1294 openConsentSettings();
1299 const playBtn = e.target && e.target.closest ? e.target.closest(".dbx-youtube-consent-play") : null;
1303 const placeholder = playBtn.closest("[data-youtube-embed-url]");
1304 if (!placeholder || placeholder.classList.contains("dbx-youtube-activated")) {
1308 if (!getConsent().youtube) {
1309 openConsentSettings();
1312 activateYoutubeEmbed(placeholder);
1315 document.addEventListener("change", function (e) {
1316 const input = e.target;
1317 if (!input || !input.matches || !input.matches("[data-dbx-consent-youtube]")) {
1320 const panel = input.closest(".dbx-consent-panel");
1322 syncConsentPanel(panel);
1326 document.addEventListener("dbx:consent-changed", function (ev) {
1327 if (ev.detail && ev.detail.youtube) {
1328 activateYoutubeEmbeds(document);
1329 } else if (ev.detail && !ev.detail.youtube) {
1330 deactivateYoutubeEmbeds(document);
1335 function initConsent(root) {
1336 initConsentDelegation();
1337 initConsentPanels(root || document);
1338 const consent = getConsent();
1339 if (!consent.decided && (!root || root === document)) {
1340 showConsentBanner();
1342 if (consent.youtube) {
1343 activateYoutubeEmbeds(root || document);
1347 function init(root) {
1348 if (!document.body) return;
1350 initClearableInputs();
1355 initCollapsible(root);
1356 initHtmlTooltips(root);
1357 initPasswordCriteria(root);
1359 if (window.dbx && typeof dbx.log === "function") {
1360 dbx.log("[utilities] init");
1364 const consentApi = {
1367 acceptAll: acceptAllConsent,
1368 acceptNecessary: acceptNecessaryConsent,
1369 rejectAll: rejectAllConsent,
1370 savePanel: saveConsentFromPanel,
1371 openSettings: openConsentSettings,
1372 openImpressum: openImpressum,
1373 activateYoutube: activateYoutubeEmbeds,
1374 syncPanels: initConsentPanels,
1375 showBanner: showConsentBanner,
1376 hideBanner: hideConsentBanner
1387 currentSkin: parseSkinFromBody,
1388 skins: skinIdsForDesign(),
1389 designSkins: skinIdsForDesign(),
1391 init: initCollapsible
1394 init: initHtmlTooltips,
1395 htmlList: tooltipHtmlList,
1400 init: initPasswordCriteria
1403 allowOnce: allowLeaveGuardNavigation,
1404 allowIfInternal: allowIfInternalNavigation,
1405 enable: function () {
1406 leaveGuardEnabled = true;
1408 disable: function () {
1409 leaveGuardEnabled = false;
1411 enabled: function () {
1412 return leaveGuardEnabled;
1419 dbx.utilities = api;
1421 if (dbx.event && typeof dbx.event.on === "function" && !dbx.utilities.__collapseAjaxAfterBound) {
1422 dbx.utilities.__collapseAjaxAfterBound = true;
1423 dbx.event.on("ajax:after", data => {
1424 const ajaxRoot = data && (data.targetElement || data.root) ? (data.targetElement || data.root) : document;
1425 initCollapsible(ajaxRoot);
1426 initHtmlTooltips(ajaxRoot);
1427 initPasswordCriteria(ajaxRoot);
1431 if (dbx.feature && typeof dbx.feature.register === "function") {
1432 dbx.feature.register(LIB, {
1434 priority: "verylast",
1443})(window, document);