dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
confirm.js
Go to the documentation of this file.
1/*!
2 * =========================================================
3 * DBX CONFIRM SYSTEM (confirm.js)
4 * =========================================================
5 *
6 * Zweck
7 * -----
8 * confirm.js ist die universelle Bestätigungs-Lib von DBX.
9 *
10 * Die Lib ist bewusst getrennt von:
11 * - ajax.js → Transport / Response
12 * - form.js → Formular-/UI-Logik
13 *
14 * confirm.js kümmert sich ausschließlich um:
15 * - Bestätigungsdialoge
16 * - Button-Sets
17 * - frei definierbare Beschriftungen
18 * - HTML-Inhalte
19 * - optionalen Hinweistext
20 *
21 *
22 * Scope-Regel
23 * -----------
24 * confirm.js arbeitet als DBX-Feature mit:
25 *
26 * scope: "element"
27 *
28 * Das bedeutet:
29 * - eine confirm-Instanz gilt nur für ihr Root-Element
30 * - plus dessen Children
31 * - niemals global als Event-Fänger
32 *
33 *
34 * Architektur
35 * -----------
36 * confirm.js bietet zwei Nutzungsarten:
37 *
38 * 1. deklarativ über data-dbx + Klassen + data-confirm-*
39 * 2. programmatisch über dbx.confirm.open(...)
40 *
41 *
42 * =========================================================
43 * ROOT-KONFIGURATION (data-dbx)
44 * =========================================================
45 *
46 * Beispiel:
47 *
48 * <div data-dbx="lib=confirm|class=dbxConfirm|bind=link,button,form">
49 * ...
50 * </div>
51 *
52 *
53 * Unterstützte Root-Parameter
54 * ---------------------------
55 *
56 * lib=confirm
57 * Pflicht. Registriert confirm.js auf diesem Root.
58 *
59 * class=dbxConfirm
60 * Match-Class. Nur Elemente mit dieser Klasse werden
61 * von dieser confirm-Instanz behandelt.
62 *
63 * Erlaubt:
64 * - class=dbxConfirm
65 * - class=dbxDeleteConfirm
66 * - class=*
67 *
68 * bind=link,button,form
69 * Welche Elementtypen bestätigt werden dürfen.
70 *
71 * Erlaubt:
72 * - bind=link
73 * - bind=button
74 * - bind=form
75 * - bind=link,button,form
76 * - bind=* / bind=all
77 *
78 * title=...
79 * Default-Titel des Dialogs.
80 *
81 * question=...
82 * Default-Fragetext des Dialogs.
83 *
84 * hint=...
85 * Optionaler Hinweistext unter der Frage.
86 * Wird nur angezeigt, wenn gesetzt und nicht leer.
87 *
88 * buttons=yesno|yesnocancel|cancel|cancelonly
89 * Default-Button-Set.
90 *
91 * yesno:
92 * - Ja / Nein
93 *
94 * yesnocancel:
95 * - Ja / Nein / Abbruch
96 *
97 * cancel / cancelonly:
98 * - nur Abbruch
99 *
100 * titlehtml=0|1
101 * questionhtml=0|1
102 * hinthtml=0|1
103 * labelhtml=0|1
104 * Steuert, ob die jeweiligen Inhalte als HTML interpretiert werden.
105 *
106 * Standard:
107 * - titlehtml=1
108 * - questionhtml=1
109 * - hinthtml=1
110 * - labelhtml=1
111 *
112 * labelyes=...
113 * labelno=...
114 * labelcancel=...
115 * Frei definierbare Beschriftungen.
116 * HTML ist erlaubt, wenn labelhtml=1.
117 *
118 * closable=0|1
119 * Darf der Dialog über "X" geschlossen werden?
120 *
121 * backdropclose=0|1
122 * Darf Klick auf Backdrop den Dialog schließen?
123 *
124 * escclose=0|1
125 * Darf Escape den Dialog schließen?
126 *
127 *
128 * =========================================================
129 * ELEMENT-OVERRIDES (data-confirm-*)
130 * =========================================================
131 *
132 * Ein Link, Button oder ein Form kann alle relevanten
133 * Einstellungen lokal überschreiben.
134 *
135 * Unterstützte data-confirm-* Parameter
136 * -------------------------------------
137 *
138 * data-confirm-title="..."
139 * data-confirm-question="..."
140 * data-confirm-hint="..."
141 * data-confirm-buttons="yesno|yesnocancel|cancel"
142 *
143 * data-confirm-titlehtml="0|1"
144 * data-confirm-questionhtml="0|1"
145 * data-confirm-hinthtml="0|1"
146 * data-confirm-labelhtml="0|1"
147 *
148 * data-confirm-labelyes="..."
149 * data-confirm-labelno="..."
150 * data-confirm-labelcancel="..."
151 *
152 * data-confirm-closable="0|1"
153 * data-confirm-backdropclose="0|1"
154 * data-confirm-escclose="0|1"
155 *
156 *
157 * Kurzform
158 * --------
159 * Zusätzlich wird akzeptiert:
160 *
161 * data-confirm="..."
162 *
163 * Das wird als question behandelt, falls data-confirm-question
164 * nicht gesetzt ist.
165 *
166 *
167 * =========================================================
168 * PROGRAMMATISCHE NUTZUNG
169 * =========================================================
170 *
171 * dbx.confirm.open({
172 * id: "delete-15",
173 * root: el,
174 * title: "<i class='bi bi-trash'></i> Löschen",
175 * question: "Datensatz wirklich löschen?",
176 * hint: "<small>Dieser Vorgang kann nicht rückgängig gemacht werden.</small>",
177 * buttons: "yesnocancel",
178 * labelyes: "<i class='bi bi-check-lg'></i> Ja",
179 * labelno: "<i class='bi bi-x-lg'></i> Nein",
180 * labelcancel: "<i class='bi bi-slash-circle'></i> Abbruch"
181 * }).then(result => {
182 * if (result.action === "yes") { ... }
183 * });
184 *
185 *
186 * Rückgabe von open(...)
187 * ---------------------
188 * Promise → resolve({
189 * id,
190 * action, // yes | no | cancel | close
191 * source,
192 * root,
193 * dialog
194 * })
195 *
196 *
197 * =========================================================
198 * BUTTON-SETS
199 * =========================================================
200 *
201 * buttons=yesno
202 * - yes
203 * - no
204 *
205 * buttons=yesnocancel
206 * - yes
207 * - no
208 * - cancel
209 *
210 * buttons=cancel / cancelonly
211 * - cancel
212 *
213 *
214 * =========================================================
215 * AUTOMATISCHES FORTSETZEN DER AUSLÖSUNG
216 * =========================================================
217 *
218 * Wenn confirm.js deklarativ auf einem:
219 * - Link
220 * - Button
221 * - Form
222 * sitzt,
223 * dann wird die ursprüngliche Aktion nach erfolgreichem "yes"
224 * automatisch fortgesetzt.
225 *
226 * Reihenfolge:
227 * 1. Falls möglich direkt über ajax.js
228 * 2. sonst normaler Browser-Default
229 *
230 * confirm.js selbst macht keine Fachlogik.
231 * Es reicht die Auslösung an ajax.js oder den Browser weiter.
232 *
233 *
234 * =========================================================
235 * EVENTS
236 * =========================================================
237 *
238 * confirm.js nutzt dbx.event.emit():
239 *
240 * confirm:open
241 * confirm:result
242 *
243 * Wenn ein id gesetzt ist, funktionieren zusätzlich die
244 * id-scoped Events von core.js:
245 *
246 * confirm:result:<id>
247 *
248 * =========================================================
249 */
250
251(function (window, document) {
252 "use strict";
253
254 if (!window.dbx || !window.dbx.feature) {
255 console.error("[dbx][confirm] dbx core missing");
256 return;
257 }
258
259 const dbx = window.dbx;
260
261 dbx.confirm = dbx.confirm || {};
262
263 const _dialogs = {};
264 let _uid = 0;
265
266
267 /* =========================================================
268 * HELPERS
269 * ========================================================= */
270
271 function bool(v, def = false) {
272 if (v === undefined || v === null || v === "") return def;
273 if (v === true || v === 1 || v === "1" || v === "on" || v === "true") return true;
274 if (v === false || v === 0 || v === "0" || v === "off" || v === "false") return false;
275 return def;
276 }
277
278 function str(v, def = "") {
279 if (v === undefined || v === null) return def;
280 return String(v);
281 }
282
283 function normalizeBind(v) {
284 if (v === undefined || v === null || v === "") return ["link", "button", "form"];
285
286 const raw = String(v).toLowerCase().trim();
287
288 if (raw === "*" || raw === "all") {
289 return ["link", "button", "form"];
290 }
291
292 return raw.split(",").map(s => s.trim()).filter(Boolean);
293 }
294
295 function normalizeClassFilter(v) {
296 if (v === undefined || v === null || v === "") return ["dbxConfirm"];
297
298 const raw = String(v).trim();
299 if (raw === "*") return ["*"];
300
301 return raw.split(",").map(s => s.trim()).filter(Boolean);
302 }
303
304 function normalizeButtons(v) {
305 const raw = String(v || "yesno").toLowerCase().trim();
306
307 if (raw === "yesnocancel") return "yesnocancel";
308 if (raw === "cancel" || raw === "cancelonly") return "cancel";
309
310 return "yesno";
311 }
312
313 function elementType(el) {
314 if (!el || !el.tagName) return "";
315
316 const tag = el.tagName.toLowerCase();
317
318 if (tag === "form") return "form";
319 if (tag === "a") return "link";
320 if (tag === "button") return "button";
321
322 if (tag === "input") {
323 const type = str(el.getAttribute("type"), "").toLowerCase();
324 if (type === "button" || type === "submit" || type === "image") {
325 return "button";
326 }
327 }
328
329 return "";
330 }
331
332 function bindMatches(type, bindList) {
333 if (!type) return false;
334 if (!Array.isArray(bindList) || !bindList.length) return false;
335 return bindList.includes(type);
336 }
337
338 function classMatches(el, classList) {
339 if (!el) return false;
340 if (!Array.isArray(classList) || !classList.length) return false;
341 if (classList.includes("*")) return true;
342
343 for (let i = 0; i < classList.length; i++) {
344 if (el.classList.contains(classList[i])) return true;
345 }
346
347 return false;
348 }
349
350 function readAttr(el, name) {
351 if (!el || !el.getAttribute) return "";
352 const v = el.getAttribute(name);
353 return v == null ? "" : String(v).trim();
354 }
355
356 function readConfirm(el, key) {
357 return readAttr(el, "data-confirm-" + key) || readAttr(el, "data-confirm_" + key);
358 }
359
360 function emit(name, data) {
361 if (dbx.event && typeof dbx.event.emit === "function") {
362 dbx.event.emit(name, data);
363 }
364 }
365
366 function htmlOrText(el, value, allowHtml) {
367 if (!el) return;
368
369 if (allowHtml) {
370 el.innerHTML = value || "";
371 } else {
372 el.textContent = value || "";
373 }
374 }
375
376 function iconFromTitle(title) {
377 const raw = String(title || "").toLowerCase();
378 if (raw.includes("trash") || raw.includes("loesch") || raw.includes("lösch")) {
379 return "bi-trash";
380 }
381 if (raw.includes("copy") || raw.includes("kopier")) {
382 return "bi-copy";
383 }
384 if (raw.includes("warn") || raw.includes("achtung")) {
385 return "bi-exclamation-triangle";
386 }
387 if (raw.includes("mail") || raw.includes("e-mail") || raw.includes("email")) {
388 return "bi-envelope-check";
389 }
390 return "bi-question-circle";
391 }
392
393 function ensureRoot(el) {
394 return el || document.body;
395 }
396
397 function getRootConfigs(root) {
398
399 if (!root) return [];
400
401 if (Array.isArray(root._dbxConfirmConfigs)) {
402 return root._dbxConfirmConfigs;
403 }
404
405 let out = [];
406
407 if (dbx.declare && dbx.declare.schemas && dbx.declare.schemas.confirm) {
408 out = dbx.declare.resolve("confirm", root);
409 } else {
410 const attr = readAttr(root, "data-dbx");
411 const list = dbx.parseData(attr).filter(cfg => cfg.lib === "confirm");
412
413 out = list.map((cfg, index) => {
414 return {
415 _index: index,
416 class: normalizeClassFilter(cfg.class),
417 bind: normalizeBind(cfg.bind),
418 title: str(cfg.title, ""),
419 question: str(cfg.question, ""),
420 hint: str(cfg.hint, ""),
421 buttons: normalizeButtons(cfg.buttons),
422 titlehtml: bool(cfg.titlehtml, true),
423 questionhtml: bool(cfg.questionhtml, true),
424 hinthtml: bool(cfg.hinthtml, true),
425 labelhtml: bool(cfg.labelhtml, true),
426 labelyes: str(cfg.labelyes, "<i class=\"bi bi-check-lg\"></i> Ja"),
427 labelno: str(cfg.labelno, "<i class=\"bi bi-x-lg\"></i> Nein"),
428 labelcancel: str(cfg.labelcancel, "<i class=\"bi bi-slash-circle\"></i> Abbruch"),
429 closable: bool(cfg.closable, true),
430 backdropclose: bool(cfg.backdropclose, false),
431 escclose: bool(cfg.escclose, true)
432 };
433 });
434
435 if (!out.length) {
436 out.push({
437 _index: 0,
438 class: normalizeClassFilter("dbxConfirm"),
439 bind: normalizeBind("link,button,form"),
440 title: "",
441 question: "",
442 hint: "",
443 buttons: "yesno",
444 titlehtml: true,
445 questionhtml: true,
446 hinthtml: true,
447 labelhtml: true,
448 labelyes: "<i class=\"bi bi-check-lg\"></i> Ja",
449 labelno: "<i class=\"bi bi-x-lg\"></i> Nein",
450 labelcancel: "<i class=\"bi bi-slash-circle\"></i> Abbruch",
451 closable: true,
452 backdropclose: false,
453 escclose: true
454 });
455 }
456 }
457
458 root._dbxConfirmConfigs = out;
459
460 return out;
461 }
462
463 function findMatchingConfig(root, source, type) {
464
465 const configs = getRootConfigs(root);
466
467 for (let i = 0; i < configs.length; i++) {
468 const cfg = configs[i];
469
470 if (!bindMatches(type, cfg.bind)) continue;
471 if (!classMatches(source, cfg.class)) continue;
472
473 return cfg;
474 }
475
476 return null;
477 }
478
479 function readOptionsFromElement(source, cfg) {
480
481 const options = {
482 id: readConfirm(source, "id") || "",
483 title: readConfirm(source, "title") || cfg.title,
484 question: readConfirm(source, "question") || readAttr(source, "data-confirm") || cfg.question,
485 hint: readConfirm(source, "hint") || cfg.hint,
486 buttons: normalizeButtons(readConfirm(source, "buttons") || cfg.buttons),
487
488 titlehtml: bool(readConfirm(source, "titlehtml"), cfg.titlehtml),
489 questionhtml: bool(readConfirm(source, "questionhtml"), cfg.questionhtml),
490 hinthtml: bool(readConfirm(source, "hinthtml"), cfg.hinthtml),
491 labelhtml: bool(readConfirm(source, "labelhtml"), cfg.labelhtml),
492
493 labelyes: readConfirm(source, "labelyes") || cfg.labelyes,
494 labelno: readConfirm(source, "labelno") || cfg.labelno,
495 labelcancel: readConfirm(source, "labelcancel") || cfg.labelcancel,
496
497 closable: bool(readConfirm(source, "closable"), cfg.closable),
498 backdropclose: bool(readConfirm(source, "backdropclose"), cfg.backdropclose),
499 escclose: bool(readConfirm(source, "escclose"), cfg.escclose),
500
501 root: null,
502 source: source
503 };
504
505 return options;
506 }
507
508 function buildButtonsMarkup(opts) {
509
510 const actions = [];
511
512 if (opts.buttons === "yesno") {
513 actions.push("yes", "no");
514 }
515
516 if (opts.buttons === "yesnocancel") {
517 actions.push("yes", "no", "cancel");
518 }
519
520 if (opts.buttons === "cancel") {
521 actions.push("cancel");
522 }
523
524 return actions;
525 }
526
527 function getMountEl(root, opts) {
528
529 if (opts && opts.mountEl) {
530 return opts.mountEl;
531 }
532
533 return document.body;
534 }
535
536 function numericZIndex(el) {
537 if (!el || el.nodeType !== 1) return 0;
538 const z = parseInt(window.getComputedStyle(el).zIndex, 10);
539 return Number.isFinite(z) ? z : 0;
540 }
541
542 function maxAncestorZIndex(el) {
543 let max = 0;
544 let cur = el && el.nodeType === 1 ? el : null;
545 while (cur && cur !== document.documentElement) {
546 max = Math.max(max, numericZIndex(cur));
547 cur = cur.parentElement;
548 }
549 return max;
550 }
551
552 function autoConfirmZIndex(root, opts) {
553 if (opts && opts.zIndex > 0) return opts.zIndex;
554 let max = 5000;
555 max = Math.max(max, maxAncestorZIndex(opts && opts.source));
556 max = Math.max(max, maxAncestorZIndex(opts && opts.callerEl));
557 max = Math.max(max, maxAncestorZIndex(root));
558 max = Math.max(max, maxAncestorZIndex(opts && opts.mountEl));
559 document.querySelectorAll(".dbx-window, .dbx-window-overlay, .dbx-confirm-overlay, .dbx-confirm-dialog").forEach(el => {
560 if (el.offsetParent !== null || el === document.body || el.classList.contains("dbx-confirm-overlay")) {
561 max = Math.max(max, numericZIndex(el));
562 }
563 });
564 return Math.min(2147483647, max + 20);
565 }
566
567 function createDialogElements(root, opts) {
568
569 const mountEl = getMountEl(root, opts);
570
571 const overlay = document.createElement("div");
572 overlay.className = "dbx-confirm-overlay";
573 overlay.style.position = "fixed";
574 overlay.style.inset = "0";
575 overlay.style.zIndex = String(autoConfirmZIndex(root, opts));
576 overlay.style.background = "rgba(0,0,0,0.35)";
577 overlay.style.backdropFilter = "blur(2px)";
578 overlay.style.display = "flex";
579 overlay.style.alignItems = "center";
580 overlay.style.justifyContent = "center";
581 overlay.style.padding = "1rem";
582
583 const dialog = document.createElement("div");
584 dialog.className = "dbx-confirm-dialog card shadow-lg";
585 dialog.style.width = "100%";
586 dialog.style.maxWidth = "640px";
587 dialog.style.border = "1px solid rgba(62, 129, 218, 0.28)";
588 dialog.style.borderRadius = "10px";
589 dialog.style.overflow = "hidden";
590 dialog.style.boxShadow = "0 22px 70px rgba(28, 57, 99, 0.28)";
591
592 const header = document.createElement("div");
593 header.className = "card-header dbx-confirm-header d-flex align-items-center justify-content-between gap-3";
594 header.style.background = "linear-gradient(180deg, #d8ebff 0%, #a9cffc 100%)";
595 header.style.borderBottom = "1px solid rgba(58, 123, 208, 0.34)";
596 header.style.color = "#132033";
597 header.style.minHeight = "70px";
598 header.style.padding = "12px 16px";
599
600 const titleWrap = document.createElement("div");
601 titleWrap.className = "dbx-confirm-titlewrap d-flex align-items-center gap-3";
602 titleWrap.style.minWidth = "0";
603
604 const titleIcon = document.createElement("span");
605 titleIcon.className = "dbx-confirm-title-icon";
606 titleIcon.innerHTML = "<i class=\"bi bi-question-circle\"></i>";
607 titleIcon.style.alignItems = "center";
608 titleIcon.style.background = "rgba(255,255,255,0.58)";
609 titleIcon.style.border = "1px solid rgba(35, 103, 194, 0.28)";
610 titleIcon.style.borderRadius = "8px";
611 titleIcon.style.color = "#0d6efd";
612 titleIcon.style.display = "inline-flex";
613 titleIcon.style.flex = "0 0 46px";
614 titleIcon.style.fontSize = "1.35rem";
615 titleIcon.style.height = "46px";
616 titleIcon.style.justifyContent = "center";
617 titleIcon.style.width = "46px";
618
619 const title = document.createElement("div");
620 title.className = "dbx-confirm-title fw-semibold";
621 title.style.fontSize = "1.08rem";
622 title.style.lineHeight = "1.25";
623 title.style.minWidth = "0";
624
625 const btnClose = document.createElement("button");
626 btnClose.type = "button";
627 btnClose.className = "btn btn-sm btn-outline-primary";
628 btnClose.innerHTML = "<i class=\"bi bi-x-lg\"></i>";
629 btnClose.style.background = "rgba(255,255,255,0.52)";
630 btnClose.style.borderColor = "rgba(35, 103, 194, 0.34)";
631 btnClose.style.flex = "0 0 auto";
632
633 const body = document.createElement("div");
634 body.className = "card-body dbx-confirm-body";
635 body.style.background = "linear-gradient(180deg, #ffffff 0%, #f6f9fd 100%)";
636 body.style.padding = "18px 20px";
637
638 const question = document.createElement("div");
639 question.className = "dbx-confirm-question mb-2";
640 question.style.color = "#182231";
641 question.style.fontSize = "1rem";
642 question.style.lineHeight = "1.45";
643
644 const hint = document.createElement("div");
645 hint.className = "dbx-confirm-hint text-muted small mb-3";
646 hint.style.background = "#f8fafc";
647 hint.style.border = "1px solid #e2e8f0";
648 hint.style.borderRadius = "8px";
649 hint.style.padding = "10px 12px";
650
651 const footer = document.createElement("div");
652 footer.className = "card-footer d-flex justify-content-end gap-2 flex-wrap";
653 footer.style.background = "#f8fbff";
654 footer.style.borderTop = "1px solid #dce8f7";
655 footer.style.padding = "12px 16px";
656
657 titleWrap.appendChild(titleIcon);
658 titleWrap.appendChild(title);
659 header.appendChild(titleWrap);
660 header.appendChild(btnClose);
661
662 body.appendChild(question);
663 body.appendChild(hint);
664
665 dialog.appendChild(header);
666 dialog.appendChild(body);
667 dialog.appendChild(footer);
668
669 overlay.appendChild(dialog);
670 mountEl.appendChild(overlay);
671
672 return {
673 mountEl,
674 overlay,
675 dialog,
676 header,
677 titleIcon,
678 title,
679 btnClose,
680 body,
681 question,
682 hint,
683 footer
684 };
685 }
686
687 function applyDialogState(entry) {
688
689 const opts = entry.options;
690 const ui = entry.ui;
691
692 htmlOrText(ui.title, opts.title, opts.titlehtml);
693 htmlOrText(ui.question, opts.question, opts.questionhtml);
694 ui.titleIcon.innerHTML = "<i class=\"bi " + iconFromTitle(opts.title) + "\"></i>";
695
696 if (opts.hint) {
697 ui.hint.style.display = "";
698 htmlOrText(ui.hint, opts.hint, opts.hinthtml);
699 } else {
700 ui.hint.style.display = "none";
701 ui.hint.innerHTML = "";
702 }
703
704 ui.btnClose.style.display = opts.closable ? "" : "none";
705
706 ui.footer.innerHTML = "";
707
708 const actions = buildButtonsMarkup(opts);
709
710 actions.forEach(action => {
711
712 const btn = document.createElement("button");
713 btn.type = "button";
714 btn.className = "btn";
715
716 if (action === "yes") {
717 btn.className += " btn-primary";
718 btn.setAttribute("data-confirm-action", "yes");
719 htmlOrText(btn, opts.labelyes, opts.labelhtml);
720 }
721
722 if (action === "no") {
723 btn.className += " btn-outline-secondary";
724 btn.setAttribute("data-confirm-action", "no");
725 htmlOrText(btn, opts.labelno, opts.labelhtml);
726 }
727
728 if (action === "cancel") {
729 btn.className += " btn-outline-danger";
730 btn.setAttribute("data-confirm-action", "cancel");
731 htmlOrText(btn, opts.labelcancel, opts.labelhtml);
732 }
733
734 ui.footer.appendChild(btn);
735 });
736 }
737
738 function closeDialog(entry, result) {
739
740 if (!entry || entry.closed) return;
741
742 entry.closed = true;
743
744 const id = entry.id;
745 const ui = entry.ui;
746
747 if (ui && ui.overlay && ui.overlay.parentNode) {
748 ui.overlay.parentNode.removeChild(ui.overlay);
749 }
750
751 delete _dialogs[id];
752
753 emit("confirm:result", {
754 id: id,
755 action: result.action,
756 source: entry.options.source || null,
757 root: entry.options.root || null,
758 dialog: null
759 });
760
761 entry.resolve(result);
762 }
763
764 function findAjaxRoot(source) {
765
766 if (!source) return null;
767
768 const root = source.closest("[data-dbx-ajax-root='1']");
769 if (!root) return null;
770
771 if (!dbx.ajax || typeof dbx.ajax.run !== "function") {
772 return null;
773 }
774
775 return root;
776 }
777
778 function continueOriginalAction(entry) {
779
780 const source = entry.options.source;
781 if (!source) return;
782
783 const type = elementType(source);
784 if (!type) return;
785
786 const ajaxRoot = findAjaxRoot(source);
787
788 if (ajaxRoot) {
789 dbx.ajax.run(ajaxRoot, source, {}, null).catch(err => {
790 dbx.error("[dbx.confirm] ajax continue failed", err);
791 });
792 return;
793 }
794
795 source.__dbxConfirmBypass = true;
796
797 if (type === "link") {
798 source.click();
799 return;
800 }
801
802 if (type === "button") {
803
804 const btnType = str(source.getAttribute("type"), "submit").toLowerCase();
805 const form = source.form || source.closest("form");
806
807 if ((btnType === "submit" || btnType === "image") && form) {
808 form.__dbxConfirmBypass = true;
809
810 if (typeof form.requestSubmit === "function") {
811 form.requestSubmit(source);
812 } else {
813 form.submit();
814 }
815
816 return;
817 }
818
819 source.click();
820 return;
821 }
822
823 if (type === "form") {
824 source.__dbxConfirmBypass = true;
825
826 if (typeof source.requestSubmit === "function") {
827 source.requestSubmit();
828 } else {
829 source.submit();
830 }
831 }
832 }
833
834 function openDialog(rawOptions) {
835
836 const opts = {
837 id: str(rawOptions.id, "dbx-confirm-" + (++_uid)),
838 root: ensureRoot(rawOptions.root),
839 source: rawOptions.source || rawOptions.callerEl || rawOptions.caller || null,
840 callerEl: rawOptions.callerEl || rawOptions.caller || rawOptions.source || null,
841 mountEl: rawOptions.mountEl || null,
842 zIndex: parseInt(rawOptions.zIndex, 10) || 0,
843
844 title: str(rawOptions.title, ""),
845 question: str(rawOptions.question, ""),
846 hint: str(rawOptions.hint, ""),
847 buttons: normalizeButtons(rawOptions.buttons),
848
849 titlehtml: bool(rawOptions.titlehtml, true),
850 questionhtml: bool(rawOptions.questionhtml, true),
851 hinthtml: bool(rawOptions.hinthtml, true),
852 labelhtml: bool(rawOptions.labelhtml, true),
853
854 labelyes: str(rawOptions.labelyes, "<i class=\"bi bi-check-lg\"></i> Ja"),
855 labelno: str(rawOptions.labelno, "<i class=\"bi bi-x-lg\"></i> Nein"),
856 labelcancel: str(rawOptions.labelcancel, "<i class=\"bi bi-slash-circle\"></i> Abbruch"),
857
858 closable: bool(rawOptions.closable, true),
859 backdropclose: bool(rawOptions.backdropclose, false),
860 escclose: bool(rawOptions.escclose, true),
861
862 onyes: (typeof rawOptions.onyes === "function") ? rawOptions.onyes : null,
863 onno: (typeof rawOptions.onno === "function") ? rawOptions.onno : null,
864 oncancel: (typeof rawOptions.oncancel === "function") ? rawOptions.oncancel : null
865 };
866
867 if (_dialogs[opts.id]) {
868 dbx.confirm.close(opts.id, { action: "close" });
869 }
870
871 return new Promise((resolve) => {
872
873 const ui = createDialogElements(opts.root, opts);
874
875 const entry = {
876 id: opts.id,
877 options: opts,
878 ui: ui,
879 resolve: resolve,
880 closed: false,
881 keyHandler: null
882 };
883
884 _dialogs[opts.id] = entry;
885
886 applyDialogState(entry);
887
888 emit("confirm:open", {
889 id: opts.id,
890 source: opts.source || null,
891 root: opts.root || null,
892 dialog: ui.dialog
893 });
894
895 ui.footer.addEventListener("click", function (e) {
896
897 const btn = e.target.closest("[data-confirm-action]");
898 if (!btn) return;
899
900 const action = btn.getAttribute("data-confirm-action");
901
902 if (action === "yes" && opts.onyes) {
903 try {
904 opts.onyes(entry);
905 } catch (err) {
906 dbx.error("[dbx.confirm] onyes failed", err);
907 }
908 }
909
910 if (action === "no" && opts.onno) {
911 try {
912 opts.onno(entry);
913 } catch (err) {
914 dbx.error("[dbx.confirm] onno failed", err);
915 }
916 }
917
918 if (action === "cancel" && opts.oncancel) {
919 try {
920 opts.oncancel(entry);
921 } catch (err) {
922 dbx.error("[dbx.confirm] oncancel failed", err);
923 }
924 }
925
926 closeDialog(entry, {
927 id: opts.id,
928 action: action,
929 source: opts.source || null,
930 root: opts.root || null,
931 dialog: null
932 });
933
934 if (action === "yes" && opts.source) {
935 continueOriginalAction(entry);
936 }
937 });
938
939 ui.btnClose.addEventListener("click", function () {
940
941 if (!opts.closable) return;
942
943 closeDialog(entry, {
944 id: opts.id,
945 action: "close",
946 source: opts.source || null,
947 root: opts.root || null,
948 dialog: null
949 });
950 });
951
952 ui.overlay.addEventListener("click", function (e) {
953
954 if (e.target !== ui.overlay) return;
955 if (!opts.backdropclose) return;
956
957 closeDialog(entry, {
958 id: opts.id,
959 action: "close",
960 source: opts.source || null,
961 root: opts.root || null,
962 dialog: null
963 });
964 });
965
966 entry.keyHandler = function (e) {
967
968 if (e.key !== "Escape") return;
969 if (!opts.escclose) return;
970
971 closeDialog(entry, {
972 id: opts.id,
973 action: "close",
974 source: opts.source || null,
975 root: opts.root || null,
976 dialog: null
977 });
978 };
979
980 document.addEventListener("keydown", entry.keyHandler, true);
981
982 const oldResolve = entry.resolve;
983
984 entry.resolve = function (result) {
985 document.removeEventListener("keydown", entry.keyHandler, true);
986 oldResolve(result);
987 };
988 });
989 }
990
991
992 /* =========================================================
993 * PUBLIC API
994 * ========================================================= */
995
996 dbx.confirm.open = function (options) {
997 return openDialog(options || {});
998 };
999
1000 dbx.confirm.update = function (id, patch) {
1001
1002 const entry = _dialogs[id];
1003 if (!entry) return false;
1004
1005 const opts = entry.options;
1006 const data = patch || {};
1007
1008 if ("title" in data) opts.title = str(data.title, "");
1009 if ("question" in data) opts.question = str(data.question, "");
1010 if ("hint" in data) opts.hint = str(data.hint, "");
1011
1012 applyDialogState(entry);
1013 return true;
1014 };
1015
1016 dbx.confirm.close = function (id, result) {
1017
1018 const entry = _dialogs[id];
1019 if (!entry) return false;
1020
1021 closeDialog(entry, {
1022 id: id,
1023 action: (result && result.action) ? result.action : "close",
1024 source: entry.options.source || null,
1025 root: entry.options.root || null,
1026 dialog: null
1027 });
1028
1029 return true;
1030 };
1031
1032 dbx.confirm.get = function (id) {
1033 return _dialogs[id] || null;
1034 };
1035
1036
1037 /* =========================================================
1038 * DECLARE SCHEMA (Defaults + data-* Aliase)
1039 * ========================================================= */
1040
1041 if (dbx.declare && typeof dbx.declare.registerSchema === "function") {
1042
1043 dbx.declare.registerSchema("confirm", {
1044 fields: {
1045 class: {
1046 default: "dbxConfirm"
1047 },
1048 bind: {
1049 default: "link,button,form",
1050 aliases: ["data-confirm-bind"]
1051 },
1052 title: {
1053 default: "",
1054 aliases: ["data-confirm-title", "data-title"]
1055 },
1056 question: {
1057 default: "",
1058 aliases: ["data-confirm-question", "data-confirm"]
1059 },
1060 hint: {
1061 default: "",
1062 aliases: ["data-confirm-hint"]
1063 },
1064 buttons: {
1065 default: "yesno",
1066 aliases: ["data-confirm-buttons"]
1067 },
1068 titlehtml: {
1069 default: "1",
1070 aliases: ["data-confirm-titlehtml"]
1071 },
1072 questionhtml: {
1073 default: "1",
1074 aliases: ["data-confirm-questionhtml"]
1075 },
1076 hinthtml: {
1077 default: "1",
1078 aliases: ["data-confirm-hinthtml"]
1079 },
1080 labelhtml: {
1081 default: "1",
1082 aliases: ["data-confirm-labelhtml"]
1083 },
1084 labelyes: {
1085 default: "<i class=\"bi bi-check-lg\"></i> Ja",
1086 aliases: ["data-confirm-labelyes"]
1087 },
1088 labelno: {
1089 default: "<i class=\"bi bi-x-lg\"></i> Nein",
1090 aliases: ["data-confirm-labelno"]
1091 },
1092 labelcancel: {
1093 default: "<i class=\"bi bi-slash-circle\"></i> Abbruch",
1094 aliases: ["data-confirm-labelcancel"]
1095 },
1096 closable: {
1097 default: "1",
1098 aliases: ["data-confirm-closable"]
1099 },
1100 backdropclose: {
1101 default: "0",
1102 aliases: ["data-confirm-backdropclose"]
1103 },
1104 escclose: {
1105 default: "1",
1106 aliases: ["data-confirm-escclose"]
1107 }
1108 }
1109 });
1110
1111 dbx.declare.transforms.confirm = function (raw) {
1112 return {
1113 _index: raw._index,
1114 class: normalizeClassFilter(raw.class),
1115 bind: normalizeBind(raw.bind),
1116 title: str(raw.title, ""),
1117 question: str(raw.question, ""),
1118 hint: str(raw.hint, ""),
1119 buttons: normalizeButtons(raw.buttons),
1120 titlehtml: bool(raw.titlehtml, true),
1121 questionhtml: bool(raw.questionhtml, true),
1122 hinthtml: bool(raw.hinthtml, true),
1123 labelhtml: bool(raw.labelhtml, true),
1124 labelyes: str(raw.labelyes, "<i class=\"bi bi-check-lg\"></i> Ja"),
1125 labelno: str(raw.labelno, "<i class=\"bi bi-x-lg\"></i> Nein"),
1126 labelcancel: str(raw.labelcancel, "<i class=\"bi bi-slash-circle\"></i> Abbruch"),
1127 closable: bool(raw.closable, true),
1128 backdropclose: bool(raw.backdropclose, false),
1129 escclose: bool(raw.escclose, true)
1130 };
1131 };
1132 }
1133
1134
1135 /* =========================================================
1136 * FEATURE
1137 * ========================================================= */
1138
1139 dbx.feature.register("confirm", {
1140
1141 scope: "element",
1142
1143 priority: "mid",
1144
1145 init(el, cfg) {
1146
1147 if (!el) return;
1148
1149 el.__dbxInitialized = el.__dbxInitialized || {};
1150 if (el.__dbxInitialized["confirm"]) return;
1151 el.__dbxInitialized["confirm"] = true;
1152
1153 el.setAttribute("data-dbx-confirm-root", "1");
1154 getRootConfigs(el);
1155
1156 dbx.log("[dbx.confirm] init", {
1157 rootId: el.id || "",
1158 configs: el._dbxConfirmConfigs
1159 });
1160
1161 el.addEventListener("click", function (e) {
1162
1163 const source = e.target.closest("a, button, input[type='button'], input[type='submit'], input[type='image']");
1164 if (!source) return;
1165 if (!el.contains(source)) return;
1166
1167 if (source.__dbxConfirmBypass === true) {
1168 delete source.__dbxConfirmBypass;
1169 return;
1170 }
1171
1172 const nearestRoot = source.closest("[data-dbx-confirm-root='1']");
1173 if (nearestRoot !== el) return;
1174
1175 const type = elementType(source);
1176 const cfgMatch = findMatchingConfig(el, source, type);
1177
1178 if (!cfgMatch) return;
1179
1180 e.preventDefault();
1181
1182 const options = readOptionsFromElement(source, cfgMatch);
1183 options.root = el;
1184
1185 openDialog(options).catch(err => {
1186 dbx.error("[dbx.confirm] open failed", err);
1187 });
1188 }, true);
1189
1190 el.addEventListener("submit", function (e) {
1191
1192 const form = e.target.closest("form");
1193 if (!form) return;
1194 if (!el.contains(form)) return;
1195
1196 if (form.__dbxConfirmBypass === true) {
1197 delete form.__dbxConfirmBypass;
1198 return;
1199 }
1200
1201 const nearestRoot = form.closest("[data-dbx-confirm-root='1']");
1202 if (nearestRoot !== el) return;
1203
1204 const type = elementType(form);
1205 const cfgMatch = findMatchingConfig(el, form, type);
1206
1207 if (!cfgMatch) return;
1208
1209 e.preventDefault();
1210
1211 const options = readOptionsFromElement(form, cfgMatch);
1212 options.root = el;
1213
1214 openDialog(options).catch(err => {
1215 dbx.error("[dbx.confirm] open failed", err);
1216 });
1217 }, true);
1218 },
1219
1220 destroy(el, cfg) {
1221
1222 if (!el) return;
1223
1224 if (el.__dbxInitialized && el.__dbxInitialized["confirm"]) {
1225 delete el.__dbxInitialized["confirm"];
1226 }
1227
1228 delete el._dbxConfirmConfigs;
1229 el.removeAttribute("data-dbx-confirm-root");
1230
1231 dbx.log("[dbx.confirm] destroy", {
1232 rootId: el.id || ""
1233 });
1234 }
1235
1236 });
1237
1238})(window, document);