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 function isPrivateNetworkHost(hostname) {
795 const host = String(hostname || "").trim().toLowerCase().replace(/^\[|\]$/g, "");
797 if (!host) return false;
798 if (host === "localhost" || host === "localhost.localdomain" || host.endsWith(".localhost")) return true;
799 if (host === "::1") return true;
800 if (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd")) return true;
802 const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
803 if (!m) return false;
805 const a = Number(m[1]);
806 const b = Number(m[2]);
808 if (a === 10 || a === 127) return true;
809 if (a === 169 && b === 254) return true;
810 if (a === 172 && b >= 16 && b <= 31) return true;
811 if (a === 192 && b === 168) return true;
816 function shouldBlockLocalNetworkUrl(parsed) {
817 if (!parsed || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) return false;
818 if (parsed.origin === window.location.origin) return false;
819 if (isPrivateNetworkHost(window.location.hostname)) return false;
820 return isPrivateNetworkHost(parsed.hostname);
824 function normalizeRequestUrl(url) {
826 const raw = String(url || "").trim();
828 if (!raw) return raw;
831 const parsed = new URL(raw, window.location.href);
833 if (shouldBlockLocalNetworkUrl(parsed)) {
834 throw new Error("blocked_local_network_request");
838 (parsed.protocol === "http:" || parsed.protocol === "https:") &&
839 parsed.hostname === window.location.hostname &&
840 parsed.port === window.location.port &&
841 parsed.protocol !== window.location.protocol
843 parsed.protocol = window.location.protocol;
846 if (parsed.origin === window.location.origin) {
847 return parsed.pathname + parsed.search + parsed.hash;
850 return parsed.toString();
852 if (e && e.message === "blocked_local_network_request") {
860 function resolveElementByTarget(target) {
862 const raw = String(target || "").trim();
864 if (!raw) return null;
866 let el = document.getElementById(raw);
870 el = document.querySelector(raw);
873 dbx.warn("[dbx.ajax] invalid target selector:", raw);
880 function getClosestForm(source, root) {
882 if (!source) return null;
884 if (source.tagName && source.tagName.toLowerCase() === "form") {
888 const dataFormId = readLegacyData(source, "dbx_form");
890 const formByDataId = document.getElementById(dataFormId);
896 const formAttr = readAttr(source, "form");
898 const formByAttr = document.getElementById(formAttr);
904 const form = source.closest("form");
910 if (root && !root.contains(form)) {
918 function getRootConfigs(root) {
920 if (!root) return [];
922 if (Array.isArray(root._dbxAjaxConfigs)) {
923 return root._dbxAjaxConfigs;
928 if (dbx.declare && dbx.declare.schemas && dbx.declare.schemas.ajax) {
929 out = dbx.declare.resolve("ajax", root);
931 const attr = readAttr(root, "data-dbx");
932 const list = dbx.parseData(attr).filter(cfg => cfg.lib === "ajax");
934 out = list.map((cfg, index) => {
938 class: normalizeClassFilter(cfg.class),
939 mode: normalizeMode(cfg.mode || "html"),
940 url: String(cfg.url || "form").trim(),
941 target: String(cfg.target || "").trim(),
942 replace: normalizeReplaceMode(cfg.replace || "target"),
943 bind: normalizeBind(cfg.bind),
944 method: normalizeMethod(cfg.method || ""),
945 params: String(cfg.params || "").trim(),
946 jsonreplace: bool(cfg.jsonreplace, false),
947 jsonkey: String(cfg.jsonkey || "html").trim(),
948 textreplace: bool(cfg.textreplace, false)
956 class: normalizeClassFilter("dbxAjax"),
961 bind: ["form", "link", "button"],
971 root._dbxAjaxConfigs = out;
977 function findMatchingConfig(root, source, type, overrides) {
979 if (overrides && overrides.config) {
980 return overrides.config;
983 const configs = getRootConfigs(root);
985 for (let i = 0; i < configs.length; i++) {
987 const cfg = configs[i];
989 if (!bindMatches(type, cfg.bind)) {
993 if (!classMatches(source, cfg.class)) {
1004 function getFormDefaultUrl(form) {
1006 if (!form) return "";
1008 const action = readAttr(form, "action");
1014 return window.location.href;
1018 function getLibDefaultUrl(cfg, form) {
1020 if (!cfg) return "";
1022 const url = String(cfg.url || "").trim();
1024 if (!url) return "";
1026 if (url === "form") {
1027 return getFormDefaultUrl(form);
1030 if (url === "lib") {
1034 if (url.charAt(0) === "&") {
1035 const base = getFormDefaultUrl(form);
1036 return base ? appendToUrl(base, url) : "";
1043 function resolveUrl(root, source, form, cfg, overrides) {
1045 const libUrl = getLibDefaultUrl(cfg, form);
1047 const formUrlOverride = readDataAjax(form, "url");
1048 const sourceUrlOverride =
1049 (overrides && overrides.url) ||
1050 readDataAjax(source, "url") ||
1051 readAttr(source, "formaction") ||
1052 ((elementType(source) === "link") ? readAttr(source, "href") : "");
1054 const formBase = formUrlOverride
1055 ? resolveUrlToken(formUrlOverride, getFormDefaultUrl(form), libUrl)
1056 : getFormDefaultUrl(form);
1058 if (sourceUrlOverride) {
1059 return resolveUrlToken(sourceUrlOverride, formBase, libUrl);
1062 if (elementType(source) === "form") {
1063 return formBase || libUrl;
1067 return formBase || libUrl;
1074 function resolveUrlToken(token, formUrl, libUrl) {
1076 const raw = String(token || "").trim();
1082 if (raw === "form") {
1083 return formUrl || libUrl || "";
1086 if (raw === "lib") {
1087 return libUrl || formUrl || "";
1090 if (raw.charAt(0) === "&") {
1091 const base = formUrl || libUrl || "";
1095 return appendToUrl(base, raw);
1102 function resolveTarget(root, source, form, cfg, overrides) {
1104 const sourceTarget =
1105 (overrides && overrides.target) ||
1106 readDataAjax(source, "target") ||
1107 readLegacyData(source, "dbx_target");
1110 return sourceTarget;
1113 const formTarget = readDataAjax(form, "target");
1118 if (cfg && cfg.target) {
1126 function resolveReplaceMode(source, form, cfg, overrides) {
1128 const sourceReplace =
1129 (overrides && overrides.replace) ||
1130 readDataAjax(source, "replace");
1132 if (sourceReplace) {
1133 return normalizeReplaceMode(sourceReplace);
1136 const formReplace = readDataAjax(form, "replace");
1138 return normalizeReplaceMode(formReplace);
1141 return normalizeReplaceMode((cfg && cfg.replace) || "target");
1145 function resolveMode(source, form, cfg, overrides) {
1148 (overrides && overrides.mode) ||
1149 readDataAjax(source, "mode");
1152 return normalizeMode(sourceMode);
1155 const formMode = readDataAjax(form, "mode");
1157 return normalizeMode(formMode);
1160 return normalizeMode((cfg && cfg.mode) || "html");
1164 function resolveMethod(source, form, type, cfg, overrides) {
1166 const sourceMethod =
1167 (overrides && overrides.method) ||
1168 readDataAjax(source, "method") ||
1169 readAttr(source, "formmethod");
1172 return normalizeMethod(sourceMethod, type === "link" ? "GET" : "POST");
1176 readDataAjax(form, "method") ||
1177 readAttr(form, "method");
1180 return normalizeMethod(formMethod, type === "link" ? "GET" : "POST");
1183 if (cfg && cfg.method) {
1184 return normalizeMethod(cfg.method, type === "link" ? "GET" : "POST");
1187 return normalizeMethod(type === "link" ? "GET" : "POST");
1191 function resolveJsonReplace(source, form, cfg, overrides) {
1193 if (overrides && typeof overrides.jsonreplace !== "undefined") {
1194 return bool(overrides.jsonreplace, false);
1197 const sourceVal = readDataAjax(source, "jsonreplace");
1198 if (sourceVal !== "") {
1199 return bool(sourceVal, false);
1202 const formVal = readDataAjax(form, "jsonreplace");
1203 if (formVal !== "") {
1204 return bool(formVal, false);
1207 return bool(cfg && cfg.jsonreplace, false);
1211 function resolveJsonKey(source, form, cfg, overrides) {
1213 if (overrides && overrides.jsonkey) {
1214 return String(overrides.jsonkey).trim();
1217 const sourceVal = readDataAjax(source, "jsonkey");
1219 return String(sourceVal).trim();
1222 const formVal = readDataAjax(form, "jsonkey");
1224 return String(formVal).trim();
1227 return String((cfg && cfg.jsonkey) || "html").trim();
1231 function resolveTextReplace(source, form, cfg, overrides) {
1233 if (overrides && typeof overrides.textreplace !== "undefined") {
1234 return bool(overrides.textreplace, false);
1237 const sourceVal = readDataAjax(source, "textreplace");
1238 if (sourceVal !== "") {
1239 return bool(sourceVal, false);
1242 const formVal = readDataAjax(form, "textreplace");
1243 if (formVal !== "") {
1244 return bool(formVal, false);
1247 return bool(cfg && cfg.textreplace, false);
1251 function collectExtraParams(source, form, cfg, overrides) {
1255 if (cfg && cfg.params) {
1256 out.push(...parseParamString(cfg.params));
1259 const formParams = readDataAjax(form, "params");
1261 out.push(...parseParamString(formParams));
1264 const sourceParams =
1265 (overrides && overrides.params) ||
1266 readDataAjax(source, "params");
1269 out.push(...parseParamString(sourceParams));
1276 function applyParamsToFormData(fd, params) {
1278 if (!fd || !params || !params.length) return;
1280 params.forEach(([key, value]) => {
1284 fd.append(key, value);
1290 function applyParamsToUrl(url, params) {
1292 let finalUrl = String(url || "").trim();
1298 if (!params || !params.length) {
1302 const hashPos = finalUrl.indexOf("#");
1305 if (hashPos !== -1) {
1306 hash = finalUrl.substring(hashPos);
1307 finalUrl = finalUrl.substring(0, hashPos);
1312 params.forEach(([key, value], idx) => {
1313 const pair = encodeURIComponent(key) + "=" + encodeURIComponent(value);
1314 qs += (idx === 0 ? "" : "&") + pair;
1318 finalUrl = appendToUrl(finalUrl, qs);
1321 return finalUrl + hash;
1325 function buildBodyAndUrl(ctx) {
1327 let url = String(ctx.url || "").trim();
1330 const params = Array.isArray(ctx.params) ? ctx.params.slice() : [];
1332 if (ctx.method === "GET") {
1335 const fd = new FormData(ctx.form);
1336 fd.forEach((value, key) => {
1337 params.push([key, value]);
1341 if (ctx.submitSource && ctx.submitSource.name) {
1342 params.push([ctx.submitSource.name, ctx.submitSource.value || ""]);
1345 url = applyParamsToUrl(url, params);
1348 url: ensureAjaxFlag(url),
1354 body = new FormData(ctx.form);
1356 body = new FormData();
1359 if (ctx.submitSource && ctx.submitSource.name) {
1360 body.set(ctx.submitSource.name, ctx.submitSource.value || "");
1363 applyParamsToFormData(body, params);
1366 url: ensureAjaxFlag(url),
1372 function emit(name, data) {
1374 if (dbx.event && typeof dbx.event.emit === "function") {
1375 dbx.event.emit(name, data);
1380 function executeScripts(scripts) {
1382 if (!scripts || !scripts.length) {
1383 return Promise.resolve();
1386 let chain = Promise.resolve();
1388 scripts.forEach(srcNode => {
1390 chain = chain.then(() => {
1391 return new Promise((resolve) => {
1393 const script = document.createElement("script");
1395 for (let i = 0; i < srcNode.attributes.length; i++) {
1396 const attr = srcNode.attributes[i];
1397 if (attr.name === "src") continue;
1398 script.setAttribute(attr.name, attr.value);
1402 script.src = srcNode.src;
1403 script.async = false;
1404 script.onload = () => resolve();
1405 script.onerror = () => {
1406 dbx.error("[dbx.ajax] script load failed:", srcNode.src);
1409 document.body.appendChild(script);
1411 script.text = srcNode.textContent || "";
1412 document.body.appendChild(script);
1423 function replaceTarget(targetEl, html, replaceMode = "target") {
1426 return Promise.resolve(targetEl);
1429 if (html === undefined || html === null) {
1430 return Promise.resolve(targetEl);
1433 const markup = String(html).trim();
1436 return Promise.resolve(targetEl);
1439 const temp = document.createElement("div");
1440 temp.innerHTML = markup;
1442 const scripts = Array.from(temp.querySelectorAll("script"));
1443 scripts.forEach(s => s.remove());
1445 let newElement = targetEl;
1446 const mode = normalizeReplaceMode(replaceMode);
1448 if (mode === "inner") {
1450 targetEl.innerHTML = temp.innerHTML;
1451 newElement = targetEl;
1453 } else if (temp.children.length === 1) {
1455 const candidate = temp.firstElementChild;
1457 if (candidate && targetEl.parentNode) {
1458 targetEl.parentNode.replaceChild(candidate, targetEl);
1459 newElement = candidate;
1461 targetEl.innerHTML = temp.innerHTML;
1462 newElement = targetEl;
1467 targetEl.innerHTML = temp.innerHTML;
1468 newElement = targetEl;
1471 return executeScripts(scripts).then(() => {
1473 if (newElement && typeof dbx.scan === "function") {
1474 dbx.scan(newElement);
1482 function requestNow() {
1483 return window.performance && typeof window.performance.now === "function"
1484 ? window.performance.now()
1489 function requestElapsedSeconds(startedAt) {
1490 return Math.max(0, (requestNow() - startedAt) / 1000);
1494 function parseRuntimeSeconds(value) {
1495 const number = Number(value);
1496 return Number.isFinite(number) && number >= 0 ? number : null;
1500 function phpRuntimeFromResponse(response, data) {
1501 if (response && response.headers && typeof response.headers.get === "function") {
1502 const headerValue = parseRuntimeSeconds(response.headers.get("X-DBX-PHP-Runtime"));
1503 if (headerValue !== null) {
1508 if (data && typeof data === "object") {
1509 const directValue = parseRuntimeSeconds(data.dbx_php_runtime || data.php_runtime);
1510 if (directValue !== null) {
1514 if (data._dbx_runtime && typeof data._dbx_runtime === "object") {
1515 const nestedValue = parseRuntimeSeconds(data._dbx_runtime.php);
1516 if (nestedValue !== null) {
1526 function shouldTrackRuntime(options, url, body) {
1527 if (dbx.footerStatus && typeof dbx.footerStatus.shouldTrackAjaxRuntime === "function") {
1528 return dbx.footerStatus.shouldTrackAjaxRuntime(url, body, options);
1532 options.skipRuntime === true ||
1533 options.footerRuntime === 'hidden' ||
1534 options.footerRuntime === 'skip' ||
1535 options.footerRuntime === '0'
1540 if (options && String(options.mode || "").toLowerCase() === "json") {
1544 const targetUrl = String(url || '');
1545 if (/[?&]dbx_sync=0(?:&|$)/.test(targetUrl)) {
1549 if (body instanceof URLSearchParams && body.get('dbx_sync') === '0') {
1553 if (typeof body === 'string' && /(?:^|&)dbx_sync=0(?:&|$)/.test(body)) {
1561 function updateFooterRuntime(startedAt, response, data) {
1562 if (!dbx.footerStatus || typeof dbx.footerStatus.updateAjax !== "function") {
1566 dbx.footerStatus.updateAjax(
1567 requestElapsedSeconds(startedAt),
1568 phpRuntimeFromResponse(response, data)
1573 /* =========================================================
1574 * PUBLIC CORE REQUEST
1575 * ========================================================= */
1577 dbx.ajax.request = function (options) {
1579 const method = normalizeMethod(options && options.method, "POST");
1580 const mode = normalizeMode(options && options.mode);
1581 let url = String((options && options.url) || "").trim();
1582 let body = (options && typeof options.body !== "undefined") ? options.body : null;
1583 const headers = (options && options.headers) ? options.headers : {};
1584 const data = (options && typeof options.data !== "undefined") ? options.data : null;
1585 const timeout = Math.max(0, Number((options && options.timeout) || 0));
1586 const keepalive = bool(options && options.keepalive, false);
1587 const controller = timeout > 0 && window.AbortController ? new AbortController() : null;
1589 let runtimeUpdated = false;
1590 const startedAt = requestNow();
1593 return Promise.reject(new Error("missing_url"));
1596 if ((method === "GET" || method === "HEAD") && data) {
1597 url = appendDataToUrl(url, data);
1598 } else if (body === null && data) {
1599 body = bodyFromData(data);
1603 url = normalizeRequestUrl(url);
1605 dbx.warn("[dbx.ajax] local network request blocked", url);
1606 return Promise.reject(err);
1609 const trackRuntime = shouldTrackRuntime(options, url, body);
1611 function markRuntime(response, responseData) {
1612 if (!trackRuntime) return;
1613 runtimeUpdated = true;
1614 updateFooterRuntime(startedAt, response, responseData);
1617 dbx.log("[dbx.ajax] request start", {
1624 timer = window.setTimeout(() => controller.abort(), timeout);
1631 credentials: "same-origin",
1632 keepalive: keepalive,
1633 signal: controller ? controller.signal : undefined
1638 markRuntime(response, null);
1639 throw new Error("HTTP " + response.status);
1642 if (mode === "json") {
1643 return response.text().then(txt => {
1645 const emptyData = {};
1646 markRuntime(response, emptyData);
1651 const data = JSON.parse(txt);
1652 markRuntime(response, data);
1655 const clean = String(txt || "")
1656 .replace(/<script[\s\S]*?<\/script>/gi, "")
1657 .replace(/<style[\s\S]*?<\/style>/gi, "")
1658 .replace(/<[^>]+>/g, " ")
1659 .replace(/\s+/g, " ")
1661 markRuntime(response, null);
1662 throw new Error(clean || "Ungueltige Serverantwort");
1667 return response.text().then(txt => {
1668 markRuntime(response, txt);
1673 if (!runtimeUpdated && trackRuntime) {
1674 updateFooterRuntime(startedAt, null, null);
1680 if (timer) window.clearTimeout(timer);
1685 /* =========================================================
1687 * ========================================================= */
1689 function executeAjax(root, source, overrides = {}, event) {
1692 dbx.warn("[dbx.ajax] missing root");
1693 return Promise.resolve(null);
1696 const type = elementType(source);
1699 dbx.warn("[dbx.ajax] unsupported source:", source);
1700 return Promise.resolve(null);
1703 const cfg = findMatchingConfig(root, source, type, overrides);
1705 if (!cfg && !overrides.url) {
1706 dbx.log("[dbx.ajax] no matching config", {
1710 return Promise.resolve(null);
1713 const form = (overrides && overrides.form) || getClosestForm(source, root);
1721 submitSource: (type === "button") ? source : null
1724 ctx.mode = resolveMode(source, form, cfg, overrides);
1725 ctx.method = resolveMethod(source, form, type, cfg, overrides);
1726 ctx.url = resolveUrl(root, source, form, cfg, overrides);
1727 ctx.target = resolveTarget(root, source, form, cfg, overrides);
1728 ctx.replace = resolveReplaceMode(source, form, cfg, overrides);
1729 ctx.params = collectExtraParams(source, form, cfg, overrides);
1730 ctx.jsonreplace = resolveJsonReplace(source, form, cfg, overrides);
1731 ctx.jsonkey = resolveJsonKey(source, form, cfg, overrides);
1732 ctx.textreplace = resolveTextReplace(source, form, cfg, overrides);
1734 if (event && typeof event.preventDefault === "function") {
1735 event.preventDefault();
1739 dbx.warn("[dbx.ajax] missing resolved url", {
1743 return Promise.resolve(null);
1746 const lockEl = ctx.form || ctx.source;
1748 if (lockEl && lockEl._dbxAjaxRunning === true) {
1749 dbx.log("[dbx.ajax] request skipped (already running)");
1750 return Promise.resolve(null);
1754 lockEl._dbxAjaxRunning = true;
1757 const targetEl = resolveElementByTarget(ctx.target);
1759 if (ctx.mode === "html" && !targetEl) {
1760 dbx.warn("[dbx.ajax] target not found for html mode:", ctx.target);
1762 lockEl._dbxAjaxRunning = false;
1764 return Promise.resolve(null);
1767 let loadingTimer = null;
1769 targetEl.classList.add("dbx-ajax-loading");
1770 loadingTimer = window.setTimeout(() => {
1771 targetEl.classList.remove("dbx-ajax-loading", "is-loading");
1775 const built = buildBodyAndUrl(ctx);
1777 ctx.url = built.url;
1778 ctx.body = built.body;
1790 replace: ctx.replace,
1794 emit("ajax:before", eventData);
1796 return dbx.ajax.request({
1802 .then(responseData => {
1804 if (ctx.mode === "html") {
1806 return replaceTarget(targetEl, responseData, ctx.replace).then(newTarget => {
1808 emit("ajax:after", {
1810 response: responseData,
1811 targetElement: newTarget
1817 response: responseData,
1818 targetElement: newTarget
1823 if (ctx.mode === "json") {
1825 if (ctx.jsonreplace === true && targetEl) {
1829 if (responseData && typeof responseData === "object") {
1830 html = responseData[ctx.jsonkey] || "";
1833 if (typeof html === "string" && html !== "") {
1834 return replaceTarget(targetEl, html, ctx.replace).then(newTarget => {
1836 emit("ajax:after", {
1838 response: responseData,
1839 targetElement: newTarget
1845 response: responseData,
1846 targetElement: newTarget
1852 emit("ajax:after", {
1854 response: responseData,
1855 targetElement: targetEl
1861 response: responseData,
1862 targetElement: targetEl
1866 if (ctx.mode === "text") {
1868 if (ctx.textreplace === true && targetEl) {
1869 return replaceTarget(targetEl, responseData, ctx.replace).then(newTarget => {
1871 emit("ajax:after", {
1873 response: responseData,
1874 targetElement: newTarget
1880 response: responseData,
1881 targetElement: newTarget
1886 emit("ajax:after", {
1888 response: responseData,
1889 targetElement: targetEl
1895 response: responseData,
1896 targetElement: targetEl
1900 emit("ajax:after", {
1902 response: responseData,
1903 targetElement: targetEl
1909 response: responseData,
1910 targetElement: targetEl
1915 dbx.error("[dbx.ajax] error:", error);
1917 emit("ajax:error", {
1927 lockEl._dbxAjaxRunning = false;
1931 window.clearTimeout(loadingTimer);
1935 targetEl.classList.remove("dbx-ajax-loading", "is-loading");
1938 const finalTargetEl = resolveElementByTarget(ctx.target);
1939 if (finalTargetEl) {
1940 finalTargetEl.classList.remove("dbx-ajax-loading", "is-loading");
1946 /* =========================================================
1948 * ========================================================= */
1950 dbx.ajax.run = function (root, source, overrides = {}, event) {
1951 return executeAjax(root, source, overrides, event);
1955 /* =========================================================
1956 * DECLARE SCHEMA (Defaults + data-* Aliase)
1957 * ========================================================= */
1959 if (dbx.declare && typeof dbx.declare.registerSchema === "function") {
1961 dbx.declare.registerSchema("ajax", {
1968 aliases: ["data-ajax-mode"]
1972 aliases: ["data-ajax-url", "data-url"]
1976 aliases: ["data-ajax-target", "data-dbx_target", "data-target"],
1977 infer: function (el, ctx) {
1978 return dbx.declare.infer.ajaxTarget(ctx.root || el);
1983 aliases: ["data-ajax-replace"]
1986 default: "form,link,button",
1987 aliases: ["data-ajax-bind"]
1991 aliases: ["data-ajax-method"]
1995 aliases: ["data-ajax-params"]
1999 aliases: ["data-ajax-jsonreplace"]
2003 aliases: ["data-ajax-jsonkey"]
2007 aliases: ["data-ajax-textreplace"]
2012 dbx.declare.transforms.ajax = function (raw, root) {
2016 class: normalizeClassFilter(raw.class),
2017 mode: normalizeMode(raw.mode),
2018 url: String(raw.url || "form").trim(),
2019 target: String(raw.target || "").trim(),
2020 replace: normalizeReplaceMode(raw.replace),
2021 bind: normalizeBind(raw.bind),
2022 method: normalizeMethod(raw.method, ""),
2023 params: String(raw.params || "").trim(),
2024 jsonreplace: bool(raw.jsonreplace, false),
2025 jsonkey: String(raw.jsonkey || "html").trim(),
2026 textreplace: bool(raw.textreplace, false)
2032 /* =========================================================
2034 * ========================================================= */
2036 dbx.feature.register("ajax", {
2043 ["css", "design", "c-ajax.css"]
2051 el.__dbxInitialized = el.__dbxInitialized || {};
2053 if (el.__dbxInitialized["ajax"]) {
2057 el.__dbxInitialized["ajax"] = true;
2058 el.setAttribute("data-dbx-ajax-root", "1");
2062 dbx.log("[dbx.ajax] init", {
2063 rootId: el.id || "",
2064 configs: el._dbxAjaxConfigs
2067 /* -------------------------------------------------
2069 * ------------------------------------------------- */
2070 el.addEventListener("submit", function (e) {
2072 if (e.defaultPrevented) return;
2074 const form = e.target.closest("form");
2076 if (!el.contains(form)) return;
2078 const nearestAjaxRoot = form.closest("[data-dbx-ajax-root='1']");
2079 if (nearestAjaxRoot !== el) return;
2081 executeAjax(el, form, {}, e).catch(() => {});
2084 /* -------------------------------------------------
2086 * ------------------------------------------------- */
2087 el.addEventListener("click", function (e) {
2089 if (e.defaultPrevented) return;
2091 const source = e.target.closest("a, button, input[type='button'], input[type='submit'], input[type='image']");
2092 if (!source) return;
2093 if (!el.contains(source)) return;
2095 const nearestAjaxRoot = source.closest("[data-dbx-ajax-root='1']");
2096 if (nearestAjaxRoot !== el) return;
2098 const type = elementType(source);
2100 if (type === "link") {
2102 executeAjax(el, source, {}, e).catch(() => {});
2106 if (type === "button") {
2108 const tag = source.tagName.toLowerCase();
2109 const btnType = (tag === "button")
2110 ? String(source.getAttribute("type") || "submit").toLowerCase()
2111 : String(source.getAttribute("type") || "").toLowerCase();
2113 const isFormAction = source.classList.contains("dbxAjaxFormAction");
2117 const formId = readLegacyData(source, "dbx_form");
2118 const target = readLegacyData(source, "dbx_target");
2119 const form = formId ? document.getElementById(formId) : getClosestForm(source, el);
2121 executeAjax(el, source, {
2123 target: target || "",
2124 url: readAttr(source, "href") || readDataAjax(source, "url") || ""
2125 }, e).catch(() => {});
2130 /* submit-button in form:
2131 normales AJAX läuft über submit-event.
2132 nur wenn expliziter Override vorhanden ist,
2133 wird direkt auf Button-Ebene ausgeführt. */
2134 const hasButtonOverride =
2135 !!readDataAjax(source, "url") ||
2136 !!readDataAjax(source, "target") ||
2137 !!readDataAjax(source, "mode") ||
2138 !!readDataAjax(source, "method") ||
2139 !!readDataAjax(source, "params") ||
2140 !!readAttr(source, "formaction") ||
2141 !!readAttr(source, "formmethod");
2143 if ((btnType === "submit" || btnType === "image") && !hasButtonOverride) {
2147 executeAjax(el, source, {}, e).catch(() => {});
2157 delete el._dbxAjaxConfigs;
2159 if (el.__dbxInitialized && el.__dbxInitialized["ajax"]) {
2160 delete el.__dbxInitialized["ajax"];
2163 el.removeAttribute("data-dbx-ajax-root");
2165 dbx.log("[dbx.ajax] destroy", {
2172})(window, document);