dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
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 function isPrivateNetworkHost(hostname) {
795 const host = String(hostname || "").trim().toLowerCase().replace(/^\‍[|\‍]$/g, "");
796
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;
801
802 const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
803 if (!m) return false;
804
805 const a = Number(m[1]);
806 const b = Number(m[2]);
807
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;
812
813 return false;
814 }
815
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);
821 }
822
823
824 function normalizeRequestUrl(url) {
825
826 const raw = String(url || "").trim();
827
828 if (!raw) return raw;
829
830 try {
831 const parsed = new URL(raw, window.location.href);
832
833 if (shouldBlockLocalNetworkUrl(parsed)) {
834 throw new Error("blocked_local_network_request");
835 }
836
837 if (
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
842 ) {
843 parsed.protocol = window.location.protocol;
844 }
845
846 if (parsed.origin === window.location.origin) {
847 return parsed.pathname + parsed.search + parsed.hash;
848 }
849
850 return parsed.toString();
851 } catch (e) {
852 if (e && e.message === "blocked_local_network_request") {
853 throw e;
854 }
855 return raw;
856 }
857 }
858
859
860 function resolveElementByTarget(target) {
861
862 const raw = String(target || "").trim();
863
864 if (!raw) return null;
865
866 let el = document.getElementById(raw);
867 if (el) return el;
868
869 try {
870 el = document.querySelector(raw);
871 if (el) return el;
872 } catch (e) {
873 dbx.warn("[dbx.ajax] invalid target selector:", raw);
874 }
875
876 return null;
877 }
878
879
880 function getClosestForm(source, root) {
881
882 if (!source) return null;
883
884 if (source.tagName && source.tagName.toLowerCase() === "form") {
885 return source;
886 }
887
888 const dataFormId = readLegacyData(source, "dbx_form");
889 if (dataFormId) {
890 const formByDataId = document.getElementById(dataFormId);
891 if (formByDataId) {
892 return formByDataId;
893 }
894 }
895
896 const formAttr = readAttr(source, "form");
897 if (formAttr) {
898 const formByAttr = document.getElementById(formAttr);
899 if (formByAttr) {
900 return formByAttr;
901 }
902 }
903
904 const form = source.closest("form");
905
906 if (!form) {
907 return null;
908 }
909
910 if (root && !root.contains(form)) {
911 return null;
912 }
913
914 return form;
915 }
916
917
918 function getRootConfigs(root) {
919
920 if (!root) return [];
921
922 if (Array.isArray(root._dbxAjaxConfigs)) {
923 return root._dbxAjaxConfigs;
924 }
925
926 let out = [];
927
928 if (dbx.declare && dbx.declare.schemas && dbx.declare.schemas.ajax) {
929 out = dbx.declare.resolve("ajax", root);
930 } else {
931 const attr = readAttr(root, "data-dbx");
932 const list = dbx.parseData(attr).filter(cfg => cfg.lib === "ajax");
933
934 out = list.map((cfg, index) => {
935 return {
936 _index: index,
937 lib: "ajax",
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)
949 };
950 });
951
952 if (!out.length) {
953 out.push({
954 _index: 0,
955 lib: "ajax",
956 class: normalizeClassFilter("dbxAjax"),
957 mode: "html",
958 url: "form",
959 target: "",
960 replace: "target",
961 bind: ["form", "link", "button"],
962 method: "",
963 params: "",
964 jsonreplace: false,
965 jsonkey: "html",
966 textreplace: false
967 });
968 }
969 }
970
971 root._dbxAjaxConfigs = out;
972
973 return out;
974 }
975
976
977 function findMatchingConfig(root, source, type, overrides) {
978
979 if (overrides && overrides.config) {
980 return overrides.config;
981 }
982
983 const configs = getRootConfigs(root);
984
985 for (let i = 0; i < configs.length; i++) {
986
987 const cfg = configs[i];
988
989 if (!bindMatches(type, cfg.bind)) {
990 continue;
991 }
992
993 if (!classMatches(source, cfg.class)) {
994 continue;
995 }
996
997 return cfg;
998 }
999
1000 return null;
1001 }
1002
1003
1004 function getFormDefaultUrl(form) {
1005
1006 if (!form) return "";
1007
1008 const action = readAttr(form, "action");
1009
1010 if (action) {
1011 return action;
1012 }
1013
1014 return window.location.href;
1015 }
1016
1017
1018 function getLibDefaultUrl(cfg, form) {
1019
1020 if (!cfg) return "";
1021
1022 const url = String(cfg.url || "").trim();
1023
1024 if (!url) return "";
1025
1026 if (url === "form") {
1027 return getFormDefaultUrl(form);
1028 }
1029
1030 if (url === "lib") {
1031 return "";
1032 }
1033
1034 if (url.charAt(0) === "&") {
1035 const base = getFormDefaultUrl(form);
1036 return base ? appendToUrl(base, url) : "";
1037 }
1038
1039 return url;
1040 }
1041
1042
1043 function resolveUrl(root, source, form, cfg, overrides) {
1044
1045 const libUrl = getLibDefaultUrl(cfg, form);
1046
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") : "");
1053
1054 const formBase = formUrlOverride
1055 ? resolveUrlToken(formUrlOverride, getFormDefaultUrl(form), libUrl)
1056 : getFormDefaultUrl(form);
1057
1058 if (sourceUrlOverride) {
1059 return resolveUrlToken(sourceUrlOverride, formBase, libUrl);
1060 }
1061
1062 if (elementType(source) === "form") {
1063 return formBase || libUrl;
1064 }
1065
1066 if (form) {
1067 return formBase || libUrl;
1068 }
1069
1070 return libUrl;
1071 }
1072
1073
1074 function resolveUrlToken(token, formUrl, libUrl) {
1075
1076 const raw = String(token || "").trim();
1077
1078 if (!raw) {
1079 return "";
1080 }
1081
1082 if (raw === "form") {
1083 return formUrl || libUrl || "";
1084 }
1085
1086 if (raw === "lib") {
1087 return libUrl || formUrl || "";
1088 }
1089
1090 if (raw.charAt(0) === "&") {
1091 const base = formUrl || libUrl || "";
1092 if (!base) {
1093 return "";
1094 }
1095 return appendToUrl(base, raw);
1096 }
1097
1098 return raw;
1099 }
1100
1101
1102 function resolveTarget(root, source, form, cfg, overrides) {
1103
1104 const sourceTarget =
1105 (overrides && overrides.target) ||
1106 readDataAjax(source, "target") ||
1107 readLegacyData(source, "dbx_target");
1108
1109 if (sourceTarget) {
1110 return sourceTarget;
1111 }
1112
1113 const formTarget = readDataAjax(form, "target");
1114 if (formTarget) {
1115 return formTarget;
1116 }
1117
1118 if (cfg && cfg.target) {
1119 return cfg.target;
1120 }
1121
1122 return "";
1123 }
1124
1125
1126 function resolveReplaceMode(source, form, cfg, overrides) {
1127
1128 const sourceReplace =
1129 (overrides && overrides.replace) ||
1130 readDataAjax(source, "replace");
1131
1132 if (sourceReplace) {
1133 return normalizeReplaceMode(sourceReplace);
1134 }
1135
1136 const formReplace = readDataAjax(form, "replace");
1137 if (formReplace) {
1138 return normalizeReplaceMode(formReplace);
1139 }
1140
1141 return normalizeReplaceMode((cfg && cfg.replace) || "target");
1142 }
1143
1144
1145 function resolveMode(source, form, cfg, overrides) {
1146
1147 const sourceMode =
1148 (overrides && overrides.mode) ||
1149 readDataAjax(source, "mode");
1150
1151 if (sourceMode) {
1152 return normalizeMode(sourceMode);
1153 }
1154
1155 const formMode = readDataAjax(form, "mode");
1156 if (formMode) {
1157 return normalizeMode(formMode);
1158 }
1159
1160 return normalizeMode((cfg && cfg.mode) || "html");
1161 }
1162
1163
1164 function resolveMethod(source, form, type, cfg, overrides) {
1165
1166 const sourceMethod =
1167 (overrides && overrides.method) ||
1168 readDataAjax(source, "method") ||
1169 readAttr(source, "formmethod");
1170
1171 if (sourceMethod) {
1172 return normalizeMethod(sourceMethod, type === "link" ? "GET" : "POST");
1173 }
1174
1175 const formMethod =
1176 readDataAjax(form, "method") ||
1177 readAttr(form, "method");
1178
1179 if (formMethod) {
1180 return normalizeMethod(formMethod, type === "link" ? "GET" : "POST");
1181 }
1182
1183 if (cfg && cfg.method) {
1184 return normalizeMethod(cfg.method, type === "link" ? "GET" : "POST");
1185 }
1186
1187 return normalizeMethod(type === "link" ? "GET" : "POST");
1188 }
1189
1190
1191 function resolveJsonReplace(source, form, cfg, overrides) {
1192
1193 if (overrides && typeof overrides.jsonreplace !== "undefined") {
1194 return bool(overrides.jsonreplace, false);
1195 }
1196
1197 const sourceVal = readDataAjax(source, "jsonreplace");
1198 if (sourceVal !== "") {
1199 return bool(sourceVal, false);
1200 }
1201
1202 const formVal = readDataAjax(form, "jsonreplace");
1203 if (formVal !== "") {
1204 return bool(formVal, false);
1205 }
1206
1207 return bool(cfg && cfg.jsonreplace, false);
1208 }
1209
1210
1211 function resolveJsonKey(source, form, cfg, overrides) {
1212
1213 if (overrides && overrides.jsonkey) {
1214 return String(overrides.jsonkey).trim();
1215 }
1216
1217 const sourceVal = readDataAjax(source, "jsonkey");
1218 if (sourceVal) {
1219 return String(sourceVal).trim();
1220 }
1221
1222 const formVal = readDataAjax(form, "jsonkey");
1223 if (formVal) {
1224 return String(formVal).trim();
1225 }
1226
1227 return String((cfg && cfg.jsonkey) || "html").trim();
1228 }
1229
1230
1231 function resolveTextReplace(source, form, cfg, overrides) {
1232
1233 if (overrides && typeof overrides.textreplace !== "undefined") {
1234 return bool(overrides.textreplace, false);
1235 }
1236
1237 const sourceVal = readDataAjax(source, "textreplace");
1238 if (sourceVal !== "") {
1239 return bool(sourceVal, false);
1240 }
1241
1242 const formVal = readDataAjax(form, "textreplace");
1243 if (formVal !== "") {
1244 return bool(formVal, false);
1245 }
1246
1247 return bool(cfg && cfg.textreplace, false);
1248 }
1249
1250
1251 function collectExtraParams(source, form, cfg, overrides) {
1252
1253 const out = [];
1254
1255 if (cfg && cfg.params) {
1256 out.push(...parseParamString(cfg.params));
1257 }
1258
1259 const formParams = readDataAjax(form, "params");
1260 if (formParams) {
1261 out.push(...parseParamString(formParams));
1262 }
1263
1264 const sourceParams =
1265 (overrides && overrides.params) ||
1266 readDataAjax(source, "params");
1267
1268 if (sourceParams) {
1269 out.push(...parseParamString(sourceParams));
1270 }
1271
1272 return out;
1273 }
1274
1275
1276 function applyParamsToFormData(fd, params) {
1277
1278 if (!fd || !params || !params.length) return;
1279
1280 params.forEach(([key, value]) => {
1281 try {
1282 fd.set(key, value);
1283 } catch (e) {
1284 fd.append(key, value);
1285 }
1286 });
1287 }
1288
1289
1290 function applyParamsToUrl(url, params) {
1291
1292 let finalUrl = String(url || "").trim();
1293
1294 if (!finalUrl) {
1295 return finalUrl;
1296 }
1297
1298 if (!params || !params.length) {
1299 return finalUrl;
1300 }
1301
1302 const hashPos = finalUrl.indexOf("#");
1303 let hash = "";
1304
1305 if (hashPos !== -1) {
1306 hash = finalUrl.substring(hashPos);
1307 finalUrl = finalUrl.substring(0, hashPos);
1308 }
1309
1310 let qs = "";
1311
1312 params.forEach(([key, value], idx) => {
1313 const pair = encodeURIComponent(key) + "=" + encodeURIComponent(value);
1314 qs += (idx === 0 ? "" : "&") + pair;
1315 });
1316
1317 if (qs) {
1318 finalUrl = appendToUrl(finalUrl, qs);
1319 }
1320
1321 return finalUrl + hash;
1322 }
1323
1324
1325 function buildBodyAndUrl(ctx) {
1326
1327 let url = String(ctx.url || "").trim();
1328 let body = null;
1329
1330 const params = Array.isArray(ctx.params) ? ctx.params.slice() : [];
1331
1332 if (ctx.method === "GET") {
1333
1334 if (ctx.form) {
1335 const fd = new FormData(ctx.form);
1336 fd.forEach((value, key) => {
1337 params.push([key, value]);
1338 });
1339 }
1340
1341 if (ctx.submitSource && ctx.submitSource.name) {
1342 params.push([ctx.submitSource.name, ctx.submitSource.value || ""]);
1343 }
1344
1345 url = applyParamsToUrl(url, params);
1346
1347 return {
1348 url: ensureAjaxFlag(url),
1349 body: null
1350 };
1351 }
1352
1353 if (ctx.form) {
1354 body = new FormData(ctx.form);
1355 } else {
1356 body = new FormData();
1357 }
1358
1359 if (ctx.submitSource && ctx.submitSource.name) {
1360 body.set(ctx.submitSource.name, ctx.submitSource.value || "");
1361 }
1362
1363 applyParamsToFormData(body, params);
1364
1365 return {
1366 url: ensureAjaxFlag(url),
1367 body: body
1368 };
1369 }
1370
1371
1372 function emit(name, data) {
1373
1374 if (dbx.event && typeof dbx.event.emit === "function") {
1375 dbx.event.emit(name, data);
1376 }
1377 }
1378
1379
1380 function executeScripts(scripts) {
1381
1382 if (!scripts || !scripts.length) {
1383 return Promise.resolve();
1384 }
1385
1386 let chain = Promise.resolve();
1387
1388 scripts.forEach(srcNode => {
1389
1390 chain = chain.then(() => {
1391 return new Promise((resolve) => {
1392
1393 const script = document.createElement("script");
1394
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);
1399 }
1400
1401 if (srcNode.src) {
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);
1407 resolve();
1408 };
1409 document.body.appendChild(script);
1410 } else {
1411 script.text = srcNode.textContent || "";
1412 document.body.appendChild(script);
1413 resolve();
1414 }
1415 });
1416 });
1417 });
1418
1419 return chain;
1420 }
1421
1422
1423 function replaceTarget(targetEl, html, replaceMode = "target") {
1424
1425 if (!targetEl) {
1426 return Promise.resolve(targetEl);
1427 }
1428
1429 if (html === undefined || html === null) {
1430 return Promise.resolve(targetEl);
1431 }
1432
1433 const markup = String(html).trim();
1434
1435 if (!markup) {
1436 return Promise.resolve(targetEl);
1437 }
1438
1439 const temp = document.createElement("div");
1440 temp.innerHTML = markup;
1441
1442 const scripts = Array.from(temp.querySelectorAll("script"));
1443 scripts.forEach(s => s.remove());
1444
1445 let newElement = targetEl;
1446 const mode = normalizeReplaceMode(replaceMode);
1447
1448 if (mode === "inner") {
1449
1450 targetEl.innerHTML = temp.innerHTML;
1451 newElement = targetEl;
1452
1453 } else if (temp.children.length === 1) {
1454
1455 const candidate = temp.firstElementChild;
1456
1457 if (candidate && targetEl.parentNode) {
1458 targetEl.parentNode.replaceChild(candidate, targetEl);
1459 newElement = candidate;
1460 } else {
1461 targetEl.innerHTML = temp.innerHTML;
1462 newElement = targetEl;
1463 }
1464
1465 } else {
1466
1467 targetEl.innerHTML = temp.innerHTML;
1468 newElement = targetEl;
1469 }
1470
1471 return executeScripts(scripts).then(() => {
1472
1473 if (newElement && typeof dbx.scan === "function") {
1474 dbx.scan(newElement);
1475 }
1476
1477 return newElement;
1478 });
1479 }
1480
1481
1482 function requestNow() {
1483 return window.performance && typeof window.performance.now === "function"
1484 ? window.performance.now()
1485 : Date.now();
1486 }
1487
1488
1489 function requestElapsedSeconds(startedAt) {
1490 return Math.max(0, (requestNow() - startedAt) / 1000);
1491 }
1492
1493
1494 function parseRuntimeSeconds(value) {
1495 const number = Number(value);
1496 return Number.isFinite(number) && number >= 0 ? number : null;
1497 }
1498
1499
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) {
1504 return headerValue;
1505 }
1506 }
1507
1508 if (data && typeof data === "object") {
1509 const directValue = parseRuntimeSeconds(data.dbx_php_runtime || data.php_runtime);
1510 if (directValue !== null) {
1511 return directValue;
1512 }
1513
1514 if (data._dbx_runtime && typeof data._dbx_runtime === "object") {
1515 const nestedValue = parseRuntimeSeconds(data._dbx_runtime.php);
1516 if (nestedValue !== null) {
1517 return nestedValue;
1518 }
1519 }
1520 }
1521
1522 return null;
1523 }
1524
1525
1526 function shouldTrackRuntime(options, url, body) {
1527 if (dbx.footerStatus && typeof dbx.footerStatus.shouldTrackAjaxRuntime === "function") {
1528 return dbx.footerStatus.shouldTrackAjaxRuntime(url, body, options);
1529 }
1530
1531 if (options && (
1532 options.skipRuntime === true ||
1533 options.footerRuntime === 'hidden' ||
1534 options.footerRuntime === 'skip' ||
1535 options.footerRuntime === '0'
1536 )) {
1537 return false;
1538 }
1539
1540 if (options && String(options.mode || "").toLowerCase() === "json") {
1541 return false;
1542 }
1543
1544 const targetUrl = String(url || '');
1545 if (/[?&]dbx_sync=0(?:&|$)/.test(targetUrl)) {
1546 return false;
1547 }
1548
1549 if (body instanceof URLSearchParams && body.get('dbx_sync') === '0') {
1550 return false;
1551 }
1552
1553 if (typeof body === 'string' && /(?:^|&)dbx_sync=0(?:&|$)/.test(body)) {
1554 return false;
1555 }
1556
1557 return true;
1558 }
1559
1560
1561 function updateFooterRuntime(startedAt, response, data) {
1562 if (!dbx.footerStatus || typeof dbx.footerStatus.updateAjax !== "function") {
1563 return;
1564 }
1565
1566 dbx.footerStatus.updateAjax(
1567 requestElapsedSeconds(startedAt),
1568 phpRuntimeFromResponse(response, data)
1569 );
1570 }
1571
1572
1573 /* =========================================================
1574 * PUBLIC CORE REQUEST
1575 * ========================================================= */
1576
1577 dbx.ajax.request = function (options) {
1578
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;
1588 let timer = null;
1589 let runtimeUpdated = false;
1590 const startedAt = requestNow();
1591
1592 if (!url) {
1593 return Promise.reject(new Error("missing_url"));
1594 }
1595
1596 if ((method === "GET" || method === "HEAD") && data) {
1597 url = appendDataToUrl(url, data);
1598 } else if (body === null && data) {
1599 body = bodyFromData(data);
1600 }
1601
1602 try {
1603 url = normalizeRequestUrl(url);
1604 } catch (err) {
1605 dbx.warn("[dbx.ajax] local network request blocked", url);
1606 return Promise.reject(err);
1607 }
1608
1609 const trackRuntime = shouldTrackRuntime(options, url, body);
1610
1611 function markRuntime(response, responseData) {
1612 if (!trackRuntime) return;
1613 runtimeUpdated = true;
1614 updateFooterRuntime(startedAt, response, responseData);
1615 }
1616
1617 dbx.log("[dbx.ajax] request start", {
1618 method: method,
1619 mode: mode,
1620 url: url
1621 });
1622
1623 if (controller) {
1624 timer = window.setTimeout(() => controller.abort(), timeout);
1625 }
1626
1627 return fetch(url, {
1628 method: method,
1629 body: body,
1630 headers: headers,
1631 credentials: "same-origin",
1632 keepalive: keepalive,
1633 signal: controller ? controller.signal : undefined
1634 })
1635 .then(response => {
1636
1637 if (!response.ok) {
1638 markRuntime(response, null);
1639 throw new Error("HTTP " + response.status);
1640 }
1641
1642 if (mode === "json") {
1643 return response.text().then(txt => {
1644 if (!txt) {
1645 const emptyData = {};
1646 markRuntime(response, emptyData);
1647 return emptyData;
1648 }
1649
1650 try {
1651 const data = JSON.parse(txt);
1652 markRuntime(response, data);
1653 return data;
1654 } catch (err) {
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, " ")
1660 .trim();
1661 markRuntime(response, null);
1662 throw new Error(clean || "Ungueltige Serverantwort");
1663 }
1664 });
1665 }
1666
1667 return response.text().then(txt => {
1668 markRuntime(response, txt);
1669 return txt;
1670 });
1671 })
1672 .catch(error => {
1673 if (!runtimeUpdated && trackRuntime) {
1674 updateFooterRuntime(startedAt, null, null);
1675 }
1676
1677 throw error;
1678 })
1679 .finally(() => {
1680 if (timer) window.clearTimeout(timer);
1681 });
1682 };
1683
1684
1685 /* =========================================================
1686 * INTERNAL EXECUTE
1687 * ========================================================= */
1688
1689 function executeAjax(root, source, overrides = {}, event) {
1690
1691 if (!root) {
1692 dbx.warn("[dbx.ajax] missing root");
1693 return Promise.resolve(null);
1694 }
1695
1696 const type = elementType(source);
1697
1698 if (!type) {
1699 dbx.warn("[dbx.ajax] unsupported source:", source);
1700 return Promise.resolve(null);
1701 }
1702
1703 const cfg = findMatchingConfig(root, source, type, overrides);
1704
1705 if (!cfg && !overrides.url) {
1706 dbx.log("[dbx.ajax] no matching config", {
1707 type: type,
1708 source: source
1709 });
1710 return Promise.resolve(null);
1711 }
1712
1713 const form = (overrides && overrides.form) || getClosestForm(source, root);
1714
1715 const ctx = {
1716 root: root,
1717 source: source,
1718 type: type,
1719 form: form,
1720 cfg: cfg || {},
1721 submitSource: (type === "button") ? source : null
1722 };
1723
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);
1733
1734 if (event && typeof event.preventDefault === "function") {
1735 event.preventDefault();
1736 }
1737
1738 if (!ctx.url) {
1739 dbx.warn("[dbx.ajax] missing resolved url", {
1740 type: ctx.type,
1741 source: ctx.source
1742 });
1743 return Promise.resolve(null);
1744 }
1745
1746 const lockEl = ctx.form || ctx.source;
1747
1748 if (lockEl && lockEl._dbxAjaxRunning === true) {
1749 dbx.log("[dbx.ajax] request skipped (already running)");
1750 return Promise.resolve(null);
1751 }
1752
1753 if (lockEl) {
1754 lockEl._dbxAjaxRunning = true;
1755 }
1756
1757 const targetEl = resolveElementByTarget(ctx.target);
1758
1759 if (ctx.mode === "html" && !targetEl) {
1760 dbx.warn("[dbx.ajax] target not found for html mode:", ctx.target);
1761 if (lockEl) {
1762 lockEl._dbxAjaxRunning = false;
1763 }
1764 return Promise.resolve(null);
1765 }
1766
1767 let loadingTimer = null;
1768 if (targetEl) {
1769 targetEl.classList.add("dbx-ajax-loading");
1770 loadingTimer = window.setTimeout(() => {
1771 targetEl.classList.remove("dbx-ajax-loading", "is-loading");
1772 }, 20000);
1773 }
1774
1775 const built = buildBodyAndUrl(ctx);
1776
1777 ctx.url = built.url;
1778 ctx.body = built.body;
1779
1780 const eventData = {
1781 id: root.id || "",
1782 root: root,
1783 source: source,
1784 form: form,
1785 type: ctx.type,
1786 mode: ctx.mode,
1787 method: ctx.method,
1788 url: ctx.url,
1789 target: ctx.target,
1790 replace: ctx.replace,
1791 params: ctx.params
1792 };
1793
1794 emit("ajax:before", eventData);
1795
1796 return dbx.ajax.request({
1797 url: ctx.url,
1798 method: ctx.method,
1799 mode: ctx.mode,
1800 body: ctx.body
1801 })
1802 .then(responseData => {
1803
1804 if (ctx.mode === "html") {
1805
1806 return replaceTarget(targetEl, responseData, ctx.replace).then(newTarget => {
1807
1808 emit("ajax:after", {
1809 ...eventData,
1810 response: responseData,
1811 targetElement: newTarget
1812 });
1813
1814 return {
1815 ok: true,
1816 mode: ctx.mode,
1817 response: responseData,
1818 targetElement: newTarget
1819 };
1820 });
1821 }
1822
1823 if (ctx.mode === "json") {
1824
1825 if (ctx.jsonreplace === true && targetEl) {
1826
1827 let html = "";
1828
1829 if (responseData && typeof responseData === "object") {
1830 html = responseData[ctx.jsonkey] || "";
1831 }
1832
1833 if (typeof html === "string" && html !== "") {
1834 return replaceTarget(targetEl, html, ctx.replace).then(newTarget => {
1835
1836 emit("ajax:after", {
1837 ...eventData,
1838 response: responseData,
1839 targetElement: newTarget
1840 });
1841
1842 return {
1843 ok: true,
1844 mode: ctx.mode,
1845 response: responseData,
1846 targetElement: newTarget
1847 };
1848 });
1849 }
1850 }
1851
1852 emit("ajax:after", {
1853 ...eventData,
1854 response: responseData,
1855 targetElement: targetEl
1856 });
1857
1858 return {
1859 ok: true,
1860 mode: ctx.mode,
1861 response: responseData,
1862 targetElement: targetEl
1863 };
1864 }
1865
1866 if (ctx.mode === "text") {
1867
1868 if (ctx.textreplace === true && targetEl) {
1869 return replaceTarget(targetEl, responseData, ctx.replace).then(newTarget => {
1870
1871 emit("ajax:after", {
1872 ...eventData,
1873 response: responseData,
1874 targetElement: newTarget
1875 });
1876
1877 return {
1878 ok: true,
1879 mode: ctx.mode,
1880 response: responseData,
1881 targetElement: newTarget
1882 };
1883 });
1884 }
1885
1886 emit("ajax:after", {
1887 ...eventData,
1888 response: responseData,
1889 targetElement: targetEl
1890 });
1891
1892 return {
1893 ok: true,
1894 mode: ctx.mode,
1895 response: responseData,
1896 targetElement: targetEl
1897 };
1898 }
1899
1900 emit("ajax:after", {
1901 ...eventData,
1902 response: responseData,
1903 targetElement: targetEl
1904 });
1905
1906 return {
1907 ok: true,
1908 mode: ctx.mode,
1909 response: responseData,
1910 targetElement: targetEl
1911 };
1912 })
1913 .catch(error => {
1914
1915 dbx.error("[dbx.ajax] error:", error);
1916
1917 emit("ajax:error", {
1918 ...eventData,
1919 error: error
1920 });
1921
1922 throw error;
1923 })
1924 .finally(() => {
1925
1926 if (lockEl) {
1927 lockEl._dbxAjaxRunning = false;
1928 }
1929
1930 if (loadingTimer) {
1931 window.clearTimeout(loadingTimer);
1932 }
1933
1934 if (targetEl) {
1935 targetEl.classList.remove("dbx-ajax-loading", "is-loading");
1936 }
1937
1938 const finalTargetEl = resolveElementByTarget(ctx.target);
1939 if (finalTargetEl) {
1940 finalTargetEl.classList.remove("dbx-ajax-loading", "is-loading");
1941 }
1942 });
1943 }
1944
1945
1946 /* =========================================================
1947 * PUBLIC EXECUTE
1948 * ========================================================= */
1949
1950 dbx.ajax.run = function (root, source, overrides = {}, event) {
1951 return executeAjax(root, source, overrides, event);
1952 };
1953
1954
1955 /* =========================================================
1956 * DECLARE SCHEMA (Defaults + data-* Aliase)
1957 * ========================================================= */
1958
1959 if (dbx.declare && typeof dbx.declare.registerSchema === "function") {
1960
1961 dbx.declare.registerSchema("ajax", {
1962 fields: {
1963 class: {
1964 default: "dbxAjax"
1965 },
1966 mode: {
1967 default: "html",
1968 aliases: ["data-ajax-mode"]
1969 },
1970 url: {
1971 default: "form",
1972 aliases: ["data-ajax-url", "data-url"]
1973 },
1974 target: {
1975 default: "",
1976 aliases: ["data-ajax-target", "data-dbx_target", "data-target"],
1977 infer: function (el, ctx) {
1978 return dbx.declare.infer.ajaxTarget(ctx.root || el);
1979 }
1980 },
1981 replace: {
1982 default: "target",
1983 aliases: ["data-ajax-replace"]
1984 },
1985 bind: {
1986 default: "form,link,button",
1987 aliases: ["data-ajax-bind"]
1988 },
1989 method: {
1990 default: "",
1991 aliases: ["data-ajax-method"]
1992 },
1993 params: {
1994 default: "",
1995 aliases: ["data-ajax-params"]
1996 },
1997 jsonreplace: {
1998 default: "0",
1999 aliases: ["data-ajax-jsonreplace"]
2000 },
2001 jsonkey: {
2002 default: "html",
2003 aliases: ["data-ajax-jsonkey"]
2004 },
2005 textreplace: {
2006 default: "0",
2007 aliases: ["data-ajax-textreplace"]
2008 }
2009 }
2010 });
2011
2012 dbx.declare.transforms.ajax = function (raw, root) {
2013 return {
2014 _index: raw._index,
2015 lib: "ajax",
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)
2027 };
2028 };
2029 }
2030
2031
2032 /* =========================================================
2033 * FEATURE
2034 * ========================================================= */
2035
2036 dbx.feature.register("ajax", {
2037
2038 scope: "element",
2039
2040 priority: "mid",
2041
2042 css: [
2043 ["css", "design", "c-ajax.css"]
2044 ],
2045
2046
2047 init(el, cfg) {
2048
2049 if (!el) return;
2050
2051 el.__dbxInitialized = el.__dbxInitialized || {};
2052
2053 if (el.__dbxInitialized["ajax"]) {
2054 return;
2055 }
2056
2057 el.__dbxInitialized["ajax"] = true;
2058 el.setAttribute("data-dbx-ajax-root", "1");
2059
2060 getRootConfigs(el);
2061
2062 dbx.log("[dbx.ajax] init", {
2063 rootId: el.id || "",
2064 configs: el._dbxAjaxConfigs
2065 });
2066
2067 /* -------------------------------------------------
2068 * SUBMIT
2069 * ------------------------------------------------- */
2070 el.addEventListener("submit", function (e) {
2071
2072 if (e.defaultPrevented) return;
2073
2074 const form = e.target.closest("form");
2075 if (!form) return;
2076 if (!el.contains(form)) return;
2077
2078 const nearestAjaxRoot = form.closest("[data-dbx-ajax-root='1']");
2079 if (nearestAjaxRoot !== el) return;
2080
2081 executeAjax(el, form, {}, e).catch(() => {});
2082 });
2083
2084 /* -------------------------------------------------
2085 * CLICK
2086 * ------------------------------------------------- */
2087 el.addEventListener("click", function (e) {
2088
2089 if (e.defaultPrevented) return;
2090
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;
2094
2095 const nearestAjaxRoot = source.closest("[data-dbx-ajax-root='1']");
2096 if (nearestAjaxRoot !== el) return;
2097
2098 const type = elementType(source);
2099
2100 if (type === "link") {
2101
2102 executeAjax(el, source, {}, e).catch(() => {});
2103 return;
2104 }
2105
2106 if (type === "button") {
2107
2108 const tag = source.tagName.toLowerCase();
2109 const btnType = (tag === "button")
2110 ? String(source.getAttribute("type") || "submit").toLowerCase()
2111 : String(source.getAttribute("type") || "").toLowerCase();
2112
2113 const isFormAction = source.classList.contains("dbxAjaxFormAction");
2114
2115 if (isFormAction) {
2116
2117 const formId = readLegacyData(source, "dbx_form");
2118 const target = readLegacyData(source, "dbx_target");
2119 const form = formId ? document.getElementById(formId) : getClosestForm(source, el);
2120
2121 executeAjax(el, source, {
2122 form: form,
2123 target: target || "",
2124 url: readAttr(source, "href") || readDataAjax(source, "url") || ""
2125 }, e).catch(() => {});
2126
2127 return;
2128 }
2129
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");
2142
2143 if ((btnType === "submit" || btnType === "image") && !hasButtonOverride) {
2144 return;
2145 }
2146
2147 executeAjax(el, source, {}, e).catch(() => {});
2148 }
2149 });
2150 },
2151
2152
2153 destroy(el, cfg) {
2154
2155 if (!el) return;
2156
2157 delete el._dbxAjaxConfigs;
2158
2159 if (el.__dbxInitialized && el.__dbxInitialized["ajax"]) {
2160 delete el.__dbxInitialized["ajax"];
2161 }
2162
2163 el.removeAttribute("data-dbx-ajax-root");
2164
2165 dbx.log("[dbx.ajax] destroy", {
2166 rootId: el.id || ""
2167 });
2168 }
2169
2170 });
2171
2172})(window, document);