dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
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 defaultLabels() {
284 return {
285 yes: '<i class="bi bi-check-lg"></i> ' + dbx.translate({
286 de: "Ja",
287 en: "Yes",
288 es: "Sí"
289 }),
290 no: '<i class="bi bi-x-lg"></i> ' + dbx.translate({
291 de: "Nein",
292 en: "No",
293 es: "No"
294 }),
295 cancel: '<i class="bi bi-slash-circle"></i> ' + dbx.translate({
296 de: "Abbrechen",
297 en: "Cancel",
298 es: "Cancelar"
299 })
300 };
301 }
302
303 function normalizeBind(v) {
304 if (v === undefined || v === null || v === "") return ["link", "button", "form"];
305
306 const raw = String(v).toLowerCase().trim();
307
308 if (raw === "*" || raw === "all") {
309 return ["link", "button", "form"];
310 }
311
312 return raw.split(",").map(s => s.trim()).filter(Boolean);
313 }
314
315 function normalizeClassFilter(v) {
316 if (v === undefined || v === null || v === "") return ["dbxConfirm"];
317
318 const raw = String(v).trim();
319 if (raw === "*") return ["*"];
320
321 return raw.split(",").map(s => s.trim()).filter(Boolean);
322 }
323
324 function normalizeButtons(v) {
325 const raw = String(v || "yesno").toLowerCase().trim();
326
327 if (raw === "yesnocancel") return "yesnocancel";
328 if (raw === "cancel" || raw === "cancelonly") return "cancel";
329
330 return "yesno";
331 }
332
333 function elementType(el) {
334 if (!el || !el.tagName) return "";
335
336 const tag = el.tagName.toLowerCase();
337
338 if (tag === "form") return "form";
339 if (tag === "a") return "link";
340 if (tag === "button") return "button";
341
342 if (tag === "input") {
343 const type = str(el.getAttribute("type"), "").toLowerCase();
344 if (type === "button" || type === "submit" || type === "image") {
345 return "button";
346 }
347 }
348
349 return "";
350 }
351
352 function bindMatches(type, bindList) {
353 if (!type) return false;
354 if (!Array.isArray(bindList) || !bindList.length) return false;
355 return bindList.includes(type);
356 }
357
358 function classMatches(el, classList) {
359 if (!el) return false;
360 if (!Array.isArray(classList) || !classList.length) return false;
361 if (classList.includes("*")) return true;
362
363 for (let i = 0; i < classList.length; i++) {
364 if (el.classList.contains(classList[i])) return true;
365 }
366
367 return false;
368 }
369
370 function readAttr(el, name) {
371 if (!el || !el.getAttribute) return "";
372 const v = el.getAttribute(name);
373 return v == null ? "" : String(v).trim();
374 }
375
376 function readConfirm(el, key) {
377 return readAttr(el, "data-confirm-" + key) || readAttr(el, "data-confirm_" + key);
378 }
379
380 function emit(name, data) {
381 if (dbx.event && typeof dbx.event.emit === "function") {
382 dbx.event.emit(name, data);
383 }
384 }
385
386 function htmlOrText(el, value, allowHtml) {
387 if (!el) return;
388
389 if (allowHtml) {
390 el.innerHTML = value || "";
391 } else {
392 el.textContent = value || "";
393 }
394 }
395
396 function iconFromTitle(title) {
397 const raw = String(title || "").toLowerCase();
398 if (raw.includes("trash") || raw.includes("loesch") || raw.includes("lösch")) {
399 return "bi-trash";
400 }
401 if (raw.includes("copy") || raw.includes("kopier")) {
402 return "bi-copy";
403 }
404 if (raw.includes("warn") || raw.includes("achtung")) {
405 return "bi-exclamation-triangle";
406 }
407 if (raw.includes("mail") || raw.includes("e-mail") || raw.includes("email")) {
408 return "bi-envelope-check";
409 }
410 return "bi-question-circle";
411 }
412
413 function ensureRoot(el) {
414 return el || document.body;
415 }
416
417 function getRootConfigs(root) {
418
419 if (!root) return [];
420
421 if (Array.isArray(root._dbxConfirmConfigs)) {
422 return root._dbxConfirmConfigs;
423 }
424
425 let out = [];
426
427 if (dbx.declare && dbx.declare.schemas && dbx.declare.schemas.confirm) {
428 out = dbx.declare.resolve("confirm", root);
429 } else {
430 const attr = readAttr(root, "data-dbx");
431 const list = dbx.parseData(attr).filter(cfg => cfg.lib === "confirm");
432 const labels = defaultLabels();
433
434 out = list.map((cfg, index) => {
435 return {
436 _index: index,
437 class: normalizeClassFilter(cfg.class),
438 bind: normalizeBind(cfg.bind),
439 title: str(cfg.title, ""),
440 question: str(cfg.question, ""),
441 hint: str(cfg.hint, ""),
442 buttons: normalizeButtons(cfg.buttons),
443 titlehtml: bool(cfg.titlehtml, true),
444 questionhtml: bool(cfg.questionhtml, true),
445 hinthtml: bool(cfg.hinthtml, true),
446 labelhtml: bool(cfg.labelhtml, true),
447 labelyes: str(cfg.labelyes, labels.yes),
448 labelno: str(cfg.labelno, labels.no),
449 labelcancel: str(cfg.labelcancel, labels.cancel),
450 closable: bool(cfg.closable, true),
451 backdropclose: bool(cfg.backdropclose, false),
452 escclose: bool(cfg.escclose, true)
453 };
454 });
455
456 if (!out.length) {
457 out.push({
458 _index: 0,
459 class: normalizeClassFilter("dbxConfirm"),
460 bind: normalizeBind("link,button,form"),
461 title: "",
462 question: "",
463 hint: "",
464 buttons: "yesno",
465 titlehtml: true,
466 questionhtml: true,
467 hinthtml: true,
468 labelhtml: true,
469 labelyes: labels.yes,
470 labelno: labels.no,
471 labelcancel: labels.cancel,
472 closable: true,
473 backdropclose: false,
474 escclose: true
475 });
476 }
477 }
478
479 root._dbxConfirmConfigs = out;
480
481 return out;
482 }
483
484 function findMatchingConfig(root, source, type) {
485
486 const configs = getRootConfigs(root);
487
488 for (let i = 0; i < configs.length; i++) {
489 const cfg = configs[i];
490
491 if (!bindMatches(type, cfg.bind)) continue;
492 if (!classMatches(source, cfg.class)) continue;
493
494 return cfg;
495 }
496
497 return null;
498 }
499
500 function readOptionsFromElement(source, cfg) {
501
502 const options = {
503 id: readConfirm(source, "id") || "",
504 title: readConfirm(source, "title") || cfg.title,
505 question: readConfirm(source, "question") || readAttr(source, "data-confirm") || cfg.question,
506 hint: readConfirm(source, "hint") || cfg.hint,
507 buttons: normalizeButtons(readConfirm(source, "buttons") || cfg.buttons),
508
509 titlehtml: bool(readConfirm(source, "titlehtml"), cfg.titlehtml),
510 questionhtml: bool(readConfirm(source, "questionhtml"), cfg.questionhtml),
511 hinthtml: bool(readConfirm(source, "hinthtml"), cfg.hinthtml),
512 labelhtml: bool(readConfirm(source, "labelhtml"), cfg.labelhtml),
513
514 labelyes: readConfirm(source, "labelyes") || cfg.labelyes,
515 labelno: readConfirm(source, "labelno") || cfg.labelno,
516 labelcancel: readConfirm(source, "labelcancel") || cfg.labelcancel,
517
518 closable: bool(readConfirm(source, "closable"), cfg.closable),
519 backdropclose: bool(readConfirm(source, "backdropclose"), cfg.backdropclose),
520 escclose: bool(readConfirm(source, "escclose"), cfg.escclose),
521
522 root: null,
523 source: source
524 };
525
526 return options;
527 }
528
529 function buildButtonsMarkup(opts) {
530
531 const actions = [];
532
533 if (opts.buttons === "yesno") {
534 actions.push("yes", "no");
535 }
536
537 if (opts.buttons === "yesnocancel") {
538 actions.push("yes", "no", "cancel");
539 }
540
541 if (opts.buttons === "cancel") {
542 actions.push("cancel");
543 }
544
545 return actions;
546 }
547
548 function getMountEl(root, opts) {
549
550 if (opts && opts.mountEl) {
551 return opts.mountEl;
552 }
553
554 return document.body;
555 }
556
557 function numericZIndex(el) {
558 if (!el || el.nodeType !== 1) return 0;
559 const z = parseInt(window.getComputedStyle(el).zIndex, 10);
560 return Number.isFinite(z) ? z : 0;
561 }
562
563 function maxAncestorZIndex(el) {
564 let max = 0;
565 let cur = el && el.nodeType === 1 ? el : null;
566 while (cur && cur !== document.documentElement) {
567 max = Math.max(max, numericZIndex(cur));
568 cur = cur.parentElement;
569 }
570 return max;
571 }
572
573 function autoConfirmZIndex(root, opts) {
574 if (opts && opts.zIndex > 0) return opts.zIndex;
575 let max = 5000;
576 max = Math.max(max, maxAncestorZIndex(opts && opts.source));
577 max = Math.max(max, maxAncestorZIndex(opts && opts.callerEl));
578 max = Math.max(max, maxAncestorZIndex(root));
579 max = Math.max(max, maxAncestorZIndex(opts && opts.mountEl));
580 document.querySelectorAll(".dbx-window, .dbx-window-overlay, .dbx-confirm-overlay, .dbx-confirm-dialog").forEach(el => {
581 if (el.offsetParent !== null || el === document.body || el.classList.contains("dbx-confirm-overlay")) {
582 max = Math.max(max, numericZIndex(el));
583 }
584 });
585 return Math.min(2147483647, max + 20);
586 }
587
588 function createDialogElements(root, opts) {
589
590 const mountEl = getMountEl(root, opts);
591
592 const overlay = document.createElement("div");
593 overlay.className = "dbx-confirm-overlay";
594 overlay.style.position = "fixed";
595 overlay.style.inset = "0";
596 overlay.style.zIndex = String(autoConfirmZIndex(root, opts));
597 overlay.style.background = "rgba(0,0,0,0.35)";
598 overlay.style.backdropFilter = "blur(2px)";
599 overlay.style.display = "flex";
600 overlay.style.alignItems = "center";
601 overlay.style.justifyContent = "center";
602 overlay.style.padding = "1rem";
603
604 const dialog = document.createElement("div");
605 dialog.className = "dbx-confirm-dialog card shadow-lg";
606 dialog.style.width = "100%";
607 dialog.style.maxWidth = "640px";
608 dialog.style.border = "1px solid rgba(62, 129, 218, 0.28)";
609 dialog.style.borderRadius = "10px";
610 dialog.style.overflow = "hidden";
611 dialog.style.boxShadow = "0 22px 70px rgba(28, 57, 99, 0.28)";
612
613 const header = document.createElement("div");
614 header.className = "card-header dbx-confirm-header d-flex align-items-center justify-content-between gap-3";
615 header.style.background = "linear-gradient(180deg, #d8ebff 0%, #a9cffc 100%)";
616 header.style.borderBottom = "1px solid rgba(58, 123, 208, 0.34)";
617 header.style.color = "#132033";
618 header.style.minHeight = "70px";
619 header.style.padding = "12px 16px";
620
621 const titleWrap = document.createElement("div");
622 titleWrap.className = "dbx-confirm-titlewrap d-flex align-items-center gap-3";
623 titleWrap.style.minWidth = "0";
624
625 const titleIcon = document.createElement("span");
626 titleIcon.className = "dbx-confirm-title-icon";
627 titleIcon.innerHTML = "<i class=\"bi bi-question-circle\"></i>";
628 titleIcon.style.alignItems = "center";
629 titleIcon.style.background = "rgba(255,255,255,0.58)";
630 titleIcon.style.border = "1px solid rgba(35, 103, 194, 0.28)";
631 titleIcon.style.borderRadius = "8px";
632 titleIcon.style.color = "#0d6efd";
633 titleIcon.style.display = "inline-flex";
634 titleIcon.style.flex = "0 0 46px";
635 titleIcon.style.fontSize = "1.35rem";
636 titleIcon.style.height = "46px";
637 titleIcon.style.justifyContent = "center";
638 titleIcon.style.width = "46px";
639
640 const title = document.createElement("div");
641 title.className = "dbx-confirm-title fw-semibold";
642 title.style.fontSize = "1.08rem";
643 title.style.lineHeight = "1.25";
644 title.style.minWidth = "0";
645
646 const btnClose = document.createElement("button");
647 btnClose.type = "button";
648 btnClose.className = "btn btn-sm btn-outline-primary";
649 btnClose.innerHTML = "<i class=\"bi bi-x-lg\"></i>";
650 btnClose.style.background = "rgba(255,255,255,0.52)";
651 btnClose.style.borderColor = "rgba(35, 103, 194, 0.34)";
652 btnClose.style.flex = "0 0 auto";
653
654 const body = document.createElement("div");
655 body.className = "card-body dbx-confirm-body";
656 body.style.background = "linear-gradient(180deg, #ffffff 0%, #f6f9fd 100%)";
657 body.style.padding = "18px 20px";
658
659 const question = document.createElement("div");
660 question.className = "dbx-confirm-question mb-2";
661 question.style.color = "#182231";
662 question.style.fontSize = "1rem";
663 question.style.lineHeight = "1.45";
664
665 const hint = document.createElement("div");
666 hint.className = "dbx-confirm-hint text-muted small mb-3";
667 hint.style.background = "#f8fafc";
668 hint.style.border = "1px solid #e2e8f0";
669 hint.style.borderRadius = "8px";
670 hint.style.padding = "10px 12px";
671
672 const footer = document.createElement("div");
673 footer.className = "card-footer d-flex justify-content-end gap-2 flex-wrap";
674 footer.style.background = "#f8fbff";
675 footer.style.borderTop = "1px solid #dce8f7";
676 footer.style.padding = "12px 16px";
677
678 titleWrap.appendChild(titleIcon);
679 titleWrap.appendChild(title);
680 header.appendChild(titleWrap);
681 header.appendChild(btnClose);
682
683 body.appendChild(question);
684 body.appendChild(hint);
685
686 dialog.appendChild(header);
687 dialog.appendChild(body);
688 dialog.appendChild(footer);
689
690 overlay.appendChild(dialog);
691 mountEl.appendChild(overlay);
692
693 return {
694 mountEl,
695 overlay,
696 dialog,
697 header,
698 titleIcon,
699 title,
700 btnClose,
701 body,
702 question,
703 hint,
704 footer
705 };
706 }
707
708 function applyDialogState(entry) {
709
710 const opts = entry.options;
711 const ui = entry.ui;
712
713 htmlOrText(ui.title, opts.title, opts.titlehtml);
714 htmlOrText(ui.question, opts.question, opts.questionhtml);
715 ui.titleIcon.innerHTML = "<i class=\"bi " + iconFromTitle(opts.title) + "\"></i>";
716
717 if (opts.hint) {
718 ui.hint.style.display = "";
719 htmlOrText(ui.hint, opts.hint, opts.hinthtml);
720 } else {
721 ui.hint.style.display = "none";
722 ui.hint.innerHTML = "";
723 }
724
725 ui.btnClose.style.display = opts.closable ? "" : "none";
726
727 ui.footer.innerHTML = "";
728
729 const actions = buildButtonsMarkup(opts);
730
731 actions.forEach(action => {
732
733 const btn = document.createElement("button");
734 btn.type = "button";
735 btn.className = "btn";
736
737 if (action === "yes") {
738 btn.className += " btn-primary";
739 btn.setAttribute("data-confirm-action", "yes");
740 htmlOrText(btn, opts.labelyes, opts.labelhtml);
741 }
742
743 if (action === "no") {
744 btn.className += " btn-outline-secondary";
745 btn.setAttribute("data-confirm-action", "no");
746 htmlOrText(btn, opts.labelno, opts.labelhtml);
747 }
748
749 if (action === "cancel") {
750 btn.className += " btn-outline-danger";
751 btn.setAttribute("data-confirm-action", "cancel");
752 htmlOrText(btn, opts.labelcancel, opts.labelhtml);
753 }
754
755 ui.footer.appendChild(btn);
756 });
757 }
758
759 function closeDialog(entry, result) {
760
761 if (!entry || entry.closed) return;
762
763 entry.closed = true;
764
765 const id = entry.id;
766 const ui = entry.ui;
767
768 if (ui && ui.overlay && ui.overlay.parentNode) {
769 ui.overlay.parentNode.removeChild(ui.overlay);
770 }
771
772 delete _dialogs[id];
773
774 emit("confirm:result", {
775 id: id,
776 action: result.action,
777 source: entry.options.source || null,
778 root: entry.options.root || null,
779 dialog: null
780 });
781
782 entry.resolve(result);
783 }
784
785 function continueOriginalAction(entry) {
786
787 const source = entry.options.source;
788 if (!source) return;
789
790 const type = elementType(source);
791 if (!type) return;
792
793 if (type === "link") {
794 source.__dbxConfirmBypass = true;
795 source.click();
796 return;
797 }
798
799 if (type === "button") {
800
801 const btnType = str(source.getAttribute("type"), "submit").toLowerCase();
802 const form = source.form || source.closest("form");
803
804 if ((btnType === "submit" || btnType === "image") && form) {
805 /*
806 * FormData(form) enthält Submit-Buttons nicht. Zusätzlich
807 * liefert requestSubmit() in älteren WebViews nicht immer ein
808 * SubmitEvent.submitter. Ein kurzlebiges Hidden-Feld erhält
809 * deshalb name/value, während der normale Submit-Pfad weiter
810 * selbst zwischen AJAX und Browsernavigation entscheidet.
811 */
812 const submitName = readAttr(source, "name");
813 let submitProxy = null;
814
815 if (submitName) {
816 submitProxy = document.createElement("input");
817 submitProxy.type = "hidden";
818 submitProxy.name = submitName;
819 submitProxy.value = readAttr(source, "value");
820 submitProxy.setAttribute("data-dbx-confirm-submitter", "1");
821 form.appendChild(submitProxy);
822 }
823
824 source.__dbxConfirmBypass = true;
825 form.__dbxConfirmBypass = true;
826
827 try {
828 if (typeof form.requestSubmit === "function") {
829 form.requestSubmit(source);
830 } else {
831 form.submit();
832 }
833 } finally {
834 if (submitProxy && submitProxy.parentNode) {
835 submitProxy.parentNode.removeChild(submitProxy);
836 }
837 }
838
839 return;
840 }
841
842 source.__dbxConfirmBypass = true;
843 source.click();
844 return;
845 }
846
847 if (type === "form") {
848 source.__dbxConfirmBypass = true;
849
850 if (typeof source.requestSubmit === "function") {
851 source.requestSubmit();
852 } else {
853 source.submit();
854 }
855 }
856 }
857
858 function openDialog(rawOptions) {
859 const labels = defaultLabels();
860
861 const opts = {
862 id: str(rawOptions.id, "dbx-confirm-" + (++_uid)),
863 root: ensureRoot(rawOptions.root),
864 source: rawOptions.source || rawOptions.callerEl || rawOptions.caller || null,
865 callerEl: rawOptions.callerEl || rawOptions.caller || rawOptions.source || null,
866 mountEl: rawOptions.mountEl || null,
867 zIndex: parseInt(rawOptions.zIndex, 10) || 0,
868
869 title: str(rawOptions.title, ""),
870 question: str(rawOptions.question, ""),
871 hint: str(rawOptions.hint, ""),
872 buttons: normalizeButtons(rawOptions.buttons),
873
874 titlehtml: bool(rawOptions.titlehtml, true),
875 questionhtml: bool(rawOptions.questionhtml, true),
876 hinthtml: bool(rawOptions.hinthtml, true),
877 labelhtml: bool(rawOptions.labelhtml, true),
878
879 labelyes: str(rawOptions.labelyes, labels.yes),
880 labelno: str(rawOptions.labelno, labels.no),
881 labelcancel: str(rawOptions.labelcancel, labels.cancel),
882
883 closable: bool(rawOptions.closable, true),
884 backdropclose: bool(rawOptions.backdropclose, false),
885 escclose: bool(rawOptions.escclose, true),
886
887 onyes: (typeof rawOptions.onyes === "function") ? rawOptions.onyes : null,
888 onno: (typeof rawOptions.onno === "function") ? rawOptions.onno : null,
889 oncancel: (typeof rawOptions.oncancel === "function") ? rawOptions.oncancel : null
890 };
891
892 if (_dialogs[opts.id]) {
893 dbx.confirm.close(opts.id, { action: "close" });
894 }
895
896 return new Promise((resolve) => {
897
898 const ui = createDialogElements(opts.root, opts);
899
900 const entry = {
901 id: opts.id,
902 options: opts,
903 ui: ui,
904 resolve: resolve,
905 closed: false,
906 keyHandler: null
907 };
908
909 _dialogs[opts.id] = entry;
910
911 applyDialogState(entry);
912
913 emit("confirm:open", {
914 id: opts.id,
915 source: opts.source || null,
916 root: opts.root || null,
917 dialog: ui.dialog
918 });
919
920 ui.footer.addEventListener("click", function (e) {
921
922 const btn = e.target.closest("[data-confirm-action]");
923 if (!btn) return;
924
925 const action = btn.getAttribute("data-confirm-action");
926
927 if (action === "yes" && opts.onyes) {
928 try {
929 opts.onyes(entry);
930 } catch (err) {
931 dbx.error("[dbx.confirm] onyes failed", err);
932 }
933 }
934
935 if (action === "no" && opts.onno) {
936 try {
937 opts.onno(entry);
938 } catch (err) {
939 dbx.error("[dbx.confirm] onno failed", err);
940 }
941 }
942
943 if (action === "cancel" && opts.oncancel) {
944 try {
945 opts.oncancel(entry);
946 } catch (err) {
947 dbx.error("[dbx.confirm] oncancel failed", err);
948 }
949 }
950
951 closeDialog(entry, {
952 id: opts.id,
953 action: action,
954 source: opts.source || null,
955 root: opts.root || null,
956 dialog: null
957 });
958
959 if (action === "yes" && opts.source) {
960 continueOriginalAction(entry);
961 }
962 });
963
964 ui.btnClose.addEventListener("click", function () {
965
966 if (!opts.closable) return;
967
968 closeDialog(entry, {
969 id: opts.id,
970 action: "close",
971 source: opts.source || null,
972 root: opts.root || null,
973 dialog: null
974 });
975 });
976
977 ui.overlay.addEventListener("click", function (e) {
978
979 if (e.target !== ui.overlay) return;
980 if (!opts.backdropclose) return;
981
982 closeDialog(entry, {
983 id: opts.id,
984 action: "close",
985 source: opts.source || null,
986 root: opts.root || null,
987 dialog: null
988 });
989 });
990
991 entry.keyHandler = function (e) {
992
993 if (e.key !== "Escape") return;
994 if (!opts.escclose) return;
995
996 closeDialog(entry, {
997 id: opts.id,
998 action: "close",
999 source: opts.source || null,
1000 root: opts.root || null,
1001 dialog: null
1002 });
1003 };
1004
1005 document.addEventListener("keydown", entry.keyHandler, true);
1006
1007 const oldResolve = entry.resolve;
1008
1009 entry.resolve = function (result) {
1010 document.removeEventListener("keydown", entry.keyHandler, true);
1011 oldResolve(result);
1012 };
1013 });
1014 }
1015
1016
1017 /* =========================================================
1018 * PUBLIC API
1019 * ========================================================= */
1020
1021 dbx.confirm.open = function (options) {
1022 return openDialog(options || {});
1023 };
1024
1025 dbx.confirm.update = function (id, patch) {
1026
1027 const entry = _dialogs[id];
1028 if (!entry) return false;
1029
1030 const opts = entry.options;
1031 const data = patch || {};
1032
1033 if ("title" in data) opts.title = str(data.title, "");
1034 if ("question" in data) opts.question = str(data.question, "");
1035 if ("hint" in data) opts.hint = str(data.hint, "");
1036
1037 applyDialogState(entry);
1038 return true;
1039 };
1040
1041 dbx.confirm.close = function (id, result) {
1042
1043 const entry = _dialogs[id];
1044 if (!entry) return false;
1045
1046 closeDialog(entry, {
1047 id: id,
1048 action: (result && result.action) ? result.action : "close",
1049 source: entry.options.source || null,
1050 root: entry.options.root || null,
1051 dialog: null
1052 });
1053
1054 return true;
1055 };
1056
1057 dbx.confirm.get = function (id) {
1058 return _dialogs[id] || null;
1059 };
1060
1061
1062 /* =========================================================
1063 * DECLARE SCHEMA (Defaults + data-* Aliase)
1064 * ========================================================= */
1065
1066 if (dbx.declare && typeof dbx.declare.registerSchema === "function") {
1067 const schemaLabels = defaultLabels();
1068
1069 dbx.declare.registerSchema("confirm", {
1070 fields: {
1071 class: {
1072 default: "dbxConfirm"
1073 },
1074 bind: {
1075 default: "link,button,form",
1076 aliases: ["data-confirm-bind"]
1077 },
1078 title: {
1079 default: "",
1080 aliases: ["data-confirm-title", "data-title"]
1081 },
1082 question: {
1083 default: "",
1084 aliases: ["data-confirm-question", "data-confirm"]
1085 },
1086 hint: {
1087 default: "",
1088 aliases: ["data-confirm-hint"]
1089 },
1090 buttons: {
1091 default: "yesno",
1092 aliases: ["data-confirm-buttons"]
1093 },
1094 titlehtml: {
1095 default: "1",
1096 aliases: ["data-confirm-titlehtml"]
1097 },
1098 questionhtml: {
1099 default: "1",
1100 aliases: ["data-confirm-questionhtml"]
1101 },
1102 hinthtml: {
1103 default: "1",
1104 aliases: ["data-confirm-hinthtml"]
1105 },
1106 labelhtml: {
1107 default: "1",
1108 aliases: ["data-confirm-labelhtml"]
1109 },
1110 labelyes: {
1111 default: schemaLabels.yes,
1112 aliases: ["data-confirm-labelyes"]
1113 },
1114 labelno: {
1115 default: schemaLabels.no,
1116 aliases: ["data-confirm-labelno"]
1117 },
1118 labelcancel: {
1119 default: schemaLabels.cancel,
1120 aliases: ["data-confirm-labelcancel"]
1121 },
1122 closable: {
1123 default: "1",
1124 aliases: ["data-confirm-closable"]
1125 },
1126 backdropclose: {
1127 default: "0",
1128 aliases: ["data-confirm-backdropclose"]
1129 },
1130 escclose: {
1131 default: "1",
1132 aliases: ["data-confirm-escclose"]
1133 }
1134 }
1135 });
1136
1137 dbx.declare.transforms.confirm = function (raw) {
1138 return {
1139 _index: raw._index,
1140 class: normalizeClassFilter(raw.class),
1141 bind: normalizeBind(raw.bind),
1142 title: str(raw.title, ""),
1143 question: str(raw.question, ""),
1144 hint: str(raw.hint, ""),
1145 buttons: normalizeButtons(raw.buttons),
1146 titlehtml: bool(raw.titlehtml, true),
1147 questionhtml: bool(raw.questionhtml, true),
1148 hinthtml: bool(raw.hinthtml, true),
1149 labelhtml: bool(raw.labelhtml, true),
1150 labelyes: str(raw.labelyes, schemaLabels.yes),
1151 labelno: str(raw.labelno, schemaLabels.no),
1152 labelcancel: str(raw.labelcancel, schemaLabels.cancel),
1153 closable: bool(raw.closable, true),
1154 backdropclose: bool(raw.backdropclose, false),
1155 escclose: bool(raw.escclose, true)
1156 };
1157 };
1158 }
1159
1160
1161 /* =========================================================
1162 * FEATURE
1163 * ========================================================= */
1164
1165 dbx.feature.register("confirm", {
1166
1167 scope: "element",
1168
1169 priority: "mid",
1170
1171 init(el, cfg) {
1172
1173 if (!el) return;
1174
1175 el.__dbxInitialized = el.__dbxInitialized || {};
1176 if (el.__dbxInitialized["confirm"]) return;
1177 el.__dbxInitialized["confirm"] = true;
1178
1179 el.setAttribute("data-dbx-confirm-root", "1");
1180 getRootConfigs(el);
1181
1182 dbx.log("[dbx.confirm] init", {
1183 rootId: el.id || "",
1184 configs: el._dbxConfirmConfigs
1185 });
1186
1187 el.addEventListener("click", function (e) {
1188
1189 const source = e.target.closest("a, button, input[type='button'], input[type='submit'], input[type='image']");
1190 if (!source) return;
1191 if (!el.contains(source)) return;
1192
1193 if (source.__dbxConfirmBypass === true) {
1194 delete source.__dbxConfirmBypass;
1195 return;
1196 }
1197
1198 const nearestRoot = source.closest("[data-dbx-confirm-root='1']");
1199 if (nearestRoot !== el) return;
1200
1201 const type = elementType(source);
1202 const cfgMatch = findMatchingConfig(el, source, type);
1203
1204 if (!cfgMatch) return;
1205
1206 e.preventDefault();
1207
1208 const options = readOptionsFromElement(source, cfgMatch);
1209 options.root = el;
1210
1211 openDialog(options).catch(err => {
1212 dbx.error("[dbx.confirm] open failed", err);
1213 });
1214 }, true);
1215
1216 el.addEventListener("submit", function (e) {
1217
1218 const form = e.target.closest("form");
1219 if (!form) return;
1220 if (!el.contains(form)) return;
1221
1222 if (form.__dbxConfirmBypass === true) {
1223 delete form.__dbxConfirmBypass;
1224 return;
1225 }
1226
1227 const nearestRoot = form.closest("[data-dbx-confirm-root='1']");
1228 if (nearestRoot !== el) return;
1229
1230 const type = elementType(form);
1231 const cfgMatch = findMatchingConfig(el, form, type);
1232
1233 if (!cfgMatch) return;
1234
1235 e.preventDefault();
1236
1237 const options = readOptionsFromElement(form, cfgMatch);
1238 options.root = el;
1239
1240 openDialog(options).catch(err => {
1241 dbx.error("[dbx.confirm] open failed", err);
1242 });
1243 }, true);
1244 },
1245
1246 destroy(el, cfg) {
1247
1248 if (!el) return;
1249
1250 if (el.__dbxInitialized && el.__dbxInitialized["confirm"]) {
1251 delete el.__dbxInitialized["confirm"];
1252 }
1253
1254 delete el._dbxConfirmConfigs;
1255 el.removeAttribute("data-dbx-confirm-root");
1256
1257 dbx.log("[dbx.confirm] destroy", {
1258 rootId: el.id || ""
1259 });
1260 }
1261
1262 });
1263
1264})(window, document);