@tsirosgeorge/toastnotification 5.3.2 → 5.4.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,18 +1,35 @@
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.4.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
+ // Load the stylesheet from the CDN, unless the page opted out by importing it itself
14
+ // (set window.TS_TOAST_NO_CSS = true before loading, or ship assets/css/toast.css yourself).
15
+ (function loadStylesheet() {
16
+ const LINK_ID = 'ts-toast-stylesheet';
17
+ if (typeof window !== 'undefined' && window.TS_TOAST_NO_CSS) return;
18
+ if (document.getElementById(LINK_ID)) return;
19
+ const link = document.createElement("link");
20
+ link.id = LINK_ID;
21
+ link.rel = "stylesheet";
22
+ link.href = `${TS_TOAST_CDN}/assets/css/toast.min.css`;
23
+ document.head.appendChild(link);
24
+ })();
25
+
26
+ // Inject minimal styles for confirm actions and overlay (kept tiny to avoid breaking existing CSS)
27
+ (function injectInlineStyles() {
28
+ const STYLE_ID = 'ts-toast-inline-extras';
29
+ if (document.getElementById(STYLE_ID)) return;
30
+ const style = document.createElement('style');
31
+ style.id = STYLE_ID;
32
+ style.textContent = `
16
33
  /* Ensure center positions exist even if external CSS lacks them */
17
34
  .ts-toast-container.top-center { top: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
18
35
  .ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
@@ -35,507 +52,541 @@ document.head.appendChild(link);
35
52
  .ts-toast.ts-toast-confirm.ts-toast-warning .ts-toast-icon { background: #fef3c7; }
36
53
  .ts-toast.ts-toast-confirm.ts-toast-error .ts-toast-icon { background: #fee2e2; }
37
54
  `;
38
- document.head.appendChild(style);
39
- })();
55
+ document.head.appendChild(style);
56
+ })();
40
57
 
41
- // Full implementation copied from toast.js but exposed as ES module
42
58
  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
- };
59
+ const {
60
+ position = 'top-right',
61
+ animation = 'slide-right', // Default fallback animation
62
+ type = 'info',
63
+ duration = 3000,
64
+ icon = null,
65
+ showLoader = false,
66
+ // behavior/mode: 'alert' (default) or 'confirm'/'swal'
67
+ mode = 'alert',
68
+ // confirm options (used when mode is 'confirm' or 'swal')
69
+ title = null,
70
+ confirmText = 'Yes',
71
+ cancelText = 'No',
72
+ // input field options
73
+ input = false, // 'text', 'email', 'password', 'number', 'textarea', or false
74
+ inputPlaceholder = '',
75
+ inputValue = '',
76
+ // confirm button color customization (optional)
77
+ confirmButtonBg = null,
78
+ confirmButtonColor = null,
79
+ cancelButtonBg = null,
80
+ cancelButtonColor = null,
81
+ onConfirm = null,
82
+ onCancel = null,
83
+ onResult = null,
84
+ useOverlay = true,
85
+ closeOnOverlayClick = true,
86
+ showClose = false,
87
+ // interactions
88
+ dismissOnClick = true, // ignored if confirm-mode
89
+ onClick = null, // Custom onClick event listener
90
+ onShow = null, // Custom onShow event listener
91
+ onDismiss = null // Custom onDismiss event listener
92
+ } = options;
93
+
94
+ const isConfirm = (mode === 'confirm' || mode === 'swal');
95
+
96
+ // Pick an animation intelligently when one wasn't explicitly provided
97
+ const resolvedAnimation = (typeof options.animation === 'string' && options.animation.trim())
98
+ ? (function mapAnim(a){
99
+ const m = {
100
+ 'slide-top':'ts-toast-slide-top',
101
+ 'slide-bottom':'ts-toast-slide-bottom',
102
+ 'slide-left':'ts-toast-slide-left',
103
+ 'slide-right':'ts-toast-slide-right',
104
+ 'zoom-in':'ts-toast-zoom-in',
105
+ 'zoom-out':'ts-toast-zoom-out',
106
+ 'flip':'ts-toast-flip'
107
+ };
108
+ return m[a] || a;
109
+ })(options.animation.trim())
110
+ : (isConfirm ? 'ts-toast-zoom-in' : (position.startsWith('top') ? 'ts-toast-slide-top'
111
+ : position.startsWith('bottom') ? 'ts-toast-slide-bottom'
112
+ : position.endsWith('left') ? 'ts-toast-slide-left'
113
+ : 'ts-toast-slide-right'));
114
+
115
+ // helper: remove with smooth CSS transition and cleanup (used by alerts)
116
+ const removeWithAnimation = (el, callback) => {
117
+ const anim = el.dataset && el.dataset.anim ? el.dataset.anim : (el.style.animation || '');
118
+ let transform = '';
119
+ if (anim.includes('ts-toast-slide-top')) {
120
+ // Entered from top, exit upwards
121
+ transform = 'translateY(-100%)';
122
+ } else if (anim.includes('ts-toast-slide-bottom')) {
123
+ // Entered from bottom, exit upwards
124
+ transform = 'translateY(100%)';
125
+ } else if (anim.includes('ts-toast-slide-left')) {
126
+ // Entered from left, exit to right
127
+ transform = 'translateX(100%)';
128
+ } else if (anim.includes('ts-toast-slide-right')) {
129
+ // Entered from right, exit to right
130
+ transform = 'translateX(100%)';
131
+ }
132
+
133
+ el.classList.add('ts-toast-slide-out');
134
+ el.classList.remove('ts-toast-show');
135
+ // Stop any running keyframe animation and drive exit via CSS transition
136
+ el.style.animation = '';
137
+ if (transform) {
138
+ el.style.transform = transform;
139
+ }
140
+ el.style.opacity = '0';
141
+
142
+ setTimeout(() => {
143
+ el.classList.remove('ts-toast-slide-out');
144
+ if (el.parentNode) el.parentNode.removeChild(el);
145
+ if (typeof callback === 'function') callback();
146
+ }, 500);
147
+ };
148
+
149
+ const toastElement = document.createElement('div');
150
+ toastElement.className = `ts-toast ts-toast-${type}${isConfirm ? ' ts-toast-confirm' : ''}`;
151
+ toastElement.dataset.anim = resolvedAnimation;
152
+ toastElement.style.animation = `${resolvedAnimation} 0.5s ease`;
153
+ // In confirm mode, we stack content vertically; in alert mode keep original layout
154
+ if (!isConfirm) {
155
+ toastElement.style.flexDirection = 'row-reverse';
156
+ toastElement.style.justifyContent = 'flex-end';
157
+ }
158
+
159
+ // Create Icon Element
160
+ const iconElement = document.createElement('span');
161
+ iconElement.className = 'ts-toast-icon';
162
+ iconElement.style.display = 'flex';
163
+ if (icon) {
164
+ iconElement.textContent = icon;
165
+ } else {
166
+ const img = document.createElement('img');
167
+ img.alt = '';
168
+ img.setAttribute('aria-hidden', 'true');
169
+ img.style.width = '30px';
170
+ img.style.height = '30px';
171
+ img.style.objectFit = 'contain';
172
+
173
+ // No cache-buster: these GIFs are immutable per version, so let the
174
+ // browser and the CDN actually cache them.
175
+ const iconFile = { success: 'success.gif', error: 'error.gif', info: 'info.gif', warning: 'warning.gif' }[type];
176
+ if (iconFile) img.src = `${TS_TOAST_CDN}/assets/img/${iconFile}`;
177
+
178
+ iconElement.appendChild(img);
179
+ }
180
+
181
+ // Create Body
182
+ const toastBody = document.createElement('div');
183
+ toastBody.className = 'ts-toast-body';
184
+ toastBody.innerHTML = message; // Allow HTML content in message
185
+
186
+ // Content row for confirm (icon + text side-by-side)
187
+ let contentRow = null;
188
+ if (isConfirm) {
189
+ contentRow = document.createElement('div');
190
+ contentRow.className = 'ts-toast-content';
191
+ contentRow.appendChild(iconElement);
192
+ if (title) {
193
+ const titleEl = document.createElement('div');
194
+ titleEl.className = 'ts-toast-title';
195
+ titleEl.textContent = title;
196
+ contentRow.appendChild(titleEl);
197
+ }
198
+ contentRow.appendChild(toastBody);
199
+ toastElement.appendChild(contentRow);
200
+ } else {
201
+ toastElement.appendChild(toastBody);
202
+ }
203
+
204
+ // Input field (for confirm mode with input)
205
+ let inputElement = null;
206
+ if (isConfirm && input) {
207
+ if (input === 'textarea') {
208
+ inputElement = document.createElement('textarea');
209
+ inputElement.rows = 3;
210
+ } else {
211
+ inputElement = document.createElement('input');
212
+ inputElement.type = input === 'text' || input === 'email' || input === 'password' || input === 'number' ? input : 'text';
213
+ }
214
+ inputElement.className = 'ts-toast-input';
215
+ inputElement.placeholder = inputPlaceholder;
216
+ inputElement.value = inputValue;
217
+ toastElement.appendChild(inputElement);
218
+ }
219
+
220
+ // Actions (for confirm mode)
221
+ let actionsContainer = null;
222
+ let resultResolver = null;
223
+ // Assigned in confirm mode; the overlay and (x) handlers below call it so that
224
+ // *every* way of dismissing the dialog settles the promise and the callbacks.
225
+ let resolveAndClose = null;
226
+ if (isConfirm) {
227
+ actionsContainer = document.createElement('div');
228
+ actionsContainer.className = 'ts-toast-actions';
229
+
230
+ const cancelBtn = document.createElement('button');
231
+ cancelBtn.className = 'ts-toast-btn cancel';
232
+ cancelBtn.textContent = cancelText;
233
+
234
+ const confirmBtn = document.createElement('button');
235
+ confirmBtn.className = 'ts-toast-btn confirm';
236
+ confirmBtn.textContent = confirmText;
237
+
238
+ // Apply custom button colors if provided (inline style overrides theme defaults)
239
+ if (cancelButtonBg) cancelBtn.style.background = cancelButtonBg;
240
+ if (cancelButtonColor) cancelBtn.style.color = cancelButtonColor;
241
+ if (confirmButtonBg) confirmBtn.style.background = confirmButtonBg;
242
+ if (confirmButtonColor) confirmBtn.style.color = confirmButtonColor;
243
+
244
+ actionsContainer.appendChild(cancelBtn);
245
+ actionsContainer.appendChild(confirmBtn);
246
+ toastElement.appendChild(actionsContainer);
247
+
248
+ // Create a Promise that resolves on user choice; expose via property
249
+ toastElement.result = new Promise((resolve) => { resultResolver = resolve; });
250
+
251
+ let settled = false;
252
+ resolveAndClose = (confirmed) => {
253
+ if (settled) return; // a button click and a backdrop click can race
254
+ settled = true;
255
+ // With an input, confirming always yields a string (possibly '') and
256
+ // cancelling yields null, so an empty submission stays distinguishable
257
+ // from a cancel. Without an input the contract is still true/false.
258
+ const result = confirmed
259
+ ? (inputElement ? inputElement.value : true)
260
+ : (inputElement ? null : false);
261
+ if (resultResolver) resultResolver(result);
262
+ if (confirmed && typeof onConfirm === 'function') onConfirm(result, toastElement);
263
+ if (!confirmed && typeof onCancel === 'function') onCancel(toastElement);
264
+ if (typeof onResult === 'function') onResult(result, toastElement);
265
+ // Use the same slide+fade removal as alerts
266
+ removeWithAnimation(toastElement, () => {
267
+ if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
268
+ // Remove overlay if present
269
+ if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
270
+ });
271
+ };
272
+
273
+ cancelBtn.addEventListener('click', (e) => { e.stopPropagation(); resolveAndClose(false); });
274
+ confirmBtn.addEventListener('click', (e) => { e.stopPropagation(); resolveAndClose(true); });
275
+ }
276
+
277
+ // Loader Element
278
+ let loader = null;
279
+ if (showLoader) {
280
+ loader = document.createElement('div');
281
+ loader.className = 'ts-toast-loader';
282
+ toastElement.appendChild(loader);
283
+ }
284
+
285
+ // Container/Overlay
286
+ let overlay = null;
287
+ if (isConfirm && useOverlay) {
288
+ overlay = document.createElement('div');
289
+ // A modal centres by default. The `position` default of 'top-right' is meant
290
+ // for toasts; applying it here parked the dialog in a corner of the backdrop.
291
+ overlay.className = 'ts-toast-overlay' + (options.position ? ` ${position}` : '');
292
+ document.body.appendChild(overlay);
293
+ overlay.appendChild(toastElement);
294
+ if (showClose) {
295
+ const closeBtn = document.createElement('button');
296
+ closeBtn.className = 'ts-toast-close';
297
+ closeBtn.setAttribute('aria-label', 'Close');
298
+ closeBtn.innerHTML = '×';
299
+ closeBtn.addEventListener('click', (e) => {
300
+ e.stopPropagation();
301
+ // Dismissing via (x) is a cancel, so it has to settle like one.
302
+ resolveAndClose(false);
303
+ });
304
+ toastElement.appendChild(closeBtn);
305
+ }
306
+ if (closeOnOverlayClick) {
307
+ overlay.addEventListener('click', (e) => {
308
+ // Backdrop click is a cancel: settle the promise and onCancel/onResult,
309
+ // then close. Previously this resolved only the internal el.result,
310
+ // so `await toast.confirm(...)` hung forever.
311
+ if (e.target === overlay) resolveAndClose(false);
312
+ });
313
+ }
314
+ } else {
315
+ // Standard positioned container
316
+ let container = document.querySelector(`.ts-toast-container.${position}`);
317
+ if (!container) {
318
+ container = document.createElement('div');
319
+ container.className = `ts-toast-container ${position}`;
320
+ document.body.appendChild(container);
321
+ }
322
+ container.appendChild(toastElement);
323
+ }
324
+
325
+ // Trigger the onShow event if provided
326
+ if (onShow && typeof onShow === 'function') {
327
+ onShow(toastElement);
328
+ }
329
+
330
+ // Show Toast with animation
331
+ setTimeout(() => {
332
+ toastElement.classList.add('ts-toast-show');
333
+ }, 100);
334
+
335
+ // Handle Loader and Icon
336
+ if (showLoader && loader) {
337
+ setTimeout(() => {
338
+ // Skip auto-complete if controlled by toast.loading()
339
+ if (toastElement._managedByLoading) return;
340
+ loader.classList.add('done');
341
+ loader.remove();
342
+ if (!toastElement.contains(iconElement)) {
343
+ if (isConfirm && contentRow) contentRow.appendChild(iconElement);
344
+ else toastElement.appendChild(iconElement); // Add icon only if not present
345
+ }
346
+ }, 2000); // Simulate a loading period of 2 seconds
347
+ }
348
+ if (!showLoader) {
349
+ // For confirm, icon already added above inside contentRow; avoid moving it
350
+ if (!isConfirm && !toastElement.contains(iconElement)) {
351
+ toastElement.appendChild(iconElement);
352
+ }
353
+ }
354
+
355
+ // Auto remove after the duration (skip for confirm mode or when duration <= 0)
356
+ if (!isConfirm && duration > 0) {
357
+ const autoRemove = setTimeout(() => {
358
+ removeWithAnimation(toastElement, () => {
359
+ if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
360
+ });
361
+ }, duration);
362
+ toastElement._autoRemove = autoRemove;
363
+ }
364
+
365
+ // Add event listener for closing the toast when clicked (disabled in confirm mode)
366
+ if (!isConfirm && dismissOnClick) {
367
+ toastElement.addEventListener('click', () => {
368
+ if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove); // Clear the auto-remove timeout
369
+ removeWithAnimation(toastElement, () => {
370
+ if (onClick && typeof onClick === 'function') onClick(toastElement);
371
+ if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
372
+ });
373
+ });
374
+ }
375
+
376
+ // Add swipe event listeners for mobile dismissal
377
+ if (!isConfirm) {
378
+ let touchStartX = 0;
379
+ let touchEndX = 0;
380
+
381
+ toastElement.addEventListener('touchstart', (e) => {
382
+ touchStartX = e.changedTouches[0].screenX;
383
+ });
384
+
385
+ toastElement.addEventListener('touchend', (e) => {
386
+ touchEndX = e.changedTouches[0].screenX;
387
+ if (Math.abs(touchStartX - touchEndX) > 50) { // Swipe distance threshold
388
+ if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
389
+ removeWithAnimation(toastElement, () => {
390
+ if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
391
+ });
392
+ }
393
+ });
394
+ }
395
+
396
+ return toastElement;
397
+ };
398
+
399
+ // Shorthands return the toast element so it can be passed to toast.update().
400
+ toast.success = function (message, options) {
401
+ return toast(message, { ...options, type: 'success' });
402
+ };
403
+
404
+ toast.error = function (message, options) {
405
+ return toast(message, { ...options, type: 'error' });
406
+ };
407
+
408
+ toast.warning = function (message, options) {
409
+ return toast(message, { ...options, type: 'warning' });
410
+ };
411
+
412
+ toast.info = function (message, options) {
413
+ return toast(message, { ...options, type: 'info' });
414
+ };
415
+
416
+ // Update toast function to handle removal with animation
417
+ toast.update = function (toastElement, message, options = {}) {
418
+ const {
419
+ type = null,
420
+ icon = null,
421
+ showLoader = false,
422
+ duration = 3000, // Default duration (in ms)
423
+ onClick = null, // Custom onClick event listener
424
+ onShow = null, // Custom onShow event listener
425
+ onDismiss = null // Custom onDismiss event listener
426
+ } = options;
427
+
428
+ // Remove old loader (if any)
429
+ const oldLoader = toastElement.querySelector('.ts-toast-loader');
430
+ const oldIcon = toastElement.querySelector('.ts-toast-icon');
431
+ if (oldLoader) oldLoader.remove();
432
+
433
+ // Update toast class and message. Swap only the type modifier: overwriting
434
+ // className dropped ts-toast-show (so the toast faded out), dropped
435
+ // ts-toast-confirm (so confirm dialogs lost their layout), and leaked the
436
+ // position onto the toast instead of the container.
437
+ if (type) {
438
+ ['success', 'error', 'info', 'warning'].forEach((t) => toastElement.classList.remove(`ts-toast-${t}`));
439
+ toastElement.classList.add('ts-toast', `ts-toast-${type}`, 'ts-toast-show');
440
+ }
441
+ const toastBody = toastElement.querySelector('.ts-toast-body');
442
+ if (toastBody) {
443
+ toastBody.innerHTML = message;
444
+ }
445
+
446
+ // Handle Icon update only if it's new or hasn't been set yet
447
+ if (oldIcon) {
448
+ oldIcon.remove(); // Remove the old icon first
449
+ }
450
+
451
+ const iconElement = document.createElement('span');
452
+ iconElement.className = 'ts-toast-icon';
453
+ iconElement.style.display = 'flex';
454
+
455
+ if (icon) {
456
+ iconElement.textContent = icon;
457
+ } else {
458
+ const img = document.createElement('img');
459
+ img.alt = '';
460
+ img.setAttribute('aria-hidden', 'true');
461
+ img.style.width = '30px';
462
+ img.style.height = '30px';
463
+ img.style.objectFit = 'contain';
464
+
465
+ const iconFile = { success: 'success.gif', error: 'error.gif', info: 'info.gif', warning: 'warning.gif' }[type];
466
+ if (iconFile) img.src = `${TS_TOAST_CDN}/assets/img/${iconFile}`;
467
+
468
+ iconElement.appendChild(img);
469
+ }
470
+
471
+ // Append the new icon immediately (inside the content row for confirm dialogs)
472
+ const contentRow = toastElement.querySelector('.ts-toast-content');
473
+ (contentRow || toastElement).appendChild(iconElement);
474
+
475
+ // Handle loader if requested
476
+ if (showLoader) {
477
+ const loader = document.createElement('div');
478
+ loader.className = 'ts-toast-loader';
479
+ toastElement.appendChild(loader);
480
+ setTimeout(() => {
481
+ loader.classList.add('done');
482
+ }, 2000); // Simulate loader completion after 2 seconds
483
+ }
484
+
485
+ // Clear previous auto-remove timer if needed
486
+ if (toastElement._autoRemove) {
487
+ clearTimeout(toastElement._autoRemove);
488
+ }
489
+
490
+ // Set the auto-remove timer again to ensure toast disappears after the duration
491
+ const autoRemove = setTimeout(() => {
492
+ const removeWithAnimation = (el, cb) => {
493
+ el.classList.add('ts-toast-slide-out');
494
+ el.classList.remove('ts-toast-show');
495
+ el.style.animation = '';
496
+ setTimeout(() => {
497
+ el.classList.remove('ts-toast-slide-out');
498
+ if (el.parentNode) el.parentNode.removeChild(el);
499
+ if (typeof cb === 'function') cb();
500
+ }, 500);
501
+ };
502
+
503
+ removeWithAnimation(toastElement, () => {
504
+ if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
505
+ });
506
+ }, duration);
507
+
508
+ toastElement._autoRemove = autoRemove; // Re-set the auto-remove timer
509
+ };
510
+
511
+ toast.loading = function (message, options = {}) {
512
+ const toastElement = toast(message, {
513
+ ...options,
514
+ type: options.type || 'info', // Default type is 'info'
515
+ duration: 0, // Sticky until manually updated/closed
516
+ showLoader: true, // Always show loader during loading
517
+ icon: null
518
+ });
519
+
520
+ // Force reflow and add animation after DOM insert
521
+ requestAnimationFrame(() => {
522
+ toastElement.classList.add('ts-toast-show');
523
+ });
524
+
525
+ const loader = toastElement.querySelector('.ts-toast-loader');
526
+ let iconElement = toastElement.querySelector('.ts-toast-icon');
527
+
528
+ // Ensure the iconElement is created and appended if it doesn't exist
529
+ if (!iconElement) {
530
+ iconElement = document.createElement('span');
531
+ iconElement.className = 'ts-toast-icon';
532
+ iconElement.style.display = 'flex';
533
+ toastElement.appendChild(iconElement);
534
+ }
535
+
536
+ // mark as managed by loading flow to avoid internal auto-complete
537
+ toastElement._managedByLoading = true;
538
+
539
+ // Ensure loader is handled properly
540
+ if (loader) {
541
+ setTimeout(() => {
542
+ // Keep spinning until update() decides otherwise
543
+ if (!toastElement._managedByLoading) loader.classList.add('done');
544
+ }, 2000); // Simulate a loading period of 2 seconds
545
+ }
546
+
547
+ return {
548
+ update: (newMessage, newOptions = {}) => {
549
+ // Let update manage completion: stop managing/finish loader
550
+ toastElement._managedByLoading = false;
551
+ toast.update(toastElement, newMessage, {
552
+ ...newOptions,
553
+ showLoader: false // Disable loader when updating the message
554
+ });
555
+ },
556
+ close: () => {
557
+ if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
558
+ toastElement.classList.add('ts-toast-slide-out');
559
+ toastElement.classList.remove('ts-toast-show');
560
+ toastElement.style.animation = '';
561
+ setTimeout(() => {
562
+ toastElement.classList.remove('ts-toast-slide-out');
563
+ if (toastElement.parentNode) toastElement.parentNode.removeChild(toastElement);
564
+ }, 500);
565
+ }
566
+ };
567
+ };
568
+
569
+ // Convenience API: swal-like confirm dialog
570
+ // Usage: toast.confirm('Are you sure?', { type: 'warning', confirmText: 'Yes', cancelText: 'No' }).then(ok => {...})
571
+ toast.confirm = function (message, options = {}) {
572
+ return new Promise((resolve) => {
573
+ const el = toast(message, {
574
+ ...options,
575
+ mode: 'confirm',
576
+ duration: 0, // prevent auto-dismiss
577
+ dismissOnClick: false,
578
+ onResult: (val) => resolve(val)
579
+ });
580
+ // If consumer needs the element, it is returned by toast() but we ignore here.
581
+ // They can still call toast(...) with mode: 'confirm' to get the element and read el.result
582
+ void el; // no-op
583
+ });
584
+ };
585
+
586
+ // Expose globally for CDN / browser usage
587
+ if (typeof window !== 'undefined') {
588
+ window.toast = toast;
589
+ }
539
590
 
540
591
  // ES module export
541
592
  export default toast;