Skip to content

Commit 3ac07f3

Browse files
Veszter-007Szilveszter
andauthored
refactor: use Bootstrap modal instead of native confirm (#1206)
* refactor: use Bootstrap modal instead of native confirm * refactor(confirm): improve AJAX confirmation handling based on CoderabbitAI feedback docs: update modal confirmation instructions in README * refactor(confirm): add runtime check for Bootstrap global and fallback to native confirm * docs: add naja runtime check and fix modal wrapper in confirm dialog * fix: prevent recursive confirm loop and preserve native link behavior * refactor: apply nitpick suggestions for cleaner confirm plugin logic * fix: Remove redundant event prevention * refactor(confirm-plugin): restructure request logic docs(confirm-plugin): clarify modal behavior and remove dynamic title mention * fix(confirm-plugin): handle missing Naja detail with safe fallback Ensure that the confirm plugin falls back to data-naja-method and href when CustomEvent.detail does not provide method or url. This prevents cases where no request would be executed and improves reliability of AJAX delete confirmations. * chore: apply nitpick suggestion (stricter type check for e.detail) --------- Co-authored-by: Szilveszter <[email protected]>
1 parent f3927dc commit 3ac07f3

File tree

2 files changed

+149
-12
lines changed

2 files changed

+149
-12
lines changed

.docs/actions.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,30 @@ $grid->addAction('delete', '', 'delete!')
9595
);
9696
```
9797

98+
### Using Bootstrap modal for confirmation
99+
100+
If you'd like to display the confirmation prompt using a Bootstrap modal instead of the native `window.confirm`, simply include the following HTML snippet in your template:
101+
102+
```html
103+
<div class="modal fade" id="datagridConfirmModal" tabindex="-1">
104+
<div class="modal-dialog">
105+
<div class="modal-content">
106+
<div class="modal-header">
107+
<h5 class="modal-title">Delete confirmation</h5>
108+
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
109+
</div>
110+
<div class="modal-body" id="datagridConfirmMessage"></div>
111+
<div class="modal-footer">
112+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
113+
<button type="button" class="btn btn-danger" id="datagridConfirmOk">Delete</button>
114+
</div>
115+
</div>
116+
</div>
117+
</div>
118+
```
119+
120+
The confirmation message is forwarded and displayed in the modal window. If the modal is not present in the template, the plugin will fall back to using the native window.confirm.
121+
98122
# Ajax
99123

100124
## Redrawing the data

assets/plugins/features/confirm.ts

Lines changed: 125 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,145 @@
1+
/**
2+
* Datagrid plugin that asks for confirmation before deleting.
3+
* If there is a modal in the DOM, use it, otherwise use a native confirm window.
4+
*/
15
import { Datagrid } from "../../datagrid";
26
import { DatagridPlugin } from "../../types";
37

8+
interface NajaInteractDetail {
9+
method: string;
10+
url: string;
11+
payload: any;
12+
options: Record<string, any>;
13+
}
14+
415
export const ConfirmAttribute = "data-datagrid-confirm";
516

617
export class ConfirmPlugin implements DatagridPlugin {
18+
private datagrid!: Datagrid;
19+
20+
private modalId = 'datagridConfirmModal';
21+
private messageBoxId = 'datagridConfirmMessage';
22+
private confirmButtonId = 'datagridConfirmOk';
23+
24+
/**
25+
* Initializes the plugin and registers event handlers.
26+
* @param datagrid The datagrid instance that the plugin is connected to.
27+
* @returns true if initialization was successful.
28+
*/
29+
730
onDatagridInit(datagrid: Datagrid): boolean {
8-
datagrid.el
9-
.querySelectorAll<HTMLElement>(`[${ConfirmAttribute}]:not(.ajax)`)
10-
.forEach(confirmEl =>
11-
confirmEl.addEventListener("click", e => this.confirmEventHandler.call(datagrid, e.target as HTMLElement, e))
12-
);
31+
this.datagrid = datagrid;
32+
33+
const confirmElements = datagrid.el.querySelectorAll<HTMLElement>(`[${ConfirmAttribute}]:not(.ajax)`);
34+
confirmElements.forEach(el => el.addEventListener("click", e => this.handleClick(el, e)));
1335

1436
datagrid.ajax.addEventListener("interact", e => {
15-
if (datagrid.el.contains(e.detail.element)) {
16-
this.confirmEventHandler.call(datagrid, e.detail.element, e);
37+
const target = e.detail.element;
38+
if (datagrid.el.contains(target)) {
39+
this.handleClick(target, e);
1740
}
1841
});
1942

2043
return true;
2144
}
2245

23-
confirmEventHandler(this: Datagrid, el: HTMLElement, e: Event) {
24-
const message = el.closest('a')?.getAttribute(ConfirmAttribute)!;
46+
private handleClick(el: HTMLElement, e: Event): void {
47+
const message = this.getConfirmationMessage(el);
2548
if (!message) return;
2649

27-
if (!window.confirm.bind(window)(message)) {
28-
e.stopPropagation();
29-
e.preventDefault();
50+
e.preventDefault();
51+
e.stopPropagation();
52+
53+
const modal = this.getElement(this.modalId);
54+
if (modal) {
55+
this.showModalConfirm(modal, message, el, e);
56+
} else {
57+
if (window.confirm(message)) {
58+
this.executeConfirmedAction(el, e);
59+
}
60+
}
61+
}
62+
63+
private getConfirmationMessage(el: HTMLElement): string | null {
64+
return el.getAttribute(ConfirmAttribute) ?? el.closest('a')?.getAttribute(ConfirmAttribute) ?? null;
65+
}
66+
67+
private showModalConfirm(modal: HTMLElement, message: string, el: HTMLElement, e: Event): void {
68+
if (typeof bootstrap === 'undefined') {
69+
if (window.confirm(message)) {
70+
this.executeConfirmedAction(el, e);
71+
}
72+
return;
73+
}
74+
75+
const messageBox = this.getElement(this.messageBoxId);
76+
const confirmButton = this.getElement(this.confirmButtonId);
77+
78+
if (!messageBox || !confirmButton) {
79+
if (window.confirm(message)) {
80+
this.executeConfirmedAction(el, e);
81+
}
82+
return;
83+
}
84+
85+
messageBox.textContent = message;
86+
87+
const newButton = confirmButton.cloneNode(true) as HTMLElement;
88+
confirmButton.parentNode!.replaceChild(newButton, confirmButton);
89+
90+
newButton.addEventListener("click", () => {
91+
bootstrap.Modal.getInstance(modal)?.hide();
92+
this.executeConfirmedAction(el, e);
93+
}, { once: true });
94+
95+
const modalInstance = bootstrap.Modal.getInstance(modal) || new bootstrap.Modal(modal);
96+
modalInstance.show();
97+
}
98+
99+
private executeConfirmedAction(el: HTMLElement, e?: Event): void {
100+
//const detail: NajaInteractDetail | null = (e instanceof CustomEvent ? (e.detail as NajaInteractDetail) : null);
101+
const detail: NajaInteractDetail | null = (
102+
e instanceof CustomEvent &&
103+
e.detail &&
104+
typeof e.detail === 'object'
105+
) ? e.detail as NajaInteractDetail : null;
106+
const isAjax = el.classList.contains('ajax');
107+
108+
if (el instanceof HTMLAnchorElement && el.href && isAjax) {
109+
if (typeof naja === 'undefined') {
110+
return;
111+
}
112+
113+
if (detail && typeof detail.method === 'string' && typeof detail.url === 'string') {
114+
const options = { ...detail.options, history: false };
115+
naja.makeRequest(detail.method, detail.url, detail.payload ?? null, options);
116+
} else {
117+
const method = el.getAttribute('data-naja-method') ?? 'GET';
118+
naja.makeRequest(method, el.href, null, { history: false });
119+
}
120+
return;
121+
}
122+
123+
this.triggerNativeInteraction(el);
124+
}
125+
126+
private getElement(id: string): HTMLElement | null {
127+
return document.getElementById(id);
128+
}
129+
130+
private triggerNativeInteraction(el: HTMLElement): void {
131+
const confirmValue = el.getAttribute(ConfirmAttribute);
132+
133+
if (confirmValue !== null) {
134+
el.removeAttribute(ConfirmAttribute);
135+
}
136+
137+
try {
138+
el.click();
139+
} finally {
140+
if (confirmValue !== null) {
141+
el.setAttribute(ConfirmAttribute, confirmValue);
142+
}
30143
}
31144
}
32145
}

0 commit comments

Comments
 (0)