@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/toast.module.js CHANGED
@@ -1,21 +1,57 @@
1
1
  "use strict";
2
2
 
3
- // Dynamically load the external CSS file (same as toast.js)
4
- const link = document.createElement("link");
5
- link.rel = "stylesheet";
6
- link.href = "https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@5.3.0/assets/css/toast.min.css";
7
- document.head.appendChild(link);
8
-
9
- // Inject minimal styles for confirm actions and overlay (same as toast.js)
10
- (function injectInlineStyles() {
11
- const STYLE_ID = "ts-toast-inline-extras";
12
- if (document.getElementById(STYLE_ID)) return;
13
- const style = document.createElement("style");
14
- style.id = STYLE_ID;
15
- style.textContent = `
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
- document.head.appendChild(style);
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
- const {
44
- position = "top-right",
45
- animation = "slide-right",
46
- type = "info",
47
- duration = 3000,
48
- icon = null,
49
- showLoader = false,
50
- mode = "alert",
51
- title = null,
52
- confirmText = "Yes",
53
- cancelText = "No",
54
- input = false,
55
- inputPlaceholder = "",
56
- inputValue = "",
57
- confirmButtonBg = null,
58
- confirmButtonColor = null,
59
- cancelButtonBg = null,
60
- cancelButtonColor = null,
61
- onConfirm = null,
62
- onCancel = null,
63
- onResult = null,
64
- useOverlay = true,
65
- closeOnOverlayClick = true,
66
- showClose = false,
67
- dismissOnClick = true,
68
- onClick = null,
69
- onShow = null,
70
- onDismiss = null,
71
- } = options;
72
-
73
- const isConfirm = mode === "confirm" || mode === "swal";
74
-
75
- const resolvedAnimation =
76
- typeof options.animation === "string" && options.animation.trim()
77
- ? (function mapAnim(a) {
78
- const m = {
79
- "slide-top": "ts-toast-slide-top",
80
- "slide-bottom": "ts-toast-slide-bottom",
81
- "slide-left": "ts-toast-slide-left",
82
- "slide-right": "ts-toast-slide-right",
83
- "zoom-in": "ts-toast-zoom-in",
84
- "zoom-out": "ts-toast-zoom-out",
85
- flip: "ts-toast-flip",
86
- };
87
- return m[a] || a;
88
- })(options.animation.trim())
89
- : isConfirm
90
- ? "ts-toast-zoom-in"
91
- : position.startsWith("top")
92
- ? "ts-toast-slide-top"
93
- : position.startsWith("bottom")
94
- ? "ts-toast-slide-bottom"
95
- : position.endsWith("left")
96
- ? "ts-toast-slide-left"
97
- : "ts-toast-slide-right";
98
-
99
- // helper: remove with smooth CSS transition and cleanup (used by alerts and confirms)
100
- const removeWithAnimation = (el, callback) => {
101
- const anim = el.dataset && el.dataset.anim ? el.dataset.anim : (el.style.animation || "");
102
- let transform = "";
103
- if (anim.includes("ts-toast-slide-top")) {
104
- // Entered from top, exit upwards
105
- transform = "translateY(-100%)";
106
- } else if (anim.includes("ts-toast-slide-bottom")) {
107
- // Entered from bottom, exit upwards
108
- transform = "translateY(100%)";
109
- } else if (anim.includes("ts-toast-slide-left")) {
110
- // Entered from left, exit to right
111
- transform = "translateX(100%)";
112
- } else if (anim.includes("ts-toast-slide-right")) {
113
- // Entered from right, exit to right
114
- transform = "translateX(100%)";
115
- }
116
-
117
- el.classList.add("ts-toast-slide-out");
118
- el.classList.remove("ts-toast-show");
119
- el.style.animation = "";
120
- if (transform) {
121
- el.style.transform = transform;
122
- }
123
- el.style.opacity = "0";
124
-
125
- setTimeout(() => {
126
- el.classList.remove("ts-toast-slide-out");
127
- if (el.parentNode) el.parentNode.removeChild(el);
128
- if (typeof callback === "function") callback();
129
- }, 500);
130
- };
131
-
132
- const toastElement = document.createElement("div");
133
- toastElement.className = `ts-toast ts-toast-${type}${isConfirm ? " ts-toast-confirm" : ""}`;
134
- toastElement.dataset.anim = resolvedAnimation;
135
- toastElement.style.animation = `${resolvedAnimation} 0.5s ease`;
136
- if (!isConfirm) {
137
- toastElement.style.flexDirection = "row-reverse";
138
- toastElement.style.justifyContent = "flex-end";
139
- }
140
-
141
- const iconElement = document.createElement("span");
142
- iconElement.className = "ts-toast-icon";
143
- iconElement.style.display = "flex";
144
- if (icon) {
145
- iconElement.textContent = icon;
146
- } else {
147
- const img = document.createElement("img");
148
- img.src = "";
149
- img.style.width = "30px";
150
- img.style.height = "30px";
151
- img.style.objectFit = "contain";
152
-
153
- const baseUrl =
154
- "https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@5.3.0/assets/img/";
155
- const timestamp = new Date().getTime();
156
-
157
- if (type === "success") {
158
- img.src = `${baseUrl}success.gif?t=${timestamp}`;
159
- } else if (type === "error") {
160
- img.src = `${baseUrl}error.gif?t=${timestamp}`;
161
- } else if (type === "info") {
162
- img.src = `${baseUrl}info.gif?t=${timestamp}`;
163
- } else if (type === "warning") {
164
- img.src = `${baseUrl}warning.gif?t=${timestamp}`;
165
- }
166
-
167
- iconElement.appendChild(img);
168
- }
169
-
170
- const toastBody = document.createElement("div");
171
- toastBody.className = "ts-toast-body";
172
- toastBody.innerHTML = message;
173
-
174
- let contentRow = null;
175
- if (isConfirm) {
176
- contentRow = document.createElement("div");
177
- contentRow.className = "ts-toast-content";
178
- contentRow.appendChild(iconElement);
179
- if (title) {
180
- const titleEl = document.createElement("div");
181
- titleEl.className = "ts-toast-title";
182
- titleEl.textContent = title;
183
- contentRow.appendChild(titleEl);
184
- }
185
- contentRow.appendChild(toastBody);
186
- toastElement.appendChild(contentRow);
187
- } else {
188
- toastElement.appendChild(toastBody);
189
- }
190
-
191
- // Input field (for confirm mode with input)
192
- let inputElement = null;
193
- if (isConfirm && input) {
194
- if (input === "textarea") {
195
- inputElement = document.createElement("textarea");
196
- inputElement.rows = 3;
197
- } else {
198
- inputElement = document.createElement("input");
199
- inputElement.type = input === "text" || input === "email" || input === "password" || input === "number" ? input : "text";
200
- }
201
- inputElement.className = "ts-toast-input";
202
- inputElement.placeholder = inputPlaceholder;
203
- inputElement.value = inputValue;
204
- toastElement.appendChild(inputElement);
205
- }
206
-
207
- let actionsContainer = null;
208
- let resultResolver = null;
209
- if (isConfirm) {
210
- actionsContainer = document.createElement("div");
211
- actionsContainer.className = "ts-toast-actions";
212
-
213
- const cancelBtn = document.createElement("button");
214
- cancelBtn.className = "ts-toast-btn cancel";
215
- cancelBtn.textContent = cancelText;
216
-
217
- const confirmBtn = document.createElement("button");
218
- confirmBtn.className = "ts-toast-btn confirm";
219
- confirmBtn.textContent = confirmText;
220
-
221
- if (cancelButtonBg) cancelBtn.style.background = cancelButtonBg;
222
- if (cancelButtonColor) cancelBtn.style.color = cancelButtonColor;
223
- if (confirmButtonBg) confirmBtn.style.background = confirmButtonBg;
224
- if (confirmButtonColor) confirmBtn.style.color = confirmButtonColor;
225
-
226
- actionsContainer.appendChild(cancelBtn);
227
- actionsContainer.appendChild(confirmBtn);
228
- toastElement.appendChild(actionsContainer);
229
-
230
- toastElement.result = new Promise((resolve) => {
231
- resultResolver = resolve;
232
- });
233
-
234
- const resolveAndClose = (value) => {
235
- const result = (value && inputElement !== null) ? inputElement.value : value;
236
- if (resultResolver) resultResolver(result);
237
- if (value && typeof onConfirm === "function") onConfirm((inputElement !== null) ? inputElement.value : value, toastElement);
238
- if (!value && typeof onCancel === "function") onCancel(toastElement);
239
- if (typeof onResult === "function") onResult(result, toastElement);
240
- // Use the same slide+fade removal as alerts
241
- removeWithAnimation(toastElement, () => {
242
- if (onDismiss && typeof onDismiss === "function") onDismiss(toastElement);
243
- if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
244
- });
245
- };
246
-
247
- cancelBtn.addEventListener("click", (e) => {
248
- e.stopPropagation();
249
- resolveAndClose(false);
250
- });
251
- confirmBtn.addEventListener("click", (e) => {
252
- e.stopPropagation();
253
- resolveAndClose(true);
254
- });
255
- }
256
-
257
- let loader = null;
258
- if (showLoader) {
259
- loader = document.createElement("div");
260
- loader.className = "ts-toast-loader";
261
- toastElement.appendChild(loader);
262
- }
263
-
264
- let overlay = null;
265
- if (isConfirm && useOverlay) {
266
- overlay = document.createElement("div");
267
- overlay.className = `ts-toast-overlay ${position}`;
268
- document.body.appendChild(overlay);
269
- overlay.appendChild(toastElement);
270
- if (showClose) {
271
- const closeBtn = document.createElement("button");
272
- closeBtn.className = "ts-toast-close";
273
- closeBtn.setAttribute("aria-label", "Close");
274
- closeBtn.innerHTML = "×";
275
- closeBtn.addEventListener("click", (e) => {
276
- e.stopPropagation();
277
- removeWithAnimation(toastElement, () => {
278
- if (onDismiss && typeof onDismiss === "function") onDismiss(toastElement);
279
- if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
280
- });
281
- });
282
- toastElement.appendChild(closeBtn);
283
- }
284
- if (closeOnOverlayClick) {
285
- overlay.addEventListener("click", (e) => {
286
- if (e.target === overlay) {
287
- if (toastElement.result) {
288
- if (typeof resultResolver === "function") {
289
- resultResolver(false);
290
- }
291
- }
292
- removeWithAnimation(toastElement, () => {
293
- if (onDismiss && typeof onDismiss === "function") onDismiss(toastElement);
294
- if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
295
- });
296
- }
297
- });
298
- }
299
- } else {
300
- let container = document.querySelector(`.ts-toast-container.${position}`);
301
- if (!container) {
302
- container = document.createElement("div");
303
- container.className = `ts-toast-container ${position}`;
304
- document.body.appendChild(container);
305
- }
306
- container.appendChild(toastElement);
307
- }
308
-
309
- if (onShow && typeof onShow === "function") {
310
- onShow(toastElement);
311
- }
312
-
313
- setTimeout(() => {
314
- toastElement.classList.add("ts-toast-show");
315
- }, 100);
316
-
317
- if (showLoader && loader) {
318
- setTimeout(() => {
319
- if (toastElement._managedByLoading) return;
320
- loader.classList.add("done");
321
- loader.remove();
322
- if (!toastElement.contains(iconElement)) {
323
- if (isConfirm && contentRow) contentRow.appendChild(iconElement);
324
- else toastElement.appendChild(iconElement);
325
- }
326
- }, 2000);
327
- }
328
- if (!showLoader) {
329
- if (!isConfirm && !toastElement.contains(iconElement)) {
330
- toastElement.appendChild(iconElement);
331
- }
332
- }
333
-
334
- if (!isConfirm && duration > 0) {
335
- const autoRemove = setTimeout(() => {
336
- removeWithAnimation(toastElement, () => {
337
- if (onDismiss && typeof onDismiss === "function") onDismiss(toastElement);
338
- });
339
- }, duration);
340
- toastElement._autoRemove = autoRemove;
341
- }
342
-
343
- if (!isConfirm && dismissOnClick) {
344
- toastElement.addEventListener("click", () => {
345
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
346
- removeWithAnimation(toastElement, () => {
347
- if (onClick && typeof onClick === "function") onClick(toastElement);
348
- if (onDismiss && typeof onDismiss === "function") onDismiss(toastElement);
349
- });
350
- });
351
- }
352
-
353
- if (!isConfirm) {
354
- let touchStartX = 0;
355
- let touchEndX = 0;
356
-
357
- toastElement.addEventListener("touchstart", (e) => {
358
- touchStartX = e.changedTouches[0].screenX;
359
- });
360
-
361
- toastElement.addEventListener("touchend", (e) => {
362
- touchEndX = e.changedTouches[0].screenX;
363
- if (Math.abs(touchStartX - touchEndX) > 50) {
364
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
365
- removeWithAnimation(toastElement, () => {
366
- if (onDismiss && typeof onDismiss === "function") onDismiss(toastElement);
367
- });
368
- }
369
- });
370
- }
371
-
372
- return toastElement;
373
- };
374
-
375
- toast.success = function (message, options) {
376
- toast(message, { ...options, type: "success" });
377
- };
378
-
379
- toast.error = function (message, options) {
380
- toast(message, { ...options, type: "error" });
381
- };
382
-
383
- toast.update = function (toastElement, message, options = {}) {
384
- const {
385
- type = null,
386
- icon = null,
387
- showLoader = false,
388
- duration = 3000,
389
- position = "top-right",
390
- onClick = null,
391
- onShow = null,
392
- onDismiss = null,
393
- } = options;
394
-
395
- const oldLoader = toastElement.querySelector(".ts-toast-loader");
396
- const oldIcon = toastElement.querySelector(".ts-toast-icon");
397
- if (oldLoader) oldLoader.remove();
398
-
399
- if (type) {
400
- toastElement.className = `ts-toast ts-toast-${type} show ${position}`;
401
- }
402
- const toastBody = toastElement.querySelector(".ts-toast-body");
403
- if (toastBody) {
404
- toastBody.innerHTML = message;
405
- }
406
-
407
- if (oldIcon) {
408
- oldIcon.remove();
409
- }
410
-
411
- const iconElement = document.createElement("span");
412
- iconElement.className = "ts-toast-icon";
413
- iconElement.style.display = "flex";
414
-
415
- if (icon) {
416
- iconElement.textContent = icon;
417
- } else {
418
- const img = document.createElement("img");
419
- img.style.width = "30px";
420
- img.style.height = "30px";
421
- img.style.objectFit = "contain";
422
-
423
- if (type === "success") {
424
- img.src =
425
- "https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@5.2.0/assets/img/success.gif";
426
- } else if (type === "error") {
427
- img.src =
428
- "https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@5.2.0/assets/img/error.gif";
429
- } else if (type === "info") {
430
- img.src =
431
- "https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@5.2.0/assets/img/info.gif";
432
- } else if (type === "warning") {
433
- img.src =
434
- "https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@5.2.0/assets/img/warning.gif";
435
- }
436
-
437
- iconElement.appendChild(img);
438
- }
439
-
440
- toastElement.appendChild(iconElement);
441
-
442
- if (showLoader) {
443
- const loader = document.createElement("div");
444
- loader.className = "ts-toast-loader";
445
- toastElement.appendChild(loader);
446
- setTimeout(() => {
447
- loader.classList.add("done");
448
- }, 2000);
449
- }
450
-
451
- if (toastElement._autoRemove) {
452
- clearTimeout(toastElement._autoRemove);
453
- }
454
-
455
- const autoRemove = setTimeout(() => {
456
- const removeWithAnimation = (el, cb) => {
457
- el.classList.add("ts-toast-slide-out");
458
- el.classList.remove("ts-toast-show");
459
- el.style.animation = "";
460
- setTimeout(() => {
461
- el.classList.remove("ts-toast-slide-out");
462
- if (el.parentNode) el.parentNode.removeChild(el);
463
- if (typeof cb === "function") cb();
464
- }, 500);
465
- };
466
-
467
- removeWithAnimation(toastElement, () => {
468
- if (onDismiss && typeof onDismiss === "function") onDismiss(toastElement);
469
- });
470
- }, duration);
471
-
472
- toastElement._autoRemove = autoRemove;
473
- };
474
-
475
- toast.loading = function (message, options = {}) {
476
- const toastElement = toast(message, {
477
- ...options,
478
- type: options.type || "info",
479
- duration: 0,
480
- showLoader: true,
481
- icon: null,
482
- });
483
-
484
- requestAnimationFrame(() => {
485
- toastElement.classList.add("ts-toast-show");
486
- });
487
-
488
- const loader = toastElement.querySelector(".ts-toast-loader");
489
- let iconElement = toastElement.querySelector(".ts-toast-icon");
490
-
491
- if (!iconElement) {
492
- iconElement = document.createElement("span");
493
- iconElement.className = "ts-toast-icon";
494
- iconElement.style.display = "flex";
495
- toastElement.appendChild(iconElement);
496
- }
497
-
498
- toastElement._managedByLoading = true;
499
-
500
- if (loader) {
501
- setTimeout(() => {
502
- if (!toastElement._managedByLoading) loader.classList.add("done");
503
- }, 2000);
504
- }
505
-
506
- return {
507
- update: (newMessage, newOptions = {}) => {
508
- toastElement._managedByLoading = false;
509
- toast.update(toastElement, newMessage, {
510
- ...newOptions,
511
- showLoader: false,
512
- });
513
- },
514
- close: () => {
515
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
516
- toastElement.classList.add("ts-toast-slide-out");
517
- toastElement.classList.remove("ts-toast-show");
518
- toastElement.style.animation = "";
519
- setTimeout(() => {
520
- toastElement.classList.remove("ts-toast-slide-out");
521
- if (toastElement.parentNode) toastElement.parentNode.removeChild(toastElement);
522
- }, 500);
523
- },
524
- };
525
- };
526
-
527
- toast.confirm = function (message, options = {}) {
528
- return new Promise((resolve) => {
529
- const el = toast(message, {
530
- ...options,
531
- mode: "confirm",
532
- duration: 0,
533
- dismissOnClick: false,
534
- onResult: (val) => resolve(val),
535
- });
536
- void el;
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;