dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
ajax.js
Go to the documentation of this file.
1/*!
2 * @file ajax.js
3 * =========================================================
4 * DBX AJAX SYSTEM (ajax.js)
5 * =========================================================
6 *
7 * Zweck
8 * -----
9 * ajax.js ist die universelle, scoped AJAX-Infrastruktur von DBX.
10 *
11 * Die Lib verarbeitet innerhalb ihres eigenen Root-Elements:
12 * - Form-Submits
13 * - Link-Klicks
14 * - Button-/Action-Klicks
15 *
16 * Standardverhalten
17 * -----------------
18 * Der DBX-Standard ist weiterhin:
19 *
20 * HTML laden → Target ersetzen
21 *
22 * Für UI-nahe Spezialfälle unterstützt ajax.js zusätzlich:
23 *
24 * JSON laden → Antwort an aufrufende Logik zurückgeben
25 *
26 *
27 * Architekturprinzip
28 * ------------------
29 * ajax.js ist bewusst NUR Transport-/Response-Infrastruktur.
30 * Die Lib enthält keine Fachlogik für Reports, Grid, Formflows usw.
31 *
32 * Zuständigkeiten:
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
37 *
38 *
39 * Scope-Regel (verbindlich)
40 * -------------------------
41 * ajax.js arbeitet immer mit:
42 *
43 * scope: "element"
44 *
45 * Das bedeutet:
46 * - eine ajax-Instanz gilt nur für ihr Root-Element
47 * - plus dessen Children
48 * - niemals global
49 *
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.
52 *
53 *
54 * =========================================================
55 * KONFIGURATION AM ROOT (data-dbx)
56 * =========================================================
57 *
58 * Beispiel:
59 *
60 * <div id="dbx_target_7"
61 * data-dbx="lib=ajax|class=dbxAjax|mode=html|url=form|target=dbx_target_7">
62 * ...
63 * </div>
64 *
65 *
66 * Unterstützte Parameter am AJAX-Root
67 * -----------------------------------
68 *
69 * lib=ajax
70 * Pflicht. Registriert die AJAX-Lib auf diesem Root.
71 *
72 * class=dbxAjax
73 * Match-Class. Nur Elemente mit dieser Klasse werden
74 * von dieser AJAX-Instanz verarbeitet.
75 *
76 * Beispiele:
77 * - class=dbxAjax
78 * - class=dbxUiAjax
79 * - class=* → alle passenden Elemente im Scope
80 *
81 * mode=html|json|text
82 * Default-Response-Typ dieser AJAX-Instanz.
83 *
84 * html:
85 * - Standard in DBX
86 * - Response wird als HTML interpretiert
87 * - Target wird ersetzt
88 *
89 * json:
90 * - Response wird als JSON interpretiert
91 * - standardmäßig KEIN automatischer Replace
92 * - die aufrufende Lib verarbeitet die Antwort selbst
93 *
94 * text:
95 * - Response wird als Text gelesen
96 * - kein automatischer Replace, außer explizit aktiviert
97 *
98 * bind=form,link,button
99 * Welche Elementtypen diese AJAX-Instanz behandeln darf.
100 *
101 * Erlaubt:
102 * - bind=form
103 * - bind=link
104 * - bind=button
105 * - bind=form,link,button
106 * - bind=* / bind=all
107 *
108 * Wenn nicht angegeben, ist Standard:
109 * - form,link,button
110 *
111 * url=form|lib|<volle URL>|&...
112 * Default-URL-Auflösung.
113 *
114 * Erlaubt:
115 *
116 * url=form
117 * → benutze die action der umgebenden Form
118 *
119 * url=lib
120 * → benutze die URL aus der AJAX-Lib selbst
121 * (nur sinnvoll, wenn dort wirklich eine echte URL definiert ist)
122 *
123 * url=?dbx_modul=...
124 * → vollständige feste URL
125 *
126 * url=&dbx_run3=delete
127 * → hänge Parameter an die Basis-URL an
128 *
129 * Basis-URL für "&..." ist:
130 * 1. Form-URL
131 * 2. sonst Lib-URL
132 *
133 * target=dbx_target_7
134 * Default-Target für Replace-Fälle.
135 *
136 * Wichtig:
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
140 *
141 * replace=target|inner
142 * Steuert, wie HTML in das Target übernommen wird.
143 *
144 * target:
145 * - Standard in DBX
146 * - das Target-Element wird durch die Response ersetzt
147 *
148 * inner:
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
152 *
153 * method=get|post|put|patch|delete
154 * Default-HTTP-Methode.
155 *
156 * Wenn nicht angegeben:
157 * - Links → GET
158 * - Form/Button → POST
159 *
160 * params=a=1&b=2
161 * Default-Zusatzparameter dieser AJAX-Instanz.
162 * Diese werden mit Form-/Element-Parametern zusammengeführt.
163 *
164 * jsonreplace=1
165 * Nur für mode=json relevant.
166 *
167 * Standard:
168 * - jsonreplace=0
169 *
170 * Wenn aktiviert:
171 * - ajax.js erwartet HTML innerhalb des JSON-Responses
172 * - dieser HTML-Teil wird in target ersetzt
173 *
174 * jsonkey=html
175 * Nur für mode=json + jsonreplace=1 relevant.
176 *
177 * Gibt an, unter welchem Key im JSON der HTML-String erwartet wird.
178 *
179 * Beispiel:
180 * Response:
181 * { "ok": 1, "html": "<div>...</div>" }
182 *
183 * Dann:
184 * jsonkey=html
185 *
186 * textreplace=1
187 * Nur für mode=text relevant.
188 * Wenn aktiv, wird der Text direkt in das Target ersetzt.
189 *
190 *
191 * =========================================================
192 * ERLAUBTE ELEMENTE IM SCOPE
193 * =========================================================
194 *
195 * ajax.js reagiert innerhalb des eigenen Roots auf:
196 *
197 * - <form>
198 * - <a>
199 * - <button>
200 * - <input type="button">
201 * - <input type="submit">
202 * - <input type="image">
203 *
204 * Voraussetzung:
205 * - Element liegt innerhalb des AJAX-Roots
206 * - Elementtyp passt zu bind
207 * - Element besitzt die passende class
208 *
209 *
210 * =========================================================
211 * VERERBUNG / OVERRIDE-PRINZIP
212 * =========================================================
213 *
214 * Ziel
215 * ----
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.
219 *
220 *
221 * 1) URL-Auflösung
222 * ----------------
223 * Reihenfolge:
224 *
225 * a) explizite Element-URL
226 * b) Form-URL
227 * c) Lib-URL
228 *
229 * Erlaubte URL-Varianten am Element/Form:
230 *
231 * - volle URL
232 * - url=form
233 * - url=lib
234 * - url=&...
235 *
236 *
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.
241 *
242 * Basis-URL ist:
243 * 1. Form-URL
244 * 2. sonst Lib-URL
245 *
246 * Beispiel:
247 * Form action:
248 * ?dbx_modul=test&dbx_run1=list
249 *
250 * Button:
251 * data-ajax-url="&dbx_run3=row_delete&rid=15"
252 *
253 * Ergebnis:
254 * ?dbx_modul=test&dbx_run1=list&dbx_run3=row_delete&rid=15
255 *
256 *
257 * 2) Target-Auflösung
258 * -------------------
259 * Reihenfolge:
260 *
261 * a) explizites Element-Target
262 * b) Form-Target
263 * c) Lib-Target
264 *
265 *
266 * 3) Mode-Auflösung
267 * -----------------
268 * Reihenfolge:
269 *
270 * a) expliziter Element-Mode
271 * b) Form-Mode
272 * c) Lib-Mode
273 *
274 *
275 * 4) Method-Auflösung
276 * -------------------
277 * Reihenfolge:
278 *
279 * a) explizite Element-Method
280 * b) Form-Method
281 * c) Lib-Method
282 * d) Default nach Elementtyp
283 *
284 *
285 * 5) Zusatzparameter
286 * ------------------
287 * Reihenfolge / Zusammenführung:
288 *
289 * a) params aus der Lib
290 * b) params aus der Form
291 * c) params aus dem Element
292 *
293 * Alle Parameter werden zusammengeführt.
294 * Spätere Ebenen dürfen frühere Werte überschreiben.
295 *
296 *
297 * =========================================================
298 * DATENQUELLEN FÜR OVERRIDES
299 * =========================================================
300 *
301 * Element/Form-spezifische Overrides können z. B. über:
302 *
303 * - data-ajax-url
304 * - data-ajax-target
305 * - data-ajax-replace
306 * - data-ajax-mode
307 * - data-ajax-method
308 * - data-ajax-params
309 * - data-ajax-jsonreplace
310 * - data-ajax-jsonkey
311 * - data-ajax-textreplace
312 *
313 * erfolgen.
314 *
315 * Zusätzlich werden weiterhin typische HTML-Quellen berücksichtigt:
316 *
317 * - form action
318 * - form method
319 * - button formaction
320 * - button formmethod
321 * - link href
322 *
323 * Historische Unterstützung:
324 * - data-dbx_form
325 * - data-dbx_target
326 * - .dbxAjaxFormAction
327 *
328 *
329 * =========================================================
330 * RESPONSE-VERHALTEN
331 * =========================================================
332 *
333 * mode=html
334 * ---------
335 * Standardfall in DBX.
336 *
337 * Ablauf:
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
342 *
343 *
344 * mode=json
345 * ---------
346 * Standardverhalten:
347 * - Response als JSON lesen
348 * - KEIN automatischer DOM-Replace
349 * - Ergebnis an aufrufende Logik zurückgeben
350 *
351 * Optional:
352 * - mit jsonreplace=1 kann HTML aus einem JSON-Key gelesen
353 * und in target ersetzt werden
354 *
355 *
356 * mode=text
357 * ---------
358 * Standardverhalten:
359 * - Response als Text lesen
360 * - kein automatischer Replace
361 *
362 * Optional:
363 * - mit textreplace=1 wird Text in target ersetzt
364 *
365 *
366 * =========================================================
367 * EVENTS
368 * =========================================================
369 *
370 * ajax.js sendet DBX-Events über dbx.event.emit():
371 *
372 * ajax:before
373 * Vor dem Request
374 *
375 * ajax:after
376 * Nach erfolgreichem Request / ggf. nach Replace
377 *
378 * ajax:error
379 * Bei Request- oder Parse-Fehlern
380 *
381 * Typische Daten:
382 * - id
383 * - root
384 * - source
385 * - form
386 * - type
387 * - mode
388 * - method
389 * - url
390 * - target
391 * - params
392 * - response / error
393 *
394 *
395 * =========================================================
396 * LOCK / DOUBLE SUBMIT PROTECTION
397 * =========================================================
398 *
399 * ajax.js verhindert parallele doppelte Ausführungen auf derselben Quelle
400 * (Form oder auslösendes Element), solange ein Request läuft.
401 *
402 * Zusätzlich erhält das aktive Target während des Requests:
403 *
404 * class="dbx-ajax-loading"
405 *
406 *
407 * =========================================================
408 * VERWENDUNGSBEISPIELE
409 * =========================================================
410 *
411 * 1) Klassischer Report mit HTML-Replace
412 * --------------------------------------
413 *
414 * <div id="dbx_target_7"
415 * data-dbx="lib=ajax|class=dbxAjax|mode=html|url=form|target=dbx_target_7">
416 *
417 * <form action="?dbx_modul=test&dbx_run1=list"
418 * class="dbxAjax">
419 * ...
420 * </form>
421 * </div>
422 *
423 * Verhalten:
424 * - Submit lädt HTML
425 * - dbx_target_7 wird ersetzt
426 *
427 *
428 * 2) Pagination-Link im selben Report
429 * -----------------------------------
430 *
431 * <a href="?dbx_modul=test&dbx_run1=list&dbx_rpos=20"
432 * class="dbxAjax">Weiter</a>
433 *
434 * Verhalten:
435 * - Link läuft über dieselbe ajax-Instanz
436 * - Target wird ersetzt
437 *
438 *
439 * 3) Button ergänzt nur Parameter an Form-URL
440 * -------------------------------------------
441 *
442 * <button class="dbxAjax"
443 * data-ajax-url="&dbx_run3=row_delete&rid=15">
444 * Löschen
445 * </button>
446 *
447 * Verhalten:
448 * - Basis-URL kommt von der Form
449 * - Parameter werden angehängt
450 *
451 *
452 * 4) UI-AJAX mit JSON
453 * -------------------
454 *
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">
458 * Select
459 * </button>
460 * </div>
461 *
462 * Verhalten:
463 * - Response wird als JSON verarbeitet
464 * - kein automatischer Replace
465 *
466 *
467 * 5) JSON mit explizitem Replace
468 * ------------------------------
469 *
470 * <div id="panel"
471 * data-dbx="lib=ajax|class=dbxUiAjax|mode=json|target=panel|jsonreplace=1|jsonkey=html">
472 * ...
473 * </div>
474 *
475 * Erwartete JSON-Antwort:
476 * {
477 * "ok": 1,
478 * "html": "<div>Neuer Inhalt</div>"
479 * }
480 *
481 *
482 * =========================================================
483 * WICHTIGE REGELN
484 * =========================================================
485 *
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
494 *
495 * =========================================================
496 */
497
498
499
500
501(function (window, document) {
502 "use strict";
503
504 if (!window.dbx || !window.dbx.feature) {
505 console.error("[dbx][ajax] dbx core missing");
506 return;
507 }
508
509 const dbx = window.dbx;
510
511 dbx.ajax = dbx.ajax || {};
512
513
514 /* =========================================================
515 * HELPERS
516 * ========================================================= */
517
518 function bool(v, def = false) {
519
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;
523
524 return def;
525 }
526
527
528 function normalizeMode(v) {
529
530 const mode = String(v || "html").toLowerCase().trim();
531
532 if (mode === "json") return "json";
533 if (mode === "text") return "text";
534
535 return "html";
536 }
537
538
539 function normalizeReplaceMode(v) {
540
541 const mode = String(v || "target").toLowerCase().trim();
542
543 if (mode === "inner" || mode === "innerhtml" || mode === "html" || mode === "content") {
544 return "inner";
545 }
546
547 return "target";
548 }
549
550
551 function normalizeMethod(v, def = "POST") {
552
553 const method = String(v || def || "POST").toUpperCase().trim();
554
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";
560
561 return String(def || "POST").toUpperCase();
562 }
563
564
565 function isPlainObject(value) {
566 return value && Object.prototype.toString.call(value) === "[object Object]";
567 }
568
569
570 function appendDataToUrl(url, data) {
571 if (!data || !isPlainObject(data)) return url;
572
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));
579 } else {
580 out.searchParams.set(key, value);
581 }
582 });
583 return out.toString();
584 }
585
586
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;
594
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));
601 } else {
602 body.set(key, value);
603 }
604 });
605 return body;
606 }
607
608
609 function normalizeBind(v) {
610
611 if (v === undefined || v === null || v === "") {
612 return ["form", "link", "button"];
613 }
614
615 const raw = String(v).toLowerCase().trim();
616
617 if (raw === "*" || raw === "all") {
618 return ["form", "link", "button"];
619 }
620
621 return raw
622 .split(",")
623 .map(s => s.trim())
624 .filter(Boolean);
625 }
626
627
628 function normalizeClassFilter(v) {
629
630 if (v === undefined || v === null || v === "") {
631 return ["dbxAjax"];
632 }
633
634 const raw = String(v).trim();
635
636 if (raw === "*") {
637 return ["*"];
638 }
639
640 return raw
641 .split(",")
642 .map(s => s.trim())
643 .filter(Boolean);
644 }
645
646
647 function elementType(el) {
648
649 if (!el || !el.tagName) return "";
650
651 const tag = el.tagName.toLowerCase();
652
653 if (tag === "form") return "form";
654 if (tag === "a") return "link";
655
656 if (tag === "button") return "button";
657
658 if (tag === "input") {
659 const type = String(el.getAttribute("type") || "text").toLowerCase();
660 if (type === "button" || type === "submit" || type === "image") {
661 return "button";
662 }
663 }
664
665 return "";
666 }
667
668
669 function bindMatches(type, bindList) {
670
671 if (!type) return false;
672 if (!Array.isArray(bindList) || !bindList.length) return false;
673
674 return bindList.includes(type);
675 }
676
677
678 function classMatches(el, classList) {
679
680 if (!el) return false;
681
682 if (!Array.isArray(classList) || !classList.length) {
683 return false;
684 }
685
686 if (classList.includes("*")) {
687 return true;
688 }
689
690 for (let i = 0; i < classList.length; i++) {
691 if (el.classList.contains(classList[i])) {
692 return true;
693 }
694 }
695
696 return false;
697 }
698
699
700 function readAttr(el, name) {
701
702 if (!el || !el.getAttribute) return "";
703
704 const v = el.getAttribute(name);
705 return v == null ? "" : String(v).trim();
706 }
707
708
709 function readDataAjax(el, key) {
710
711 if (!el) return "";
712
713 return readAttr(el, "data-ajax-" + key);
714 }
715
716
717 function readLegacyData(el, key) {
718
719 if (!el) return "";
720
721 return readAttr(el, "data-" + key);
722 }
723
724
725 function parseParamString(str) {
726
727 const out = [];
728
729 if (str === undefined || str === null) {
730 return out;
731 }
732
733 let raw = String(str).trim();
734
735 if (!raw) {
736 return out;
737 }
738
739 raw = raw.replace(/^[?&]+/, "");
740
741 if (!raw) {
742 return out;
743 }
744
745 const usp = new URLSearchParams(raw);
746
747 usp.forEach((value, key) => {
748 out.push([key, value]);
749 });
750
751 return out;
752 }
753
754
755 function appendToUrl(baseUrl, extra) {
756
757 let base = String(baseUrl || "").trim();
758 let add = String(extra || "").trim();
759
760 if (!add) return base;
761 if (!base) return "";
762
763 add = add.replace(/^[?&]+/, "");
764
765 if (!add) return base;
766
767 const hashPos = base.indexOf("#");
768 let hash = "";
769
770 if (hashPos !== -1) {
771 hash = base.substring(hashPos);
772 base = base.substring(0, hashPos);
773 }
774
775 const sep = base.includes("?") ? "&" : "?";
776
777 return base + sep + add + hash;
778 }
779
780
781 function ensureAjaxFlag(url) {
782
783 let finalUrl = String(url || "").trim();
784
785 if (!finalUrl) return finalUrl;
786
787 if (finalUrl.indexOf("dbx_ajax=") === -1) {
788 finalUrl = appendToUrl(finalUrl, "dbx_ajax=1");
789 }
790
791 return finalUrl;
792 }
793
794 // Zentrale Stelle fuer das AJAX-Kennzeichen. Andere Module liefern
795 // grundsaetzlich normale, auch direkt aufrufbare URLs.
796 dbx.ajax.url = ensureAjaxFlag;
797
798 function isPrivateNetworkHost(hostname) {
799 const host = String(hostname || "").trim().toLowerCase().replace(/^\‍[|\‍]$/g, "");
800
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;
805
806 const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
807 if (!m) return false;
808
809 const a = Number(m[1]);
810 const b = Number(m[2]);
811
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;
816
817 return false;
818 }
819
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);
825 }
826
827
828 function normalizeRequestUrl(url) {
829
830 const raw = String(url || "").trim();
831
832 if (!raw) return raw;
833
834 try {
835 const parsed = new URL(raw, window.location.href);
836
837 if (shouldBlockLocalNetworkUrl(parsed)) {
838 throw new Error("blocked_local_network_request");
839 }
840
841 if (
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
846 ) {
847 parsed.protocol = window.location.protocol;
848 }
849
850 if (parsed.origin === window.location.origin) {
851 return parsed.pathname + parsed.search + parsed.hash;
852 }
853
854 return parsed.toString();
855 } catch (e) {
856 if (e && e.message === "blocked_local_network_request") {
857 throw e;
858 }
859 return raw;
860 }
861 }
862
863
864 function resolveElementByTarget(target) {
865
866 const raw = String(target || "").trim();
867
868 if (!raw) return null;
869
870 let el = document.getElementById(raw);
871 if (el) return el;
872
873 try {
874 el = document.querySelector(raw);
875 if (el) return el;
876 } catch (e) {
877 dbx.warn("[dbx.ajax] invalid target selector:", raw);
878 }
879
880 return null;
881 }
882
883
884 function getClosestForm(source, root) {
885
886 if (!source) return null;
887
888 if (source.tagName && source.tagName.toLowerCase() === "form") {
889 return source;
890 }
891
892 const dataFormId = readLegacyData(source, "dbx_form");
893 if (dataFormId) {
894 const formByDataId = document.getElementById(dataFormId);
895 if (formByDataId) {
896 return formByDataId;
897 }
898 }
899
900 const formAttr = readAttr(source, "form");
901 if (formAttr) {
902 const formByAttr = document.getElementById(formAttr);
903 if (formByAttr) {
904 return formByAttr;
905 }
906 }
907
908 const form = source.closest("form");
909
910 if (!form) {
911 return null;
912 }
913
914 if (root && !root.contains(form)) {
915 return null;
916 }
917
918 return form;
919 }
920
921
922 function getRootConfigs(root) {
923
924 if (!root) return [];
925
926 if (Array.isArray(root._dbxAjaxConfigs)) {
927 return root._dbxAjaxConfigs;
928 }
929
930 let out = [];
931
932 if (dbx.declare && dbx.declare.schemas && dbx.declare.schemas.ajax) {
933 out = dbx.declare.resolve("ajax", root);
934 } else {
935 const attr = readAttr(root, "data-dbx");
936 const list = dbx.parseData(attr).filter(cfg => cfg.lib === "ajax");
937
938 out = list.map((cfg, index) => {
939 return {
940 _index: index,
941 lib: "ajax",
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)
953 };
954 });
955
956 if (!out.length) {
957 out.push({
958 _index: 0,
959 lib: "ajax",
960 class: normalizeClassFilter("dbxAjax"),
961 mode: "html",
962 url: "form",
963 target: "",
964 replace: "target",
965 bind: ["form", "link", "button"],
966 method: "",
967 params: "",
968 jsonreplace: false,
969 jsonkey: "html",
970 textreplace: false
971 });
972 }
973 }
974
975 root._dbxAjaxConfigs = out;
976
977 return out;
978 }
979
980
981 function findMatchingConfig(root, source, type, overrides) {
982
983 if (overrides && overrides.config) {
984 return overrides.config;
985 }
986
987 const configs = getRootConfigs(root);
988
989 for (let i = 0; i < configs.length; i++) {
990
991 const cfg = configs[i];
992
993 if (!bindMatches(type, cfg.bind)) {
994 continue;
995 }
996
997 if (!classMatches(source, cfg.class)) {
998 continue;
999 }
1000
1001 return cfg;
1002 }
1003
1004 return null;
1005 }
1006
1007
1008 function getFormDefaultUrl(form) {
1009
1010 if (!form) return "";
1011
1012 const action = readAttr(form, "action");
1013
1014 if (action) {
1015 return action;
1016 }
1017
1018 return window.location.href;
1019 }
1020
1021
1022 function getLibDefaultUrl(cfg, form) {
1023
1024 if (!cfg) return "";
1025
1026 const url = String(cfg.url || "").trim();
1027
1028 if (!url) return "";
1029
1030 if (url === "form") {
1031 return getFormDefaultUrl(form);
1032 }
1033
1034 if (url === "lib") {
1035 return "";
1036 }
1037
1038 if (url.charAt(0) === "&") {
1039 const base = getFormDefaultUrl(form);
1040 return base ? appendToUrl(base, url) : "";
1041 }
1042
1043 return url;
1044 }
1045
1046
1047 function resolveUrl(root, source, form, cfg, overrides) {
1048
1049 const libUrl = getLibDefaultUrl(cfg, form);
1050
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") : "");
1057
1058 const formBase = formUrlOverride
1059 ? resolveUrlToken(formUrlOverride, getFormDefaultUrl(form), libUrl)
1060 : getFormDefaultUrl(form);
1061
1062 if (sourceUrlOverride) {
1063 return resolveUrlToken(sourceUrlOverride, formBase, libUrl);
1064 }
1065
1066 if (elementType(source) === "form") {
1067 return formBase || libUrl;
1068 }
1069
1070 if (form) {
1071 return formBase || libUrl;
1072 }
1073
1074 return libUrl;
1075 }
1076
1077
1078 function resolveUrlToken(token, formUrl, libUrl) {
1079
1080 const raw = String(token || "").trim();
1081
1082 if (!raw) {
1083 return "";
1084 }
1085
1086 if (raw === "form") {
1087 return formUrl || libUrl || "";
1088 }
1089
1090 if (raw === "lib") {
1091 return libUrl || formUrl || "";
1092 }
1093
1094 if (raw.charAt(0) === "&") {
1095 const base = formUrl || libUrl || "";
1096 if (!base) {
1097 return "";
1098 }
1099 return appendToUrl(base, raw);
1100 }
1101
1102 return raw;
1103 }
1104
1105
1106 function resolveTarget(root, source, form, cfg, overrides) {
1107
1108 const sourceTarget =
1109 (overrides && overrides.target) ||
1110 readDataAjax(source, "target") ||
1111 readLegacyData(source, "dbx_target");
1112
1113 if (sourceTarget) {
1114 return sourceTarget;
1115 }
1116
1117 const formTarget = readDataAjax(form, "target");
1118 if (formTarget) {
1119 return formTarget;
1120 }
1121
1122 if (cfg && cfg.target) {
1123 return cfg.target;
1124 }
1125
1126 return "";
1127 }
1128
1129
1130 function resolveReplaceMode(source, form, cfg, overrides) {
1131
1132 const sourceReplace =
1133 (overrides && overrides.replace) ||
1134 readDataAjax(source, "replace");
1135
1136 if (sourceReplace) {
1137 return normalizeReplaceMode(sourceReplace);
1138 }
1139
1140 const formReplace = readDataAjax(form, "replace");
1141 if (formReplace) {
1142 return normalizeReplaceMode(formReplace);
1143 }
1144
1145 return normalizeReplaceMode((cfg && cfg.replace) || "target");
1146 }
1147
1148
1149 function resolveMode(source, form, cfg, overrides) {
1150
1151 const sourceMode =
1152 (overrides && overrides.mode) ||
1153 readDataAjax(source, "mode");
1154
1155 if (sourceMode) {
1156 return normalizeMode(sourceMode);
1157 }
1158
1159 const formMode = readDataAjax(form, "mode");
1160 if (formMode) {
1161 return normalizeMode(formMode);
1162 }
1163
1164 return normalizeMode((cfg && cfg.mode) || "html");
1165 }
1166
1167
1168 function resolveMethod(source, form, type, cfg, overrides) {
1169
1170 const sourceMethod =
1171 (overrides && overrides.method) ||
1172 readDataAjax(source, "method") ||
1173 readAttr(source, "formmethod");
1174
1175 if (sourceMethod) {
1176 return normalizeMethod(sourceMethod, type === "link" ? "GET" : "POST");
1177 }
1178
1179 const formMethod =
1180 readDataAjax(form, "method") ||
1181 readAttr(form, "method");
1182
1183 if (formMethod) {
1184 return normalizeMethod(formMethod, type === "link" ? "GET" : "POST");
1185 }
1186
1187 if (cfg && cfg.method) {
1188 return normalizeMethod(cfg.method, type === "link" ? "GET" : "POST");
1189 }
1190
1191 return normalizeMethod(type === "link" ? "GET" : "POST");
1192 }
1193
1194
1195 function resolveJsonReplace(source, form, cfg, overrides) {
1196
1197 if (overrides && typeof overrides.jsonreplace !== "undefined") {
1198 return bool(overrides.jsonreplace, false);
1199 }
1200
1201 const sourceVal = readDataAjax(source, "jsonreplace");
1202 if (sourceVal !== "") {
1203 return bool(sourceVal, false);
1204 }
1205
1206 const formVal = readDataAjax(form, "jsonreplace");
1207 if (formVal !== "") {
1208 return bool(formVal, false);
1209 }
1210
1211 return bool(cfg && cfg.jsonreplace, false);
1212 }
1213
1214
1215 function resolveJsonKey(source, form, cfg, overrides) {
1216
1217 if (overrides && overrides.jsonkey) {
1218 return String(overrides.jsonkey).trim();
1219 }
1220
1221 const sourceVal = readDataAjax(source, "jsonkey");
1222 if (sourceVal) {
1223 return String(sourceVal).trim();
1224 }
1225
1226 const formVal = readDataAjax(form, "jsonkey");
1227 if (formVal) {
1228 return String(formVal).trim();
1229 }
1230
1231 return String((cfg && cfg.jsonkey) || "html").trim();
1232 }
1233
1234
1235 function resolveTextReplace(source, form, cfg, overrides) {
1236
1237 if (overrides && typeof overrides.textreplace !== "undefined") {
1238 return bool(overrides.textreplace, false);
1239 }
1240
1241 const sourceVal = readDataAjax(source, "textreplace");
1242 if (sourceVal !== "") {
1243 return bool(sourceVal, false);
1244 }
1245
1246 const formVal = readDataAjax(form, "textreplace");
1247 if (formVal !== "") {
1248 return bool(formVal, false);
1249 }
1250
1251 return bool(cfg && cfg.textreplace, false);
1252 }
1253
1254
1255 function collectExtraParams(source, form, cfg, overrides) {
1256
1257 const out = [];
1258
1259 if (cfg && cfg.params) {
1260 out.push(...parseParamString(cfg.params));
1261 }
1262
1263 const formParams = readDataAjax(form, "params");
1264 if (formParams) {
1265 out.push(...parseParamString(formParams));
1266 }
1267
1268 const sourceParams =
1269 (overrides && overrides.params) ||
1270 readDataAjax(source, "params");
1271
1272 if (sourceParams) {
1273 out.push(...parseParamString(sourceParams));
1274 }
1275
1276 return out;
1277 }
1278
1279
1280 function applyParamsToFormData(fd, params) {
1281
1282 if (!fd || !params || !params.length) return;
1283
1284 params.forEach(([key, value]) => {
1285 try {
1286 fd.set(key, value);
1287 } catch (e) {
1288 fd.append(key, value);
1289 }
1290 });
1291 }
1292
1293
1294 function applyParamsToUrl(url, params) {
1295
1296 let finalUrl = String(url || "").trim();
1297
1298 if (!finalUrl) {
1299 return finalUrl;
1300 }
1301
1302 if (!params || !params.length) {
1303 return finalUrl;
1304 }
1305
1306 const hashPos = finalUrl.indexOf("#");
1307 let hash = "";
1308
1309 if (hashPos !== -1) {
1310 hash = finalUrl.substring(hashPos);
1311 finalUrl = finalUrl.substring(0, hashPos);
1312 }
1313
1314 let qs = "";
1315
1316 params.forEach(([key, value], idx) => {
1317 const pair = encodeURIComponent(key) + "=" + encodeURIComponent(value);
1318 qs += (idx === 0 ? "" : "&") + pair;
1319 });
1320
1321 if (qs) {
1322 finalUrl = appendToUrl(finalUrl, qs);
1323 }
1324
1325 return finalUrl + hash;
1326 }
1327
1328
1329 function buildBodyAndUrl(ctx) {
1330
1331 let url = String(ctx.url || "").trim();
1332 let body = null;
1333
1334 const params = Array.isArray(ctx.params) ? ctx.params.slice() : [];
1335
1336 if (ctx.method === "GET") {
1337
1338 if (ctx.form) {
1339 const fd = new FormData(ctx.form);
1340 fd.forEach((value, key) => {
1341 params.push([key, value]);
1342 });
1343 }
1344
1345 if (ctx.submitName) {
1346 params.push([ctx.submitName, ctx.submitValue]);
1347 }
1348
1349 url = applyParamsToUrl(url, params);
1350
1351 return {
1352 url: url,
1353 body: null
1354 };
1355 }
1356
1357 if (ctx.form) {
1358 body = new FormData(ctx.form);
1359 } else {
1360 body = new FormData();
1361 }
1362
1363 if (ctx.submitName) {
1364 body.set(ctx.submitName, ctx.submitValue);
1365 }
1366
1367 applyParamsToFormData(body, params);
1368
1369 return {
1370 url: url,
1371 body: body
1372 };
1373 }
1374
1375
1376 function emit(name, data) {
1377
1378 if (dbx.event && typeof dbx.event.emit === "function") {
1379 dbx.event.emit(name, data);
1380 }
1381 }
1382
1383
1384 function executeScripts(scripts) {
1385
1386 if (!scripts || !scripts.length) {
1387 return Promise.resolve();
1388 }
1389
1390 let chain = Promise.resolve();
1391
1392 scripts.forEach(srcNode => {
1393
1394 chain = chain.then(() => {
1395 return new Promise((resolve) => {
1396
1397 const script = document.createElement("script");
1398
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);
1403 }
1404
1405 if (srcNode.src) {
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);
1411 resolve();
1412 };
1413 document.body.appendChild(script);
1414 } else {
1415 script.text = srcNode.textContent || "";
1416 document.body.appendChild(script);
1417 resolve();
1418 }
1419 });
1420 });
1421 });
1422
1423 return chain;
1424 }
1425
1426
1427 function replaceTarget(targetEl, html, replaceMode = "target") {
1428
1429 if (!targetEl) {
1430 return Promise.resolve(targetEl);
1431 }
1432
1433 if (html === undefined || html === null) {
1434 return Promise.resolve(targetEl);
1435 }
1436
1437 const markup = String(html).trim();
1438
1439 if (!markup) {
1440 return Promise.resolve(targetEl);
1441 }
1442
1443 const temp = document.createElement("div");
1444 temp.innerHTML = markup;
1445
1446 const scripts = Array.from(temp.querySelectorAll("script"));
1447 scripts.forEach(s => s.remove());
1448
1449 let newElement = targetEl;
1450 const mode = normalizeReplaceMode(replaceMode);
1451
1452 if (mode === "inner") {
1453
1454 targetEl.innerHTML = temp.innerHTML;
1455 newElement = targetEl;
1456
1457 } else if (temp.children.length === 1) {
1458
1459 const candidate = temp.firstElementChild;
1460
1461 if (candidate && targetEl.parentNode) {
1462 targetEl.parentNode.replaceChild(candidate, targetEl);
1463 newElement = candidate;
1464 } else {
1465 targetEl.innerHTML = temp.innerHTML;
1466 newElement = targetEl;
1467 }
1468
1469 } else {
1470
1471 targetEl.innerHTML = temp.innerHTML;
1472 newElement = targetEl;
1473 }
1474
1475 return executeScripts(scripts).then(() => {
1476
1477 if (newElement && typeof dbx.scan === "function") {
1478 dbx.scan(newElement);
1479 }
1480
1481 return newElement;
1482 });
1483 }
1484
1485
1486 function requestNow() {
1487 return window.performance && typeof window.performance.now === "function"
1488 ? window.performance.now()
1489 : Date.now();
1490 }
1491
1492
1493 function requestElapsedSeconds(startedAt) {
1494 return Math.max(0, (requestNow() - startedAt) / 1000);
1495 }
1496
1497
1498 function parseRuntimeSeconds(value) {
1499 const number = Number(value);
1500 return Number.isFinite(number) && number >= 0 ? number : null;
1501 }
1502
1503
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) {
1508 return headerValue;
1509 }
1510 }
1511
1512 if (data && typeof data === "object") {
1513 const directValue = parseRuntimeSeconds(data.dbx_php_runtime || data.php_runtime);
1514 if (directValue !== null) {
1515 return directValue;
1516 }
1517
1518 if (data._dbx_runtime && typeof data._dbx_runtime === "object") {
1519 const nestedValue = parseRuntimeSeconds(data._dbx_runtime.php);
1520 if (nestedValue !== null) {
1521 return nestedValue;
1522 }
1523 }
1524 }
1525
1526 return null;
1527 }
1528
1529
1530 function shouldTrackRuntime(options, url, body) {
1531 if (dbx.footerStatus && typeof dbx.footerStatus.shouldTrackAjaxRuntime === "function") {
1532 return dbx.footerStatus.shouldTrackAjaxRuntime(url, body, options);
1533 }
1534
1535 if (options && (
1536 options.skipRuntime === true ||
1537 options.footerRuntime === 'hidden' ||
1538 options.footerRuntime === 'skip' ||
1539 options.footerRuntime === '0'
1540 )) {
1541 return false;
1542 }
1543
1544 if (options && String(options.mode || "").toLowerCase() === "json") {
1545 return false;
1546 }
1547
1548 const targetUrl = String(url || '');
1549 if (/[?&]dbx_sync=0(?:&|$)/.test(targetUrl)) {
1550 return false;
1551 }
1552
1553 if (body instanceof URLSearchParams && body.get('dbx_sync') === '0') {
1554 return false;
1555 }
1556
1557 if (typeof body === 'string' && /(?:^|&)dbx_sync=0(?:&|$)/.test(body)) {
1558 return false;
1559 }
1560
1561 return true;
1562 }
1563
1564
1565 function updateFooterRuntime(startedAt, response, data) {
1566 if (!dbx.footerStatus || typeof dbx.footerStatus.updateAjax !== "function") {
1567 return;
1568 }
1569
1570 dbx.footerStatus.updateAjax(
1571 requestElapsedSeconds(startedAt),
1572 phpRuntimeFromResponse(response, data)
1573 );
1574 }
1575
1576
1577 /* =========================================================
1578 * PUBLIC CORE REQUEST
1579 * ========================================================= */
1580
1581 dbx.ajax.request = function (options) {
1582
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;
1592 let timer = null;
1593 let runtimeUpdated = false;
1594 const startedAt = requestNow();
1595
1596 if (!url) {
1597 return Promise.reject(new Error("missing_url"));
1598 }
1599
1600 if ((method === "GET" || method === "HEAD") && data) {
1601 url = appendDataToUrl(url, data);
1602 } else if (body === null && data) {
1603 body = bodyFromData(data);
1604 }
1605
1606 url = ensureAjaxFlag(url);
1607
1608 try {
1609 url = normalizeRequestUrl(url);
1610 } catch (err) {
1611 dbx.warn("[dbx.ajax] local network request blocked", url);
1612 return Promise.reject(err);
1613 }
1614
1615 const trackRuntime = shouldTrackRuntime(options, url, body);
1616
1617 function markRuntime(response, responseData) {
1618 if (!trackRuntime) return;
1619 runtimeUpdated = true;
1620 updateFooterRuntime(startedAt, response, responseData);
1621 }
1622
1623 dbx.log("[dbx.ajax] request start", {
1624 method: method,
1625 mode: mode,
1626 url: url
1627 });
1628
1629 if (controller) {
1630 timer = window.setTimeout(() => controller.abort(), timeout);
1631 }
1632
1633 return fetch(url, {
1634 method: method,
1635 body: body,
1636 headers: headers,
1637 credentials: "same-origin",
1638 keepalive: keepalive,
1639 signal: controller ? controller.signal : undefined
1640 })
1641 .then(response => {
1642
1643 if (!response.ok) {
1644 markRuntime(response, null);
1645 throw new Error("HTTP " + response.status);
1646 }
1647
1648 if (mode === "json") {
1649 return response.text().then(txt => {
1650 if (!txt) {
1651 const emptyData = {};
1652 markRuntime(response, emptyData);
1653 return emptyData;
1654 }
1655
1656 try {
1657 const data = JSON.parse(txt);
1658 markRuntime(response, data);
1659 return data;
1660 } catch (err) {
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, " ")
1666 .trim();
1667 markRuntime(response, null);
1668 throw new Error(clean || "Ungueltige Serverantwort");
1669 }
1670 });
1671 }
1672
1673 return response.text().then(txt => {
1674 markRuntime(response, txt);
1675 return txt;
1676 });
1677 })
1678 .catch(error => {
1679 if (!runtimeUpdated && trackRuntime) {
1680 updateFooterRuntime(startedAt, null, null);
1681 }
1682
1683 throw error;
1684 })
1685 .finally(() => {
1686 if (timer) window.clearTimeout(timer);
1687 });
1688 };
1689
1690
1691 /* =========================================================
1692 * INTERNAL EXECUTE
1693 * ========================================================= */
1694
1695 function executeAjax(root, source, overrides = {}, event) {
1696
1697 if (!root) {
1698 dbx.warn("[dbx.ajax] missing root");
1699 return Promise.resolve(null);
1700 }
1701
1702 const type = elementType(source);
1703
1704 if (!type) {
1705 dbx.warn("[dbx.ajax] unsupported source:", source);
1706 return Promise.resolve(null);
1707 }
1708
1709 const cfg = findMatchingConfig(root, source, type, overrides);
1710
1711 if (!cfg && !overrides.url) {
1712 dbx.log("[dbx.ajax] no matching config", {
1713 type: type,
1714 source: source
1715 });
1716 return Promise.resolve(null);
1717 }
1718
1719 const form = (overrides && overrides.form) || getClosestForm(source, root);
1720
1721 /*
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.
1725 */
1726 const eventSubmitter = event && event.submitter ? event.submitter : null;
1727 const overrideSubmitter = overrides && overrides.submitSource
1728 ? overrides.submitSource
1729 : null;
1730 const submitSource = overrideSubmitter || eventSubmitter || ((type === "button") ? source : null);
1731 const submitName = String(
1732 (overrides && overrides.submitName) ||
1733 readAttr(submitSource, "name") ||
1734 ""
1735 );
1736 const submitValueOverride = overrides && Object.prototype.hasOwnProperty.call(overrides, "submitValue")
1737 ? overrides.submitValue
1738 : null;
1739 const submitValue = submitValueOverride !== null
1740 ? String(submitValueOverride)
1741 : String(readAttr(submitSource, "value") || "");
1742
1743 const ctx = {
1744 root: root,
1745 source: source,
1746 type: type,
1747 form: form,
1748 cfg: cfg || {},
1749 submitSource: submitSource,
1750 submitName: submitName,
1751 submitValue: submitValue
1752 };
1753
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);
1763
1764 if (event && typeof event.preventDefault === "function") {
1765 event.preventDefault();
1766 }
1767
1768 if (!ctx.url) {
1769 dbx.warn("[dbx.ajax] missing resolved url", {
1770 type: ctx.type,
1771 source: ctx.source
1772 });
1773 return Promise.resolve(null);
1774 }
1775
1776 const lockEl = ctx.form || ctx.source;
1777
1778 if (lockEl && lockEl._dbxAjaxRunning === true) {
1779 dbx.log("[dbx.ajax] request skipped (already running)");
1780 return Promise.resolve(null);
1781 }
1782
1783 if (lockEl) {
1784 lockEl._dbxAjaxRunning = true;
1785 }
1786
1787 const targetEl = resolveElementByTarget(ctx.target);
1788
1789 if (ctx.mode === "html" && !targetEl) {
1790 dbx.warn("[dbx.ajax] target not found for html mode:", ctx.target);
1791 if (lockEl) {
1792 lockEl._dbxAjaxRunning = false;
1793 }
1794 return Promise.resolve(null);
1795 }
1796
1797 let loadingTimer = null;
1798 if (targetEl) {
1799 targetEl.classList.add("dbx-ajax-loading");
1800 loadingTimer = window.setTimeout(() => {
1801 targetEl.classList.remove("dbx-ajax-loading", "is-loading");
1802 }, 20000);
1803 }
1804
1805 const built = buildBodyAndUrl(ctx);
1806
1807 ctx.url = built.url;
1808 ctx.body = built.body;
1809
1810 const eventData = {
1811 id: root.id || "",
1812 root: root,
1813 source: source,
1814 form: form,
1815 type: ctx.type,
1816 mode: ctx.mode,
1817 method: ctx.method,
1818 url: ctx.url,
1819 target: ctx.target,
1820 replace: ctx.replace,
1821 params: ctx.params
1822 };
1823
1824 emit("ajax:before", eventData);
1825
1826 return dbx.ajax.request({
1827 url: ctx.url,
1828 method: ctx.method,
1829 mode: ctx.mode,
1830 body: ctx.body
1831 })
1832 .then(responseData => {
1833
1834 if (ctx.mode === "html") {
1835
1836 return replaceTarget(targetEl, responseData, ctx.replace).then(newTarget => {
1837
1838 emit("ajax:after", {
1839 ...eventData,
1840 response: responseData,
1841 targetElement: newTarget
1842 });
1843
1844 return {
1845 ok: true,
1846 mode: ctx.mode,
1847 response: responseData,
1848 targetElement: newTarget
1849 };
1850 });
1851 }
1852
1853 if (ctx.mode === "json") {
1854
1855 if (ctx.jsonreplace === true && targetEl) {
1856
1857 let html = "";
1858
1859 if (responseData && typeof responseData === "object") {
1860 html = responseData[ctx.jsonkey] || "";
1861 }
1862
1863 if (typeof html === "string" && html !== "") {
1864 return replaceTarget(targetEl, html, ctx.replace).then(newTarget => {
1865
1866 emit("ajax:after", {
1867 ...eventData,
1868 response: responseData,
1869 targetElement: newTarget
1870 });
1871
1872 return {
1873 ok: true,
1874 mode: ctx.mode,
1875 response: responseData,
1876 targetElement: newTarget
1877 };
1878 });
1879 }
1880 }
1881
1882 emit("ajax:after", {
1883 ...eventData,
1884 response: responseData,
1885 targetElement: targetEl
1886 });
1887
1888 return {
1889 ok: true,
1890 mode: ctx.mode,
1891 response: responseData,
1892 targetElement: targetEl
1893 };
1894 }
1895
1896 if (ctx.mode === "text") {
1897
1898 if (ctx.textreplace === true && targetEl) {
1899 return replaceTarget(targetEl, responseData, ctx.replace).then(newTarget => {
1900
1901 emit("ajax:after", {
1902 ...eventData,
1903 response: responseData,
1904 targetElement: newTarget
1905 });
1906
1907 return {
1908 ok: true,
1909 mode: ctx.mode,
1910 response: responseData,
1911 targetElement: newTarget
1912 };
1913 });
1914 }
1915
1916 emit("ajax:after", {
1917 ...eventData,
1918 response: responseData,
1919 targetElement: targetEl
1920 });
1921
1922 return {
1923 ok: true,
1924 mode: ctx.mode,
1925 response: responseData,
1926 targetElement: targetEl
1927 };
1928 }
1929
1930 emit("ajax:after", {
1931 ...eventData,
1932 response: responseData,
1933 targetElement: targetEl
1934 });
1935
1936 return {
1937 ok: true,
1938 mode: ctx.mode,
1939 response: responseData,
1940 targetElement: targetEl
1941 };
1942 })
1943 .catch(error => {
1944
1945 dbx.error("[dbx.ajax] error:", error);
1946
1947 emit("ajax:error", {
1948 ...eventData,
1949 error: error
1950 });
1951
1952 throw error;
1953 })
1954 .finally(() => {
1955
1956 if (lockEl) {
1957 lockEl._dbxAjaxRunning = false;
1958 }
1959
1960 if (loadingTimer) {
1961 window.clearTimeout(loadingTimer);
1962 }
1963
1964 if (targetEl) {
1965 targetEl.classList.remove("dbx-ajax-loading", "is-loading");
1966 }
1967
1968 const finalTargetEl = resolveElementByTarget(ctx.target);
1969 if (finalTargetEl) {
1970 finalTargetEl.classList.remove("dbx-ajax-loading", "is-loading");
1971 }
1972 });
1973 }
1974
1975
1976 /* =========================================================
1977 * PUBLIC EXECUTE
1978 * ========================================================= */
1979
1980 dbx.ajax.run = function (root, source, overrides = {}, event) {
1981 return executeAjax(root, source, overrides, event);
1982 };
1983
1984
1985 /* =========================================================
1986 * DECLARE SCHEMA (Defaults + data-* Aliase)
1987 * ========================================================= */
1988
1989 if (dbx.declare && typeof dbx.declare.registerSchema === "function") {
1990
1991 dbx.declare.registerSchema("ajax", {
1992 fields: {
1993 class: {
1994 default: "dbxAjax"
1995 },
1996 mode: {
1997 default: "html",
1998 aliases: ["data-ajax-mode"]
1999 },
2000 url: {
2001 default: "form",
2002 aliases: ["data-ajax-url", "data-url"]
2003 },
2004 target: {
2005 default: "",
2006 aliases: ["data-ajax-target", "data-dbx_target", "data-target"],
2007 infer: function (el, ctx) {
2008 return dbx.declare.infer.ajaxTarget(ctx.root || el);
2009 }
2010 },
2011 replace: {
2012 default: "target",
2013 aliases: ["data-ajax-replace"]
2014 },
2015 bind: {
2016 default: "form,link,button",
2017 aliases: ["data-ajax-bind"]
2018 },
2019 method: {
2020 default: "",
2021 aliases: ["data-ajax-method"]
2022 },
2023 params: {
2024 default: "",
2025 aliases: ["data-ajax-params"]
2026 },
2027 jsonreplace: {
2028 default: "0",
2029 aliases: ["data-ajax-jsonreplace"]
2030 },
2031 jsonkey: {
2032 default: "html",
2033 aliases: ["data-ajax-jsonkey"]
2034 },
2035 textreplace: {
2036 default: "0",
2037 aliases: ["data-ajax-textreplace"]
2038 }
2039 }
2040 });
2041
2042 dbx.declare.transforms.ajax = function (raw, root) {
2043 return {
2044 _index: raw._index,
2045 lib: "ajax",
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)
2057 };
2058 };
2059 }
2060
2061
2062 /* =========================================================
2063 * FEATURE
2064 * ========================================================= */
2065
2066 dbx.feature.register("ajax", {
2067
2068 scope: "element",
2069
2070 priority: "mid",
2071
2072 css: [
2073 ["css", "design", "c-ajax.css"]
2074 ],
2075
2076
2077 init(el, cfg) {
2078
2079 if (!el) return;
2080
2081 el.__dbxInitialized = el.__dbxInitialized || {};
2082
2083 if (el.__dbxInitialized["ajax"]) {
2084 return;
2085 }
2086
2087 el.__dbxInitialized["ajax"] = true;
2088 el.setAttribute("data-dbx-ajax-root", "1");
2089
2090 getRootConfigs(el);
2091
2092 dbx.log("[dbx.ajax] init", {
2093 rootId: el.id || "",
2094 configs: el._dbxAjaxConfigs
2095 });
2096
2097 /* -------------------------------------------------
2098 * SUBMIT
2099 * ------------------------------------------------- */
2100 el.addEventListener("submit", function (e) {
2101
2102 if (e.defaultPrevented) return;
2103
2104 const form = e.target.closest("form");
2105 if (!form) return;
2106 if (!el.contains(form)) return;
2107
2108 const nearestAjaxRoot = form.closest("[data-dbx-ajax-root='1']");
2109 if (nearestAjaxRoot !== el) return;
2110
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(() => {});
2116 });
2117
2118 /* -------------------------------------------------
2119 * CLICK
2120 * ------------------------------------------------- */
2121 el.addEventListener("click", function (e) {
2122
2123 if (e.defaultPrevented) return;
2124
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;
2128
2129 const nearestAjaxRoot = source.closest("[data-dbx-ajax-root='1']");
2130 if (nearestAjaxRoot !== el) return;
2131
2132 const type = elementType(source);
2133
2134 if (type === "link") {
2135
2136 executeAjax(el, source, {}, e).catch(() => {});
2137 return;
2138 }
2139
2140 if (type === "button") {
2141
2142 const tag = source.tagName.toLowerCase();
2143 const btnType = (tag === "button")
2144 ? String(source.getAttribute("type") || "submit").toLowerCase()
2145 : String(source.getAttribute("type") || "").toLowerCase();
2146
2147 const isFormAction = source.classList.contains("dbxAjaxFormAction");
2148
2149 if (isFormAction) {
2150
2151 const formId = readLegacyData(source, "dbx_form");
2152 const target = readLegacyData(source, "dbx_target");
2153 const form = formId ? document.getElementById(formId) : getClosestForm(source, el);
2154
2155 executeAjax(el, source, {
2156 form: form,
2157 target: target || "",
2158 url: readAttr(source, "href") || readDataAjax(source, "url") || ""
2159 }, e).catch(() => {});
2160
2161 return;
2162 }
2163
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");
2176
2177 if ((btnType === "submit" || btnType === "image") && !hasButtonOverride) {
2178 return;
2179 }
2180
2181 executeAjax(el, source, {}, e).catch(() => {});
2182 }
2183 });
2184 },
2185
2186
2187 destroy(el, cfg) {
2188
2189 if (!el) return;
2190
2191 delete el._dbxAjaxConfigs;
2192
2193 if (el.__dbxInitialized && el.__dbxInitialized["ajax"]) {
2194 delete el.__dbxInitialized["ajax"];
2195 }
2196
2197 el.removeAttribute("data-dbx-ajax-root");
2198
2199 dbx.log("[dbx.ajax] destroy", {
2200 rootId: el.id || ""
2201 });
2202 }
2203
2204 });
2205
2206})(window, document);