3 * =========================================================
4 * DBX AJAX SYSTEM (ajax.js)
5 * =========================================================
9 * ajax.js ist die universelle, scoped AJAX-Infrastruktur von DBX.
11 * Die Lib verarbeitet innerhalb ihres eigenen Root-Elements:
14 * - Button-/Action-Klicks
18 * Der DBX-Standard ist weiterhin:
20 * HTML laden → Target ersetzen
22 * Für UI-nahe Spezialfälle unterstützt ajax.js zusätzlich:
24 * JSON laden → Antwort an aufrufende Logik zurückgeben
29 * ajax.js ist bewusst NUR Transport-/Response-Infrastruktur.
30 * Die Lib enthält keine Fachlogik für Reports, Grid, Formflows usw.
33 * - ajax.js → URL/Target/Mode/Request/Response
34 * - form.js → Form-/Report-UI-Logik
35 * - grid.js → Grid-Logik / Sort / Sync / Save / Restore
36 * - confirm.js→ Bestätigungsdialoge vor Aktionen
39 * Scope-Regel (verbindlich)
40 * -------------------------
41 * ajax.js arbeitet immer mit:
46 * - eine ajax-Instanz gilt nur für ihr Root-Element
47 * - plus dessen Children
50 * Mehrere lib=ajax Instanzen auf derselben Seite sind ausdrücklich erlaubt.
51 * Der jeweils nächste passende AJAX-Root im DOM ist zuständig.
54 * =========================================================
55 * KONFIGURATION AM ROOT (data-dbx)
56 * =========================================================
60 * <div id="dbx_target_7"
61 * data-dbx="lib=ajax|class=dbxAjax|mode=html|url=form|target=dbx_target_7">
66 * Unterstützte Parameter am AJAX-Root
67 * -----------------------------------
70 * Pflicht. Registriert die AJAX-Lib auf diesem Root.
73 * Match-Class. Nur Elemente mit dieser Klasse werden
74 * von dieser AJAX-Instanz verarbeitet.
79 * - class=* → alle passenden Elemente im Scope
82 * Default-Response-Typ dieser AJAX-Instanz.
86 * - Response wird als HTML interpretiert
87 * - Target wird ersetzt
90 * - Response wird als JSON interpretiert
91 * - standardmäßig KEIN automatischer Replace
92 * - die aufrufende Lib verarbeitet die Antwort selbst
95 * - Response wird als Text gelesen
96 * - kein automatischer Replace, außer explizit aktiviert
98 * bind=form,link,button
99 * Welche Elementtypen diese AJAX-Instanz behandeln darf.
105 * - bind=form,link,button
106 * - bind=* / bind=all
108 * Wenn nicht angegeben, ist Standard:
111 * url=form|lib|<volle URL>|&...
112 * Default-URL-Auflösung.
117 * → benutze die action der umgebenden Form
120 * → benutze die URL aus der AJAX-Lib selbst
121 * (nur sinnvoll, wenn dort wirklich eine echte URL definiert ist)
124 * → vollständige feste URL
126 * url=&dbx_run3=delete
127 * → hänge Parameter an die Basis-URL an
129 * Basis-URL für "&..." ist:
133 * target=dbx_target_7
134 * Default-Target für Replace-Fälle.
137 * - vor allem relevant bei mode=html
138 * - bei json standardmäßig nicht nötig
139 * - kann aber für explizite JSON-Replace-Fälle verwendet werden
141 * replace=target|inner
142 * Steuert, wie HTML in das Target übernommen wird.
146 * - das Target-Element wird durch die Response ersetzt
149 * - nur der Inhalt des Target-Elements wird ersetzt
150 * - das Target-Element selbst bleibt im DOM bestehen
151 * - sinnvoll für AJAX-Menüs, Panels oder Bereiche, deren Root erhalten bleiben muss
153 * method=get|post|put|patch|delete
154 * Default-HTTP-Methode.
156 * Wenn nicht angegeben:
158 * - Form/Button → POST
161 * Default-Zusatzparameter dieser AJAX-Instanz.
162 * Diese werden mit Form-/Element-Parametern zusammengeführt.
165 * Nur für mode=json relevant.
171 * - ajax.js erwartet HTML innerhalb des JSON-Responses
172 * - dieser HTML-Teil wird in target ersetzt
175 * Nur für mode=json + jsonreplace=1 relevant.
177 * Gibt an, unter welchem Key im JSON der HTML-String erwartet wird.
181 * { "ok": 1, "html": "<div>...</div>" }
187 * Nur für mode=text relevant.
188 * Wenn aktiv, wird der Text direkt in das Target ersetzt.
191 * =========================================================
192 * ERLAUBTE ELEMENTE IM SCOPE
193 * =========================================================
195 * ajax.js reagiert innerhalb des eigenen Roots auf:
200 * - <input type="button">
201 * - <input type="submit">
202 * - <input type="image">
205 * - Element liegt innerhalb des AJAX-Roots
206 * - Elementtyp passt zu bind
207 * - Element besitzt die passende class
210 * =========================================================
211 * VERERBUNG / OVERRIDE-PRINZIP
212 * =========================================================
216 * Möglichst wenig Angaben in Templates.
217 * Standardwerte kommen vom AJAX-Root oder von der Form.
218 * Links/Buttons/Formulare dürfen bei Bedarf gezielt abweichen.
225 * a) explizite Element-URL
229 * Erlaubte URL-Varianten am Element/Form:
237 * Bedeutung von url=&...
238 * ----------------------
239 * Beginnt eine URL mit "&", dann wird diese NICHT als vollständige URL
240 * behandelt, sondern als Erweiterung der Basis-URL.
248 * ?dbx_modul=test&dbx_run1=list
251 * data-ajax-url="&dbx_run3=row_delete&rid=15"
254 * ?dbx_modul=test&dbx_run1=list&dbx_run3=row_delete&rid=15
257 * 2) Target-Auflösung
258 * -------------------
261 * a) explizites Element-Target
270 * a) expliziter Element-Mode
275 * 4) Method-Auflösung
276 * -------------------
279 * a) explizite Element-Method
282 * d) Default nach Elementtyp
287 * Reihenfolge / Zusammenführung:
289 * a) params aus der Lib
290 * b) params aus der Form
291 * c) params aus dem Element
293 * Alle Parameter werden zusammengeführt.
294 * Spätere Ebenen dürfen frühere Werte überschreiben.
297 * =========================================================
298 * DATENQUELLEN FÜR OVERRIDES
299 * =========================================================
301 * Element/Form-spezifische Overrides können z. B. über:
305 * - data-ajax-replace
309 * - data-ajax-jsonreplace
310 * - data-ajax-jsonkey
311 * - data-ajax-textreplace
315 * Zusätzlich werden weiterhin typische HTML-Quellen berücksichtigt:
319 * - button formaction
320 * - button formmethod
323 * Historische Unterstützung:
326 * - .dbxAjaxFormAction
329 * =========================================================
331 * =========================================================
335 * Standardfall in DBX.
338 * - Response als HTML lesen
339 * - Target ersetzen oder bei replace=inner nur Target-Inhalt ersetzen
340 * - <script>-Tags ausführen
341 * - dbx.scan(newElement) auf neuem Inhalt ausführen
347 * - Response als JSON lesen
348 * - KEIN automatischer DOM-Replace
349 * - Ergebnis an aufrufende Logik zurückgeben
352 * - mit jsonreplace=1 kann HTML aus einem JSON-Key gelesen
353 * und in target ersetzt werden
359 * - Response als Text lesen
360 * - kein automatischer Replace
363 * - mit textreplace=1 wird Text in target ersetzt
366 * =========================================================
368 * =========================================================
370 * ajax.js sendet DBX-Events über dbx.event.emit():
376 * Nach erfolgreichem Request / ggf. nach Replace
379 * Bei Request- oder Parse-Fehlern
395 * =========================================================
396 * LOCK / DOUBLE SUBMIT PROTECTION
397 * =========================================================
399 * ajax.js verhindert parallele doppelte Ausführungen auf derselben Quelle
400 * (Form oder auslösendes Element), solange ein Request läuft.
402 * Zusätzlich erhält das aktive Target während des Requests:
404 * class="dbx-ajax-loading"
407 * =========================================================
408 * VERWENDUNGSBEISPIELE
409 * =========================================================
411 * 1) Klassischer Report mit HTML-Replace
412 * --------------------------------------
414 * <div id="dbx_target_7"
415 * data-dbx="lib=ajax|class=dbxAjax|mode=html|url=form|target=dbx_target_7">
417 * <form action="?dbx_modul=test&dbx_run1=list"
425 * - dbx_target_7 wird ersetzt
428 * 2) Pagination-Link im selben Report
429 * -----------------------------------
431 * <a href="?dbx_modul=test&dbx_run1=list&dbx_rpos=20"
432 * class="dbxAjax">Weiter</a>
435 * - Link läuft über dieselbe ajax-Instanz
436 * - Target wird ersetzt
439 * 3) Button ergänzt nur Parameter an Form-URL
440 * -------------------------------------------
442 * <button class="dbxAjax"
443 * data-ajax-url="&dbx_run3=row_delete&rid=15">
448 * - Basis-URL kommt von der Form
449 * - Parameter werden angehängt
452 * 4) UI-AJAX mit JSON
453 * -------------------
455 * <div data-dbx="lib=ajax|class=dbxUiAjax|mode=json|bind=button,link">
456 * <button class="dbxUiAjax"
457 * data-ajax-url="&dbx_mode=report_select&dbx_select_action=row">
463 * - Response wird als JSON verarbeitet
464 * - kein automatischer Replace
467 * 5) JSON mit explizitem Replace
468 * ------------------------------
471 * data-dbx="lib=ajax|class=dbxUiAjax|mode=json|target=panel|jsonreplace=1|jsonkey=html">
475 * Erwartete JSON-Antwort:
478 * "html": "<div>Neuer Inhalt</div>"
482 * =========================================================
484 * =========================================================
486 * - ajax.js ist immer scoped, nie global
487 * - HTML-Replace bleibt DBX-Standard
488 * - JSON ist für UI-/Spezialfälle gedacht
489 * - confirm gehört NICHT in ajax.js
490 * - confirm ist eigene Fachlib (confirm.js)
491 * - Fachlogik gehört nicht in ajax.js
492 * - URL/Target/Mode sollen möglichst geerbt werden
493 * - nur Abweichungen werden lokal angegeben
495 * =========================================================
501(function (window, document) {
504 if (!window.dbx || !window.dbx.feature) {
505 console.error("[dbx][ajax] dbx core missing");
509 const dbx = window.dbx;
511 dbx.ajax = dbx.ajax || {};
514 /* =========================================================
516 * ========================================================= */
518 function bool(v, def = false) {
520 if (v === undefined || v === null || v === "") return def;
521 if (v === true || v === 1 || v === "1" || v === "on" || v === "true") return true;
522 if (v === false || v === 0 || v === "0" || v === "off" || v === "false") return false;
528 function normalizeMode(v) {
530 const mode = String(v || "html").toLowerCase().trim();
532 if (mode === "json") return "json";
533 if (mode === "text") return "text";
539 function normalizeReplaceMode(v) {
541 const mode = String(v || "target").toLowerCase().trim();
543 if (mode === "inner" || mode === "innerhtml" || mode === "html" || mode === "content") {
551 function normalizeMethod(v, def = "POST") {
553 const method = String(v || def || "POST").toUpperCase().trim();
555 if (method === "GET") return "GET";
556 if (method === "POST") return "POST";
557 if (method === "PUT") return "PUT";
558 if (method === "PATCH") return "PATCH";
559 if (method === "DELETE") return "DELETE";
561 return String(def || "POST").toUpperCase();
565 function isPlainObject(value) {
566 return value && Object.prototype.toString.call(value) === "[object Object]";
570 function appendDataToUrl(url, data) {
571 if (!data || !isPlainObject(data)) return url;
573 const out = new URL(String(url), window.location.href);
574 Object.keys(data).forEach(key => {
575 const value = data[key];
576 if (value === undefined || value === null) return;
577 if (Array.isArray(value)) {
578 value.forEach(item => out.searchParams.append(key, item));
580 out.searchParams.set(key, value);
583 return out.toString();
587 function bodyFromData(data) {
588 if (!data) return null;
589 if (window.FormData && data instanceof FormData) return data;
590 if (window.URLSearchParams && data instanceof URLSearchParams) return data;
591 if (typeof Blob !== "undefined" && data instanceof Blob) return data;
592 if (typeof data === "string") return data;
593 if (!isPlainObject(data)) return data;
595 const body = new URLSearchParams();
596 Object.keys(data).forEach(key => {
597 const value = data[key];
598 if (value === undefined || value === null) return;
599 if (Array.isArray(value)) {
600 value.forEach(item => body.append(key, item));
602 body.set(key, value);
609 function normalizeBind(v) {
611 if (v === undefined || v === null || v === "") {
612 return ["form", "link", "button"];
615 const raw = String(v).toLowerCase().trim();
617 if (raw === "*" || raw === "all") {
618 return ["form", "link", "button"];
628 function normalizeClassFilter(v) {
630 if (v === undefined || v === null || v === "") {
634 const raw = String(v).trim();
647 function elementType(el) {
649 if (!el || !el.tagName) return "";
651 const tag = el.tagName.toLowerCase();
653 if (tag === "form") return "form";
654 if (tag === "a") return "link";
656 if (tag === "button") return "button";
658 if (tag === "input") {
659 const type = String(el.getAttribute("type") || "text").toLowerCase();
660 if (type === "button" || type === "submit" || type === "image") {
669 function bindMatches(type, bindList) {
671 if (!type) return false;
672 if (!Array.isArray(bindList) || !bindList.length) return false;
674 return bindList.includes(type);
678 function classMatches(el, classList) {
680 if (!el) return false;
682 if (!Array.isArray(classList) || !classList.length) {
686 if (classList.includes("*")) {
690 for (let i = 0; i < classList.length; i++) {
691 if (el.classList.contains(classList[i])) {
700 function readAttr(el, name) {
702 if (!el || !el.getAttribute) return "";
704 const v = el.getAttribute(name);
705 return v == null ? "" : String(v).trim();
709 function readDataAjax(el, key) {
713 return readAttr(el, "data-ajax-" + key);
717 function readLegacyData(el, key) {
721 return readAttr(el, "data-" + key);
725 function parseParamString(str) {
729 if (str === undefined || str === null) {
733 let raw = String(str).trim();
739 raw = raw.replace(/^[?&]+/, "");
745 const usp = new URLSearchParams(raw);
747 usp.forEach((value, key) => {
748 out.push([key, value]);
755 function appendToUrl(baseUrl, extra) {
757 let base = String(baseUrl || "").trim();
758 let add = String(extra || "").trim();
760 if (!add) return base;
761 if (!base) return "";
763 add = add.replace(/^[?&]+/, "");
765 if (!add) return base;
767 const hashPos = base.indexOf("#");
770 if (hashPos !== -1) {
771 hash = base.substring(hashPos);
772 base = base.substring(0, hashPos);
775 const sep = base.includes("?") ? "&" : "?";
777 return base + sep + add + hash;
781 function ensureAjaxFlag(url) {
783 let finalUrl = String(url || "").trim();
785 if (!finalUrl) return finalUrl;
787 if (finalUrl.indexOf("dbx_ajax=") === -1) {
788 finalUrl = appendToUrl(finalUrl, "dbx_ajax=1");
794 // Zentrale Stelle fuer das AJAX-Kennzeichen. Andere Module liefern
795 // grundsaetzlich normale, auch direkt aufrufbare URLs.
796 dbx.ajax.url = ensureAjaxFlag;
798 function isPrivateNetworkHost(hostname) {
799 const host = String(hostname || "").trim().toLowerCase().replace(/^\[|\]$/g, "");
801 if (!host) return false;
802 if (host === "localhost" || host === "localhost.localdomain" || host.endsWith(".localhost")) return true;
803 if (host === "::1") return true;
804 if (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd")) return true;
806 const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
807 if (!m) return false;
809 const a = Number(m[1]);
810 const b = Number(m[2]);
812 if (a === 10 || a === 127) return true;
813 if (a === 169 && b === 254) return true;
814 if (a === 172 && b >= 16 && b <= 31) return true;
815 if (a === 192 && b === 168) return true;
820 function shouldBlockLocalNetworkUrl(parsed) {
821 if (!parsed || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) return false;
822 if (parsed.origin === window.location.origin) return false;
823 if (isPrivateNetworkHost(window.location.hostname)) return false;
824 return isPrivateNetworkHost(parsed.hostname);
828 function normalizeRequestUrl(url) {
830 const raw = String(url || "").trim();
832 if (!raw) return raw;
835 const parsed = new URL(raw, window.location.href);
837 if (shouldBlockLocalNetworkUrl(parsed)) {
838 throw new Error("blocked_local_network_request");
842 (parsed.protocol === "http:" || parsed.protocol === "https:") &&
843 parsed.hostname === window.location.hostname &&
844 parsed.port === window.location.port &&
845 parsed.protocol !== window.location.protocol
847 parsed.protocol = window.location.protocol;
850 if (parsed.origin === window.location.origin) {
851 return parsed.pathname + parsed.search + parsed.hash;
854 return parsed.toString();
856 if (e && e.message === "blocked_local_network_request") {
864 function resolveElementByTarget(target) {
866 const raw = String(target || "").trim();
868 if (!raw) return null;
870 let el = document.getElementById(raw);
874 el = document.querySelector(raw);
877 dbx.warn("[dbx.ajax] invalid target selector:", raw);
884 function getClosestForm(source, root) {
886 if (!source) return null;
888 if (source.tagName && source.tagName.toLowerCase() === "form") {
892 const dataFormId = readLegacyData(source, "dbx_form");
894 const formByDataId = document.getElementById(dataFormId);
900 const formAttr = readAttr(source, "form");
902 const formByAttr = document.getElementById(formAttr);
908 const form = source.closest("form");
914 if (root && !root.contains(form)) {
922 function getRootConfigs(root) {
924 if (!root) return [];
926 if (Array.isArray(root._dbxAjaxConfigs)) {
927 return root._dbxAjaxConfigs;
932 if (dbx.declare && dbx.declare.schemas && dbx.declare.schemas.ajax) {
933 out = dbx.declare.resolve("ajax", root);
935 const attr = readAttr(root, "data-dbx");
936 const list = dbx.parseData(attr).filter(cfg => cfg.lib === "ajax");
938 out = list.map((cfg, index) => {
942 class: normalizeClassFilter(cfg.class),
943 mode: normalizeMode(cfg.mode || "html"),
944 url: String(cfg.url || "form").trim(),
945 target: String(cfg.target || "").trim(),
946 replace: normalizeReplaceMode(cfg.replace || "target"),
947 bind: normalizeBind(cfg.bind),
948 method: normalizeMethod(cfg.method || ""),
949 params: String(cfg.params || "").trim(),
950 jsonreplace: bool(cfg.jsonreplace, false),
951 jsonkey: String(cfg.jsonkey || "html").trim(),
952 textreplace: bool(cfg.textreplace, false)
960 class: normalizeClassFilter("dbxAjax"),
965 bind: ["form", "link", "button"],
975 root._dbxAjaxConfigs = out;
981 function findMatchingConfig(root, source, type, overrides) {
983 if (overrides && overrides.config) {
984 return overrides.config;
987 const configs = getRootConfigs(root);
989 for (let i = 0; i < configs.length; i++) {
991 const cfg = configs[i];
993 if (!bindMatches(type, cfg.bind)) {
997 if (!classMatches(source, cfg.class)) {
1008 function getFormDefaultUrl(form) {
1010 if (!form) return "";
1012 const action = readAttr(form, "action");
1018 return window.location.href;
1022 function getLibDefaultUrl(cfg, form) {
1024 if (!cfg) return "";
1026 const url = String(cfg.url || "").trim();
1028 if (!url) return "";
1030 if (url === "form") {
1031 return getFormDefaultUrl(form);
1034 if (url === "lib") {
1038 if (url.charAt(0) === "&") {
1039 const base = getFormDefaultUrl(form);
1040 return base ? appendToUrl(base, url) : "";
1047 function resolveUrl(root, source, form, cfg, overrides) {
1049 const libUrl = getLibDefaultUrl(cfg, form);
1051 const formUrlOverride = readDataAjax(form, "url");
1052 const sourceUrlOverride =
1053 (overrides && overrides.url) ||
1054 readDataAjax(source, "url") ||
1055 readAttr(source, "formaction") ||
1056 ((elementType(source) === "link") ? readAttr(source, "href") : "");
1058 const formBase = formUrlOverride
1059 ? resolveUrlToken(formUrlOverride, getFormDefaultUrl(form), libUrl)
1060 : getFormDefaultUrl(form);
1062 if (sourceUrlOverride) {
1063 return resolveUrlToken(sourceUrlOverride, formBase, libUrl);
1066 if (elementType(source) === "form") {
1067 return formBase || libUrl;
1071 return formBase || libUrl;
1078 function resolveUrlToken(token, formUrl, libUrl) {
1080 const raw = String(token || "").trim();
1086 if (raw === "form") {
1087 return formUrl || libUrl || "";
1090 if (raw === "lib") {
1091 return libUrl || formUrl || "";
1094 if (raw.charAt(0) === "&") {
1095 const base = formUrl || libUrl || "";
1099 return appendToUrl(base, raw);
1106 function resolveTarget(root, source, form, cfg, overrides) {
1108 const sourceTarget =
1109 (overrides && overrides.target) ||
1110 readDataAjax(source, "target") ||
1111 readLegacyData(source, "dbx_target");
1114 return sourceTarget;
1117 const formTarget = readDataAjax(form, "target");
1122 if (cfg && cfg.target) {
1130 function resolveReplaceMode(source, form, cfg, overrides) {
1132 const sourceReplace =
1133 (overrides && overrides.replace) ||
1134 readDataAjax(source, "replace");
1136 if (sourceReplace) {
1137 return normalizeReplaceMode(sourceReplace);
1140 const formReplace = readDataAjax(form, "replace");
1142 return normalizeReplaceMode(formReplace);
1145 return normalizeReplaceMode((cfg && cfg.replace) || "target");
1149 function resolveMode(source, form, cfg, overrides) {
1152 (overrides && overrides.mode) ||
1153 readDataAjax(source, "mode");
1156 return normalizeMode(sourceMode);
1159 const formMode = readDataAjax(form, "mode");
1161 return normalizeMode(formMode);
1164 return normalizeMode((cfg && cfg.mode) || "html");
1168 function resolveMethod(source, form, type, cfg, overrides) {
1170 const sourceMethod =
1171 (overrides && overrides.method) ||
1172 readDataAjax(source, "method") ||
1173 readAttr(source, "formmethod");
1176 return normalizeMethod(sourceMethod, type === "link" ? "GET" : "POST");
1180 readDataAjax(form, "method") ||
1181 readAttr(form, "method");
1184 return normalizeMethod(formMethod, type === "link" ? "GET" : "POST");
1187 if (cfg && cfg.method) {
1188 return normalizeMethod(cfg.method, type === "link" ? "GET" : "POST");
1191 return normalizeMethod(type === "link" ? "GET" : "POST");
1195 function resolveJsonReplace(source, form, cfg, overrides) {
1197 if (overrides && typeof overrides.jsonreplace !== "undefined") {
1198 return bool(overrides.jsonreplace, false);
1201 const sourceVal = readDataAjax(source, "jsonreplace");
1202 if (sourceVal !== "") {
1203 return bool(sourceVal, false);
1206 const formVal = readDataAjax(form, "jsonreplace");
1207 if (formVal !== "") {
1208 return bool(formVal, false);
1211 return bool(cfg && cfg.jsonreplace, false);
1215 function resolveJsonKey(source, form, cfg, overrides) {
1217 if (overrides && overrides.jsonkey) {
1218 return String(overrides.jsonkey).trim();
1221 const sourceVal = readDataAjax(source, "jsonkey");
1223 return String(sourceVal).trim();
1226 const formVal = readDataAjax(form, "jsonkey");
1228 return String(formVal).trim();
1231 return String((cfg && cfg.jsonkey) || "html").trim();
1235 function resolveTextReplace(source, form, cfg, overrides) {
1237 if (overrides && typeof overrides.textreplace !== "undefined") {
1238 return bool(overrides.textreplace, false);
1241 const sourceVal = readDataAjax(source, "textreplace");
1242 if (sourceVal !== "") {
1243 return bool(sourceVal, false);
1246 const formVal = readDataAjax(form, "textreplace");
1247 if (formVal !== "") {
1248 return bool(formVal, false);
1251 return bool(cfg && cfg.textreplace, false);
1255 function collectExtraParams(source, form, cfg, overrides) {
1259 if (cfg && cfg.params) {
1260 out.push(...parseParamString(cfg.params));
1263 const formParams = readDataAjax(form, "params");
1265 out.push(...parseParamString(formParams));
1268 const sourceParams =
1269 (overrides && overrides.params) ||
1270 readDataAjax(source, "params");
1273 out.push(...parseParamString(sourceParams));
1280 function applyParamsToFormData(fd, params) {
1282 if (!fd || !params || !params.length) return;
1284 params.forEach(([key, value]) => {
1288 fd.append(key, value);
1294 function applyParamsToUrl(url, params) {
1296 let finalUrl = String(url || "").trim();
1302 if (!params || !params.length) {
1306 const hashPos = finalUrl.indexOf("#");
1309 if (hashPos !== -1) {
1310 hash = finalUrl.substring(hashPos);
1311 finalUrl = finalUrl.substring(0, hashPos);
1316 params.forEach(([key, value], idx) => {
1317 const pair = encodeURIComponent(key) + "=" + encodeURIComponent(value);
1318 qs += (idx === 0 ? "" : "&") + pair;
1322 finalUrl = appendToUrl(finalUrl, qs);
1325 return finalUrl + hash;
1329 function buildBodyAndUrl(ctx) {
1331 let url = String(ctx.url || "").trim();
1334 const params = Array.isArray(ctx.params) ? ctx.params.slice() : [];
1336 if (ctx.method === "GET") {
1339 const fd = new FormData(ctx.form);
1340 fd.forEach((value, key) => {
1341 params.push([key, value]);
1345 if (ctx.submitName) {
1346 params.push([ctx.submitName, ctx.submitValue]);
1349 url = applyParamsToUrl(url, params);
1358 body = new FormData(ctx.form);
1360 body = new FormData();
1363 if (ctx.submitName) {
1364 body.set(ctx.submitName, ctx.submitValue);
1367 applyParamsToFormData(body, params);
1376 function emit(name, data) {
1378 if (dbx.event && typeof dbx.event.emit === "function") {
1379 dbx.event.emit(name, data);
1384 function executeScripts(scripts) {
1386 if (!scripts || !scripts.length) {
1387 return Promise.resolve();
1390 let chain = Promise.resolve();
1392 scripts.forEach(srcNode => {
1394 chain = chain.then(() => {
1395 return new Promise((resolve) => {
1397 const script = document.createElement("script");
1399 for (let i = 0; i < srcNode.attributes.length; i++) {
1400 const attr = srcNode.attributes[i];
1401 if (attr.name === "src") continue;
1402 script.setAttribute(attr.name, attr.value);
1406 script.src = srcNode.src;
1407 script.async = false;
1408 script.onload = () => resolve();
1409 script.onerror = () => {
1410 dbx.error("[dbx.ajax] script load failed:", srcNode.src);
1413 document.body.appendChild(script);
1415 script.text = srcNode.textContent || "";
1416 document.body.appendChild(script);
1427 function replaceTarget(targetEl, html, replaceMode = "target") {
1430 return Promise.resolve(targetEl);
1433 if (html === undefined || html === null) {
1434 return Promise.resolve(targetEl);
1437 const markup = String(html).trim();
1440 return Promise.resolve(targetEl);
1443 const temp = document.createElement("div");
1444 temp.innerHTML = markup;
1446 const scripts = Array.from(temp.querySelectorAll("script"));
1447 scripts.forEach(s => s.remove());
1449 let newElement = targetEl;
1450 const mode = normalizeReplaceMode(replaceMode);
1452 if (mode === "inner") {
1454 targetEl.innerHTML = temp.innerHTML;
1455 newElement = targetEl;
1457 } else if (temp.children.length === 1) {
1459 const candidate = temp.firstElementChild;
1461 if (candidate && targetEl.parentNode) {
1462 targetEl.parentNode.replaceChild(candidate, targetEl);
1463 newElement = candidate;
1465 targetEl.innerHTML = temp.innerHTML;
1466 newElement = targetEl;
1471 targetEl.innerHTML = temp.innerHTML;
1472 newElement = targetEl;
1475 return executeScripts(scripts).then(() => {
1477 if (newElement && typeof dbx.scan === "function") {
1478 dbx.scan(newElement);
1486 function requestNow() {
1487 return window.performance && typeof window.performance.now === "function"
1488 ? window.performance.now()
1493 function requestElapsedSeconds(startedAt) {
1494 return Math.max(0, (requestNow() - startedAt) / 1000);
1498 function parseRuntimeSeconds(value) {
1499 const number = Number(value);
1500 return Number.isFinite(number) && number >= 0 ? number : null;
1504 function phpRuntimeFromResponse(response, data) {
1505 if (response && response.headers && typeof response.headers.get === "function") {
1506 const headerValue = parseRuntimeSeconds(response.headers.get("X-DBX-PHP-Runtime"));
1507 if (headerValue !== null) {
1512 if (data && typeof data === "object") {
1513 const directValue = parseRuntimeSeconds(data.dbx_php_runtime || data.php_runtime);
1514 if (directValue !== null) {
1518 if (data._dbx_runtime && typeof data._dbx_runtime === "object") {
1519 const nestedValue = parseRuntimeSeconds(data._dbx_runtime.php);
1520 if (nestedValue !== null) {
1530 function shouldTrackRuntime(options, url, body) {
1531 if (dbx.footerStatus && typeof dbx.footerStatus.shouldTrackAjaxRuntime === "function") {
1532 return dbx.footerStatus.shouldTrackAjaxRuntime(url, body, options);
1536 options.skipRuntime === true ||
1537 options.footerRuntime === 'hidden' ||
1538 options.footerRuntime === 'skip' ||
1539 options.footerRuntime === '0'
1544 if (options && String(options.mode || "").toLowerCase() === "json") {
1548 const targetUrl = String(url || '');
1549 if (/[?&]dbx_sync=0(?:&|$)/.test(targetUrl)) {
1553 if (body instanceof URLSearchParams && body.get('dbx_sync') === '0') {
1557 if (typeof body === 'string' && /(?:^|&)dbx_sync=0(?:&|$)/.test(body)) {
1565 function updateFooterRuntime(startedAt, response, data) {
1566 if (!dbx.footerStatus || typeof dbx.footerStatus.updateAjax !== "function") {
1570 dbx.footerStatus.updateAjax(
1571 requestElapsedSeconds(startedAt),
1572 phpRuntimeFromResponse(response, data)
1577 /* =========================================================
1578 * PUBLIC CORE REQUEST
1579 * ========================================================= */
1581 dbx.ajax.request = function (options) {
1583 const method = normalizeMethod(options && options.method, "POST");
1584 const mode = normalizeMode(options && options.mode);
1585 let url = String((options && options.url) || "").trim();
1586 let body = (options && typeof options.body !== "undefined") ? options.body : null;
1587 const headers = (options && options.headers) ? options.headers : {};
1588 const data = (options && typeof options.data !== "undefined") ? options.data : null;
1589 const timeout = Math.max(0, Number((options && options.timeout) || 0));
1590 const keepalive = bool(options && options.keepalive, false);
1591 const controller = timeout > 0 && window.AbortController ? new AbortController() : null;
1593 let runtimeUpdated = false;
1594 const startedAt = requestNow();
1597 return Promise.reject(new Error("missing_url"));
1600 if ((method === "GET" || method === "HEAD") && data) {
1601 url = appendDataToUrl(url, data);
1602 } else if (body === null && data) {
1603 body = bodyFromData(data);
1606 url = ensureAjaxFlag(url);
1609 url = normalizeRequestUrl(url);
1611 dbx.warn("[dbx.ajax] local network request blocked", url);
1612 return Promise.reject(err);
1615 const trackRuntime = shouldTrackRuntime(options, url, body);
1617 function markRuntime(response, responseData) {
1618 if (!trackRuntime) return;
1619 runtimeUpdated = true;
1620 updateFooterRuntime(startedAt, response, responseData);
1623 dbx.log("[dbx.ajax] request start", {
1630 timer = window.setTimeout(() => controller.abort(), timeout);
1637 credentials: "same-origin",
1638 keepalive: keepalive,
1639 signal: controller ? controller.signal : undefined
1644 markRuntime(response, null);
1645 throw new Error("HTTP " + response.status);
1648 if (mode === "json") {
1649 return response.text().then(txt => {
1651 const emptyData = {};
1652 markRuntime(response, emptyData);
1657 const data = JSON.parse(txt);
1658 markRuntime(response, data);
1661 const clean = String(txt || "")
1662 .replace(/<script[\s\S]*?<\/script>/gi, "")
1663 .replace(/<style[\s\S]*?<\/style>/gi, "")
1664 .replace(/<[^>]+>/g, " ")
1665 .replace(/\s+/g, " ")
1667 markRuntime(response, null);
1668 throw new Error(clean || "Ungueltige Serverantwort");
1673 return response.text().then(txt => {
1674 markRuntime(response, txt);
1679 if (!runtimeUpdated && trackRuntime) {
1680 updateFooterRuntime(startedAt, null, null);
1686 if (timer) window.clearTimeout(timer);
1691 /* =========================================================
1693 * ========================================================= */
1695 function executeAjax(root, source, overrides = {}, event) {
1698 dbx.warn("[dbx.ajax] missing root");
1699 return Promise.resolve(null);
1702 const type = elementType(source);
1705 dbx.warn("[dbx.ajax] unsupported source:", source);
1706 return Promise.resolve(null);
1709 const cfg = findMatchingConfig(root, source, type, overrides);
1711 if (!cfg && !overrides.url) {
1712 dbx.log("[dbx.ajax] no matching config", {
1716 return Promise.resolve(null);
1719 const form = (overrides && overrides.form) || getClosestForm(source, root);
1722 * Bei einem nativen Submit ist die eigentliche Schaltfläche über
1723 * SubmitEvent.submitter verfügbar. Sie muss separat erhalten bleiben,
1724 * weil new FormData(form) ihren name/value-Wert nicht übernimmt.
1726 const eventSubmitter = event && event.submitter ? event.submitter : null;
1727 const overrideSubmitter = overrides && overrides.submitSource
1728 ? overrides.submitSource
1730 const submitSource = overrideSubmitter || eventSubmitter || ((type === "button") ? source : null);
1731 const submitName = String(
1732 (overrides && overrides.submitName) ||
1733 readAttr(submitSource, "name") ||
1736 const submitValueOverride = overrides && Object.prototype.hasOwnProperty.call(overrides, "submitValue")
1737 ? overrides.submitValue
1739 const submitValue = submitValueOverride !== null
1740 ? String(submitValueOverride)
1741 : String(readAttr(submitSource, "value") || "");
1749 submitSource: submitSource,
1750 submitName: submitName,
1751 submitValue: submitValue
1754 ctx.mode = resolveMode(source, form, cfg, overrides);
1755 ctx.method = resolveMethod(source, form, type, cfg, overrides);
1756 ctx.url = resolveUrl(root, source, form, cfg, overrides);
1757 ctx.target = resolveTarget(root, source, form, cfg, overrides);
1758 ctx.replace = resolveReplaceMode(source, form, cfg, overrides);
1759 ctx.params = collectExtraParams(source, form, cfg, overrides);
1760 ctx.jsonreplace = resolveJsonReplace(source, form, cfg, overrides);
1761 ctx.jsonkey = resolveJsonKey(source, form, cfg, overrides);
1762 ctx.textreplace = resolveTextReplace(source, form, cfg, overrides);
1764 if (event && typeof event.preventDefault === "function") {
1765 event.preventDefault();
1769 dbx.warn("[dbx.ajax] missing resolved url", {
1773 return Promise.resolve(null);
1776 const lockEl = ctx.form || ctx.source;
1778 if (lockEl && lockEl._dbxAjaxRunning === true) {
1779 dbx.log("[dbx.ajax] request skipped (already running)");
1780 return Promise.resolve(null);
1784 lockEl._dbxAjaxRunning = true;
1787 const targetEl = resolveElementByTarget(ctx.target);
1789 if (ctx.mode === "html" && !targetEl) {
1790 dbx.warn("[dbx.ajax] target not found for html mode:", ctx.target);
1792 lockEl._dbxAjaxRunning = false;
1794 return Promise.resolve(null);
1797 let loadingTimer = null;
1799 targetEl.classList.add("dbx-ajax-loading");
1800 loadingTimer = window.setTimeout(() => {
1801 targetEl.classList.remove("dbx-ajax-loading", "is-loading");
1805 const built = buildBodyAndUrl(ctx);
1807 ctx.url = built.url;
1808 ctx.body = built.body;
1820 replace: ctx.replace,
1824 emit("ajax:before", eventData);
1826 return dbx.ajax.request({
1832 .then(responseData => {
1834 if (ctx.mode === "html") {
1836 return replaceTarget(targetEl, responseData, ctx.replace).then(newTarget => {
1838 emit("ajax:after", {
1840 response: responseData,
1841 targetElement: newTarget
1847 response: responseData,
1848 targetElement: newTarget
1853 if (ctx.mode === "json") {
1855 if (ctx.jsonreplace === true && targetEl) {
1859 if (responseData && typeof responseData === "object") {
1860 html = responseData[ctx.jsonkey] || "";
1863 if (typeof html === "string" && html !== "") {
1864 return replaceTarget(targetEl, html, ctx.replace).then(newTarget => {
1866 emit("ajax:after", {
1868 response: responseData,
1869 targetElement: newTarget
1875 response: responseData,
1876 targetElement: newTarget
1882 emit("ajax:after", {
1884 response: responseData,
1885 targetElement: targetEl
1891 response: responseData,
1892 targetElement: targetEl
1896 if (ctx.mode === "text") {
1898 if (ctx.textreplace === true && targetEl) {
1899 return replaceTarget(targetEl, responseData, ctx.replace).then(newTarget => {
1901 emit("ajax:after", {
1903 response: responseData,
1904 targetElement: newTarget
1910 response: responseData,
1911 targetElement: newTarget
1916 emit("ajax:after", {
1918 response: responseData,
1919 targetElement: targetEl
1925 response: responseData,
1926 targetElement: targetEl
1930 emit("ajax:after", {
1932 response: responseData,
1933 targetElement: targetEl
1939 response: responseData,
1940 targetElement: targetEl
1945 dbx.error("[dbx.ajax] error:", error);
1947 emit("ajax:error", {
1957 lockEl._dbxAjaxRunning = false;
1961 window.clearTimeout(loadingTimer);
1965 targetEl.classList.remove("dbx-ajax-loading", "is-loading");
1968 const finalTargetEl = resolveElementByTarget(ctx.target);
1969 if (finalTargetEl) {
1970 finalTargetEl.classList.remove("dbx-ajax-loading", "is-loading");
1976 /* =========================================================
1978 * ========================================================= */
1980 dbx.ajax.run = function (root, source, overrides = {}, event) {
1981 return executeAjax(root, source, overrides, event);
1985 /* =========================================================
1986 * DECLARE SCHEMA (Defaults + data-* Aliase)
1987 * ========================================================= */
1989 if (dbx.declare && typeof dbx.declare.registerSchema === "function") {
1991 dbx.declare.registerSchema("ajax", {
1998 aliases: ["data-ajax-mode"]
2002 aliases: ["data-ajax-url", "data-url"]
2006 aliases: ["data-ajax-target", "data-dbx_target", "data-target"],
2007 infer: function (el, ctx) {
2008 return dbx.declare.infer.ajaxTarget(ctx.root || el);
2013 aliases: ["data-ajax-replace"]
2016 default: "form,link,button",
2017 aliases: ["data-ajax-bind"]
2021 aliases: ["data-ajax-method"]
2025 aliases: ["data-ajax-params"]
2029 aliases: ["data-ajax-jsonreplace"]
2033 aliases: ["data-ajax-jsonkey"]
2037 aliases: ["data-ajax-textreplace"]
2042 dbx.declare.transforms.ajax = function (raw, root) {
2046 class: normalizeClassFilter(raw.class),
2047 mode: normalizeMode(raw.mode),
2048 url: String(raw.url || "form").trim(),
2049 target: String(raw.target || "").trim(),
2050 replace: normalizeReplaceMode(raw.replace),
2051 bind: normalizeBind(raw.bind),
2052 method: normalizeMethod(raw.method, ""),
2053 params: String(raw.params || "").trim(),
2054 jsonreplace: bool(raw.jsonreplace, false),
2055 jsonkey: String(raw.jsonkey || "html").trim(),
2056 textreplace: bool(raw.textreplace, false)
2062 /* =========================================================
2064 * ========================================================= */
2066 dbx.feature.register("ajax", {
2073 ["css", "design", "c-ajax.css"]
2081 el.__dbxInitialized = el.__dbxInitialized || {};
2083 if (el.__dbxInitialized["ajax"]) {
2087 el.__dbxInitialized["ajax"] = true;
2088 el.setAttribute("data-dbx-ajax-root", "1");
2092 dbx.log("[dbx.ajax] init", {
2093 rootId: el.id || "",
2094 configs: el._dbxAjaxConfigs
2097 /* -------------------------------------------------
2099 * ------------------------------------------------- */
2100 el.addEventListener("submit", function (e) {
2102 if (e.defaultPrevented) return;
2104 const form = e.target.closest("form");
2106 if (!el.contains(form)) return;
2108 const nearestAjaxRoot = form.closest("[data-dbx-ajax-root='1']");
2109 if (nearestAjaxRoot !== el) return;
2111 executeAjax(el, form, {
2112 submitSource: e.submitter || null,
2113 submitName: e.submitter ? readAttr(e.submitter, "name") : "",
2114 submitValue: e.submitter ? readAttr(e.submitter, "value") : ""
2115 }, e).catch(() => {});
2118 /* -------------------------------------------------
2120 * ------------------------------------------------- */
2121 el.addEventListener("click", function (e) {
2123 if (e.defaultPrevented) return;
2125 const source = e.target.closest("a, button, input[type='button'], input[type='submit'], input[type='image']");
2126 if (!source) return;
2127 if (!el.contains(source)) return;
2129 const nearestAjaxRoot = source.closest("[data-dbx-ajax-root='1']");
2130 if (nearestAjaxRoot !== el) return;
2132 const type = elementType(source);
2134 if (type === "link") {
2136 executeAjax(el, source, {}, e).catch(() => {});
2140 if (type === "button") {
2142 const tag = source.tagName.toLowerCase();
2143 const btnType = (tag === "button")
2144 ? String(source.getAttribute("type") || "submit").toLowerCase()
2145 : String(source.getAttribute("type") || "").toLowerCase();
2147 const isFormAction = source.classList.contains("dbxAjaxFormAction");
2151 const formId = readLegacyData(source, "dbx_form");
2152 const target = readLegacyData(source, "dbx_target");
2153 const form = formId ? document.getElementById(formId) : getClosestForm(source, el);
2155 executeAjax(el, source, {
2157 target: target || "",
2158 url: readAttr(source, "href") || readDataAjax(source, "url") || ""
2159 }, e).catch(() => {});
2164 /* submit-button in form:
2165 normales AJAX läuft über submit-event.
2166 nur wenn expliziter Override vorhanden ist,
2167 wird direkt auf Button-Ebene ausgeführt. */
2168 const hasButtonOverride =
2169 !!readDataAjax(source, "url") ||
2170 !!readDataAjax(source, "target") ||
2171 !!readDataAjax(source, "mode") ||
2172 !!readDataAjax(source, "method") ||
2173 !!readDataAjax(source, "params") ||
2174 !!readAttr(source, "formaction") ||
2175 !!readAttr(source, "formmethod");
2177 if ((btnType === "submit" || btnType === "image") && !hasButtonOverride) {
2181 executeAjax(el, source, {}, e).catch(() => {});
2191 delete el._dbxAjaxConfigs;
2193 if (el.__dbxInitialized && el.__dbxInitialized["ajax"]) {
2194 delete el.__dbxInitialized["ajax"];
2197 el.removeAttribute("data-dbx-ajax-root");
2199 dbx.log("[dbx.ajax] destroy", {
2206})(window, document);