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
105105let 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+
106179function showAlert ( type , icon , title , message , toast ) {
107- const options = {
180+ const alertState = {
108181 title : " <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 = " <strong>Error, something went wrong!</strong><br>" ;
212+ alertState . title = " <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 = " <strong>" + escapeHtml ( data . error . message ) + "</strong><br>" ;
227+ alertState . title = " <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
194404function datetime ( date , html , humanReadable ) {
0 commit comments