@tsirosgeorge/toastnotification 5.3.3 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +34 -4
- package/assets/css/toast.css +9 -0
- package/assets/css/toast.min.css +1 -1
- package/package.json +29 -13
- package/toast.d.ts +102 -0
- package/toast.js +249 -74
- package/toast.min.js +1 -1
- package/toast.module.js +717 -512
- package/LICENCE +0 -16
- package/assets/img/old/error.gif +0 -0
- package/assets/img/old/info.gif +0 -0
- package/assets/img/old/success.gif +0 -0
- package/assets/img/old/warning.gif +0 -0
package/toast.module.js
CHANGED
|
@@ -1,21 +1,57 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
// Single source of truth for the CDN this build points at.
|
|
4
|
+
// `npm run sync:version` rewrites it from package.json, so it can never go stale.
|
|
5
|
+
const TS_TOAST_VERSION = "5.5.0";
|
|
6
|
+
// Point this at your own copy of assets/ to self-host the CSS and icons
|
|
7
|
+
// (useful offline, behind a strict CSP, or when you don't want a CDN dependency):
|
|
8
|
+
// window.TS_TOAST_ASSET_BASE = '/vendor/toastnotification';
|
|
9
|
+
const TS_TOAST_CDN = (typeof window !== 'undefined' && window.TS_TOAST_ASSET_BASE)
|
|
10
|
+
? String(window.TS_TOAST_ASSET_BASE).replace(/\/+$/, '')
|
|
11
|
+
: `https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@${TS_TOAST_VERSION}`;
|
|
12
|
+
|
|
13
|
+
// How many confirm dialogs are currently open. The body scroll lock belongs to the
|
|
14
|
+
// group, so only the last dialog to close may release it.
|
|
15
|
+
let tsToastOpenModals = 0;
|
|
16
|
+
let tsToastPrevOverflow = '';
|
|
17
|
+
|
|
18
|
+
const tsToastReducedMotion = () =>
|
|
19
|
+
typeof window !== 'undefined' &&
|
|
20
|
+
typeof window.matchMedia === 'function' &&
|
|
21
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
22
|
+
|
|
23
|
+
// Everything inside the dialog a keyboard can reach, in DOM order.
|
|
24
|
+
const tsToastFocusable = (root) => Array.from(
|
|
25
|
+
root.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
|
26
|
+
).filter((el) => !el.disabled && el.offsetParent !== null);
|
|
27
|
+
|
|
28
|
+
let tsToastIdCounter = 0;
|
|
29
|
+
|
|
30
|
+
// Load the stylesheet from the CDN, unless the page opted out by importing it itself
|
|
31
|
+
// (set window.TS_TOAST_NO_CSS = true before loading, or ship assets/css/toast.css yourself).
|
|
32
|
+
(function loadStylesheet() {
|
|
33
|
+
const LINK_ID = 'ts-toast-stylesheet';
|
|
34
|
+
if (typeof window !== 'undefined' && window.TS_TOAST_NO_CSS) return;
|
|
35
|
+
if (document.getElementById(LINK_ID)) return;
|
|
36
|
+
const link = document.createElement("link");
|
|
37
|
+
link.id = LINK_ID;
|
|
38
|
+
link.rel = "stylesheet";
|
|
39
|
+
link.href = `${TS_TOAST_CDN}/assets/css/toast.min.css`;
|
|
40
|
+
document.head.appendChild(link);
|
|
41
|
+
})();
|
|
42
|
+
|
|
43
|
+
// Inject minimal styles for confirm actions and overlay (kept tiny to avoid breaking existing CSS)
|
|
44
|
+
(function injectInlineStyles() {
|
|
45
|
+
const STYLE_ID = 'ts-toast-inline-extras';
|
|
46
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
47
|
+
const style = document.createElement('style');
|
|
48
|
+
style.id = STYLE_ID;
|
|
49
|
+
style.textContent = `
|
|
16
50
|
/* Ensure center positions exist even if external CSS lacks them */
|
|
17
51
|
.ts-toast-container.top-center { top: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
|
|
18
52
|
.ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
|
|
53
|
+
.ts-toast-container.center { top: 50%; left: 50%; transform: translate(-50%, -50%); align-items: center; }
|
|
54
|
+
.ts-toast-overlay.center { align-items: center; justify-content: center; }
|
|
19
55
|
.ts-toast-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 2147483646; }
|
|
20
56
|
.ts-toast.ts-toast-confirm { max-width: min(92vw, 440px); width: max(320px, 60%); flex-direction: column; gap: 12px; padding: 16px 20px; background: var(--toast-bg, #fff); color: var(--toast-color, #000); border: 1px solid var(--toast-border, #e5e7eb); border-radius: 12px; box-shadow: var(--toast-shadow, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)); text-align: center; }
|
|
21
57
|
.ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
|
|
@@ -35,507 +71,676 @@ document.head.appendChild(link);
|
|
|
35
71
|
.ts-toast.ts-toast-confirm.ts-toast-warning .ts-toast-icon { background: #fef3c7; }
|
|
36
72
|
.ts-toast.ts-toast-confirm.ts-toast-error .ts-toast-icon { background: #fee2e2; }
|
|
37
73
|
`;
|
|
38
|
-
|
|
39
|
-
})();
|
|
74
|
+
document.head.appendChild(style);
|
|
75
|
+
})();
|
|
40
76
|
|
|
41
|
-
// Full implementation copied from toast.js but exposed as ES module
|
|
42
77
|
const toast = function (message, options = {}) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
78
|
+
const {
|
|
79
|
+
position = 'top-right',
|
|
80
|
+
animation = 'slide-right', // Default fallback animation
|
|
81
|
+
type = 'info',
|
|
82
|
+
duration = 3000,
|
|
83
|
+
icon = null,
|
|
84
|
+
showLoader = false,
|
|
85
|
+
// behavior/mode: 'alert' (default) or 'confirm'/'swal'
|
|
86
|
+
mode = 'alert',
|
|
87
|
+
// confirm options (used when mode is 'confirm' or 'swal')
|
|
88
|
+
title = null,
|
|
89
|
+
confirmText = 'Yes',
|
|
90
|
+
cancelText = 'No',
|
|
91
|
+
// input field options
|
|
92
|
+
input = false, // 'text', 'email', 'password', 'number', 'textarea', or false
|
|
93
|
+
inputPlaceholder = '',
|
|
94
|
+
inputValue = '',
|
|
95
|
+
// confirm button color customization (optional)
|
|
96
|
+
confirmButtonBg = null,
|
|
97
|
+
confirmButtonColor = null,
|
|
98
|
+
cancelButtonBg = null,
|
|
99
|
+
cancelButtonColor = null,
|
|
100
|
+
onConfirm = null,
|
|
101
|
+
onCancel = null,
|
|
102
|
+
onResult = null,
|
|
103
|
+
useOverlay = true,
|
|
104
|
+
closeOnOverlayClick = true,
|
|
105
|
+
showClose = false,
|
|
106
|
+
// Escape cancels a confirm dialog
|
|
107
|
+
closeOnEscape = true,
|
|
108
|
+
// `message` is written as HTML for backwards compatibility. Pass false to
|
|
109
|
+
// render it as plain text, which is what you want for anything user-supplied.
|
|
110
|
+
allowHtml = true,
|
|
111
|
+
// interactions
|
|
112
|
+
dismissOnClick = true, // ignored if confirm-mode
|
|
113
|
+
onClick = null, // Custom onClick event listener
|
|
114
|
+
onShow = null, // Custom onShow event listener
|
|
115
|
+
onDismiss = null // Custom onDismiss event listener
|
|
116
|
+
} = options;
|
|
117
|
+
|
|
118
|
+
const isConfirm = (mode === 'confirm' || mode === 'swal');
|
|
119
|
+
const reducedMotion = tsToastReducedMotion();
|
|
120
|
+
|
|
121
|
+
// Pick an animation intelligently when one wasn't explicitly provided
|
|
122
|
+
const resolvedAnimation = (typeof options.animation === 'string' && options.animation.trim())
|
|
123
|
+
? (function mapAnim(a){
|
|
124
|
+
const m = {
|
|
125
|
+
'slide-top':'ts-toast-slide-top',
|
|
126
|
+
'slide-bottom':'ts-toast-slide-bottom',
|
|
127
|
+
'slide-left':'ts-toast-slide-left',
|
|
128
|
+
'slide-right':'ts-toast-slide-right',
|
|
129
|
+
'zoom-in':'ts-toast-zoom-in',
|
|
130
|
+
'zoom-out':'ts-toast-zoom-out',
|
|
131
|
+
'flip':'ts-toast-flip'
|
|
132
|
+
};
|
|
133
|
+
return m[a] || a;
|
|
134
|
+
})(options.animation.trim())
|
|
135
|
+
// 'center' has no edge to slide in from, so it zooms like a dialog.
|
|
136
|
+
: (isConfirm || position === 'center' ? 'ts-toast-zoom-in'
|
|
137
|
+
: position.startsWith('top') ? 'ts-toast-slide-top'
|
|
138
|
+
: position.startsWith('bottom') ? 'ts-toast-slide-bottom'
|
|
139
|
+
: position.endsWith('left') ? 'ts-toast-slide-left'
|
|
140
|
+
: 'ts-toast-slide-right');
|
|
141
|
+
|
|
142
|
+
// helper: remove with smooth CSS transition and cleanup (used by alerts)
|
|
143
|
+
const removeWithAnimation = (el, callback) => {
|
|
144
|
+
const anim = el.dataset && el.dataset.anim ? el.dataset.anim : (el.style.animation || '');
|
|
145
|
+
let transform = '';
|
|
146
|
+
if (anim.includes('ts-toast-slide-top')) {
|
|
147
|
+
// Entered from top, exit upwards
|
|
148
|
+
transform = 'translateY(-100%)';
|
|
149
|
+
} else if (anim.includes('ts-toast-slide-bottom')) {
|
|
150
|
+
// Entered from bottom, exit upwards
|
|
151
|
+
transform = 'translateY(100%)';
|
|
152
|
+
} else if (anim.includes('ts-toast-slide-left')) {
|
|
153
|
+
// Entered from left, exit to right
|
|
154
|
+
transform = 'translateX(100%)';
|
|
155
|
+
} else if (anim.includes('ts-toast-slide-right')) {
|
|
156
|
+
// Entered from right, exit to right
|
|
157
|
+
transform = 'translateX(100%)';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
el.classList.add('ts-toast-slide-out');
|
|
161
|
+
el.classList.remove('ts-toast-show');
|
|
162
|
+
// Stop any running keyframe animation and drive exit via CSS transition
|
|
163
|
+
el.style.animation = '';
|
|
164
|
+
if (transform) {
|
|
165
|
+
el.style.transform = transform;
|
|
166
|
+
}
|
|
167
|
+
el.style.opacity = '0';
|
|
168
|
+
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
el.classList.remove('ts-toast-slide-out');
|
|
171
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
172
|
+
if (typeof callback === 'function') callback();
|
|
173
|
+
}, reducedMotion ? 0 : 500);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const toastElement = document.createElement('div');
|
|
177
|
+
toastElement.className = `ts-toast ts-toast-${type}${isConfirm ? ' ts-toast-confirm' : ''}`;
|
|
178
|
+
toastElement.dataset.anim = resolvedAnimation;
|
|
179
|
+
if (!reducedMotion) toastElement.style.animation = `${resolvedAnimation} 0.5s ease`;
|
|
180
|
+
|
|
181
|
+
const uid = `ts-toast-${++tsToastIdCounter}`;
|
|
182
|
+
if (isConfirm) {
|
|
183
|
+
// Without these a screen reader announces nothing, and without tabindex the
|
|
184
|
+
// dialog cannot take focus away from whatever opened it.
|
|
185
|
+
toastElement.setAttribute('role', 'dialog');
|
|
186
|
+
toastElement.setAttribute('aria-modal', 'true');
|
|
187
|
+
toastElement.tabIndex = -1;
|
|
188
|
+
} else if (type === 'error' || type === 'warning') {
|
|
189
|
+
toastElement.setAttribute('role', 'alert');
|
|
190
|
+
}
|
|
191
|
+
// In confirm mode, we stack content vertically; in alert mode keep original layout
|
|
192
|
+
if (!isConfirm) {
|
|
193
|
+
toastElement.style.flexDirection = 'row-reverse';
|
|
194
|
+
toastElement.style.justifyContent = 'flex-end';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Create Icon Element
|
|
198
|
+
const iconElement = document.createElement('span');
|
|
199
|
+
iconElement.className = 'ts-toast-icon';
|
|
200
|
+
iconElement.style.display = 'flex';
|
|
201
|
+
if (icon) {
|
|
202
|
+
iconElement.textContent = icon;
|
|
203
|
+
} else {
|
|
204
|
+
const img = document.createElement('img');
|
|
205
|
+
img.alt = '';
|
|
206
|
+
img.setAttribute('aria-hidden', 'true');
|
|
207
|
+
img.style.width = '30px';
|
|
208
|
+
img.style.height = '30px';
|
|
209
|
+
img.style.objectFit = 'contain';
|
|
210
|
+
|
|
211
|
+
// No cache-buster: these GIFs are immutable per version, so let the
|
|
212
|
+
// browser and the CDN actually cache them.
|
|
213
|
+
const iconFile = { success: 'success.gif', error: 'error.gif', info: 'info.gif', warning: 'warning.gif' }[type];
|
|
214
|
+
if (iconFile) img.src = `${TS_TOAST_CDN}/assets/img/${iconFile}`;
|
|
215
|
+
|
|
216
|
+
iconElement.appendChild(img);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Create Body
|
|
220
|
+
const toastBody = document.createElement('div');
|
|
221
|
+
toastBody.className = 'ts-toast-body';
|
|
222
|
+
toastBody.id = `${uid}-body`;
|
|
223
|
+
// HTML by default for backwards compatibility; pass allowHtml: false for
|
|
224
|
+
// anything that came from a user.
|
|
225
|
+
if (allowHtml) toastBody.innerHTML = message;
|
|
226
|
+
else toastBody.textContent = message;
|
|
227
|
+
|
|
228
|
+
// Content row for confirm (icon + text side-by-side)
|
|
229
|
+
let contentRow = null;
|
|
230
|
+
if (isConfirm) {
|
|
231
|
+
contentRow = document.createElement('div');
|
|
232
|
+
contentRow.className = 'ts-toast-content';
|
|
233
|
+
contentRow.appendChild(iconElement);
|
|
234
|
+
if (title) {
|
|
235
|
+
const titleEl = document.createElement('div');
|
|
236
|
+
titleEl.className = 'ts-toast-title';
|
|
237
|
+
titleEl.id = `${uid}-title`;
|
|
238
|
+
titleEl.textContent = title;
|
|
239
|
+
contentRow.appendChild(titleEl);
|
|
240
|
+
toastElement.setAttribute('aria-labelledby', titleEl.id);
|
|
241
|
+
}
|
|
242
|
+
contentRow.appendChild(toastBody);
|
|
243
|
+
toastElement.setAttribute('aria-describedby', toastBody.id);
|
|
244
|
+
toastElement.appendChild(contentRow);
|
|
245
|
+
} else {
|
|
246
|
+
toastElement.appendChild(toastBody);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Input field (for confirm mode with input)
|
|
250
|
+
let inputElement = null;
|
|
251
|
+
if (isConfirm && input) {
|
|
252
|
+
if (input === 'textarea') {
|
|
253
|
+
inputElement = document.createElement('textarea');
|
|
254
|
+
inputElement.rows = 3;
|
|
255
|
+
} else {
|
|
256
|
+
inputElement = document.createElement('input');
|
|
257
|
+
inputElement.type = input === 'text' || input === 'email' || input === 'password' || input === 'number' ? input : 'text';
|
|
258
|
+
}
|
|
259
|
+
inputElement.className = 'ts-toast-input';
|
|
260
|
+
inputElement.placeholder = inputPlaceholder;
|
|
261
|
+
inputElement.value = inputValue;
|
|
262
|
+
// Enter submits a single-line field, the way a native prompt does.
|
|
263
|
+
// A textarea keeps Enter for newlines.
|
|
264
|
+
if (input !== 'textarea') {
|
|
265
|
+
inputElement.addEventListener('keydown', (e) => {
|
|
266
|
+
if (e.key === 'Enter') { e.preventDefault(); resolveAndClose(true); }
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
toastElement.appendChild(inputElement);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Actions (for confirm mode)
|
|
273
|
+
let actionsContainer = null;
|
|
274
|
+
let resultResolver = null;
|
|
275
|
+
// Assigned in confirm mode; the overlay and (x) handlers below call it so that
|
|
276
|
+
// *every* way of dismissing the dialog settles the promise and the callbacks.
|
|
277
|
+
let resolveAndClose = null;
|
|
278
|
+
// Releases the scroll lock, the key handler and the focus this dialog took.
|
|
279
|
+
let releaseModal = () => {};
|
|
280
|
+
if (isConfirm) {
|
|
281
|
+
actionsContainer = document.createElement('div');
|
|
282
|
+
actionsContainer.className = 'ts-toast-actions';
|
|
283
|
+
|
|
284
|
+
const cancelBtn = document.createElement('button');
|
|
285
|
+
cancelBtn.className = 'ts-toast-btn cancel';
|
|
286
|
+
cancelBtn.textContent = cancelText;
|
|
287
|
+
|
|
288
|
+
const confirmBtn = document.createElement('button');
|
|
289
|
+
confirmBtn.className = 'ts-toast-btn confirm';
|
|
290
|
+
confirmBtn.textContent = confirmText;
|
|
291
|
+
|
|
292
|
+
// Apply custom button colors if provided (inline style overrides theme defaults)
|
|
293
|
+
if (cancelButtonBg) cancelBtn.style.background = cancelButtonBg;
|
|
294
|
+
if (cancelButtonColor) cancelBtn.style.color = cancelButtonColor;
|
|
295
|
+
if (confirmButtonBg) confirmBtn.style.background = confirmButtonBg;
|
|
296
|
+
if (confirmButtonColor) confirmBtn.style.color = confirmButtonColor;
|
|
297
|
+
|
|
298
|
+
actionsContainer.appendChild(cancelBtn);
|
|
299
|
+
actionsContainer.appendChild(confirmBtn);
|
|
300
|
+
toastElement.appendChild(actionsContainer);
|
|
301
|
+
|
|
302
|
+
// Create a Promise that resolves on user choice; expose via property
|
|
303
|
+
toastElement.result = new Promise((resolve) => { resultResolver = resolve; });
|
|
304
|
+
|
|
305
|
+
let settled = false;
|
|
306
|
+
resolveAndClose = (confirmed) => {
|
|
307
|
+
if (settled) return; // a button click and a backdrop click can race
|
|
308
|
+
settled = true;
|
|
309
|
+
// With an input, confirming always yields a string (possibly '') and
|
|
310
|
+
// cancelling yields null, so an empty submission stays distinguishable
|
|
311
|
+
// from a cancel. Without an input the contract is still true/false.
|
|
312
|
+
const result = confirmed
|
|
313
|
+
? (inputElement ? inputElement.value : true)
|
|
314
|
+
: (inputElement ? null : false);
|
|
315
|
+
if (resultResolver) resultResolver(result);
|
|
316
|
+
if (confirmed && typeof onConfirm === 'function') onConfirm(result, toastElement);
|
|
317
|
+
if (!confirmed && typeof onCancel === 'function') onCancel(toastElement);
|
|
318
|
+
if (typeof onResult === 'function') onResult(result, toastElement);
|
|
319
|
+
releaseModal();
|
|
320
|
+
// Use the same slide+fade removal as alerts
|
|
321
|
+
removeWithAnimation(toastElement, () => {
|
|
322
|
+
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
323
|
+
// Remove overlay if present
|
|
324
|
+
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
cancelBtn.addEventListener('click', (e) => { e.stopPropagation(); resolveAndClose(false); });
|
|
329
|
+
confirmBtn.addEventListener('click', (e) => { e.stopPropagation(); resolveAndClose(true); });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Loader Element
|
|
333
|
+
let loader = null;
|
|
334
|
+
if (showLoader) {
|
|
335
|
+
loader = document.createElement('div');
|
|
336
|
+
loader.className = 'ts-toast-loader';
|
|
337
|
+
toastElement.appendChild(loader);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Container/Overlay
|
|
341
|
+
let overlay = null;
|
|
342
|
+
if (isConfirm && useOverlay) {
|
|
343
|
+
overlay = document.createElement('div');
|
|
344
|
+
// A modal centres by default. The `position` default of 'top-right' is meant
|
|
345
|
+
// for toasts; applying it here parked the dialog in a corner of the backdrop.
|
|
346
|
+
overlay.className = 'ts-toast-overlay' + (options.position ? ` ${position}` : '');
|
|
347
|
+
document.body.appendChild(overlay);
|
|
348
|
+
overlay.appendChild(toastElement);
|
|
349
|
+
if (showClose) {
|
|
350
|
+
const closeBtn = document.createElement('button');
|
|
351
|
+
closeBtn.className = 'ts-toast-close';
|
|
352
|
+
closeBtn.setAttribute('aria-label', 'Close');
|
|
353
|
+
closeBtn.innerHTML = '×';
|
|
354
|
+
closeBtn.addEventListener('click', (e) => {
|
|
355
|
+
e.stopPropagation();
|
|
356
|
+
// Dismissing via (x) is a cancel, so it has to settle like one.
|
|
357
|
+
resolveAndClose(false);
|
|
358
|
+
});
|
|
359
|
+
toastElement.appendChild(closeBtn);
|
|
360
|
+
}
|
|
361
|
+
if (closeOnOverlayClick) {
|
|
362
|
+
// The cancel must both start and end on the backdrop. Selecting text in
|
|
363
|
+
// the input and releasing the mouse outside the card produced a click
|
|
364
|
+
// whose target was the overlay, which threw the dialog away mid-edit.
|
|
365
|
+
let pressedOnBackdrop = false;
|
|
366
|
+
overlay.addEventListener('pointerdown', (e) => { pressedOnBackdrop = e.target === overlay; });
|
|
367
|
+
overlay.addEventListener('click', (e) => {
|
|
368
|
+
// Backdrop click is a cancel: settle the promise and onCancel/onResult,
|
|
369
|
+
// then close. Previously this resolved only the internal el.result,
|
|
370
|
+
// so `await toast.confirm(...)` hung forever.
|
|
371
|
+
if (e.target === overlay && pressedOnBackdrop) resolveAndClose(false);
|
|
372
|
+
pressedOnBackdrop = false;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
// Standard positioned container
|
|
377
|
+
let container = document.querySelector(`.ts-toast-container.${position}`);
|
|
378
|
+
if (!container) {
|
|
379
|
+
container = document.createElement('div');
|
|
380
|
+
container.className = `ts-toast-container ${position}`;
|
|
381
|
+
document.body.appendChild(container);
|
|
382
|
+
}
|
|
383
|
+
if (!container.hasAttribute('aria-live')) {
|
|
384
|
+
// Without this a toast is invisible to a screen reader.
|
|
385
|
+
container.setAttribute('role', 'status');
|
|
386
|
+
container.setAttribute('aria-live', 'polite');
|
|
387
|
+
container.setAttribute('aria-relevant', 'additions');
|
|
388
|
+
}
|
|
389
|
+
container.appendChild(toastElement);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (isConfirm) {
|
|
393
|
+
// The dialog has to own the keyboard while it is open. Without this the
|
|
394
|
+
// element that opened it keeps focus, so pressing Enter or Space activates
|
|
395
|
+
// it again and stacks a second dialog on top of the first — and Tab walks
|
|
396
|
+
// through the page behind the backdrop.
|
|
397
|
+
const previouslyFocused = document.activeElement;
|
|
398
|
+
|
|
399
|
+
tsToastOpenModals += 1;
|
|
400
|
+
if (tsToastOpenModals === 1) {
|
|
401
|
+
tsToastPrevOverflow = document.body.style.overflow;
|
|
402
|
+
document.body.style.overflow = 'hidden';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// With dialogs stacked, only the top one should answer the keyboard.
|
|
406
|
+
const isTopmost = () => {
|
|
407
|
+
const open = document.querySelectorAll('.ts-toast.ts-toast-confirm');
|
|
408
|
+
return open.length === 0 || open[open.length - 1] === toastElement;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const onKeydown = (e) => {
|
|
412
|
+
if (!isTopmost()) return;
|
|
413
|
+
|
|
414
|
+
if (e.key === 'Escape' && closeOnEscape) {
|
|
415
|
+
e.preventDefault();
|
|
416
|
+
resolveAndClose(false);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (e.key !== 'Tab') return;
|
|
420
|
+
|
|
421
|
+
const focusables = tsToastFocusable(toastElement);
|
|
422
|
+
if (!focusables.length) { e.preventDefault(); return; }
|
|
423
|
+
|
|
424
|
+
const first = focusables[0];
|
|
425
|
+
const last = focusables[focusables.length - 1];
|
|
426
|
+
|
|
427
|
+
// Focus can start outside the dialog (the trigger button); pull it back.
|
|
428
|
+
if (!toastElement.contains(document.activeElement)) {
|
|
429
|
+
e.preventDefault();
|
|
430
|
+
(e.shiftKey ? last : first).focus();
|
|
431
|
+
} else if (e.shiftKey && document.activeElement === first) {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
last.focus();
|
|
434
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
first.focus();
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
document.addEventListener('keydown', onKeydown, true);
|
|
440
|
+
|
|
441
|
+
releaseModal = () => {
|
|
442
|
+
document.removeEventListener('keydown', onKeydown, true);
|
|
443
|
+
tsToastOpenModals = Math.max(0, tsToastOpenModals - 1);
|
|
444
|
+
if (tsToastOpenModals === 0) document.body.style.overflow = tsToastPrevOverflow;
|
|
445
|
+
// Hand the keyboard back to whatever opened the dialog.
|
|
446
|
+
if (previouslyFocused && typeof previouslyFocused.focus === 'function' &&
|
|
447
|
+
document.contains(previouslyFocused)) {
|
|
448
|
+
previouslyFocused.focus();
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// Land on the input when there is one, otherwise the confirm button.
|
|
453
|
+
(inputElement || toastElement.querySelector('.ts-toast-btn.confirm') || toastElement).focus();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Trigger the onShow event if provided
|
|
457
|
+
if (onShow && typeof onShow === 'function') {
|
|
458
|
+
onShow(toastElement);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Show Toast with animation
|
|
462
|
+
setTimeout(() => {
|
|
463
|
+
toastElement.classList.add('ts-toast-show');
|
|
464
|
+
}, 100);
|
|
465
|
+
|
|
466
|
+
// Handle Loader and Icon
|
|
467
|
+
if (showLoader && loader) {
|
|
468
|
+
setTimeout(() => {
|
|
469
|
+
// Skip auto-complete if controlled by toast.loading()
|
|
470
|
+
if (toastElement._managedByLoading) return;
|
|
471
|
+
loader.classList.add('done');
|
|
472
|
+
loader.remove();
|
|
473
|
+
if (!toastElement.contains(iconElement)) {
|
|
474
|
+
if (isConfirm && contentRow) contentRow.appendChild(iconElement);
|
|
475
|
+
else toastElement.appendChild(iconElement); // Add icon only if not present
|
|
476
|
+
}
|
|
477
|
+
}, 2000); // Simulate a loading period of 2 seconds
|
|
478
|
+
}
|
|
479
|
+
if (!showLoader) {
|
|
480
|
+
// For confirm, icon already added above inside contentRow; avoid moving it
|
|
481
|
+
if (!isConfirm && !toastElement.contains(iconElement)) {
|
|
482
|
+
toastElement.appendChild(iconElement);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Auto remove after the duration (skip for confirm mode or when duration <= 0)
|
|
487
|
+
if (!isConfirm && duration > 0) {
|
|
488
|
+
const autoRemove = setTimeout(() => {
|
|
489
|
+
removeWithAnimation(toastElement, () => {
|
|
490
|
+
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
491
|
+
});
|
|
492
|
+
}, duration);
|
|
493
|
+
toastElement._autoRemove = autoRemove;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Add event listener for closing the toast when clicked (disabled in confirm mode)
|
|
497
|
+
if (!isConfirm && dismissOnClick) {
|
|
498
|
+
toastElement.addEventListener('click', () => {
|
|
499
|
+
if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove); // Clear the auto-remove timeout
|
|
500
|
+
// onClick belongs to the click, not to the end of the exit animation,
|
|
501
|
+
// which is where it used to fire half a second late.
|
|
502
|
+
if (onClick && typeof onClick === 'function') onClick(toastElement);
|
|
503
|
+
removeWithAnimation(toastElement, () => {
|
|
504
|
+
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Add swipe event listeners for mobile dismissal
|
|
510
|
+
if (!isConfirm) {
|
|
511
|
+
let touchStartX = 0;
|
|
512
|
+
let touchStartY = 0;
|
|
513
|
+
let touchEndX = 0;
|
|
514
|
+
|
|
515
|
+
// Passive: these never preventDefault, and a non-passive touchstart blocks
|
|
516
|
+
// scrolling on the whole toast.
|
|
517
|
+
toastElement.addEventListener('touchstart', (e) => {
|
|
518
|
+
touchStartX = e.changedTouches[0].screenX;
|
|
519
|
+
touchStartY = e.changedTouches[0].screenY;
|
|
520
|
+
}, { passive: true });
|
|
521
|
+
|
|
522
|
+
toastElement.addEventListener('touchend', (e) => {
|
|
523
|
+
touchEndX = e.changedTouches[0].screenX;
|
|
524
|
+
const dx = Math.abs(touchStartX - touchEndX);
|
|
525
|
+
const dy = Math.abs(touchStartY - e.changedTouches[0].screenY);
|
|
526
|
+
// Only a mostly-horizontal swipe dismisses, so scrolling the page past a
|
|
527
|
+
// toast no longer throws it away on a bit of sideways drift.
|
|
528
|
+
if (dx > 50 && dx > dy) {
|
|
529
|
+
if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
|
|
530
|
+
removeWithAnimation(toastElement, () => {
|
|
531
|
+
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Let callers dismiss a toast they are holding, instead of only waiting out
|
|
538
|
+
// the duration or making the user click it.
|
|
539
|
+
toastElement.close = () => {
|
|
540
|
+
if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
|
|
541
|
+
if (isConfirm) { resolveAndClose(false); return; }
|
|
542
|
+
removeWithAnimation(toastElement, () => {
|
|
543
|
+
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
return toastElement;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// Shorthands return the toast element so it can be passed to toast.update().
|
|
551
|
+
toast.success = function (message, options) {
|
|
552
|
+
return toast(message, { ...options, type: 'success' });
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
toast.error = function (message, options) {
|
|
556
|
+
return toast(message, { ...options, type: 'error' });
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
toast.warning = function (message, options) {
|
|
560
|
+
return toast(message, { ...options, type: 'warning' });
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
toast.info = function (message, options) {
|
|
564
|
+
return toast(message, { ...options, type: 'info' });
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// Update toast function to handle removal with animation
|
|
568
|
+
toast.update = function (toastElement, message, options = {}) {
|
|
569
|
+
const {
|
|
570
|
+
type = null,
|
|
571
|
+
icon = null,
|
|
572
|
+
showLoader = false,
|
|
573
|
+
duration = 3000, // Default duration (in ms)
|
|
574
|
+
onClick = null, // Custom onClick event listener
|
|
575
|
+
onShow = null, // Custom onShow event listener
|
|
576
|
+
onDismiss = null // Custom onDismiss event listener
|
|
577
|
+
} = options;
|
|
578
|
+
|
|
579
|
+
// Remove old loader (if any)
|
|
580
|
+
const oldLoader = toastElement.querySelector('.ts-toast-loader');
|
|
581
|
+
const oldIcon = toastElement.querySelector('.ts-toast-icon');
|
|
582
|
+
if (oldLoader) oldLoader.remove();
|
|
583
|
+
|
|
584
|
+
// Update toast class and message. Swap only the type modifier: overwriting
|
|
585
|
+
// className dropped ts-toast-show (so the toast faded out), dropped
|
|
586
|
+
// ts-toast-confirm (so confirm dialogs lost their layout), and leaked the
|
|
587
|
+
// position onto the toast instead of the container.
|
|
588
|
+
if (type) {
|
|
589
|
+
['success', 'error', 'info', 'warning'].forEach((t) => toastElement.classList.remove(`ts-toast-${t}`));
|
|
590
|
+
toastElement.classList.add('ts-toast', `ts-toast-${type}`, 'ts-toast-show');
|
|
591
|
+
}
|
|
592
|
+
const toastBody = toastElement.querySelector('.ts-toast-body');
|
|
593
|
+
if (toastBody) {
|
|
594
|
+
toastBody.innerHTML = message;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Handle Icon update only if it's new or hasn't been set yet
|
|
598
|
+
if (oldIcon) {
|
|
599
|
+
oldIcon.remove(); // Remove the old icon first
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const iconElement = document.createElement('span');
|
|
603
|
+
iconElement.className = 'ts-toast-icon';
|
|
604
|
+
iconElement.style.display = 'flex';
|
|
605
|
+
|
|
606
|
+
if (icon) {
|
|
607
|
+
iconElement.textContent = icon;
|
|
608
|
+
} else {
|
|
609
|
+
const img = document.createElement('img');
|
|
610
|
+
img.alt = '';
|
|
611
|
+
img.setAttribute('aria-hidden', 'true');
|
|
612
|
+
img.style.width = '30px';
|
|
613
|
+
img.style.height = '30px';
|
|
614
|
+
img.style.objectFit = 'contain';
|
|
615
|
+
|
|
616
|
+
const iconFile = { success: 'success.gif', error: 'error.gif', info: 'info.gif', warning: 'warning.gif' }[type];
|
|
617
|
+
if (iconFile) img.src = `${TS_TOAST_CDN}/assets/img/${iconFile}`;
|
|
618
|
+
|
|
619
|
+
iconElement.appendChild(img);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Append the new icon immediately (inside the content row for confirm dialogs)
|
|
623
|
+
const contentRow = toastElement.querySelector('.ts-toast-content');
|
|
624
|
+
(contentRow || toastElement).appendChild(iconElement);
|
|
625
|
+
|
|
626
|
+
// Handle loader if requested
|
|
627
|
+
if (showLoader) {
|
|
628
|
+
const loader = document.createElement('div');
|
|
629
|
+
loader.className = 'ts-toast-loader';
|
|
630
|
+
toastElement.appendChild(loader);
|
|
631
|
+
setTimeout(() => {
|
|
632
|
+
loader.classList.add('done');
|
|
633
|
+
}, 2000); // Simulate loader completion after 2 seconds
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Clear previous auto-remove timer if needed
|
|
637
|
+
if (toastElement._autoRemove) {
|
|
638
|
+
clearTimeout(toastElement._autoRemove);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Set the auto-remove timer again to ensure toast disappears after the duration
|
|
642
|
+
const autoRemove = setTimeout(() => {
|
|
643
|
+
const removeWithAnimation = (el, cb) => {
|
|
644
|
+
el.classList.add('ts-toast-slide-out');
|
|
645
|
+
el.classList.remove('ts-toast-show');
|
|
646
|
+
el.style.animation = '';
|
|
647
|
+
setTimeout(() => {
|
|
648
|
+
el.classList.remove('ts-toast-slide-out');
|
|
649
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
650
|
+
if (typeof cb === 'function') cb();
|
|
651
|
+
}, 500);
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
removeWithAnimation(toastElement, () => {
|
|
655
|
+
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
656
|
+
});
|
|
657
|
+
}, duration);
|
|
658
|
+
|
|
659
|
+
toastElement._autoRemove = autoRemove; // Re-set the auto-remove timer
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
toast.loading = function (message, options = {}) {
|
|
663
|
+
const toastElement = toast(message, {
|
|
664
|
+
...options,
|
|
665
|
+
type: options.type || 'info', // Default type is 'info'
|
|
666
|
+
duration: 0, // Sticky until manually updated/closed
|
|
667
|
+
showLoader: true, // Always show loader during loading
|
|
668
|
+
icon: null
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Force reflow and add animation after DOM insert
|
|
672
|
+
requestAnimationFrame(() => {
|
|
673
|
+
toastElement.classList.add('ts-toast-show');
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
const loader = toastElement.querySelector('.ts-toast-loader');
|
|
677
|
+
let iconElement = toastElement.querySelector('.ts-toast-icon');
|
|
678
|
+
|
|
679
|
+
// Ensure the iconElement is created and appended if it doesn't exist
|
|
680
|
+
if (!iconElement) {
|
|
681
|
+
iconElement = document.createElement('span');
|
|
682
|
+
iconElement.className = 'ts-toast-icon';
|
|
683
|
+
iconElement.style.display = 'flex';
|
|
684
|
+
toastElement.appendChild(iconElement);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// mark as managed by loading flow to avoid internal auto-complete
|
|
688
|
+
toastElement._managedByLoading = true;
|
|
689
|
+
|
|
690
|
+
// Ensure loader is handled properly
|
|
691
|
+
if (loader) {
|
|
692
|
+
setTimeout(() => {
|
|
693
|
+
// Keep spinning until update() decides otherwise
|
|
694
|
+
if (!toastElement._managedByLoading) loader.classList.add('done');
|
|
695
|
+
}, 2000); // Simulate a loading period of 2 seconds
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return {
|
|
699
|
+
update: (newMessage, newOptions = {}) => {
|
|
700
|
+
// Let update manage completion: stop managing/finish loader
|
|
701
|
+
toastElement._managedByLoading = false;
|
|
702
|
+
toast.update(toastElement, newMessage, {
|
|
703
|
+
...newOptions,
|
|
704
|
+
showLoader: false // Disable loader when updating the message
|
|
705
|
+
});
|
|
706
|
+
},
|
|
707
|
+
// Reuse the element's own close so onDismiss fires, which this
|
|
708
|
+
// hand-rolled copy of the removal never did.
|
|
709
|
+
close: () => {
|
|
710
|
+
toastElement._managedByLoading = false;
|
|
711
|
+
toastElement.close();
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// Convenience API: swal-like confirm dialog
|
|
717
|
+
// Usage: toast.confirm('Are you sure?', { type: 'warning', confirmText: 'Yes', cancelText: 'No' }).then(ok => {...})
|
|
718
|
+
toast.confirm = function (message, options = {}) {
|
|
719
|
+
return new Promise((resolve) => {
|
|
720
|
+
const el = toast(message, {
|
|
721
|
+
...options,
|
|
722
|
+
mode: 'confirm',
|
|
723
|
+
duration: 0, // prevent auto-dismiss
|
|
724
|
+
dismissOnClick: false,
|
|
725
|
+
onResult: (val) => resolve(val)
|
|
726
|
+
});
|
|
727
|
+
// If consumer needs the element, it is returned by toast() but we ignore here.
|
|
728
|
+
// They can still call toast(...) with mode: 'confirm' to get the element and read el.result
|
|
729
|
+
void el; // no-op
|
|
730
|
+
});
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// Close every toast currently on screen. Confirm dialogs settle as a cancel.
|
|
734
|
+
toast.dismissAll = function () {
|
|
735
|
+
document.querySelectorAll('.ts-toast').forEach((el) => {
|
|
736
|
+
if (typeof el.close === 'function') el.close();
|
|
737
|
+
});
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// Expose globally for CDN / browser usage
|
|
741
|
+
if (typeof window !== 'undefined') {
|
|
742
|
+
window.toast = toast;
|
|
743
|
+
}
|
|
539
744
|
|
|
540
745
|
// ES module export
|
|
541
746
|
export default toast;
|