3 * Clientseitiger dbXapp-Kernel.
6 * - stellt den globalen Namespace `window.dbx` bereit
7 * - stellt `dbx.feature.register()` fuer selbstregistrierende Feature-Libs bereit
8 * - scannt `data-dbx`-Marker im DOM
9 * - stellt Events, Logging, Runtime-Footer und Task-Phasen bereit
13 * dbx.feature.register("beispiel", {
14 * selector: ".dbxBeispiel",
16 * init: function (el, cfg) {
17 * el.textContent = cfg.label || "bereit";
24 * <div data-dbx="lib=beispiel|label=Hallo"></div>
28(function (window, document, $) {
32 /* =====================================================
34 * ===================================================== */
35 if (!window.dbx) window.dbx = {};
36 const dbx = window.dbx;
39 dbx.assetVersion = "";
42 * Liefert einen Framework-Text in der aktiven Dokumentensprache.
44 * Fachliche Formular- und Reportmeldungen kommen weiterhin aus der
45 * sprachabhängigen FD. Diese kleine Funktion ist ausschließlich für
46 * generische JavaScript-Chrome wie Laufzeit, Grid oder Sortierstatus.
48 dbx.translate = function (translations, fallbackLanguage) {
49 const language = String(document.documentElement.lang || "de")
52 const fallback = String(fallbackLanguage || "de")
55 const values = translations && typeof translations === "object"
59 return values[language] ?? values[fallback] ?? "";
63 const currentScript = document.currentScript;
64 if (currentScript && currentScript.src) {
65 dbx.assetVersion = new URL(currentScript.src, location.href).searchParams.get("v") || "";
70 /* =====================================================
71 * WINDOW HELPER (GLOBAL BRIDGE)
72 * ===================================================== */
75 getCurrentWindowEl() {
76 const script = document.currentScript;
78 return script.closest?.('.dbx-window') || null;
81 const el = document.activeElement;
82 return el?.closest?.('.dbx-window') || null;
85 getCurrentWindowId() {
86 const win = this.getCurrentWindowEl();
87 return win ? win.id : null;
91 if (target === 'all' || target === '*') {
92 if (dbx.openWin && dbx.openWin.closeAll) {
93 dbx.openWin.closeAll();
98 const id = this.getCurrentWindowId();
100 if (id && dbx.openWin && dbx.openWin.close) {
101 dbx.openWin.close(id);
106 if (target === 'all' || target === '*') {
108 if (!dbx.openWin || !dbx.openWin.getAll) return;
110 dbx.openWin.getAll().forEach(w => {
112 this._reloadSingle(w.id);
119 const id = this.getCurrentWindowId();
121 this._reloadSingle(id);
125 _reloadSingle(windowId) {
127 if (!dbx.openWin || !dbx.openWin.get) return;
129 const w = dbx.openWin.get(windowId);
130 if (!w || !w.url) return;
134 const sep = url.includes("?") ? "&" : "?";
135 url = url + sep + "_=" + Date.now();
137 if (dbx.openWin && dbx.openWin.open) {
138 dbx.openWin.open(w.cfg);
146 /* =====================================================
148 * ===================================================== */
151 dbx.log = (...a) => {
152 if (dbx.LOG) console.log("[dbx " + dbx._ts() + "]", ...a);
155 dbx.warn = function (...a) {
156 console.warn("[dbx " + dbx._ts() + "]", ...a);
157 if (a.length >= 3 && typeof a[0] === "string" && typeof a[1] === "string") {
158 dbx.diag("warn", a[0], a[1], String(a[2] || ""), a[3] || {});
162 dbx.isIgnorableBrowserError = function (msg) {
163 const text = String(msg || "");
166 text.includes("ResizeObserver loop completed with undelivered notifications") ||
167 text.includes("ResizeObserver loop limit exceeded")
171 dbx.error = (...a) => {
173 const msg = "[dbx " + dbx._ts() + "] " + a.join(" ");
175 // -------------------------------------------------
176 // 1. console (immer)
177 // -------------------------------------------------
180 // -------------------------------------------------
181 // 2. intern speichern
182 // -------------------------------------------------
183 dbx._errors = dbx._errors || [];
189 dbx._hasCriticalError = true;
191 if (dbx.diag && dbx.diag.queueReport) {
201 dbx._diagnostics.push(diagEntry);
202 dbx.diag.queueReport(diagEntry);
205 // -------------------------------------------------
206 // 3. DEV MODE → optional alert
207 // -------------------------------------------------
209 // ⚠️ kein Spam – nur erste Meldung als Alert
210 if (!dbx.__errorAlertShown) {
215 dbx.__errorAlertShown = true;
219 // -------------------------------------------------
220 // 4. LIVE MODE → visueller Indikator (minimal)
221 // -------------------------------------------------
224 if (!dbx.__errorIndicator) {
226 const el = document.createElement("div");
228 el.className = "dbx-error-indicator";
229 el.style.position = "fixed";
230 el.style.top = "10px";
231 el.style.right = "10px";
232 el.style.width = "12px";
233 el.style.height = "12px";
234 el.style.background = "#dc3545";
235 el.style.borderRadius = "50%";
236 el.style.zIndex = "99999";
237 el.style.boxShadow = "0 0 6px rgba(0,0,0,0.3)";
238 el.title = dbx.translate({
239 de: "DBX-Fehler aufgetreten",
240 en: "A DBX error occurred",
241 es: "Se ha producido un error de DBX"
244 document.body.appendChild(el);
246 dbx.__errorIndicator = el;
251 dbx._ts = function () {
252 const d = new Date();
253 const t = d.toISOString().split("T")[1];
254 return t.replace("Z", "");
258 /* =====================================================
259 * DIAGNOSTICS + DECLARE (Defaults, Aliase, Autoload)
260 * ===================================================== */
261 dbx._diagnostics = dbx._diagnostics || [];
263 dbx.diag = function (level, lib, code, message, ctx) {
267 level: String(level || "info"),
268 lib: String(lib || "core"),
269 code: String(code || "DIAG"),
270 message: String(message || ""),
272 element: dbx.diag.elementHint(ctx && ctx.el)
275 dbx._diagnostics.push(entry);
277 if (entry.level === "info" && !dbx.LOG) {
281 const line = dbx.diag.format(entry);
283 if (entry.level === "error") {
285 } else if (entry.level === "warn") {
287 } else if (dbx.LOG) {
291 if (entry.level === "warn" || entry.level === "error") {
292 dbx.diag.queueReport(entry);
298 dbx.diag.elementHint = function (el) {
300 if (!el || el.nodeType !== 1) return "";
305 parts.push("#" + el.id);
306 } else if (el.classList && el.classList.length) {
307 parts.push("." + Array.from(el.classList).slice(0, 3).join("."));
309 parts.push(el.tagName.toLowerCase());
312 if (el.getAttribute && el.getAttribute("data-dbx")) {
313 const raw = el.getAttribute("data-dbx");
314 const m = String(raw).match(/lib=([^|]+)/);
315 if (m) parts.push("lib=" + m[1]);
318 return parts.join("");
321 dbx.diag.format = function (entry) {
323 const el = entry.element ? " @ " + entry.element : "";
324 const field = entry.ctx && entry.ctx.field ? " field=" + entry.ctx.field : "";
325 const src = entry.ctx && entry.ctx.source ? " source=" + entry.ctx.source : "";
326 const val = entry.ctx && entry.ctx.value !== undefined ? " value=" + JSON.stringify(entry.ctx.value) : "";
328 return "[dbx][" + entry.lib + "][" + entry.code + "] " + entry.message + el + field + src + val;
331 dbx.diag.queueReport = function (entry) {
333 dbx.diag._reportQueue = dbx.diag._reportQueue || [];
334 dbx.diag._reportQueue.push(entry);
336 if (dbx.diag._reportTimer) return;
338 dbx.diag._reportTimer = setTimeout(function () {
339 dbx.diag._reportTimer = null;
340 dbx.diag.flushReports();
344 dbx.diag.reportUrl = function () {
345 const root = (dbx.config && dbx.config.rootPath) ? dbx.config.rootPath : "/";
346 return root + "?dbx_modul=dbxAdmin&dbx_run1=sysmsg&dbx_run2=client_diag";
349 dbx.diag.flushReports = function () {
351 const queue = (dbx.diag._reportQueue || []).slice();
352 dbx.diag._reportQueue = [];
354 if (!queue.length) return;
357 entries: queue.map(function (e) {
369 if (!dbx.ajax || typeof dbx.ajax.request !== "function") return;
372 url: dbx.diag.reportUrl(),
375 headers: { "Content-Type": "application/json" },
376 body: JSON.stringify(payload),
379 }).catch(function (err) {
380 dbx.log("diag report failed:", err);
384 dbx.declare = dbx.declare || {};
386 dbx.declare.schemas = dbx.declare.schemas || {};
387 dbx.declare.transforms = dbx.declare.transforms || {};
388 dbx.declare.infer = dbx.declare.infer || {};
390 dbx.declare.registerSchema = function (lib, schema) {
391 if (!lib || !schema) return;
392 dbx.declare.schemas[lib] = schema;
395 dbx.declare.readAttr = function (el, name) {
396 if (!el || !el.getAttribute) return null;
397 const v = el.getAttribute(name);
398 if (v === null) return null;
402 dbx.declare.readField = function (el, lib, field, spec, ctx) {
404 const aliases = [spec.attr].concat(spec.aliases || []).filter(Boolean);
405 let source = "missing";
408 if (ctx && ctx.base && ctx.base[field] !== undefined && String(ctx.base[field]).trim() !== "") {
410 value: String(ctx.base[field]).trim(),
416 for (let i = 0; i < aliases.length; i++) {
417 const raw = dbx.declare.readAttr(el, aliases[i]);
418 if (raw !== null && String(raw).trim() !== "") {
420 value = String(raw).trim();
425 if (value === undefined && typeof spec.infer === "function") {
426 const inferred = spec.infer(el, ctx || {});
427 if (inferred !== undefined && inferred !== null && String(inferred).trim() !== "") {
429 value = String(inferred).trim();
433 if (value === undefined) {
434 if (spec.default !== undefined) {
435 value = spec.default;
437 dbx.diag("info", lib, field.toUpperCase() + "_DEFAULT",
438 "Attribut nicht gesetzt, Standard verwendet",
439 { el: el, field: field, source: source, value: value });
440 } else if (spec.required) {
441 dbx.diag("warn", lib, field.toUpperCase() + "_MISSING",
442 "Pflicht-Attribut fehlt",
443 { el: el, field: field, source: source });
450 if (typeof spec.coerce === "function") {
451 value = spec.coerce(value, el, ctx || {});
454 return { value: value, source: source, field: field };
457 dbx.declare.infer.ajaxTarget = function (root) {
459 if (!root || root.nodeType !== 1) return "";
461 const id = String(root.id || "").trim();
462 if (/^dbx_target_\d+$/i.test(id)) return id;
464 const form = root.tagName === "FORM"
466 : root.querySelector("form.dbxAjax, form[id^='dbx_form_']");
468 if (form && form.id) {
469 const m = form.id.match(/dbx_form_(\d+)/i);
470 if (m) return "dbx_target_" + m[1];
473 const target = root.querySelector("[id^='dbx_target_']");
474 if (target && target.id) return target.id;
479 dbx.declare.infer.confirmRoot = function (el) {
481 if (!el || el.nodeType !== 1) return null;
483 const form = el.closest("form.dbxAjax");
484 if (form) return form;
486 const panel = el.closest(".dbx-confirm-root, [data-dbx*='lib=confirm']");
487 if (panel) return panel;
489 return el.parentElement || document.body;
492 dbx.declare.buildFromSchema = function (lib, root, baseCfg, index) {
494 const schema = dbx.declare.schemas[lib];
495 if (!schema || !schema.fields) return null;
497 const ctx = { root: root, index: index, base: baseCfg || {} };
498 const raw = { _index: index, lib: lib };
500 Object.keys(schema.fields).forEach(function (field) {
501 const spec = schema.fields[field];
502 const res = dbx.declare.readField(root, lib, field, spec, ctx);
503 raw[field] = res.value;
504 raw["_" + field + "Source"] = res.source;
507 if (typeof dbx.declare.transforms[lib] === "function") {
508 return dbx.declare.transforms[lib](raw, root);
514 dbx.declare.resolve = function (lib, root) {
516 if (!root) return [];
518 const cacheKey = "_dbx" + lib.charAt(0).toUpperCase() + lib.slice(1) + "Configs";
520 if (Array.isArray(root[cacheKey])) {
521 return root[cacheKey];
524 const attr = dbx.declare.readAttr(root, "data-dbx") || "";
525 const parsed = dbx.parseData(attr).filter(function (cfg) {
526 return cfg.lib === lib;
532 out = parsed.map(function (cfg, index) {
533 return dbx.declare.buildFromSchema(lib, root, cfg, index) || cfg;
535 } else if (dbx.declare.schemas[lib]) {
536 out = [dbx.declare.buildFromSchema(lib, root, {}, 0)];
539 if (!out.length && dbx.declare.schemas[lib]) {
540 out = [dbx.declare.buildFromSchema(lib, root, {}, 0)];
543 root[cacheKey] = out;
548 dbx.declare.hasLibInDataDbx = function (el, lib) {
549 const attr = dbx.declare.readAttr(el, "data-dbx") || "";
550 return dbx.parseData(attr).some(function (cfg) {
551 return cfg.lib === lib;
555 dbx.declare.markAutoload = function (el, lib) {
556 el.__dbxAutoload = el.__dbxAutoload || {};
557 if (el.__dbxAutoload[lib]) return false;
558 el.__dbxAutoload[lib] = true;
562 dbx.ensureElementScopeStores = function (el) {
564 if (!el || el.nodeType !== 1) return;
566 el.__dbxInitialized = el.__dbxInitialized || {};
567 el.__dbxState = el.__dbxState || {};
568 el.__dbxError = el.__dbxError || {};
571 dbx.declare.queueFeature = function (lib, el, scopeType, cfg) {
573 if (!dbx.declare.markAutoload(el, lib)) return;
575 dbx.ensureElementScopeStores(el);
577 const f = dbx.feature._features[lib];
578 let scope = scopeType || "element";
584 const id = (cfg && cfg.id) ? cfg.id : (el.id || "undef");
585 const keyScoped = scope + ":" + lib + ":" + id;
590 cfg: Object.assign({ lib: lib, id: id }, cfg || {}),
596 dbx.declare.scanAutoload = function (ctx) {
598 const root = ctx || document;
600 root.querySelectorAll("form.dbxAjax").forEach(function (form) {
601 if (!dbx.declare.hasLibInDataDbx(form, "ajax")) {
602 dbx.declare.queueFeature("ajax", form, "element", { id: form.id || "undef" });
604 if (!dbx.declare.hasLibInDataDbx(form, "form")) {
605 const wrap = form.closest(".dbxForm_wrapper") || form;
606 dbx.declare.queueFeature("form", wrap, "element", { id: wrap.id || form.id || "undef" });
608 if (form.querySelector(".dbxConfirm, [data-confirm]") && !dbx.declare.hasLibInDataDbx(form, "confirm")) {
609 dbx.declare.queueFeature("confirm", form, "element", { id: form.id || "undef" });
613 root.querySelectorAll(".dbx-ajax-root").forEach(function (el) {
614 if (!dbx.declare.hasLibInDataDbx(el, "ajax")) {
615 dbx.declare.queueFeature("ajax", el, "element", { id: el.id || "undef" });
619 root.querySelectorAll(".dbxConfirm:not(form), [data-confirm]:not(form)").forEach(function (el) {
620 const confirmRoot = dbx.declare.infer.confirmRoot(el);
621 if (confirmRoot && !dbx.declare.hasLibInDataDbx(confirmRoot, "confirm")) {
622 dbx.declare.queueFeature("confirm", confirmRoot, "element", { id: confirmRoot.id || "undef" });
626 root.querySelectorAll(".dbx-win, .dbx-win-preload").forEach(function (el) {
627 if (!dbx.declare.hasLibInDataDbx(el, "openWin")) {
628 dbx.declare.queueFeature("openWin", el, "global", { id: el.id || "undef" });
632 dbx.log("declare.scanAutoload → tasks:", dbx._tasks.length);
636 /* =====================================================
637 * GLOBAL ERROR SHIELD (FAIL-SAFE)
638 * ===================================================== */
640 if (!dbx.__errorShield) {
642 window.onerror = function (msg, src, line, col, err) {
644 if (dbx.isIgnorableBrowserError(msg)) {
645 dbx.log("IGNORED BROWSER ERROR:", msg);
652 "at", src + ":" + line + ":" + col
655 // DEV → Fehler NICHT unterdrücken (Browser zeigt Stacktrace)
656 if (dbx.LOG) return false;
658 // LIVE → Fehler unterdrücken (silent)
662 window.onunhandledrejection = function (e) {
664 const reason = e && e.reason ? e.reason : "";
665 const msg = reason && reason.message ? reason.message : reason;
666 if (dbx.isIgnorableBrowserError(msg)) {
667 dbx.log("IGNORED PROMISE ERROR:", msg);
677 dbx.__errorShield = true;
683 /* =====================================================
685 * ===================================================== */
686 dbx.config = dbx.config || (function () {
688 let scriptPath = "/";
689 let design = "default";
693 const scripts = document.getElementsByTagName("script");
695 for (let s of scripts) {
697 if (!s.src || !s.src.includes("core.js")) continue;
699 const url = new URL(s.src, window.location.origin);
701 scriptPath = url.pathname.substring(0, url.pathname.lastIndexOf("/") + 1);
703 const d = url.searchParams.get("design");
706 const l = url.searchParams.get("log");
707 if (l && l.toLowerCase() === "on") log = true;
709 if (scriptPath.includes("/js/lib/")) {
710 rootPath = scriptPath.split("/js/lib/")[0] + "/";
725 dbx.LOG = dbx.config.log;
726 dbx.log("core init → config:", dbx.config);
728 /* =====================================================
730 * ===================================================== */
731 dbx.uiSet = function (lib, id, key, val) {
732 if (!lib || !id || !key || id === "undef") return;
733 const k = `dbx.UI.${lib}.${id}.${key}`;
735 localStorage.setItem(k, JSON.stringify(val));
736 dbx.log("uiSet:", k, val);
738 dbx.warn("uiSet failed:", k, e);
742 dbx.uiGet = function (lib, id, key, def) {
743 if (!lib || !id || !key || id === "undef") return def;
744 const k = `dbx.UI.${lib}.${id}.${key}`;
746 const v = localStorage.getItem(k);
747 if (v === null) return def;
748 const parsed = JSON.parse(v);
749 dbx.log("uiGet:", k, parsed);
752 dbx.warn("uiGet failed:", k, e);
757 dbx.getDesign = () => dbx.config.design;
758 dbx.getLibId = cfg => (cfg && cfg.id) ? cfg.id : "undef";
760 /* =====================================================
762 * ===================================================== */
763 dbx.footerStatus = dbx.footerStatus || (function () {
765 function formatSeconds(seconds) {
766 seconds = Math.max(0, Number(seconds) || 0);
767 return seconds.toFixed(3);
770 function formatPhpSeconds(seconds) {
771 seconds = Math.max(0, Number(seconds) || 0);
772 return Math.max(0.001, seconds).toFixed(3);
775 function runtimeElement() {
776 return document.querySelector("[data-dbx-runtime]");
779 function responseRuntimeSeconds() {
780 const perf = window.performance;
781 const nav = perf && perf.getEntriesByType && perf.getEntriesByType("navigation")[0];
782 if (nav && nav.duration) {
783 return nav.duration / 1000;
786 return perf && perf.now ? perf.now() / 1000 : 0;
789 function navigationPhpRuntimeSeconds() {
790 const perf = window.performance;
791 const nav = perf && perf.getEntriesByType && perf.getEntriesByType("navigation")[0];
792 const entries = nav && nav.serverTiming ? Array.from(nav.serverTiming) : [];
793 const timer = entries.find(item => item && item.name === "dbxphp");
794 const duration = timer ? Number(timer.duration) : NaN;
796 return Number.isFinite(duration) && duration >= 0 ? duration / 1000 : null;
799 function phpRuntimeSeconds(el) {
800 const headerRuntime = navigationPhpRuntimeSeconds();
801 if (headerRuntime !== null) {
802 return headerRuntime;
805 const raw = el ? el.getAttribute("data-dbx-php-runtime") : "";
806 const value = Number(raw);
808 return Number.isFinite(value) && value >= 0 ? value : null;
811 function write(responseSeconds, phpSeconds, label) {
812 const el = runtimeElement();
815 const responseLabel = formatSeconds(responseSeconds);
816 const hasPhp = phpSeconds != null && phpSeconds !== ""
817 && Number.isFinite(Number(phpSeconds)) && Number(phpSeconds) >= 0;
818 const phpValue = hasPhp ? Number(phpSeconds) : null;
819 const phpLabel = hasPhp ? "/" + formatPhpSeconds(phpValue) : "";
820 const text = responseLabel + phpLabel + " sec";
822 el.textContent = text;
825 el.setAttribute("data-dbx-php-runtime", formatPhpSeconds(phpValue));
829 ? label + ": " + responseLabel + " / " + formatPhpSeconds(phpValue) + " sec"
830 : label + ": " + responseLabel + " sec";
832 el.setAttribute("title", title);
833 el.setAttribute("data-dbx-page-runtime-title", title);
837 const el = runtimeElement();
840 const update = function () {
842 responseRuntimeSeconds(),
843 phpRuntimeSeconds(el),
845 de: "Komplette Antwortlaufzeit / PHP-Laufzeit",
846 en: "Complete response time / PHP runtime",
847 es: "Tiempo total de respuesta / tiempo de ejecución de PHP"
852 if (document.readyState === "complete") {
855 window.addEventListener("load", update, { once: true });
856 setTimeout(update, 0);
860 function requestHasAjaxFlag(url, body) {
861 const targetUrl = String(url || "");
862 if (/[?&]dbx_ajax=1(?:&|$)/.test(targetUrl)) {
866 if (body instanceof URLSearchParams && body.get("dbx_ajax") === "1") {
870 if (body instanceof FormData && body.get("dbx_ajax") === "1") {
874 if (typeof body === "string" && /(?:^|&)dbx_ajax=1(?:&|$)/.test(body)) {
881 function shouldTrackAjaxRuntime(url, body, options) {
883 options.skipRuntime === true ||
884 options.footerRuntime === "hidden" ||
885 options.footerRuntime === "skip" ||
886 options.footerRuntime === "0"
891 if (options && String(options.mode || "").toLowerCase() === "json") {
895 const targetUrl = String(url || "");
896 if (/[?&]dbx_sync=0(?:&|$)/.test(targetUrl)) {
900 if (body instanceof URLSearchParams && body.get("dbx_sync") === "0") {
904 if (body instanceof FormData && body.get("dbx_sync") === "0") {
908 if (typeof body === "string" && /(?:^|&)dbx_sync=0(?:&|$)/.test(body)) {
918 shouldTrackAjaxRuntime,
919 updateAjax(responseSeconds, phpSeconds) {
924 de: "Letzte AJAX-Anfrage / PHP-Laufzeit",
925 en: "Latest AJAX request / PHP runtime",
926 es: "Última solicitud AJAX / tiempo de ejecución de PHP"
934 /* =====================================================
936 * ===================================================== */
943 const searchParams = new URLSearchParams(location.search);
944 const cacheBust = searchParams.get("dbx_nocache") || searchParams.get("cachebust") || dbx.assetVersion;
946 if (!cacheBust) return url;
949 const parsed = new URL(url, location.origin);
950 if (parsed.searchParams.has("v")) return url;
953 return url + (url.includes("?") ? "&" : "?") + "v=" + encodeURIComponent(cacheBust);
959 cb && cb({ ok: false });
963 url = this._cacheBustUrl(url);
965 if (this._loaded[url] === "loaded") {
966 dbx.log("CSS already loaded:", url);
967 return cb && cb({ ok: true });
970 if (this._loaded[url] === "loading") {
971 dbx.log("CSS already loading:", url);
972 (this._callbacks[url] ||= []).push(cb);
976 // 🔥 FIX: sauberes URL Matching
977 const requested = new URL(url, location.origin);
978 const target = requested.pathname;
979 const targetSearch = requested.search;
981 const links = document.querySelectorAll('link[rel="stylesheet"]');
982 for (let l of links) {
984 const current = new URL(l.href);
985 if (current.pathname === target && (!targetSearch || current.search === targetSearch)) {
986 this._loaded[url] = "loaded";
987 dbx.log("CSS already in DOM:", url);
988 return cb && cb({ ok: true });
993 dbx.log("CSS load start:", url);
995 this._loaded[url] = "loading";
996 this._callbacks[url] = cb ? [cb] : [];
998 const link = document.createElement("link");
999 link.rel = "stylesheet";
1002 let finished = false;
1004 function done(self, status) {
1005 if (finished) return;
1008 self._callbacks[url].forEach(f => f && f(status));
1009 self._callbacks[url] = [];
1012 link.onload = () => {
1013 this._loaded[url] = "loaded";
1014 dbx.log("CSS loaded:", url);
1015 done(this, { ok: true });
1018 link.onerror = () => {
1019 this._loaded[url] = "error";
1020 dbx.error("CSS load failed:", url);
1021 done(this, { ok: false });
1024 document.head.appendChild(link);
1030 cb && cb({ ok: false });
1034 url = this._cacheBustUrl(url);
1036 if (this._loaded[url] === "loaded") {
1037 dbx.log("JS already loaded:", url);
1038 return cb && cb({ ok: true });
1041 if (this._loaded[url] === "loading") {
1042 dbx.log("JS already loading:", url);
1043 (this._callbacks[url] ||= []).push(cb);
1047 // 🔥 FIX: sauberes URL Matching
1048 const requested = new URL(url, location.origin);
1049 const target = requested.pathname;
1050 const targetSearch = requested.search;
1052 const scripts = document.querySelectorAll('script[src]');
1053 for (let s of scripts) {
1055 const current = new URL(s.src);
1056 if (current.pathname === target && (!targetSearch || current.search === targetSearch)) {
1057 this._loaded[url] = "loaded";
1058 dbx.log("JS already in DOM:", url);
1059 return cb && cb({ ok: true });
1064 dbx.log("JS load start:", url);
1066 this._loaded[url] = "loading";
1067 this._callbacks[url] = cb ? [cb] : [];
1069 const s = document.createElement("script");
1073 let finished = false;
1075 function done(self, status) {
1076 if (finished) return;
1079 self._callbacks[url].forEach(f => f && f(status));
1080 self._callbacks[url] = [];
1084 this._loaded[url] = "loaded";
1085 dbx.log("JS loaded:", url);
1086 done(this, { ok: true });
1090 this._loaded[url] = "error";
1091 dbx.error("JS load failed:", url);
1092 done(this, { ok: false });
1095 document.body.appendChild(s);
1100 dbx.add_css = function (type, file, cb) {
1102 if (!type || !file) {
1103 cb && cb({ ok: false }); // 🔥 FIX
1109 if (type === "lib") {
1110 url = dbx.config.libPath + file;
1113 if (type === "design") {
1114 url = dbx.config.rootPath + "design/" + dbx.config.design + "/css/" + file;
1117 if (type === "root") {
1118 url = dbx.config.rootPath + file;
1121 const searchParams = new URLSearchParams(location.search);
1122 const cacheBust = searchParams.get("dbx_nocache") || searchParams.get("cachebust");
1124 url += (url.includes("?") ? "&" : "?") + "dbx_nocache=" + encodeURIComponent(cacheBust);
1128 dbx.warn("add_css invalid type:", type);
1132 "Invalid CSS scope\n\n" +
1133 "type=" + type + "\n" +
1137 cb && cb({ ok: false }); // 🔥 FIX
1141 dbx.log("add_css:", type, "→", url);
1142 dbx.loader.css(url, cb);
1145 dbx.add_js = function (type, file, cb) {
1147 if (!type || !file) {
1148 cb && cb({ ok: false }); // 🔥 FIX
1154 if (type === "lib") {
1155 url = dbx.config.libPath + file;
1158 if (type === "design") {
1159 url = dbx.config.rootPath + "design/" + dbx.config.design + "/js/" + file;
1162 if (type === "root") {
1163 url = dbx.config.rootPath + file;
1166 const searchParams = new URLSearchParams(location.search);
1167 const cacheBust = searchParams.get("dbx_nocache") || searchParams.get("cachebust");
1169 url += (url.includes("?") ? "&" : "?") + "dbx_nocache=" + encodeURIComponent(cacheBust);
1173 dbx.warn("add_js invalid type:", type);
1177 "Invalid JS scope\n\n" +
1178 "type=" + type + "\n" +
1182 cb && cb({ ok: false }); // 🔥 FIX
1186 dbx.log("add_js:", type, "→", url);
1187 dbx.loader.js(url, cb);
1190 /* =====================================================
1192 * ===================================================== */
1193 dbx.load = function (list, done) {
1199 if (i >= list.length) {
1200 dbx.log("dbx.load → complete");
1201 return done && done();
1204 const [type, scope, file] = list[i++];
1206 dbx.log("dbx.load → step:", type, scope, file);
1208 let finished = false;
1210 function safeNext(res) {
1212 if (finished) return;
1215 if (!res || res.ok !== true) {
1216 dbx.warn("dbx.load → asset failed:", type, file);
1222 // 🔥 FAILSAFE TIMEOUT (z.B. 5s)
1223 const timer = setTimeout(() => {
1225 dbx.error("dbx.load timeout:", type, file);
1227 safeNext({ ok: false });
1231 function wrappedNext(res) {
1232 clearTimeout(timer);
1236 if (type === "js") {
1237 dbx.add_js(scope, file, wrappedNext);
1238 } else if (type === "css") {
1239 dbx.add_css(scope, file, wrappedNext);
1241 wrappedNext({ ok: true });
1248 /* =====================================================
1250 * ===================================================== */
1255 register(name, obj) {
1256 this._features[name] = obj;
1257 dbx.log("feature registered:", name);
1261 return Object.prototype.hasOwnProperty.call(this._features, name);
1264 init(name, el, cfg) {
1266 dbx.log("init feature:", name, "id:", cfg.id);
1268 if (this.has(name)) {
1270 this._features[name].init(el, cfg);
1272 dbx.error("Feature init failed:", name, e);
1277 dbx.error("Feature not registered:", name);
1281 dbx.loadUtilities = function () {
1282 const existing = document.querySelector('script[src*="/utilities.js"],script[src$="utilities.js"]');
1287 dbx.add_js("lib", "utilities.js");
1290 if (document.body) {
1291 dbx.loadUtilities();
1293 document.addEventListener("DOMContentLoaded", dbx.loadUtilities, { once: true });
1296 /* =====================================================
1298 * ===================================================== */
1299 dbx.parseData = function (str) {
1302 if (!str) return result;
1304 const blocks = str.split("||");
1306 function normalize(val) {
1308 if (val === undefined || val === null) return val;
1310 const raw = val.toString().trim();
1311 const v = raw.toLowerCase();
1313 if (v === "on" || v === "1") return 1;
1314 if (v === "off" || v === "0") return 0;
1315 if (v === "true" || v === "1") return 1;
1316 if (v === "false" || v === "0") return 0;
1318 // 🔥 JSON AUTO PARSE
1320 (raw.startsWith("{") && raw.endsWith("}")) ||
1321 (raw.startsWith("[") && raw.endsWith("]"))
1324 return JSON.parse(raw);
1326 dbx.warn("parseData JSON failed:", raw);
1333 blocks.forEach(block => {
1338 block.split("|").forEach(part => {
1342 const idx = part.indexOf("=");
1343 if (idx === -1) return;
1345 // 🔥 FIX 1: key lowercase
1346 const key = part.substring(0, idx).trim().toLowerCase();
1348 // 🔥 FIX 2: kein trim hier (macht normalize)
1349 let val = part.substring(idx + 1);
1351 val = normalize(val);
1356 if (cfg.lib) result.push(cfg);
1362 /* =====================================================
1363 * RESOLVER (EXACT + FALLBACK BEHALTEN!)
1364 * ===================================================== */
1365 function waitForFeatureRegistration(libName, callback, attempts) {
1367 if (dbx.feature.has(libName)) {
1372 attempts = attempts === undefined ? 8 : attempts;
1374 if (attempts <= 0) {
1379 window.setTimeout(function () {
1380 waitForFeatureRegistration(libName, callback, attempts - 1);
1384 function reloadFeatureScript(libName, callback) {
1385 const url = dbx.config.libPath + libName + ".js";
1386 const target = new URL(url, location.origin).pathname;
1388 document.querySelectorAll("script[src]").forEach(script => {
1390 if (new URL(script.src).pathname === target) {
1391 script.parentNode && script.parentNode.removeChild(script);
1396 const reloadUrl = url + (url.includes("?") ? "&" : "?") + "dbx_reload=" + Date.now();
1397 dbx.loader._loaded[url] = null;
1398 dbx.loader._loaded[reloadUrl] = null;
1400 dbx.log("resolveFeature → reload script:", libName);
1401 dbx.loader.js(reloadUrl, function (res) {
1402 if (!res || res.ok !== true) {
1406 waitForFeatureRegistration(libName, callback, 8);
1410 dbx.resolveFeature = function (libName, callback) {
1413 callback && callback(false);
1417 if (dbx.feature.has(libName)) {
1418 dbx.log("resolveFeature → already loaded:", libName);
1419 return callback && callback(true);
1422 const exactUrl = dbx.config.libPath + libName + ".js";
1424 dbx.log("resolveFeature → try exact:", exactUrl);
1426 dbx.loader.js(exactUrl, function (res) {
1428 // 🔥 NEW: loader status prüfen
1429 if (!res || res.ok !== true) {
1430 dbx.error("resolveFeature → load failed:", libName);
1431 return callback && callback(false);
1434 waitForFeatureRegistration(libName, function (registered) {
1437 dbx.log("resolveFeature → loaded exact:", libName);
1438 return callback && callback(true);
1441 dbx.warn("resolveFeature → script loaded but feature NOT registered:", libName);
1443 // 🔥 FALLBACK bleibt
1444 if (libName.includes("-")) {
1446 const base = libName.split("-")[0];
1448 if (dbx.feature.has(base)) {
1449 dbx.log("resolveFeature → base already loaded:", base);
1450 return callback && callback(true);
1453 const baseUrl = dbx.config.libPath + base + ".js";
1455 dbx.log("resolveFeature → fallback base:", baseUrl);
1457 dbx.loader.js(baseUrl, function (res2) {
1460 if (!res2 || res2.ok !== true) {
1461 dbx.error("resolveFeature → fallback load failed:", base);
1462 return callback && callback(false);
1465 waitForFeatureRegistration(base, function (baseRegistered) {
1467 if (baseRegistered) {
1468 dbx.log("resolveFeature → loaded base:", base);
1469 return callback && callback(true);
1472 dbx.error("Feature not registered after fallback:", libName);
1473 return callback && callback(false);
1479 reloadFeatureScript(libName, function (reloaded) {
1481 dbx.log("resolveFeature → loaded after reload:", libName);
1482 return callback && callback(true);
1484 dbx.error("Feature not registered:", libName);
1485 return callback && callback(false);
1492 /* =====================================================
1494 * ===================================================== */
1495 dbx.scan = function (root) {
1497 const ctx = root || document;
1499 if (dbx.declare && typeof dbx.declare.scanAutoload === "function") {
1500 dbx.declare.scanAutoload(ctx);
1503 // 🔥 FIX: root selbst + children
1506 if (ctx.nodeType === 1 && ctx.hasAttribute && ctx.hasAttribute("data-dbx")) {
1510 ctx.querySelectorAll("[data-dbx]").forEach(n => nodes.push(n));
1512 // 🔥 NEW: global scoped state store
1513 dbx._scopeState = dbx._scopeState || {};
1514 dbx._scopeError = dbx._scopeError || {};
1515 dbx._scopeInit = dbx._scopeInit || {};
1517 nodes.forEach(el => {
1519 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
1520 if (!cfgList.length) return;
1522 el.__dbxInitialized = el.__dbxInitialized || {};
1523 el.__dbxState = el.__dbxState || {};
1524 el.__dbxError = el.__dbxError || {};
1526 cfgList.forEach(cfg => {
1528 if (!cfg.id) cfg.id = "undef";
1530 const f = dbx.feature._features[cfg.lib];
1533 dbx.log("scan → feature not yet loaded:", cfg.lib);
1536 let scopeType = "element";
1540 if (!["element","group","global"].includes(f.scope)) {
1541 dbx.error("Invalid scope:", cfg.lib, f.scope);
1545 scopeType = f.scope;
1548 dbx.log("scan → fallback scope=element:", cfg.lib);
1553 if (scopeType === "global") {
1555 scopeKey = "global";
1557 } else if (scopeType === "group") {
1559 scopeKey = cfg.group || cfg.id || "group";
1563 if (!el.__dbxScopeId) {
1564 dbx._scopeUid = (dbx._scopeUid || 0) + 1;
1565 el.__dbxScopeId = dbx._scopeUid;
1568 scopeKey = el.__dbxScopeId;
1571 const keyScoped = cfg.lib + "::" + scopeKey;
1573 const stateStore = (scopeType === "element") ? el.__dbxState : dbx._scopeState;
1574 const errorStore = (scopeType === "element") ? el.__dbxError : dbx._scopeError;
1575 const initStore = (scopeType === "element") ? el.__dbxInitialized : dbx._scopeInit;
1577 const state = stateStore[keyScoped];
1579 if (state === "pending") {
1580 // 🔥 FIX: pending darf nicht blockieren wenn kein task mehr existiert
1581 if (!dbx._taskMap || !dbx._taskMap[keyScoped]) {
1582 stateStore[keyScoped] = undefined;
1587 if (state === "done") return;
1589 let allowRetry = false;
1591 if (state === "error") {
1593 const err = errorStore[keyScoped] || {};
1595 const count = err.count || 1;
1596 const last = err.last || 0;
1598 const delay = Math.min(10000, count * 2000);
1601 dbx.warn("retry limit reached:", keyScoped);
1605 if (Date.now() - last < delay) {
1609 dbx.log("retry allowed:", keyScoped, "attempt:", count + 1);
1613 if (initStore[keyScoped] && stateStore[keyScoped] === "done") return;
1615 const key = keyScoped;
1617 dbx._taskMap = dbx._taskMap || {};
1619 //if (!allowRetry && dbx._taskMap[key]) return;
1621 stateStore[keyScoped] = "pending";
1623 dbx._taskMap[key] = true;
1630 _scopeType: scopeType
1639 /* =====================================================
1641 * ===================================================== */
1642 dbx.runTasks = function () {
1644 if (!dbx._tasks.length) return;
1647 dbx.log("runTasks → already running, skip");
1651 dbx._running = true;
1653 const tasks = dbx._tasks.slice();
1656 dbx.log("runTasks → start", tasks.length);
1666 // -------------------------------------------------
1667 // 🔥 HELPER: richtiger Store je Scope
1668 // -------------------------------------------------
1669 function getStores(t) {
1671 let scopeType = t._scopeType;
1674 const f = dbx.feature._features[t.lib];
1675 scopeType = (f && f.scope) ? f.scope : "element";
1676 t._scopeType = scopeType;
1679 if (scopeType === "element") {
1681 if (!t.el || t.el.nodeType !== 1) {
1682 dbx._scopeState = dbx._scopeState || {};
1683 dbx._scopeError = dbx._scopeError || {};
1684 dbx._scopeInit = dbx._scopeInit || {};
1686 state: dbx._scopeState,
1687 error: dbx._scopeError,
1688 init: dbx._scopeInit
1692 dbx.ensureElementScopeStores(t.el);
1695 state: t.el.__dbxState,
1696 error: t.el.__dbxError,
1697 init: t.el.__dbxInitialized
1701 dbx._scopeState = dbx._scopeState || {};
1702 dbx._scopeError = dbx._scopeError || {};
1703 dbx._scopeInit = dbx._scopeInit || {};
1706 state: dbx._scopeState,
1707 error: dbx._scopeError,
1708 init: dbx._scopeInit
1712 function requeueTask(t, store, key, reason) {
1714 const err = (store.error && store.error[key]) || { count: 0 };
1715 const attempt = err.count + 1;
1716 const maxAttempts = 3;
1719 store.error[key] = { count: attempt, last: Date.now() };
1722 if (attempt < maxAttempts) {
1723 dbx.log("runTasks → requeue (" + attempt + "/" + maxAttempts + "):", t.lib, reason || "");
1724 if (store.state) delete store.state[key];
1725 if (dbx._taskMap) delete dbx._taskMap[key];
1733 function getScopeType(t) {
1734 const f = dbx.feature._features[t.lib];
1735 const scopeType = (f && f.scope) ? f.scope : "element";
1737 if (!["element","group","global"].includes(scopeType)) {
1738 dbx.error("Invalid scope:", t.lib, scopeType);
1745 function makeTaskKey(t, scopeType) {
1746 if (scopeType === "global") {
1747 return t.lib + "::global";
1750 if (scopeType === "group") {
1751 return t.lib + "::" + (t.cfg.group || t.cfg.id || "group");
1754 if (!t.el.__dbxScopeId) {
1755 dbx._scopeUid = (dbx._scopeUid || 0) + 1;
1756 t.el.__dbxScopeId = dbx._scopeUid;
1759 return t.lib + "::" + t.el.__dbxScopeId;
1762 function storeForScope(t, scopeType) {
1763 if (scopeType === "element") {
1764 if (t.el && t.el.nodeType === 1) {
1765 dbx.ensureElementScopeStores(t.el);
1768 state: t.el.__dbxState,
1769 error: t.el.__dbxError,
1770 init: t.el.__dbxInitialized
1774 dbx._scopeState = dbx._scopeState || {};
1775 dbx._scopeError = dbx._scopeError || {};
1776 dbx._scopeInit = dbx._scopeInit || {};
1779 state: dbx._scopeState,
1780 error: dbx._scopeError,
1781 init: dbx._scopeInit
1785 function normalizeTaskScope(t) {
1786 const oldKey = t._key;
1787 const oldScopeType = t._scopeType || "element";
1788 const newScopeType = getScopeType(t);
1789 const newKey = makeTaskKey(t, newScopeType);
1791 if (oldKey && oldKey !== newKey) {
1792 const oldStore = storeForScope(t, oldScopeType);
1794 if (oldStore.state && oldStore.state[oldKey] === "pending") {
1795 delete oldStore.state[oldKey];
1798 if (oldStore.error && oldStore.error[oldKey]) {
1799 delete oldStore.error[oldKey];
1803 delete dbx._taskMap[oldKey];
1807 t._scopeType = newScopeType;
1813 function assignPhases() {
1816 tasks.forEach(t => {
1818 normalizeTaskScope(t);
1820 const store = getStores(t);
1822 if (store.init[t._key] || store.state[t._key] === "done") {
1823 if (dbx._taskMap) delete dbx._taskMap[t._key];
1831 seen[t._key] = true;
1832 store.state[t._key] = "pending";
1833 dbx._taskMap = dbx._taskMap || {};
1834 dbx._taskMap[t._key] = true;
1836 const f = dbx.feature._features[t.lib];
1837 const libPrio = f && f.priority ? f.priority : "mid";
1838 const cfgPrio = t.cfg.prio;
1839 const prio = cfgPrio || libPrio || "mid";
1841 if (!phases[prio]) phases.mid.push(t);
1842 else phases[prio].push(t);
1846 // =================================================
1848 // =================================================
1849 function runVeryFirstImmediate(done) {
1851 const list = phases.veryfirst;
1852 if (!list.length) return done();
1858 if (i >= list.length) return done();
1860 const t = list[i++];
1863 const store = getStores(t);
1865 if (store.state[key] !== "pending") {
1866 store.state[key] = "pending";
1869 dbx.resolveFeature(t.lib, function (ok) {
1872 if (requeueTask(t, store, key, "resolveFeature failed")) {
1875 dbx.error("runTasks → lib load failed:", t.lib);
1876 store.state[key] = "error";
1880 const f = dbx.feature._features[t.lib];
1884 if (f && Array.isArray(f.css)) {
1885 assets.push(...f.css);
1888 if (f && Array.isArray(f.js)) {
1889 f.js.forEach(entry => {
1890 if (Array.isArray(entry)) {
1893 assets.push(['js', 'lib', entry]);
1898 if (assets.length) {
1899 dbx.load(assets, runInit);
1904 function runInit() {
1908 const feature = dbx.feature._features[t.lib];
1910 if (!feature || !feature.init) {
1911 if (requeueTask(t, store, key, "init missing")) {
1914 dbx.error("runTasks → no init for lib:", t.lib);
1915 store.state[key] = "error";
1919 feature.init.call(feature, t.el, t.cfg);
1921 store.init[key] = true;
1922 store.state[key] = "done";
1924 delete store.error[key];
1926 if (dbx._taskMap) delete dbx._taskMap[key];
1930 if (requeueTask(t, store, key, "init error: " + e.message)) {
1934 dbx.error("runTasks → INIT ERROR:", t.lib, e);
1935 store.state[key] = "error";
1937 if (dbx._taskMap) delete dbx._taskMap[key];
1948 // -------------------------------------------------
1949 // PREPARE (unverändert)
1950 // -------------------------------------------------
1951 function runPrepare(done) {
1954 const assetSet = new Set();
1958 if (i >= tasks.length) {
1960 const list = Array.from(assetSet).map(s => JSON.parse(s));
1962 if (!list.length) return done();
1964 return dbx.load(list, done);
1967 const t = tasks[i++];
1969 dbx.resolveFeature(t.lib, function (ok) {
1971 if (ok !== true) return next();
1973 const f = dbx.feature._features[t.lib];
1975 if (f && Array.isArray(f.css)) {
1976 f.css.forEach(entry => assetSet.add(JSON.stringify(entry)));
1979 if (f && Array.isArray(f.js)) {
1980 f.js.forEach(entry => {
1981 if (Array.isArray(entry)) {
1982 assetSet.add(JSON.stringify(entry));
1984 assetSet.add(JSON.stringify(['js', 'lib', entry]));
1996 // -------------------------------------------------
1998 // -------------------------------------------------
1999 function runPhase(name, list, done) {
2001 dbx.log("runPhase →", name, "tasks:", list.length);
2007 if (i >= list.length) {
2008 return done && done();
2011 const t = list[i++];
2013 dbx.log("runPhase → task:", name, t.lib, t._key);
2015 if (name === "veryfirst") return next();
2018 const store = getStores(t);
2020 if (store.state[key] !== "pending") {
2021 store.state[key] = "pending";
2024 new Promise((resolve) => {
2026 function runInit() {
2030 const f = dbx.feature._features[t.lib];
2032 if (!f || !f.init) {
2034 if (requeueTask(t, store, key, "init missing")) {
2038 dbx.error("runPhase → NO INIT:", t.lib);
2039 store.state[key] = "error";
2041 if (dbx._taskMap) delete dbx._taskMap[key];
2046 dbx.log("runPhase → INIT:", t.lib);
2048 // 🔥 FIX: korrektes this wiederherstellen (einzige Änderung)
2049 const res = f.init.call(f, t.el, t.cfg);
2051 if (res && typeof res.then === "function") {
2053 store.init[key] = true;
2054 store.state[key] = "done";
2055 delete store.error[key];
2057 if (dbx._taskMap) delete dbx._taskMap[key];
2062 store.init[key] = true;
2063 store.state[key] = "done";
2064 delete store.error[key];
2066 if (dbx._taskMap) delete dbx._taskMap[key];
2073 if (requeueTask(t, store, key, "init error: " + e.message)) {
2077 dbx.error("runPhase → INIT ERROR:", t.lib, e);
2078 store.state[key] = "error";
2080 if (dbx._taskMap) delete dbx._taskMap[key];
2086 const libName = t.lib || t.cfg?.lib;
2088 dbx.resolveFeature(libName, (ok) => {
2091 if (requeueTask(t, store, key, "resolveFeature failed")) {
2094 dbx.error("runPhase → lib load failed:", libName);
2095 store.state[key] = "error";
2096 if (dbx._taskMap) delete dbx._taskMap[key];
2100 const f = dbx.feature._features[libName];
2104 if (f && Array.isArray(f.css)) {
2105 assets.push(...f.css);
2108 if (f && Array.isArray(f.js)) {
2109 f.js.forEach(entry => {
2110 if (Array.isArray(entry)) {
2113 assets.push(['js', 'lib', entry]);
2118 if (assets.length) {
2119 dbx.load(assets, runInit);
2132 // =================================================
2134 // =================================================
2135 runPrepare(function () {
2139 runVeryFirstImmediate(function () {
2141 runPhase("first", phases.first, function () {
2143 runPhase("mid", phases.mid, function () {
2145 runPhase("last", phases.last, function () {
2147 runPhase("verylast", phases.verylast, function () {
2150 dbx._running = false;
2152 if (dbx._tasks.length) {
2153 setTimeout(dbx.runTasks, 0);
2165 /* =====================================================
2166 * 🔥 MUTATION OBSERVER (ADD ONLY)
2167 * ===================================================== */
2168 if (!dbx.__observerInit) {
2170 dbx.log("observer → init");
2172 const observer = new MutationObserver(function (mutations) {
2174 mutations.forEach(m => {
2176 // =================================================
2177 // 🔥 ADD (bestehend)
2178 // =================================================
2179 m.addedNodes.forEach(node => {
2181 if (node.nodeType !== 1) return;
2183 if (node.hasAttribute && node.hasAttribute("data-dbx")) {
2184 dbx.log("observer → node");
2188 if (node.querySelectorAll) {
2189 const found = node.querySelectorAll("[data-dbx]");
2191 dbx.log("observer → subtree:", found.length);
2197 // =================================================
2198 // 🔥 NEW: REMOVE → CLEANUP + DESTROY
2199 // =================================================
2200 m.removedNodes.forEach(node => {
2202 if (node.nodeType !== 1) return;
2207 if (node.hasAttribute && node.hasAttribute("data-dbx")) {
2212 if (node.querySelectorAll) {
2213 node.querySelectorAll("[data-dbx]").forEach(n => list.push(n));
2216 if (!list.length) return;
2218 list.forEach(el => {
2220 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
2221 if (!cfgList.length) return;
2223 cfgList.forEach(cfg => {
2225 if (!cfg.id) cfg.id = "undef";
2227 const f = dbx.feature._features[cfg.lib];
2228 if (!f || !f.scope) return;
2230 const scopeType = f.scope;
2234 if (scopeType === "global") {
2235 // global → NICHT löschen
2239 if (scopeType === "group") {
2240 scopeKey = cfg.group || cfg.id || "group";
2242 scopeKey = el.__dbxScopeId;
2245 if (!scopeKey) return;
2247 const keyScoped = cfg.lib + "::" + scopeKey;
2249 const storeState = (scopeType === "element") ? el.__dbxState : dbx._scopeState;
2250 const storeError = (scopeType === "element") ? el.__dbxError : dbx._scopeError;
2251 const storeInit = (scopeType === "element") ? el.__dbxInitialized : dbx._scopeInit;
2253 // -------------------------------------------------
2254 // 🔥 DESTROY (optional)
2255 // -------------------------------------------------
2257 if (f.destroy && storeInit && storeInit[keyScoped]) {
2261 dbx.error("destroy failed:", cfg.lib, e);
2264 // -------------------------------------------------
2266 // -------------------------------------------------
2267 if (storeState && keyScoped in storeState) {
2268 delete storeState[keyScoped];
2271 if (storeError && keyScoped in storeError) {
2272 delete storeError[keyScoped];
2275 if (storeInit && keyScoped in storeInit) {
2276 delete storeInit[keyScoped];
2279 dbx.log("GC cleanup:", keyScoped);
2286 observer.observe(document.body, {
2291 dbx.__observerInit = true;
2294 /* =====================================================
2295 * 🔥 EVENT DELEGATION HELPER
2296 * ===================================================== */
2297 dbx.on = function (event, selector, handler) {
2299 document.addEventListener(event, function (e) {
2301 const el = e.target.closest(selector);
2304 handler.call(el, e, el);
2306 }, true); // 🔥 WICHTIG: CAPTURE PHASE
2308 dbx.log("delegation registered:", event, selector);
2312 /* =====================================================
2313 * 🔥 EVENT SYSTEM (CUSTOM)
2314 * ===================================================== */
2316 dbx.event = dbx.event || {
2321 if (!name || typeof handler !== "function") return;
2323 this._events[name] = this._events[name] || [];
2324 this._events[name].push(handler);
2326 dbx.log("event:on →", name);
2332 const list = this._events[name];
2333 if (!list || !list.length) return;
2335 dbx.log("event:emit →", name, data);
2337 list.forEach(fn => {
2341 dbx.error("event handler failed:", name, e);
2345 // 🔥 NEU: scoped event
2346 if (data && data.id) {
2347 const scoped = name + ":" + data.id;
2348 const list2 = this._events[scoped];
2351 list2.forEach(fn => {
2355 dbx.error("event handler failed:", scoped, e);
2364 /* =====================================================
2365 * DEVICE (STATE-BASED ABSTRACTION)
2366 * ===================================================== */
2367 dbx.device = (function(){
2373 lastActivity: Date.now()
2376 const IDLE_MS = 30000; // 30s
2378 /* =====================================================
2380 * ===================================================== */
2382 function updateVisibility(){
2383 state.visible = !document.hidden;
2386 function updateOnline(){
2387 state.online = navigator.onLine !== false;
2390 function markActive(){
2391 state.lastActivity = Date.now();
2392 state.active = true;
2395 function checkIdle(){
2396 const now = Date.now();
2397 state.active = (now - state.lastActivity) < IDLE_MS;
2400 /* =====================================================
2401 * BIND EVENTS (BROWSER)
2402 * ===================================================== */
2404 document.addEventListener('visibilitychange', updateVisibility, true);
2405 window.addEventListener('online', updateOnline, true);
2406 window.addEventListener('offline', updateOnline, true);
2408 document.addEventListener('mousemove', markActive, true);
2409 document.addEventListener('keydown', markActive, true);
2410 document.addEventListener('click', markActive, true);
2412 // idle checker (leichtgewichtig)
2413 setInterval(checkIdle, 5000);
2419 /* =====================================================
2421 * ===================================================== */
2425 /* ===== READ ===== */
2428 return state.visible === true;
2432 return state.online === true;
2436 return state.active === true;
2440 return Object.assign({}, state);
2443 /* =====================================================
2444 * OPTIONAL: EXTERNAL OVERRIDE (FUTURE: APP / PWA)
2445 * ===================================================== */
2449 if(!partial || typeof partial !== 'object') return;
2451 if(typeof partial.visible === 'boolean'){
2452 state.visible = partial.visible;
2455 if(typeof partial.online === 'boolean'){
2456 state.online = partial.online;
2459 if(typeof partial.active === 'boolean'){
2460 state.active = partial.active;
2463 if(typeof partial.lastActivity === 'number'){
2464 state.lastActivity = partial.lastActivity;
2467 dbx.log('[device] override', partial);
2474 /* =====================================================
2475 * LOOP (SMART POLLING CORE)
2476 * ===================================================== */
2477 dbx.loop = (function(){
2481 function clamp(v, min, max){
2482 if(min != null && v < min) return min;
2483 if(max != null && v > max) return max;
2487 function resolveInterval(task){
2489 const t = task.timing || {};
2502 case 'fast': interval = t.min || t.base; break;
2503 case 'slow': interval = Math.max((t.base||2000)*2, t.idle||3000); break;
2504 case 'idle': interval = t.idle || t.base; break;
2505 case 'boost': interval = (t.base||2000) / 2; break;
2506 default: interval = t.base || 2000;
2511 if(!dbx.device.isVisible()){
2512 interval = t.hidden || (t.base||2000)*3;
2514 else if(!dbx.device.isActive()){
2515 interval = t.idle || (t.base||2000)*2;
2518 interval = t.base || 2000;
2529 function schedule(task){
2531 if(task.paused) return;
2534 clearTimeout(task.timer); // 🔥 FIX
2535 task.timer = null; // 🔥 FIX
2538 const interval = resolveInterval(task);
2539 if(interval == null) return;
2541 task.timer = setTimeout(() => run(task), interval);
2546 if(task.running) return;
2548 task.running = true;
2559 dbx.error('[loop] run error', task.id, e);
2564 Promise.resolve(res)
2566 dbx.error('[loop] async error', task.id, err);
2568 .finally(() => finish());
2572 task.running = false;
2573 task.lastRun = Date.now();
2575 task.timer = null; // 🔥 FIX
2577 if(task.hintUntil && Date.now() > task.hintUntil){
2590 if(!cfg || !cfg.id || !cfg.onRun){
2591 dbx.error('[loop] invalid task', cfg);
2596 dbx.warn('[loop] already exists', cfg.id);
2603 timing: cfg.timing || {},
2612 schedule(tasks[cfg.id]);
2614 dbx.log('[loop] add', cfg.id);
2617 hint(id, mode, opts){
2619 const t = tasks[id];
2622 if(mode === 'pause'){
2625 clearTimeout(t.timer);
2631 if(mode === 'resume'){
2633 if(t.timer) clearTimeout(t.timer);
2638 if(mode === 'none'){
2646 if(opts && opts.duration){
2647 t.hintUntil = Date.now() + opts.duration;
2650 if(mode === 'boost'){
2652 // 🔥 FIX: niemals während running starten
2654 return; // einfach nächsten Zyklus beschleunigen
2658 clearTimeout(t.timer);
2669 Object.values(tasks).forEach(t => {
2688 /* =====================================================
2689 * DEVICE CAPABILITIES (BRIDGE)
2690 * ===================================================== */
2692 dbx.device.camera = {
2696 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
2697 dbx.error('[device] camera not supported');
2698 throw new Error('camera_not_supported');
2701 const constraints = opts?.constraints || { video: true };
2705 const stream = await navigator.mediaDevices.getUserMedia(constraints);
2710 stream.getTracks().forEach(t => t.stop());
2716 dbx.error('[device] camera error', e);
2728 return new Promise((resolve, reject) => {
2730 const input = document.createElement('input');
2731 input.type = 'file';
2733 if (opts?.accept) input.accept = opts.accept;
2734 if (opts?.multiple) input.multiple = true;
2736 input.onchange = () => {
2738 if(!input.files || !input.files.length){
2739 resolve(null); // 🔥 FIX
2741 resolve(input.files);
2745 input.onerror = reject;
2754 dbx.device.clipboard = {
2758 if (!navigator.clipboard) {
2759 dbx.error('[device] clipboard not supported');
2760 throw new Error('clipboard_not_supported');
2763 return navigator.clipboard.writeText(text);
2768 if (!navigator.clipboard) {
2769 dbx.error('[device] clipboard not supported');
2770 throw new Error('clipboard_not_supported');
2773 return navigator.clipboard.readText();
2779 dbx.device.share = {
2783 if (!navigator.share) {
2784 dbx.error('[device] share not supported');
2785 throw new Error('share_not_supported');
2788 return navigator.share(data);
2794 /* =====================================================
2796 * ===================================================== */
2798 dbx.log("DOM ready → scan");
2800 dbx.footerStatus.init();
2803 /* =====================================================
2805 * ===================================================== */
2806 dbx.rescan = function (root) {
2808 const ctx = root || document;
2811 Object.keys(dbx.feature._features || {}).forEach(function (name) {
2812 const f = dbx.feature._features[name];
2813 if (!f || typeof f.rescan !== "function") return;
2818 dbx.error("rescan → feature error:", name, e);
2823 dbx.loadFeature = function (name, cb) {
2824 dbx.resolveFeature(name, cb || function () {});
2827})(window, document, jQuery);