dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
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 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;
20
21 function hasDbx() {
22 return !!(window.dbx && window.dbx.feature);
23 }
24
25 function onReady(fn) {
26 if (document.readyState === "loading") {
27 document.addEventListener("DOMContentLoaded", fn, { once: true });
28 } else {
29 fn();
30 }
31 }
32
33 function allowLeaveGuardNavigation(durationMs = 2000) {
34 leaveGuardAllowUntil = Date.now() + Math.max(250, Number(durationMs) || 2000);
35 }
36
37 function isSameWebsiteNavigation(url) {
38 try {
39 const target = new URL(url, window.location.href);
40 return target.origin === window.location.origin;
41 } catch (_) {
42 return false;
43 }
44 }
45
46 function allowIfInternalNavigation(url, durationMs = 2000) {
47 if (!isSameWebsiteNavigation(url)) {
48 return false;
49 }
50 allowLeaveGuardNavigation(durationMs);
51 return true;
52 }
53
54 function initLeaveGuard() {
55 if (document.__dbxUtilitiesLeaveGuardBound) {
56 return;
57 }
58 document.__dbxUtilitiesLeaveGuardBound = true;
59
60 document.addEventListener("click", function (event) {
61 const explicit = event.target && event.target.closest
62 ? event.target.closest("[data-dbx-leave-allow]")
63 : null;
64 if (explicit) {
65 allowLeaveGuardNavigation();
66 return;
67 }
68
69 const link = event.target && event.target.closest
70 ? event.target.closest("a[href]")
71 : null;
72 if (!link) {
73 return;
74 }
75 const target = String(link.getAttribute("target") || "").toLowerCase();
76 if (target && target !== "_self") {
77 return;
78 }
79 if (isSameWebsiteNavigation(link.href)) {
80 allowLeaveGuardNavigation();
81 }
82 }, true);
83
84 document.addEventListener("submit", function (event) {
85 const form = event.target;
86 if (!form || !form.action) {
87 return;
88 }
89 if (isSameWebsiteNavigation(form.action)) {
90 allowLeaveGuardNavigation();
91 }
92 }, true);
93
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);
102 }
103 }, true);
104
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);
109 }
110 });
111 }
112
113 window.addEventListener("beforeunload", function (event) {
114 if (!leaveGuardEnabled || Date.now() <= leaveGuardAllowUntil) {
115 return;
116 }
117 event.preventDefault();
118 event.returnValue = "";
119 });
120 }
121
122 function storeGet(key, def) {
123 return hasDbx() && typeof dbx.uiGet === "function" ? dbx.uiGet(LIB, ID, key, def) : def;
124 }
125
126 function storeSet(key, value) {
127 if (hasDbx() && typeof dbx.uiSet === "function") {
128 dbx.uiSet(LIB, ID, key, value);
129 }
130 }
131
132 function currentMode() {
133 return document.body.classList.contains("dbx-web") ? "dbx-web" : "dbx-app";
134 }
135
136 function currentTheme() {
137 return document.body.classList.contains("theme-dark") || document.body.classList.contains("dark")
138 ? "dark"
139 : "light";
140 }
141
142 function skinIdsForDesign() {
143 const design = String(getDesign() || "").toLowerCase();
144 const found = [];
145
146 document.querySelectorAll(".dbx-design-skin-opt[data-design][data-skin]").forEach(opt => {
147 if (String(opt.getAttribute("data-design") || "").toLowerCase() !== design) {
148 return;
149 }
150 const skin = String(opt.getAttribute("data-skin") || "").toLowerCase();
151 if (/^[a-z0-9][a-z0-9_-]*$/.test(skin) && found.indexOf(skin) === -1) {
152 found.push(skin);
153 }
154 });
155
156 if (!found.length) {
157 const serverSkin = String(getServerSkin() || "").toLowerCase();
158 if (/^[a-z0-9][a-z0-9_-]*$/.test(serverSkin)) {
159 found.push(serverSkin);
160 }
161 }
162
163 return found;
164 }
165
166 function defaultSkinForDesign() {
167 const skins = skinIdsForDesign();
168 const serverSkin = String(getServerSkin() || "").toLowerCase();
169 if (skins.indexOf(serverSkin) !== -1) {
170 return serverSkin;
171 }
172 if (skins.indexOf("blau") !== -1) {
173 return "blau";
174 }
175 if (skins.indexOf("hell") !== -1) {
176 return "hell";
177 }
178 return skins[0] || "blau";
179 }
180
181 function isValidSkin(skin) {
182 return skinIdsForDesign().indexOf(String(skin || "")) !== -1;
183 }
184
185 function getDesign() {
186 if (document.body && document.body.getAttribute("data-dbx-design")) {
187 return document.body.getAttribute("data-dbx-design");
188 }
189 if (window.dbx && dbx.config && dbx.config.design) {
190 return dbx.config.design;
191 }
192 return "dbxapp";
193 }
194
195 function getServerSkin() {
196 if (document.body && document.body.getAttribute("data-dbx-skin")) {
197 return document.body.getAttribute("data-dbx-skin");
198 }
199 return "blau";
200 }
201
202 function skinStoreKey() {
203 const design = String(getDesign() || "dbxapp").toLowerCase().replace(/[^a-z0-9_-]/g, "");
204 return SKIN_KEY + ":" + (design || "dbxapp");
205 }
206
207 function requestDefinesSkin() {
208 try {
209 return new URL(window.location.href).searchParams.has("dbx_color");
210 } catch (_) {
211 return false;
212 }
213 }
214
215 function parseSkinFromBody() {
216 if (!document.body || !document.body.className) {
217 return getServerSkin();
218 }
219
220 const match = document.body.className.match(/\bskin-([a-z0-9][a-z0-9_-]*)\b/);
221 return match ? match[1] : getServerSkin();
222 }
223
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";
228 }
229
230 function findSkinStylesheet() {
231 return document.querySelector('link[rel="stylesheet"][href*="skin-"]');
232 }
233
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");
238 });
239 }
240
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");
245 });
246 }
247
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);
253 if (item) {
254 item.classList.toggle("is-active", active);
255 }
256
257 let mark = opt.querySelector(".dbx-skin-check");
258 if (active) {
259 if (!mark) {
260 mark = document.createElement("i");
261 mark.className = "bi bi-check2 dbx-skin-check";
262 opt.appendChild(mark);
263 }
264 } else if (mark) {
265 mark.remove();
266 }
267 });
268 }
269
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();
274 }
275
276 function cssEscape(value) {
277 if (window.CSS && typeof window.CSS.escape === "function") {
278 return window.CSS.escape(value);
279 }
280
281 return String(value || "").replace(/["\\‍]/g, "\\$&");
282 }
283
284 function escapeHtml(value) {
285 const div = document.createElement("div");
286 div.textContent = value == null ? "" : String(value);
287 return div.innerHTML;
288 }
289
290 let tooltipEl = null;
291 let tooltipTarget = null;
292 let tooltipBound = false;
293 const TOOLTIP_SELECTOR = "[data-dbx-tooltip],[data-dbx-errormsg]";
294
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 {
301 position: fixed;
302 z-index: 2147483000;
303 min-width: 9rem;
304 max-width: min(460px, calc(100vw - 24px));
305 padding: .55rem .7rem;
306 color: #111827;
307 background: #fff1a8;
308 border: 1px solid #d2a800;
309 border-radius: .45rem;
310 box-shadow: 0 .55rem 1.25rem rgba(15, 23, 42, .22);
311 font-size: .82rem;
312 line-height: 1.35;
313 pointer-events: none;
314 white-space: normal;
315}
316.dbx-utility-tooltip::before,
317.dbx-utility-tooltip::after {
318 content: "";
319 position: absolute;
320 left: var(--dbx-tooltip-arrow-left, 22px);
321 width: 0;
322 height: 0;
323 border-left: 8px solid transparent;
324 border-right: 8px solid transparent;
325}
326.dbx-utility-tooltip::before {
327 bottom: -9px;
328 border-top: 9px solid #d2a800;
329}
330.dbx-utility-tooltip::after {
331 bottom: -8px;
332 border-top: 8px solid #fff1a8;
333}
334.dbx-utility-tooltip[data-placement="bottom"]::before {
335 top: -9px;
336 bottom: auto;
337 border-top: 0;
338 border-bottom: 9px solid #d2a800;
339}
340.dbx-utility-tooltip[data-placement="bottom"]::after {
341 top: -8px;
342 bottom: auto;
343 border-top: 0;
344 border-bottom: 8px solid #fff1a8;
345}
346.dbx-utility-tooltip[data-kind="error"] {
347 color: #4a0306;
348 background: #ffe4e6;
349 border-color: #dc3545;
350}
351.dbx-utility-tooltip[data-kind="error"]::before {
352 border-top-color: #dc3545;
353}
354.dbx-utility-tooltip[data-kind="error"]::after {
355 border-top-color: #ffe4e6;
356}
357.dbx-utility-tooltip[data-kind="error"][data-placement="bottom"]::before {
358 border-top-color: transparent;
359 border-bottom-color: #dc3545;
360}
361.dbx-utility-tooltip[data-kind="error"][data-placement="bottom"]::after {
362 border-top-color: transparent;
363 border-bottom-color: #ffe4e6;
364}
365.dbx-utility-tooltip-title {
366 display: block;
367 margin-bottom: .35rem;
368 font-weight: 700;
369}
370.dbx-utility-tooltip-list {
371 display: grid;
372 gap: .28rem;
373}
374.dbx-utility-tooltip-row {
375 display: grid;
376 grid-template-columns: max-content minmax(4.5rem, max-content) minmax(8rem, 1fr);
377 gap: .35rem .55rem;
378 align-items: start;
379}
380.dbx-utility-tooltip-id {
381 font-weight: 700;
382 white-space: nowrap;
383}
384.dbx-utility-tooltip-folder {
385 white-space: nowrap;
386 color: #1f2937;
387}
388.dbx-utility-tooltip-text,
389.dbx-utility-tooltip-title-text {
390 min-width: 0;
391}
392.dbx-utility-tooltip-empty {
393 font-weight: 600;
394}
395`;
396 document.head.appendChild(style);
397 }
398
399 function normalizeTooltipHtml(value, asHtml) {
400 let html = String(value == null ? "" : value).trim();
401 if (!html || html === "{tooltip}" || html === "#tooltip#") return "";
402 if (/^(?:&nbsp;|\u00a0|\s)+$/i.test(html)) return "";
403 if (!asHtml) {
404 html = escapeHtml(html);
405 }
406
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 "";
412 return html;
413 }
414
415 function tooltipHtmlList(rows, options) {
416 options = options || {};
417 rows = Array.isArray(rows) ? rows : [];
418 if (!rows.length) {
419 return `<div class="dbx-utility-tooltip-empty">${escapeHtml(options.empty || "Keine Verwendung")}</div>`;
420 }
421 const title = options.title
422 ? `<span class="dbx-utility-tooltip-title">${escapeHtml(options.title)}</span>`
423 : "";
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>`
432 + `</div>`;
433 }).join("") + `</div>`;
434 }
435
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) {
441 return {
442 html: normalizedErrorText,
443 kind: "error"
444 };
445 }
446 const text = target.getAttribute("data-dbx-tooltip") || "";
447 const normalizedText = normalizeTooltipHtml(text, true);
448 if (normalizedText) {
449 return {
450 html: normalizedText,
451 kind: "tooltip"
452 };
453 }
454 return null;
455 }
456
457 function positionTooltip(target) {
458 if (!tooltipEl || !target || !target.getBoundingClientRect) return;
459 const rect = target.getBoundingClientRect();
460 const gap = 10;
461 const margin = 8;
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";
467
468 if (top < margin) {
469 top = rect.bottom + gap;
470 placement = "bottom";
471 }
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));
474
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`);
479 }
480
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");
487 }
488 ensureTooltipStyle();
489 if (!tooltipEl) {
490 tooltipEl = document.createElement("div");
491 tooltipEl.className = "dbx-utility-tooltip";
492 tooltipEl.setAttribute("role", "tooltip");
493 document.body.appendChild(tooltipEl);
494 }
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);
502 }
503
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");
510 }
511 tooltipTarget = null;
512 if (tooltipEl) {
513 tooltipEl.hidden = true;
514 }
515 }
516
517 function initHtmlTooltips(root) {
518 if (tooltipBound) return;
519 tooltipBound = true;
520
521 document.addEventListener("mouseover", function (e) {
522 const target = e.target && e.target.closest
523 ? e.target.closest(TOOLTIP_SELECTOR)
524 : null;
525 if (!target || target.contains(e.relatedTarget)) return;
526 showTooltip(target);
527 }, true);
528
529 document.addEventListener("mouseout", function (e) {
530 let target = e.target && e.target.closest
531 ? e.target.closest(TOOLTIP_SELECTOR)
532 : null;
533 if (!target && tooltipTarget && (e.target === tooltipTarget || (tooltipTarget.contains && tooltipTarget.contains(e.target)))) {
534 target = tooltipTarget;
535 }
536 if (!target || target.contains(e.relatedTarget)) return;
537 hideTooltip(target);
538 }, true);
539
540 document.addEventListener("focusin", function (e) {
541 const target = e.target && e.target.closest
542 ? e.target.closest(TOOLTIP_SELECTOR)
543 : null;
544 if (target) showTooltip(target);
545 }, true);
546
547 document.addEventListener("focusout", function (e) {
548 let target = e.target && e.target.closest
549 ? e.target.closest(TOOLTIP_SELECTOR)
550 : null;
551 if (!target && tooltipTarget && (e.target === tooltipTarget || (tooltipTarget.contains && tooltipTarget.contains(e.target)))) {
552 target = tooltipTarget;
553 }
554 if (target) hideTooltip(target);
555 }, true);
556
557 document.addEventListener("scroll", function () {
558 hideTooltip();
559 }, true);
560 window.addEventListener("resize", function () {
561 hideTooltip();
562 });
563 }
564
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);
570 }
571 if (root.querySelectorAll) {
572 root.querySelectorAll("[data-dbx-password-rules]").forEach(
573 rules => rulesList.push(rules)
574 );
575 }
576
577 rulesList.forEach(function (rules) {
578 if (rules.dataset.dbxPasswordRulesBound === "1") {
579 return;
580 }
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)
586 : null;
587 const repeat = repeatName && form.elements
588 ? form.elements.namedItem(repeatName)
589 : null;
590 if (!password || !repeat) {
591 return;
592 }
593 rules.dataset.dbxPasswordRulesBound = "1";
594
595 function setRule(name, valid, active) {
596 const item = rules.querySelector(
597 "[data-password-rule='" + name + "']"
598 );
599 if (!item) return;
600 const icon = item.querySelector("i");
601 item.classList.toggle("is-valid", active && valid);
602 item.classList.toggle("is-missing", active && !valid);
603 if (icon) {
604 icon.className = "bi " + (!active
605 ? "bi-circle"
606 : (valid
607 ? "bi-check-circle-fill"
608 : "bi-x-circle-fill"));
609 }
610 }
611
612 function updateRules() {
613 const value = password.value || "";
614 const repeated = repeat.value || "";
615 const active = value !== "" || repeated !== "";
616 const minimumLength = Math.max(
617 6,
618 Math.min(128, Number(rules.dataset.passwordMin) || 6)
619 );
620 const minimumLabel = rules.querySelector(
621 "[data-password-min-label]"
622 );
623 if (minimumLabel) {
624 minimumLabel.textContent = String(minimumLength);
625 }
626 setRule(
627 "length",
628 Array.from(value).length >= minimumLength,
629 active
630 );
631 setRule(
632 "letters",
633 /[A-Z]/.test(value) && /[a-z]/.test(value),
634 active
635 );
636 setRule("number", /[0-9]/.test(value), active);
637 setRule("special", /[^A-Za-z0-9]/.test(value), active);
638 setRule(
639 "match",
640 value !== "" && value === repeated,
641 active
642 );
643 }
644
645 password.addEventListener("input", updateRules);
646 repeat.addEventListener("input", updateRules);
647 updateRules();
648 });
649 }
650
651 function getCollapseState(key) {
652 if (!key || !window.dbx || typeof dbx.uiGet !== "function") return "";
653 return dbx.uiGet(COLLAPSE_LIB, key, "state", "");
654 }
655
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");
659 }
660
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)
666 )
667 )
668 );
669 }
670
671 function setCollapseUi(button, panel, collapsed) {
672 panel.classList.toggle("is-collapsed", collapsed);
673 button.setAttribute("aria-expanded", collapsed ? "false" : "true");
674
675 const label = button.querySelector("[data-collapse-label]") || button.querySelector("span");
676 if (label) {
677 label.textContent = collapsed ? "Aufklappen" : "Zuklappen";
678 }
679 }
680
681 function initCollapsible(root) {
682 const ctx = root && root.querySelectorAll ? root : document;
683 const buttons = [];
684
685 if (ctx.nodeType === 1 && ctx.matches && ctx.matches("[data-collapse-toggle],[data-admin-collapse-toggle]")) {
686 buttons.push(ctx);
687 }
688
689 ctx.querySelectorAll("[data-collapse-toggle],[data-admin-collapse-toggle]").forEach(button => {
690 buttons.push(button);
691 });
692
693 buttons.forEach(button => {
694 if (button.__dbxUtilityCollapse) return;
695 button.__dbxUtilityCollapse = true;
696
697 const target = attr(button, "data-collapse-toggle", attr(button, "data-admin-collapse-toggle", ""));
698 if (!target) return;
699
700 const safeTarget = cssEscape(target);
701 const selector = [
702 `[data-collapse-panel="${safeTarget}"]`,
703 `[data-admin-collapsible-panel="${safeTarget}"]`
704 ].join(",");
705 const panel = button.closest(selector) || ctx.querySelector(selector);
706 if (!panel) return;
707
708 const stateKey = collapseStateKey(button, panel, target);
709 const storedState = getCollapseState(stateKey);
710 if (storedState === "collapsed" || storedState === "open") {
711 setCollapseUi(button, panel, storedState === "collapsed");
712 }
713
714 button.addEventListener("click", () => {
715 const collapsed = !panel.classList.contains("is-collapsed");
716 setCollapseUi(button, panel, collapsed);
717 setCollapseState(stateKey, collapsed);
718
719 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
720 dbx.event.emit("ui:collapse", {
721 key: stateKey,
722 target,
723 collapsed,
724 panel,
725 button
726 });
727 }
728 });
729 });
730 }
731
732 function syncSkinToServer(skin) {
733 try {
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);
737 } catch (_) {}
738
739 try {
740 if (dbx.ajax && typeof dbx.ajax.request === "function") {
741 const syncUrl = new URL(window.location.href);
742 syncUrl.searchParams.set("dbx_color", skin);
743 dbx.ajax.request({
744 url: syncUrl.toString(),
745 method: "GET",
746 mode: "html"
747 }).catch(function () {});
748 }
749 } catch (_) {}
750 }
751
752 function applySkin(skin, persist) {
753 if (!isValidSkin(skin)) {
754 skin = defaultSkinForDesign();
755 }
756
757 const link = findSkinStylesheet();
758 if (link) {
759 link.href = skinCssHref(skin);
760 }
761
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);
765 }
766 });
767 document.body.classList.add("skin-" + skin);
768 document.body.setAttribute("data-dbx-skin", skin);
769
770 document.body.classList.remove("light", "dark", "theme-light", "theme-dark");
771 if (skin === "dunkel") {
772 document.body.classList.add("theme-dark");
773 } else {
774 document.body.classList.add("theme-light");
775 }
776
777 updateSkinMenuState(skin);
778 updateThemeIcons(skin === "dunkel" ? "dark" : "light");
779
780 if (persist) {
781 storeSet(skinStoreKey(), skin);
782 storeSet(THEME_KEY, skin === "dunkel" ? "dark" : "light");
783 syncSkinToServer(skin);
784 }
785
786 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
787 dbx.event.emit("utilities:skin", { id: ID, skin: skin });
788 }
789 }
790
791 function applyMode(mode, persist) {
792 if (mode !== "dbx-web" && mode !== "dbx-app") {
793 mode = currentMode();
794 }
795
796 document.body.classList.remove("dbx-web", "dbx-app");
797 document.body.classList.add(mode);
798 updateModeIcons(mode);
799
800 if (persist) {
801 storeSet(MODE_KEY, mode);
802 }
803
804 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
805 dbx.event.emit("utilities:mode", { id: ID, mode: mode });
806 }
807
808 updateBackToTopState();
809 }
810
811 function applyTheme(theme, persist) {
812 if (theme !== "light" && theme !== "dark") {
813 theme = currentTheme();
814 }
815
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);
819
820 if (persist) {
821 storeSet(THEME_KEY, theme);
822 }
823
824 if (window.dbx && dbx.event && typeof dbx.event.emit === "function") {
825 dbx.event.emit("utilities:theme", { id: ID, theme: theme });
826 }
827 }
828
829 function appScrollElement() {
830 return document.querySelector(".dbx-content") || document.scrollingElement || document.documentElement;
831 }
832
833 function currentScrollTop() {
834 if (document.body.classList.contains("dbx-app")) {
835 const el = appScrollElement();
836 return el ? el.scrollTop : 0;
837 }
838
839 return window.scrollY || document.documentElement.scrollTop || 0;
840 }
841
842 function updateBackToTopState() {
843 const btn = document.getElementById("dbxBackToTop");
844 if (!btn) return;
845 btn.classList.toggle("show", currentScrollTop() > 200);
846 }
847
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" });
853 }
854 return;
855 }
856
857 window.scrollTo({ top: 0, behavior: "smooth" });
858 }
859
860 function initClearableInputs() {
861 if (document.__dbxUtilitiesClearableBound) {
862 document.querySelectorAll(".dbx-clearable").forEach(bindClearableInput);
863 return;
864 }
865 document.__dbxUtilitiesClearableBound = true;
866
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);
871 }, true);
872
873 document.addEventListener("click", function (e) {
874 const btn = e.target && e.target.closest ? e.target.closest(".dbx-clear-btn") : null;
875 if (!btn) return;
876 const wrap = btn.closest(".dbx-input-clearable");
877 const input = wrap ? wrap.querySelector(".dbx-clearable") : null;
878 if (!input) return;
879 e.preventDefault();
880 input.value = "";
881 input.dispatchEvent(new Event("input", { bubbles: true }));
882 input.dispatchEvent(new Event("change", { bubbles: true }));
883 input.focus();
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();
888 } else {
889 input.form.submit();
890 }
891 }
892 }, true);
893
894 document.querySelectorAll(".dbx-clearable").forEach(bindClearableInput);
895 }
896
897 function syncClearableButton(input, btn) {
898 if (!input || !btn) return;
899 if (String(input.value || "").length > 0) {
900 btn.removeAttribute("hidden");
901 } else {
902 btn.setAttribute("hidden", "");
903 }
904 }
905
906 function bindClearableInput(input) {
907 if (!input || !input.classList || !input.classList.contains("dbx-clearable")) return;
908
909 const wrap = input.closest(".dbx-input-clearable");
910 if (!wrap) return;
911
912 let btn = wrap.querySelector(".dbx-clear-btn");
913 if (!btn) {
914 btn = document.createElement("button");
915 btn.type = "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);
921 }
922
923 syncClearableButton(input, btn);
924 }
925
926 function initBackToTop() {
927 const btn = document.getElementById("dbxBackToTop");
928 if (!btn) return;
929
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();
936 }
937 }, true);
938 }
939
940 if (!btn.__dbxUtilitiesBackToTopBound) {
941 btn.__dbxUtilitiesBackToTopBound = true;
942 btn.addEventListener("click", function (e) {
943 e.preventDefault();
944 scrollBackToTop();
945 });
946 }
947
948 updateBackToTopState();
949 }
950
951 function initSkin() {
952 let storedSkin = storeGet(skinStoreKey(), null);
953 if (storedSkin === null && String(getDesign() || "").toLowerCase() === "dbxapp") {
954 storedSkin = storeGet(SKIN_KEY, null);
955 }
956 const bodySkin = parseSkinFromBody();
957 const skin = requestDefinesSkin() && isValidSkin(bodySkin)
958 ? bodySkin
959 : (isValidSkin(storedSkin)
960 ? storedSkin
961 : (isValidSkin(bodySkin) ? bodySkin : defaultSkinForDesign()));
962
963 applySkin(skin, false);
964
965 if (storedSkin !== skin) {
966 storeSet(skinStoreKey(), skin);
967 }
968
969 if (bodySkin !== skin) {
970 syncSkinToServer(skin);
971 }
972 }
973
974 function initModeTheme() {
975 const storedMode = storeGet(MODE_KEY, null);
976 applyMode(storedMode === "dbx-web" || storedMode === "dbx-app" ? storedMode : currentMode(), false);
977
978 initSkin();
979
980 if (document.__dbxUtilitiesModeThemeBound) return;
981 document.__dbxUtilitiesModeThemeBound = true;
982
983 document.addEventListener("click", function (e) {
984 const modeBtn = e.target && e.target.closest ? e.target.closest(".dbxModeToggle") : null;
985 if (modeBtn) {
986 e.preventDefault();
987 applyMode(currentMode() === "dbx-app" ? "dbx-web" : "dbx-app", true);
988 return;
989 }
990
991 const skinOpt = e.target && e.target.closest ? e.target.closest(".dbx-skin-opt") : null;
992 if (skinOpt) {
993 const href = String(skinOpt.getAttribute("href") || "").trim();
994 if (href && href !== "#") {
995 return;
996 }
997 e.preventDefault();
998 const skin = skinOpt.getAttribute("data-skin");
999 if (skin) {
1000 applySkin(skin, true);
1001 }
1002 }
1003 }, true);
1004 }
1005
1006 function getConsent() {
1007 const stored = storeGet(CONSENT_KEY, null);
1008 if (stored && typeof stored === "object") {
1009 return Object.assign({}, DEFAULT_CONSENT, stored);
1010 }
1011 return Object.assign({}, DEFAULT_CONSENT);
1012 }
1013
1014 function setConsent(patch) {
1015 const next = Object.assign({}, getConsent(), patch || {}, { ts: Date.now() });
1016 storeSet(CONSENT_KEY, next);
1017 return next;
1018 }
1019
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);
1024 }
1025 }
1026
1027 function isAdminEditorContext() {
1028 try {
1029 const url = new URL(window.location.href);
1030 if (url.searchParams.get("dbx_modul") === "dbxContent_admin") {
1031 return true;
1032 }
1033 } catch (_) {}
1034 if (document.body && document.body.classList.contains("dbx-cms-admin")) {
1035 return true;
1036 }
1037 return !!document.querySelector(".dbx-cms-editor, .cms-admin, [data-dbx-cms-editor]");
1038 }
1039
1040 function openConsentHelpWindow(url, title) {
1041 const cfg = {
1042 url: url,
1043 title: title,
1044 width: "900",
1045 height: "85%",
1046 position: "center-top",
1047 reload: "1",
1048 minimizable: "1",
1049 maximizable: "1"
1050 };
1051 if (window.dbx && dbx.openWin && typeof dbx.openWin.open === "function") {
1052 dbx.openWin.open(cfg);
1053 return;
1054 }
1055 allowIfInternalNavigation(url);
1056 window.location.href = url;
1057 }
1058
1059 function openConsentSettings() {
1060 openConsentHelpWindow(CONSENT_PRIVACY_URL, "Datenschutz");
1061 }
1062
1063 function openImpressum() {
1064 openConsentHelpWindow(CONSENT_IMPRESSUM_URL, "Impressum");
1065 }
1066
1067 function hideConsentBanner() {
1068 const banner = document.getElementById("dbxConsentBanner");
1069 if (banner) {
1070 banner.remove();
1071 }
1072 document.body.classList.remove("dbx-consent-open");
1073 }
1074
1075 function showConsentBanner() {
1076 if (document.getElementById("dbxConsentBanner")) {
1077 return;
1078 }
1079 if (isAdminEditorContext()) {
1080 return;
1081 }
1082 if (getConsent().decided) {
1083 return;
1084 }
1085
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 &amp; Cookies</h5>'
1097 + '<p class="dbx-consent-modal-text">'
1098 + 'Auf dieser Website setzen wir technisch notwendige Cookies ein, damit dbXapp '
1099 + 'funktioniert (z.&nbsp;B. Anmeldung, Sprache, Ihre Einstellungen). '
1100 + 'Externe Medien wie YouTube-Videos laden wir erst nach Ihrer Zustimmung.'
1101 + '</p>'
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>'
1106 + '</p>'
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>'
1111 + '</div>'
1112 + '</div>'
1113 + '</div>';
1114
1115 document.body.classList.add("dbx-consent-open");
1116 document.body.appendChild(banner);
1117 }
1118
1119 function syncConsentPanel(panel) {
1120 if (!panel) {
1121 return;
1122 }
1123 const consent = getConsent();
1124 const youtube = panel.querySelector("[data-dbx-consent-youtube]");
1125 if (youtube) {
1126 youtube.checked = !!consent.youtube;
1127 }
1128 const cookies = panel.querySelector("[data-dbx-consent-cookies]");
1129 if (cookies) {
1130 cookies.checked = true;
1131 cookies.disabled = true;
1132 }
1133 }
1134
1135 function initConsentPanels(root) {
1136 const scope = root && root.querySelectorAll ? root : document;
1137 scope.querySelectorAll(".dbx-consent-panel").forEach(syncConsentPanel);
1138 }
1139
1140 function youtubeVideoIdFromUrl(url) {
1141 const value = String(url || "");
1142 const patterns = [
1143 /(?:embed\/|v=|youtu\.be\/)([A-Za-z0-9_-]{11})/,
1144 /[?&]v=([A-Za-z0-9_-]{11})/
1145 ];
1146 for (let i = 0; i < patterns.length; i++) {
1147 const match = value.match(patterns[i]);
1148 if (match && match[1]) {
1149 return match[1];
1150 }
1151 }
1152 return "";
1153 }
1154
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">'
1161 : "";
1162 return thumb
1163 + '<button type="button" class="dbx-youtube-consent-play" aria-label="Video abspielen">'
1164 + '<i class="bi bi-play-fill"></i></button>';
1165 }
1166
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);
1172 });
1173 }
1174
1175 function activateYoutubeEmbed(el) {
1176 if (!el || !el.getAttribute || el.classList.contains("dbx-youtube-activated")) {
1177 return;
1178 }
1179 if (!getConsent().youtube) {
1180 return;
1181 }
1182
1183 const url = el.getAttribute("data-youtube-embed-url");
1184 if (!url) {
1185 return;
1186 }
1187
1188 const iframe = document.createElement("iframe");
1189 iframe.className = "dbx-content-video-player";
1190 iframe.src = url;
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;
1195
1196 el.classList.add("dbx-youtube-activated");
1197 el.innerHTML = "";
1198 el.appendChild(iframe);
1199 }
1200
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);
1204 }
1205
1206 function closeConsentHostWindow(fromEl) {
1207 if (!fromEl || typeof fromEl.closest !== "function") {
1208 return;
1209 }
1210 const winEl = fromEl.closest(".dbx-window");
1211 if (!winEl || !winEl.id) {
1212 return;
1213 }
1214 if (window.dbx && dbx.openWin && typeof dbx.openWin.close === "function") {
1215 dbx.openWin.close(winEl.id);
1216 }
1217 }
1218
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);
1225 }
1226
1227 function acceptNecessaryConsent(fromEl) {
1228 const consent = setConsent({ cookies: true, youtube: false, decided: true });
1229 hideConsentBanner();
1230 dispatchConsentChanged(consent);
1231 closeConsentHostWindow(fromEl);
1232 }
1233
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();
1241 }
1242
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({
1247 cookies: true,
1248 youtube: youtube ? !!youtube.checked : false,
1249 decided: true
1250 });
1251 hideConsentBanner();
1252 dispatchConsentChanged(consent);
1253 activateYoutubeEmbeds(document);
1254 closeConsentHostWindow(fromEl || panel);
1255 }
1256
1257 function initConsentDelegation() {
1258 if (document.__dbxUtilitiesConsentBound) {
1259 return;
1260 }
1261 document.__dbxUtilitiesConsentBound = true;
1262
1263 document.addEventListener("click", function (e) {
1264 const linkBtn = e.target && e.target.closest ? e.target.closest("[data-dbx-consent-link]") : null;
1265 if (linkBtn) {
1266 e.preventDefault();
1267 const link = linkBtn.getAttribute("data-dbx-consent-link");
1268 if (link === "privacy") {
1269 openConsentSettings();
1270 } else if (link === "impressum") {
1271 openImpressum();
1272 }
1273 return;
1274 }
1275
1276 const actionBtn = e.target && e.target.closest ? e.target.closest("[data-dbx-consent-action]") : null;
1277 if (actionBtn) {
1278 const action = actionBtn.getAttribute("data-dbx-consent-action");
1279 const panel = actionBtn.closest(".dbx-consent-panel");
1280 if (action === "accept-all") {
1281 e.preventDefault();
1282 acceptAllConsent(actionBtn);
1283 } else if (action === "necessary" || action === "accept-necessary") {
1284 e.preventDefault();
1285 acceptNecessaryConsent(actionBtn);
1286 } else if (action === "save") {
1287 e.preventDefault();
1288 saveConsentFromPanel(panel, actionBtn);
1289 } else if (action === "reject") {
1290 e.preventDefault();
1291 rejectAllConsent(actionBtn);
1292 } else if (action === "settings") {
1293 e.preventDefault();
1294 openConsentSettings();
1295 }
1296 return;
1297 }
1298
1299 const playBtn = e.target && e.target.closest ? e.target.closest(".dbx-youtube-consent-play") : null;
1300 if (!playBtn) {
1301 return;
1302 }
1303 const placeholder = playBtn.closest("[data-youtube-embed-url]");
1304 if (!placeholder || placeholder.classList.contains("dbx-youtube-activated")) {
1305 return;
1306 }
1307 e.preventDefault();
1308 if (!getConsent().youtube) {
1309 openConsentSettings();
1310 return;
1311 }
1312 activateYoutubeEmbed(placeholder);
1313 }, true);
1314
1315 document.addEventListener("change", function (e) {
1316 const input = e.target;
1317 if (!input || !input.matches || !input.matches("[data-dbx-consent-youtube]")) {
1318 return;
1319 }
1320 const panel = input.closest(".dbx-consent-panel");
1321 if (panel) {
1322 syncConsentPanel(panel);
1323 }
1324 }, true);
1325
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);
1331 }
1332 });
1333 }
1334
1335 function initConsent(root) {
1336 initConsentDelegation();
1337 initConsentPanels(root || document);
1338 const consent = getConsent();
1339 if (!consent.decided && (!root || root === document)) {
1340 showConsentBanner();
1341 }
1342 if (consent.youtube) {
1343 activateYoutubeEmbeds(root || document);
1344 }
1345 }
1346
1347 function init(root) {
1348 if (!document.body) return;
1349
1350 initClearableInputs();
1351 initBackToTop();
1352 initLeaveGuard();
1353 initModeTheme();
1354 initConsent(root);
1355 initCollapsible(root);
1356 initHtmlTooltips(root);
1357 initPasswordCriteria(root);
1358
1359 if (window.dbx && typeof dbx.log === "function") {
1360 dbx.log("[utilities] init");
1361 }
1362 }
1363
1364 const consentApi = {
1365 get: getConsent,
1366 set: setConsent,
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
1377 };
1378
1379 const api = {
1380 init,
1381 rescan: init,
1382 applyMode,
1383 applyTheme,
1384 applySkin,
1385 currentMode,
1386 currentTheme,
1387 currentSkin: parseSkinFromBody,
1388 skins: skinIdsForDesign(),
1389 designSkins: skinIdsForDesign(),
1390 collapsible: {
1391 init: initCollapsible
1392 },
1393 tooltip: {
1394 init: initHtmlTooltips,
1395 htmlList: tooltipHtmlList,
1396 show: showTooltip,
1397 hide: hideTooltip
1398 },
1399 passwordRules: {
1400 init: initPasswordCriteria
1401 },
1402 leaveGuard: {
1403 allowOnce: allowLeaveGuardNavigation,
1404 allowIfInternal: allowIfInternalNavigation,
1405 enable: function () {
1406 leaveGuardEnabled = true;
1407 },
1408 disable: function () {
1409 leaveGuardEnabled = false;
1410 },
1411 enabled: function () {
1412 return leaveGuardEnabled;
1413 }
1414 },
1415 consent: consentApi
1416 };
1417
1418 if (window.dbx) {
1419 dbx.utilities = api;
1420
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);
1428 });
1429 }
1430
1431 if (dbx.feature && typeof dbx.feature.register === "function") {
1432 dbx.feature.register(LIB, {
1433 scope: "global",
1434 priority: "verylast",
1435 init,
1436 rescan: init
1437 });
1438 }
1439 }
1440
1441 onReady(init);
1442
1443})(window, document);