Skip to content

Commit 9f978eb

Browse files
committed
Replace bootstrap-notify.js by bootstrapv5-toast
Signed-off-by: yubiuser <github@yubiuser.dev>
1 parent d1836fd commit 9f978eb

5 files changed

Lines changed: 246 additions & 52 deletions

File tree

package-lock.json

Lines changed: 3 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
"admin-lte": "4.1.0",
3030
"animate.css": "4.1.1",
3131
"bootstrap": "5.3.8",
32-
"bootstrap-notify": "3.1.3",
3332
"bootstrap5-toggle": "5.4.1",
3433
"bstreeview": "1.2.0",
3534
"chart.js": "4.5.1",

scripts/js/utils.js

Lines changed: 240 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* This file is copyright under the latest version of the EUPL.
66
* Please see LICENSE file for your rights under this license. */
77

8-
/* global moment:false, apiFailure: false, updateFtlInfo: false, NProgress:false, WaitMe:false, TomSelect: false */
8+
/* global moment:false, apiFailure: false, updateFtlInfo: false, NProgress:false, WaitMe:false, TomSelect: false, bootstrap: false */
99

1010
"use strict";
1111

@@ -103,52 +103,131 @@ function padNumber(num) {
103103
}
104104

105105
let showAlertBox = null;
106+
function getToastContainer() {
107+
const existing =
108+
document.getElementById("toast-container") || document.querySelector(".toast-container");
109+
110+
if (existing !== null) {
111+
return existing;
112+
}
113+
114+
const container = document.createElement("div");
115+
container.className = "toast-container position-fixed top-0 end-0 p-3";
116+
container.id = "toast-container";
117+
container.setAttribute("aria-live", "polite");
118+
container.setAttribute("aria-atomic", "true");
119+
container.style.zIndex = "9999";
120+
document.body.append(container);
121+
122+
return container;
123+
}
124+
125+
function createToastElement(state) {
126+
const toast = document.createElement("div");
127+
toast.className = "toast align-items-center border-0 shadow rounded overflow-hidden";
128+
toast.dataset.toastType = state.type;
129+
toast.setAttribute("role", state.role);
130+
toast.setAttribute("aria-live", state.live);
131+
toast.setAttribute("aria-atomic", state.ariaAtomic);
132+
133+
const content = document.createElement("div");
134+
content.className = `toast-content ${state.className}`;
135+
136+
const layout = document.createElement("div");
137+
layout.className = "d-flex align-items-start p-3";
138+
139+
const body = document.createElement("div");
140+
body.className = "d-flex flex-grow-1 gap-2";
141+
142+
if (state.icon !== "") {
143+
const icon = document.createElement("i");
144+
icon.className = `${state.icon} mt-1`;
145+
icon.setAttribute("aria-hidden", "true");
146+
body.append(icon);
147+
}
148+
149+
const text = document.createElement("div");
150+
text.className = "toast-text flex-grow-1";
151+
152+
const title = document.createElement("strong");
153+
title.className = "d-block";
154+
title.innerHTML = state.title;
155+
156+
const message = document.createElement("div");
157+
message.className = "toast-message";
158+
message.style.whiteSpace = "pre-line";
159+
message.style.overflowWrap = "anywhere";
160+
message.innerHTML = state.message;
161+
162+
text.append(title, message);
163+
body.append(text);
164+
layout.append(body);
165+
166+
const closeButton = document.createElement("button");
167+
closeButton.type = "button";
168+
closeButton.className = `${state.closeButtonClass} ms-2 mt-1`;
169+
closeButton.dataset.bsDismiss = "toast";
170+
closeButton.setAttribute("aria-label", "Close");
171+
layout.append(closeButton);
172+
173+
content.append(layout);
174+
toast.append(content);
175+
176+
return toast;
177+
}
178+
106179
function showAlert(type, icon, title, message, toast) {
107-
const options = {
180+
const alertState = {
108181
title: "&nbsp;<strong>" + escapeHtml(title) + "</strong><br>",
109182
message: escapeHtml(message),
110183
icon,
111-
};
112-
const settings = {
113184
type,
185+
className: "",
186+
animation: true,
114187
delay: 5000, // default value
115-
z_index: 9999, // increased to avoid overlapping issues with the AdminLTE header
116-
mouse_over: "pause",
117-
animate: {
118-
enter: "animate__animated animate__fadeInDown",
119-
exit: "animate__animated animate__fadeOutUp",
120-
},
188+
autohide: true,
189+
role: "status",
190+
live: "polite",
191+
ariaAtomic: "true",
192+
closeButtonClass: "btn-close",
121193
};
194+
122195
switch (type) {
123196
case "info":
124-
options.icon = icon !== null && icon.length > 0 ? icon : "fas fa-clock";
125-
197+
alertState.icon = icon !== null && icon.length > 0 ? icon : "fas fa-clock";
198+
alertState.className = "text-bg-info";
126199
break;
127200
case "success":
201+
alertState.className = "text-bg-success";
202+
alertState.closeButtonClass += " btn-close-white";
128203
break;
129204
case "warning":
130-
options.icon = "fas fa-exclamation-triangle";
131-
settings.delay *= 2;
132-
205+
alertState.icon = "fas fa-exclamation-triangle";
206+
alertState.delay *= 2;
207+
alertState.className = "text-bg-warning";
133208
break;
134209
case "error":
135-
options.icon = "fas fa-times";
210+
alertState.icon = "fas fa-times";
136211
if (title.length === 0) {
137-
options.title = "&nbsp;<strong>Error, something went wrong!</strong><br>";
212+
alertState.title = "&nbsp;<strong>Error, something went wrong!</strong><br>";
138213
}
139214

140-
settings.delay *= 2;
215+
alertState.delay *= 2;
216+
alertState.className = "text-bg-danger";
217+
alertState.role = "alert";
218+
alertState.live = "assertive";
219+
alertState.closeButtonClass += " btn-close-white";
141220

142221
// If the message is an API object, nicely format the error message
143222
// Try to parse message as JSON
144223
try {
145224
const data = JSON.parse(message);
146225
console.log(data); // eslint-disable-line no-console
147226
if (data.error !== undefined) {
148-
options.title = "&nbsp;<strong>" + escapeHtml(data.error.message) + "</strong><br>";
227+
alertState.title = "&nbsp;<strong>" + escapeHtml(data.error.message) + "</strong><br>";
149228

150229
if (data.error.hint !== null) {
151-
options.message = escapeHtml(data.error.hint);
230+
alertState.message = escapeHtml(data.error.hint);
152231
}
153232
}
154233
} catch {
@@ -162,33 +241,164 @@ function showAlert(type, icon, title, message, toast) {
162241
return;
163242
}
164243

244+
const container = getToastContainer();
245+
246+
const controller = toast === undefined ? (type === "info" ? null : showAlertBox) : toast;
247+
248+
const toastController = controller ?? {
249+
element: createToastElement(alertState),
250+
instance: null,
251+
hideTimer: null,
252+
alertState,
253+
update(partialState) {
254+
this.alertState = { ...this.alertState, ...partialState };
255+
this.render();
256+
this.show();
257+
return this;
258+
},
259+
render() {
260+
this.element.dataset.toastType = this.alertState.type;
261+
this.element.className = "toast align-items-center border-0 shadow rounded overflow-hidden";
262+
this.element.setAttribute("role", this.alertState.role);
263+
this.element.setAttribute("aria-live", this.alertState.live);
264+
this.element.setAttribute("aria-atomic", this.alertState.ariaAtomic);
265+
266+
const content = this.element.querySelector(".toast-content");
267+
const body = this.element.querySelector(".d-flex.flex-grow-1.gap-2");
268+
const text = this.element.querySelector(".toast-text");
269+
const closeButton = this.element.querySelector("button[data-bs-dismiss='toast']");
270+
271+
if (content !== null) {
272+
content.className = `toast-content ${this.alertState.className}`;
273+
}
274+
275+
if (body !== null) {
276+
body.replaceChildren();
277+
278+
if (this.alertState.icon !== "") {
279+
const iconElement = document.createElement("i");
280+
iconElement.className = `${this.alertState.icon} mt-1`;
281+
iconElement.setAttribute("aria-hidden", "true");
282+
body.append(iconElement);
283+
}
284+
285+
if (text !== null) {
286+
text.replaceChildren();
287+
288+
const titleElement = document.createElement("strong");
289+
titleElement.className = "d-block";
290+
titleElement.innerHTML = this.alertState.title;
291+
292+
const messageElement = document.createElement("div");
293+
messageElement.className = "toast-message";
294+
messageElement.style.whiteSpace = "pre-line";
295+
messageElement.style.overflowWrap = "anywhere";
296+
messageElement.innerHTML = this.alertState.message;
297+
298+
text.append(titleElement, messageElement);
299+
body.append(text);
300+
}
301+
}
302+
303+
if (closeButton !== null) {
304+
closeButton.className = `${this.alertState.closeButtonClass} ms-2 mt-1`;
305+
}
306+
},
307+
clearTimer() {
308+
if (this.hideTimer === null) {
309+
return;
310+
}
311+
312+
clearTimeout(this.hideTimer);
313+
this.hideTimer = null;
314+
},
315+
scheduleHide() {
316+
this.clearTimer();
317+
if (!this.alertState.autohide) {
318+
return;
319+
}
320+
321+
this.hideTimer = setTimeout(() => {
322+
this.hide();
323+
}, this.alertState.delay);
324+
},
325+
ensureInstance() {
326+
this.instance = bootstrap.Toast.getOrCreateInstance(this.element, {
327+
autohide: false,
328+
});
329+
return this.instance;
330+
},
331+
show() {
332+
if (!this.element.isConnected) {
333+
container.prepend(this.element);
334+
}
335+
336+
this.ensureInstance().show();
337+
this.scheduleHide();
338+
return this;
339+
},
340+
hide() {
341+
this.clearTimer();
342+
if (this.instance !== null) {
343+
this.instance.hide();
344+
}
345+
346+
return this;
347+
},
348+
dispose() {
349+
this.clearTimer();
350+
if (this.instance !== null) {
351+
this.instance.dispose();
352+
this.instance = null;
353+
}
354+
355+
this.element.remove();
356+
if (showAlertBox === this) {
357+
showAlertBox = null;
358+
}
359+
360+
return this;
361+
},
362+
};
363+
364+
toastController.element.addEventListener("hidden.bs.toast", () => {
365+
toastController.clearTimer();
366+
if (toastController.instance !== null) {
367+
toastController.instance.dispose();
368+
toastController.instance = null;
369+
}
370+
371+
toastController.element.remove();
372+
if (showAlertBox === toastController) {
373+
showAlertBox = null;
374+
}
375+
});
376+
377+
toastController.render();
378+
165379
if (toast === undefined) {
166380
if (type === "info") {
167381
// Create a new notification for info boxes
168-
showAlertBox = $.notify(options, settings);
169-
return showAlertBox;
382+
showAlertBox = toastController;
383+
return toastController.show();
170384
}
171385

172386
if (showAlertBox !== null) {
173387
// Update existing notification for other boxes (if available)
174-
showAlertBox.update(options);
175-
showAlertBox.update(settings);
176-
return showAlertBox;
388+
return showAlertBox.update(alertState);
177389
}
178390

179391
// Create a new notification for other boxes if no previous info box exists
180-
return $.notify(options, settings);
392+
return toastController.show();
181393
}
182394

183395
if (toast === null) {
184396
// Always create a new toast
185-
return $.notify(options, settings);
397+
return toastController.show();
186398
}
187399

188400
// Update existing toast
189-
toast.update(options);
190-
toast.update(settings);
191-
return toast;
401+
return toast.update(alertState);
192402
}
193403

194404
function datetime(date, html, humanReadable) {

scripts/lua/header_authenticated.lp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
]]--
1010
mg.include('header.lp','r')
1111
?>
12-
<script src="<?=pihole.fileversion('vendor/bootstrap-notify/bootstrap-notify.min.js')?>"></script>
1312
<script src="<?=pihole.fileversion('vendor/select2/select2.min.js')?>"></script>
1413
<script src="<?=pihole.fileversion('vendor/datatables/dataTables.min.js')?>"></script>
1514
<script src="<?=pihole.fileversion('vendor/datatables/dataTables.bootstrap5.min.js')?>"></script>
@@ -39,6 +38,9 @@ mg.include('header.lp','r')
3938
<!-- /JS Warning -->
4039
</noscript>
4140

41+
<!-- Toast container to display notifications, should be present before the toast is generated to be recognized by by assistive technologies-->
42+
<div class="toast-container position-fixed top-0 end-0 p-3" id="toast-container" aria-live="polite" aria-atomic="true" style="z-index: 9999"></div>
43+
4244
<!-- Send token to JS -->
4345
<div id="enableTimer" hidden></div>
4446
<div class="app-wrapper">

0 commit comments

Comments
 (0)