@tsirosgeorge/toastnotification 5.5.0 → 5.6.1

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 CHANGED
@@ -103,6 +103,37 @@ toast('Are you sure?', {
103
103
  });
104
104
  ```
105
105
 
106
+ ### 🔄 Wrapping an async action
107
+
108
+ ```javascript
109
+ const order = await toast.promise(saveOrder(cart), {
110
+ loading: 'Saving your order…',
111
+ success: (order) => `Order #${order.id} saved.`,
112
+ error: (err) => `Could not save: ${err.message}`,
113
+ });
114
+ ```
115
+
116
+ One loading toast becomes the success or error message. It resolves with the promise's
117
+ value and re-throws its rejection, so the caller still handles the failure.
118
+
119
+ ### ↩️ Action button
120
+
121
+ ```javascript
122
+ toast.success('Item removed from your cart.', {
123
+ duration: 8000,
124
+ showProgress: true,
125
+ action: { text: 'Undo', onClick: () => restoreItem() },
126
+ });
127
+ ```
128
+
129
+ ### ⚙️ Site-wide defaults
130
+
131
+ ```javascript
132
+ toast.defaults = { position: 'bottom-right', showProgress: true, duration: 4000 };
133
+ ```
134
+
135
+ Applied to every toast unless the individual call overrides them.
136
+
106
137
  ### ⌨️ Keyboard and screen readers
107
138
 
108
139
  Confirm dialogs take focus when they open, keep Tab inside themselves while they are
@@ -138,6 +169,9 @@ t.update('Done!', { type: 'success', duration: 2000 });
138
169
  | `duration` | `number` | `3000` | Duration in milliseconds before the toast automatically dismisses. |
139
170
  | `icon` | `string` or `null` | `null` | Optional custom icon displayed as text (e.g., emoji) before the toast message. If not set, a default GIF icon is used based on the `type`. |
140
171
  | `showLoader` | `boolean` | `false` | Whether to show a loader/progress bar animation on the toast during its visible duration. |
172
+ | `pauseOnHover` | `boolean` | `true` | Freeze the countdown while the pointer or keyboard focus is on the toast. |
173
+ | `showProgress` | `boolean` | `false` | Thin bar counting the remaining time down. Needs `duration > 0`; pauses with `pauseOnHover`. |
174
+ | `action` | `object` or `null` | `null` | `{ text, onClick }` renders a button inside the toast, e.g. Undo. Clicking it runs `onClick` and closes the toast. |
141
175
  | `allowHtml` | `boolean` | `true` | `message` is written as HTML. Pass `false` to render it as plain text — do that for anything user-supplied, or you have an XSS hole. |
142
176
  | `closeOnEscape` | `boolean` | `true` | Confirm dialogs only. Escape cancels the dialog. |
143
177
  | `onClick` | `function` or `null` | `null` | Callback function executed when the toast is clicked. |
@@ -343,6 +343,8 @@
343
343
  .ts-toast-overlay.bottom-center { align-items: flex-end; justify-content: center; padding: 1rem; }
344
344
  .ts-toast-overlay.bottom-right { align-items: flex-end; justify-content: flex-end; padding: 1rem; }
345
345
  .ts-toast-overlay.center { align-items: center; justify-content: center; padding: 1rem; }
346
+ /* A dialog opening over another must not dim the page a second time. */
347
+ .ts-toast-overlay.ts-toast-overlay-stacked { background: transparent; }
346
348
 
347
349
  /* Tiny utility: flex icon container */
348
350
  /* Scoped utility to avoid conflicts */
@@ -351,4 +353,50 @@
351
353
  /* Prevent scroll behind confirm overlay */
352
354
  body.ts-toast-no-scroll {
353
355
  overflow: hidden;
354
- }
356
+ }
357
+
358
+ /* Progress bar counting down the remaining time */
359
+ @keyframes ts-toast-progress {
360
+ from { transform: scaleX(1); }
361
+ to { transform: scaleX(0); }
362
+ }
363
+
364
+ .ts-toast-container .ts-toast-progress {
365
+ position: absolute;
366
+ left: 0;
367
+ right: 0;
368
+ bottom: 0;
369
+ height: 3px;
370
+ transform-origin: left center;
371
+ border-radius: 0 0 8px 8px;
372
+ background: var(--toast-progress-color, currentColor);
373
+ opacity: 0.35;
374
+ pointer-events: none;
375
+ }
376
+
377
+ .ts-toast-container .ts-toast-success .ts-toast-progress { background: var(--toast-success-color, #17a35c); }
378
+ .ts-toast-container .ts-toast-error .ts-toast-progress { background: var(--toast-error-color, #ef4444); }
379
+ .ts-toast-container .ts-toast-warning .ts-toast-progress { background: var(--toast-warning-color, #f0a020); }
380
+ .ts-toast-container .ts-toast-info .ts-toast-progress { background: var(--toast-info-color, #3b82f6); }
381
+
382
+ /* Action button inside a toast (e.g. Undo) */
383
+ .ts-toast-container .ts-toast-action {
384
+ /* Toasts lay out row-reverse, so the lowest order sits at the right-hand end:
385
+ icon, message, then the action button. */
386
+ order: -1;
387
+ flex: none;
388
+ appearance: none;
389
+ border: 0;
390
+ background: transparent;
391
+ color: var(--toast-action-color, #3b82f6);
392
+ font: inherit;
393
+ font-weight: 600;
394
+ padding: 4px 8px;
395
+ margin-left: 4px;
396
+ border-radius: 6px;
397
+ cursor: pointer;
398
+ }
399
+
400
+ .ts-toast-container .ts-toast-action:hover {
401
+ background: var(--toast-action-hover, rgba(59, 130, 246, 0.12));
402
+ }
@@ -1 +1 @@
1
- .ts-toast-container{position:fixed;z-index:9999;display:flex;flex-direction:column;gap:10px;pointer-events:none;width:fit-content;max-width:calc(100% - 2rem)}.ts-toast-container>:not(:last-child){margin-bottom:0!important}.ts-toast-container.top-right{top:1rem;right:1rem;align-items:flex-end}.ts-toast-container.top-left{top:1rem;left:1rem;align-items:flex-start}.ts-toast-container.top-center{top:1rem;left:50%;transform:translateX(-50%);align-items:center}.ts-toast-container.bottom-right{bottom:1rem;right:1rem;align-items:flex-end}.ts-toast-container.bottom-left{bottom:1rem;left:1rem;align-items:flex-start}.ts-toast-container.bottom-center{bottom:1rem;left:50%;transform:translateX(-50%);align-items:center}.ts-toast-container.center{top:50%;left:50%;transform:translate(-50%,-50%);align-items:center}.ts-toast-container .ts-toast{display:flex;align-items:center;gap:.75rem;background-color:var(--toast-bg,#fff);color:var(--toast-color,#000);padding:.75rem 1rem;border-radius:8px;border:1px solid var(--toast-border,#e5e7eb);box-shadow:var(--toast-shadow,0 10px 15px -3px rgba(0,0,0,.1));font-family:sans-serif;font-size:.95rem;pointer-events:all;position:relative;opacity:0;transform:translateY(20px);transition:opacity .5s ease,transform .5s ease;width:auto!important}.ts-toast.ts-toast-confirm{display:flex;align-items:center;justify-content:center;background-color:var(--toast-bg,#fff);color:var(--toast-color,#000);border:1px solid var(--toast-border,#e5e7eb);border-radius:12px;padding:16px 20px;box-shadow:var(--toast-shadow,0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1));transition:opacity .5s ease,transform .5s ease}.ts-toast.ts-toast-confirm .ts-toast-content{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;text-align:center}.ts-toast.ts-toast-confirm .ts-toast-body{text-align:center}.ts-toast-actions{display:flex;gap:10px;justify-content:center;margin-top:12px}.ts-toast-input{width:100%;padding:8px 12px;border:1px solid var(--toast-border,#e5e7eb);border-radius:8px;font-family:inherit;font-size:.95rem;margin-top:12px;background:var(--toast-bg,#fff);color:var(--toast-color,#000);outline:0;transition:border-color .2s ease}.ts-toast-input:focus{border-color:#3b82f6}.ts-toast-container .ts-toast.ts-toast-show{opacity:1;transform:translateY(0)}@keyframes ts-toast-slide-left{from{transform:translateX(-100%)}to{transform:translateX(0)}}@keyframes ts-toast-slide-right{from{transform:translateX(100%)}to{transform:translateX(0)}}@keyframes ts-toast-slide-top{from{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes ts-toast-slide-bottom{from{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes ts-toast-zoom-in{from{transform:scale(0)}to{transform:scale(1)}}@keyframes ts-toast-zoom-out{from{transform:scale(1)}to{transform:scale(0)}}@keyframes ts-toast-flip{from{transform:rotateY(90deg)}to{transform:rotateY(0)}}.ts-toast-container .ts-toast-icon{font-size:1.2rem}.ts-toast-container .ts-toast-success .ts-toast-icon{color:var(--toast-success-color,#66ee78)}.ts-toast-container .ts-toast-error .ts-toast-icon{color:var(--toast-error-color,#ef4444)}.ts-toast-container .ts-toast-loader{width:20px;height:20px;border:3px solid #f3f3f3;border-top:3px solid var(--toast-loader-color,#66ee78);border-radius:50%;animation:ts-toast-spin 1s linear infinite}@keyframes ts-toast-spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.ts-toast-container .ts-toast-loader.done{animation:none;opacity:0;transition:opacity .5s ease}.ts-toast-container .ts-toast-icon{font-size:1.2rem;opacity:0;transition:opacity .5s ease}.ts-toast-container .ts-toast.ts-toast-show .ts-toast-icon{opacity:1}@keyframes progress-bar{0%{width:0%}100%{width:100%}}@keyframes ts-toast-slide-top-reverse{from{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes ts-toast-slide-bottom-reverse{from{transform:translateY(0)}to{transform:translateY(100%)}}@keyframes ts-toast-slide-left-reverse{from{transform:translateX(0)}to{transform:translateX(-100%)}}@keyframes ts-toast-slide-right-reverse{from{transform:translateX(0)}to{transform:translateX(100%)}}.ts-toast.ts-toast-slide-out{opacity:0}.ts-toast-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:2147483646}.ts-toast-overlay.top-left{align-items:flex-start;justify-content:flex-start;padding:1rem}.ts-toast-overlay.top-center{align-items:flex-start;justify-content:center;padding:1rem}.ts-toast-overlay.top-right{align-items:flex-start;justify-content:flex-end;padding:1rem}.ts-toast-overlay.bottom-left{align-items:flex-end;justify-content:flex-start;padding:1rem}.ts-toast-overlay.bottom-center{align-items:flex-end;justify-content:center;padding:1rem}.ts-toast-overlay.bottom-right{align-items:flex-end;justify-content:flex-end;padding:1rem}.ts-toast-overlay.center{align-items:center;justify-content:center;padding:1rem}.ts-toast-d-flex{display:inline-flex;align-items:center}body.ts-toast-no-scroll{overflow:hidden}
1
+ .ts-toast-container{position:fixed;z-index:9999;display:flex;flex-direction:column;gap:10px;pointer-events:none;width:fit-content;max-width:calc(100% - 2rem)}.ts-toast-container>:not(:last-child){margin-bottom:0!important}.ts-toast-container.top-right{top:1rem;right:1rem;align-items:flex-end}.ts-toast-container.top-left{top:1rem;left:1rem;align-items:flex-start}.ts-toast-container.top-center{top:1rem;left:50%;transform:translateX(-50%);align-items:center}.ts-toast-container.bottom-right{bottom:1rem;right:1rem;align-items:flex-end}.ts-toast-container.bottom-left{bottom:1rem;left:1rem;align-items:flex-start}.ts-toast-container.bottom-center{bottom:1rem;left:50%;transform:translateX(-50%);align-items:center}.ts-toast-container.center{top:50%;left:50%;transform:translate(-50%,-50%);align-items:center}.ts-toast-container .ts-toast{display:flex;align-items:center;gap:.75rem;background-color:var(--toast-bg,#fff);color:var(--toast-color,#000);padding:.75rem 1rem;border-radius:8px;border:1px solid var(--toast-border,#e5e7eb);box-shadow:var(--toast-shadow,0 10px 15px -3px rgba(0,0,0,.1));font-family:sans-serif;font-size:.95rem;pointer-events:all;position:relative;opacity:0;transform:translateY(20px);transition:opacity .5s ease,transform .5s ease;width:auto!important}.ts-toast.ts-toast-confirm{display:flex;align-items:center;justify-content:center;background-color:var(--toast-bg,#fff);color:var(--toast-color,#000);border:1px solid var(--toast-border,#e5e7eb);border-radius:12px;padding:16px 20px;box-shadow:var(--toast-shadow,0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1));transition:opacity .5s ease,transform .5s ease}.ts-toast.ts-toast-confirm .ts-toast-content{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;text-align:center}.ts-toast.ts-toast-confirm .ts-toast-body{text-align:center}.ts-toast-actions{display:flex;gap:10px;justify-content:center;margin-top:12px}.ts-toast-input{width:100%;padding:8px 12px;border:1px solid var(--toast-border,#e5e7eb);border-radius:8px;font-family:inherit;font-size:.95rem;margin-top:12px;background:var(--toast-bg,#fff);color:var(--toast-color,#000);outline:0;transition:border-color .2s ease}.ts-toast-input:focus{border-color:#3b82f6}.ts-toast-container .ts-toast.ts-toast-show{opacity:1;transform:translateY(0)}@keyframes ts-toast-slide-left{from{transform:translateX(-100%)}to{transform:translateX(0)}}@keyframes ts-toast-slide-right{from{transform:translateX(100%)}to{transform:translateX(0)}}@keyframes ts-toast-slide-top{from{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes ts-toast-slide-bottom{from{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes ts-toast-zoom-in{from{transform:scale(0)}to{transform:scale(1)}}@keyframes ts-toast-zoom-out{from{transform:scale(1)}to{transform:scale(0)}}@keyframes ts-toast-flip{from{transform:rotateY(90deg)}to{transform:rotateY(0)}}.ts-toast-container .ts-toast-icon{font-size:1.2rem}.ts-toast-container .ts-toast-success .ts-toast-icon{color:var(--toast-success-color,#66ee78)}.ts-toast-container .ts-toast-error .ts-toast-icon{color:var(--toast-error-color,#ef4444)}.ts-toast-container .ts-toast-loader{width:20px;height:20px;border:3px solid #f3f3f3;border-top:3px solid var(--toast-loader-color,#66ee78);border-radius:50%;animation:ts-toast-spin 1s linear infinite}@keyframes ts-toast-spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.ts-toast-container .ts-toast-loader.done{animation:none;opacity:0;transition:opacity .5s ease}.ts-toast-container .ts-toast-icon{font-size:1.2rem;opacity:0;transition:opacity .5s ease}.ts-toast-container .ts-toast.ts-toast-show .ts-toast-icon{opacity:1}@keyframes progress-bar{0%{width:0%}100%{width:100%}}@keyframes ts-toast-slide-top-reverse{from{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes ts-toast-slide-bottom-reverse{from{transform:translateY(0)}to{transform:translateY(100%)}}@keyframes ts-toast-slide-left-reverse{from{transform:translateX(0)}to{transform:translateX(-100%)}}@keyframes ts-toast-slide-right-reverse{from{transform:translateX(0)}to{transform:translateX(100%)}}.ts-toast.ts-toast-slide-out{opacity:0}.ts-toast-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:2147483646}.ts-toast-overlay.top-left{align-items:flex-start;justify-content:flex-start;padding:1rem}.ts-toast-overlay.top-center{align-items:flex-start;justify-content:center;padding:1rem}.ts-toast-overlay.top-right{align-items:flex-start;justify-content:flex-end;padding:1rem}.ts-toast-overlay.bottom-left{align-items:flex-end;justify-content:flex-start;padding:1rem}.ts-toast-overlay.bottom-center{align-items:flex-end;justify-content:center;padding:1rem}.ts-toast-overlay.bottom-right{align-items:flex-end;justify-content:flex-end;padding:1rem}.ts-toast-overlay.center{align-items:center;justify-content:center;padding:1rem}.ts-toast-overlay.ts-toast-overlay-stacked{background:0 0}.ts-toast-d-flex{display:inline-flex;align-items:center}body.ts-toast-no-scroll{overflow:hidden}@keyframes ts-toast-progress{from{transform:scaleX(1)}to{transform:scaleX(0)}}.ts-toast-container .ts-toast-progress{position:absolute;left:0;right:0;bottom:0;height:3px;transform-origin:left center;border-radius:0 0 8px 8px;background:var(--toast-progress-color,currentColor);opacity:.35;pointer-events:none}.ts-toast-container .ts-toast-success .ts-toast-progress{background:var(--toast-success-color,#17a35c)}.ts-toast-container .ts-toast-error .ts-toast-progress{background:var(--toast-error-color,#ef4444)}.ts-toast-container .ts-toast-warning .ts-toast-progress{background:var(--toast-warning-color,#f0a020)}.ts-toast-container .ts-toast-info .ts-toast-progress{background:var(--toast-info-color,#3b82f6)}.ts-toast-container .ts-toast-action{order:-1;flex:none;appearance:none;border:0;background:0 0;color:var(--toast-action-color,#3b82f6);font:inherit;font-weight:600;padding:4px 8px;margin-left:4px;border-radius:6px;cursor:pointer}.ts-toast-container .ts-toast-action:hover{background:var(--toast-action-hover,rgba(59,130,246,.12))}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsirosgeorge/toastnotification",
3
- "version": "5.5.0",
3
+ "version": "5.6.1",
4
4
  "description": "a toast notification plugin",
5
5
  "main": "toast.min.js",
6
6
  "module": "toast.module.js",
@@ -21,7 +21,7 @@
21
21
  "build:css": "cleancss -o assets/css/toast.min.css assets/css/toast.css",
22
22
  "build:js": "terser toast.js --compress --mangle --output toast.min.js",
23
23
  "build": "npm run sync:version && npm run build:module && npm run build:css && npm run build:js",
24
- "version": "npm run build && git add -A",
24
+ "version": "node scripts/stamp-changelog.mjs && npm run build && git add -A",
25
25
  "prepublishOnly": "npm run build",
26
26
  "release": "npm version ${V:-patch} && git push origin main --follow-tags"
27
27
  },
package/toast.d.ts CHANGED
@@ -31,6 +31,12 @@ export interface ToastOptions {
31
31
  /** Emoji or text used instead of the bundled animated icon. */
32
32
  icon?: string | null;
33
33
  showLoader?: boolean;
34
+ /** Freeze the countdown while the pointer or keyboard focus is on the toast. */
35
+ pauseOnHover?: boolean;
36
+ /** Thin bar counting the remaining time down. Needs `duration > 0`. */
37
+ showProgress?: boolean;
38
+ /** Renders a button inside the toast, e.g. Undo. */
39
+ action?: ToastAction | null;
34
40
  /**
35
41
  * `message` is written as HTML by default, for backwards compatibility.
36
42
  * Pass `false` to render it as plain text — do that for anything user-supplied.
@@ -66,6 +72,21 @@ export interface ConfirmOptions extends ToastOptions {
66
72
  onResult?: (result: ConfirmResult, el: ToastElement) => void;
67
73
  }
68
74
 
75
+ export interface ToastAction {
76
+ /** Button label. */
77
+ text: string;
78
+ /** Runs on click; the toast closes afterwards. */
79
+ onClick?: (el: ToastElement) => void;
80
+ }
81
+
82
+ export interface PromiseMessages<T = unknown> {
83
+ loading?: string;
84
+ /** A function receives the resolved value. */
85
+ success?: string | ((value: T) => string);
86
+ /** A function receives the rejection reason. */
87
+ error?: string | ((err: unknown) => string);
88
+ }
89
+
69
90
  export interface LoadingHandle {
70
91
  update(message: string, options?: ToastOptions): void;
71
92
  close(): void;
@@ -87,6 +108,14 @@ export interface Toast {
87
108
  confirm(message: string, options?: ConfirmOptions): Promise<ConfirmResult>;
88
109
  /** Dismiss every toast on screen; open confirm dialogs settle as a cancel. */
89
110
  dismissAll(): void;
111
+ /**
112
+ * Shows a loading toast that becomes the success or error message when
113
+ * `promise` settles. Resolves with the promise's value, and re-throws its
114
+ * rejection so the caller still handles the failure.
115
+ */
116
+ promise<T>(promise: Promise<T>, messages?: PromiseMessages<T>, options?: ToastOptions): Promise<T>;
117
+ /** Options applied to every toast unless the call overrides them. */
118
+ defaults: ToastOptions;
90
119
  }
91
120
 
92
121
  declare const toast: Toast;
package/toast.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // Single source of truth for the CDN this build points at.
4
4
  // `npm run sync:version` rewrites it from package.json, so it can never go stale.
5
- const TS_TOAST_VERSION = "5.5.0";
5
+ const TS_TOAST_VERSION = "5.6.1";
6
6
  // Point this at your own copy of assets/ to self-host the CSS and icons
7
7
  // (useful offline, behind a strict CSP, or when you don't want a CDN dependency):
8
8
  // window.TS_TOAST_ASSET_BASE = '/vendor/toastnotification';
@@ -14,6 +14,7 @@ const TS_TOAST_CDN = (typeof window !== 'undefined' && window.TS_TOAST_ASSET_BAS
14
14
  // group, so only the last dialog to close may release it.
15
15
  let tsToastOpenModals = 0;
16
16
  let tsToastPrevOverflow = '';
17
+ let tsToastPrevPaddingRight = '';
17
18
 
18
19
  const tsToastReducedMotion = () =>
19
20
  typeof window !== 'undefined' &&
@@ -52,6 +53,11 @@ let tsToastIdCounter = 0;
52
53
  .ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
53
54
  .ts-toast-container.center { top: 50%; left: 50%; transform: translate(-50%, -50%); align-items: center; }
54
55
  .ts-toast-overlay.center { align-items: center; justify-content: center; }
56
+ .ts-toast-overlay.ts-toast-overlay-stacked { background: transparent; }
57
+ @keyframes ts-toast-progress { from { transform: scaleX(1); } to { transform: scaleX(0); } }
58
+ .ts-toast .ts-toast-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; transform-origin: left center; border-radius: 0 0 8px 8px; background: currentColor; opacity: 0.35; pointer-events: none; }
59
+ .ts-toast .ts-toast-action { order: -1; flex: none; appearance: none; border: 0; background: transparent; color: #3b82f6; font: inherit; font-weight: 600; padding: 4px 8px; margin-left: 4px; border-radius: 6px; cursor: pointer; }
60
+ .ts-toast .ts-toast-action:hover { background: rgba(59,130,246,0.12); }
55
61
  .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; }
56
62
  .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; }
57
63
  .ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
@@ -75,6 +81,9 @@ let tsToastIdCounter = 0;
75
81
  })();
76
82
 
77
83
  const toast = function (message, options = {}) {
84
+ // Site-wide defaults, so a page can turn on things like showProgress once
85
+ // instead of repeating them at every call site.
86
+ options = { ...(toast.defaults || {}), ...options };
78
87
  const {
79
88
  position = 'top-right',
80
89
  animation = 'slide-right', // Default fallback animation
@@ -103,6 +112,12 @@ const toast = function (message, options = {}) {
103
112
  useOverlay = true,
104
113
  closeOnOverlayClick = true,
105
114
  showClose = false,
115
+ // Freeze the countdown while the pointer or keyboard focus is on the toast
116
+ pauseOnHover = true,
117
+ // Thin bar counting the remaining time down
118
+ showProgress = false,
119
+ // { text, onClick } renders a button inside the toast, e.g. Undo
120
+ action = null,
106
121
  // Escape cancels a confirm dialog
107
122
  closeOnEscape = true,
108
123
  // `message` is written as HTML for backwards compatibility. Pass false to
@@ -329,6 +344,31 @@ const toast = function (message, options = {}) {
329
344
  confirmBtn.addEventListener('click', (e) => { e.stopPropagation(); resolveAndClose(true); });
330
345
  }
331
346
 
347
+ // Action button (e.g. Undo). Clicking it runs the callback and closes the toast.
348
+ if (!isConfirm && action && typeof action === 'object' && action.text) {
349
+ const actionBtn = document.createElement('button');
350
+ actionBtn.className = 'ts-toast-action';
351
+ actionBtn.type = 'button';
352
+ actionBtn.textContent = action.text;
353
+ actionBtn.addEventListener('click', (e) => {
354
+ e.stopPropagation(); // do not also trigger dismissOnClick
355
+ if (typeof action.onClick === 'function') action.onClick(toastElement);
356
+ toastElement._dismiss();
357
+ });
358
+ toastElement.appendChild(actionBtn);
359
+ }
360
+
361
+ // Progress bar. The width is driven by a CSS animation whose duration is the
362
+ // toast's own, so pausing it is one property and never drifts from the timer.
363
+ let progressBar = null;
364
+ if (!isConfirm && showProgress && duration > 0 && !reducedMotion) {
365
+ progressBar = document.createElement('div');
366
+ progressBar.className = 'ts-toast-progress';
367
+ progressBar.style.animation = `ts-toast-progress ${duration}ms linear forwards`;
368
+ progressBar.style.animationPlayState = 'paused';
369
+ toastElement.appendChild(progressBar);
370
+ }
371
+
332
372
  // Loader Element
333
373
  let loader = null;
334
374
  if (showLoader) {
@@ -339,11 +379,15 @@ const toast = function (message, options = {}) {
339
379
 
340
380
  // Container/Overlay
341
381
  let overlay = null;
382
+ let containerEl = null;
342
383
  if (isConfirm && useOverlay) {
343
384
  overlay = document.createElement('div');
344
385
  // A modal centres by default. The `position` default of 'top-right' is meant
345
386
  // for toasts; applying it here parked the dialog in a corner of the backdrop.
346
- overlay.className = 'ts-toast-overlay' + (options.position ? ` ${position}` : '');
387
+ // Each backdrop paints its own 50% black, so a second dialog opening over a
388
+ // first turned the page almost fully dark. Only the bottom one dims.
389
+ const stacked = tsToastOpenModals > 0 ? ' ts-toast-overlay-stacked' : '';
390
+ overlay.className = 'ts-toast-overlay' + stacked + (options.position ? ` ${position}` : '');
347
391
  document.body.appendChild(overlay);
348
392
  overlay.appendChild(toastElement);
349
393
  if (showClose) {
@@ -387,6 +431,7 @@ const toast = function (message, options = {}) {
387
431
  container.setAttribute('aria-relevant', 'additions');
388
432
  }
389
433
  container.appendChild(toastElement);
434
+ containerEl = container;
390
435
  }
391
436
 
392
437
  if (isConfirm) {
@@ -399,6 +444,15 @@ const toast = function (message, options = {}) {
399
444
  tsToastOpenModals += 1;
400
445
  if (tsToastOpenModals === 1) {
401
446
  tsToastPrevOverflow = document.body.style.overflow;
447
+ tsToastPrevPaddingRight = document.body.style.paddingRight;
448
+ // Hiding the scrollbar makes the page wider, which shifts the whole
449
+ // layout sideways as the dialog opens. Pad by the scrollbar's width to
450
+ // hold it still. A no-op where scrollbars are overlays, as on macOS.
451
+ const scrollbar = window.innerWidth - document.documentElement.clientWidth;
452
+ if (scrollbar > 0) {
453
+ const current = parseFloat(window.getComputedStyle(document.body).paddingRight) || 0;
454
+ document.body.style.paddingRight = `${current + scrollbar}px`;
455
+ }
402
456
  document.body.style.overflow = 'hidden';
403
457
  }
404
458
 
@@ -441,7 +495,10 @@ const toast = function (message, options = {}) {
441
495
  releaseModal = () => {
442
496
  document.removeEventListener('keydown', onKeydown, true);
443
497
  tsToastOpenModals = Math.max(0, tsToastOpenModals - 1);
444
- if (tsToastOpenModals === 0) document.body.style.overflow = tsToastPrevOverflow;
498
+ if (tsToastOpenModals === 0) {
499
+ document.body.style.overflow = tsToastPrevOverflow;
500
+ document.body.style.paddingRight = tsToastPrevPaddingRight;
501
+ }
445
502
  // Hand the keyboard back to whatever opened the dialog.
446
503
  if (previouslyFocused && typeof previouslyFocused.focus === 'function' &&
447
504
  document.contains(previouslyFocused)) {
@@ -463,6 +520,10 @@ const toast = function (message, options = {}) {
463
520
  toastElement.classList.add('ts-toast-show');
464
521
  }, 100);
465
522
 
523
+ // The loader always ran for 2s, so with a shorter duration the toast was gone
524
+ // before the icon it reveals ever appeared.
525
+ const loaderMs = duration > 0 ? Math.min(2000, Math.max(0, duration - 500)) : 2000;
526
+
466
527
  // Handle Loader and Icon
467
528
  if (showLoader && loader) {
468
529
  setTimeout(() => {
@@ -474,7 +535,7 @@ const toast = function (message, options = {}) {
474
535
  if (isConfirm && contentRow) contentRow.appendChild(iconElement);
475
536
  else toastElement.appendChild(iconElement); // Add icon only if not present
476
537
  }
477
- }, 2000); // Simulate a loading period of 2 seconds
538
+ }, loaderMs);
478
539
  }
479
540
  if (!showLoader) {
480
541
  // For confirm, icon already added above inside contentRow; avoid moving it
@@ -483,26 +544,70 @@ const toast = function (message, options = {}) {
483
544
  }
484
545
  }
485
546
 
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;
547
+ // Dismissal state lives on the element so toast.update() can rebind the callback
548
+ // and reuse this exact removal path instead of keeping its own copy of it.
549
+ toastElement._onDismiss = typeof onDismiss === 'function' ? onDismiss : null;
550
+
551
+ // A plain setTimeout cannot be paused, so the countdown is tracked by hand:
552
+ // `remaining` is what is left, and hovering banks it.
553
+ let remaining = duration;
554
+ let timerId = null;
555
+ let startedAt = 0;
556
+
557
+ const stopTimer = () => {
558
+ if (timerId) { clearTimeout(timerId); timerId = null; }
559
+ };
560
+
561
+ const startTimer = () => {
562
+ if (isConfirm || remaining <= 0 || timerId) return;
563
+ startedAt = Date.now();
564
+ timerId = setTimeout(() => { timerId = null; toastElement._dismiss(); }, remaining);
565
+ if (progressBar) progressBar.style.animationPlayState = 'running';
566
+ };
567
+
568
+ const pauseTimer = () => {
569
+ if (!timerId) return;
570
+ clearTimeout(timerId);
571
+ timerId = null;
572
+ remaining -= Date.now() - startedAt;
573
+ if (progressBar) progressBar.style.animationPlayState = 'paused';
574
+ };
575
+
576
+ toastElement._dismiss = () => {
577
+ if (toastElement._removing) return; // never run the exit twice
578
+ toastElement._removing = true;
579
+ stopTimer();
580
+ removeWithAnimation(toastElement, () => {
581
+ if (toastElement._onDismiss) toastElement._onDismiss(toastElement);
582
+ // Containers used to pile up in the DOM, one per position, forever.
583
+ if (containerEl && !containerEl.children.length) containerEl.remove();
584
+ });
585
+ };
586
+
587
+ // Lets toast.update() re-arm the countdown without reaching into internals.
588
+ toastElement._setDuration = (ms) => {
589
+ stopTimer();
590
+ remaining = ms;
591
+ startTimer();
592
+ };
593
+
594
+ startTimer();
595
+
596
+ if (!isConfirm && pauseOnHover) {
597
+ // Give the reader a chance to finish the sentence.
598
+ toastElement.addEventListener('mouseenter', pauseTimer);
599
+ toastElement.addEventListener('mouseleave', startTimer);
600
+ toastElement.addEventListener('focusin', pauseTimer);
601
+ toastElement.addEventListener('focusout', startTimer);
494
602
  }
495
603
 
496
604
  // Add event listener for closing the toast when clicked (disabled in confirm mode)
497
605
  if (!isConfirm && dismissOnClick) {
498
606
  toastElement.addEventListener('click', () => {
499
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove); // Clear the auto-remove timeout
500
607
  // onClick belongs to the click, not to the end of the exit animation,
501
608
  // which is where it used to fire half a second late.
502
609
  if (onClick && typeof onClick === 'function') onClick(toastElement);
503
- removeWithAnimation(toastElement, () => {
504
- if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
505
- });
610
+ toastElement._dismiss();
506
611
  });
507
612
  }
508
613
 
@@ -525,23 +630,15 @@ const toast = function (message, options = {}) {
525
630
  const dy = Math.abs(touchStartY - e.changedTouches[0].screenY);
526
631
  // Only a mostly-horizontal swipe dismisses, so scrolling the page past a
527
632
  // 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
- }
633
+ if (dx > 50 && dx > dy) toastElement._dismiss();
534
634
  });
535
635
  }
536
636
 
537
637
  // Let callers dismiss a toast they are holding, instead of only waiting out
538
638
  // the duration or making the user click it.
539
639
  toastElement.close = () => {
540
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
541
640
  if (isConfirm) { resolveAndClose(false); return; }
542
- removeWithAnimation(toastElement, () => {
543
- if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
544
- });
641
+ toastElement._dismiss();
545
642
  };
546
643
 
547
644
  return toastElement;
@@ -570,10 +667,9 @@ const toast = function (message, options = {}) {
570
667
  type = null,
571
668
  icon = null,
572
669
  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
670
+ duration = 3000, // Default duration (in ms); 0 keeps the toast on screen
671
+ allowHtml = true,
672
+ onDismiss = null // Replaces the callback the toast was created with
577
673
  } = options;
578
674
 
579
675
  // Remove old loader (if any)
@@ -591,7 +687,8 @@ const toast = function (message, options = {}) {
591
687
  }
592
688
  const toastBody = toastElement.querySelector('.ts-toast-body');
593
689
  if (toastBody) {
594
- toastBody.innerHTML = message;
690
+ if (allowHtml) toastBody.innerHTML = message;
691
+ else toastBody.textContent = message;
595
692
  }
596
693
 
597
694
  // Handle Icon update only if it's new or hasn't been set yet
@@ -619,9 +716,12 @@ const toast = function (message, options = {}) {
619
716
  iconElement.appendChild(img);
620
717
  }
621
718
 
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);
719
+ // Only attach an icon we actually have. Without a type and without an explicit
720
+ // icon this used to append an empty <span><img></span>.
721
+ if (icon || iconElement.querySelector('img[src]')) {
722
+ const contentRow = toastElement.querySelector('.ts-toast-content');
723
+ (contentRow || toastElement).appendChild(iconElement);
724
+ }
625
725
 
626
726
  // Handle loader if requested
627
727
  if (showLoader) {
@@ -630,33 +730,19 @@ const toast = function (message, options = {}) {
630
730
  toastElement.appendChild(loader);
631
731
  setTimeout(() => {
632
732
  loader.classList.add('done');
633
- }, 2000); // Simulate loader completion after 2 seconds
733
+ }, duration > 0 ? Math.min(2000, Math.max(0, duration - 500)) : 2000);
634
734
  }
635
735
 
636
- // Clear previous auto-remove timer if needed
637
- if (toastElement._autoRemove) {
638
- clearTimeout(toastElement._autoRemove);
639
- }
736
+ // Rebind the dismiss callback rather than leaving the creation-time one in place,
737
+ // which meant an updated toast fired two different onDismiss handlers.
738
+ if (typeof onDismiss === 'function') toastElement._onDismiss = onDismiss;
640
739
 
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
740
+ // Re-arm the countdown through the toast's own timer, so duration: 0 keeps the
741
+ // toast on screen. This used to schedule setTimeout(..., 0) and remove it at once,
742
+ // which broke every `toast.loading(...).update(msg, { duration: 0 })`.
743
+ if (typeof toastElement._setDuration === 'function') {
744
+ toastElement._setDuration(duration);
745
+ }
660
746
  };
661
747
 
662
748
  toast.loading = function (message, options = {}) {
@@ -730,6 +816,34 @@ const toast = function (message, options = {}) {
730
816
  });
731
817
  };
732
818
 
819
+ // Wraps an async action: one loading toast that becomes the success or error
820
+ // message, instead of hand-rolling loading/update/catch at every call site.
821
+ toast.promise = function (promise, messages = {}, options = {}) {
822
+ const {
823
+ loading = 'Loading…',
824
+ success = 'Done',
825
+ error = 'Something went wrong'
826
+ } = messages;
827
+
828
+ const handle = toast.loading(loading, options);
829
+ // Messages may be functions so they can name what actually came back.
830
+ const text = (msg, value) => (typeof msg === 'function' ? msg(value) : msg);
831
+
832
+ return Promise.resolve(promise).then(
833
+ (value) => {
834
+ handle.update(text(success, value), { type: 'success', duration: options.duration });
835
+ return value;
836
+ },
837
+ (err) => {
838
+ handle.update(text(error, err), { type: 'error', duration: options.duration });
839
+ throw err; // the caller still owns the failure
840
+ }
841
+ );
842
+ };
843
+
844
+ // Options applied to every toast unless the call overrides them.
845
+ toast.defaults = {};
846
+
733
847
  // Close every toast currently on screen. Confirm dialogs settle as a cancel.
734
848
  toast.dismissAll = function () {
735
849
  document.querySelectorAll('.ts-toast').forEach((el) => {
package/toast.min.js CHANGED
@@ -1 +1 @@
1
- "use strict";const TS_TOAST_VERSION="5.5.0",TS_TOAST_CDN="undefined"!=typeof window&&window.TS_TOAST_ASSET_BASE?String(window.TS_TOAST_ASSET_BASE).replace(/\/+$/,""):"https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@5.5.0";let tsToastOpenModals=0,tsToastPrevOverflow="";const tsToastReducedMotion=()=>"undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches,tsToastFocusable=t=>Array.from(t.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter((t=>!t.disabled&&null!==t.offsetParent));let tsToastIdCounter=0;!function(){const t="ts-toast-stylesheet";if("undefined"!=typeof window&&window.TS_TOAST_NO_CSS)return;if(document.getElementById(t))return;const e=document.createElement("link");e.id=t,e.rel="stylesheet",e.href=`${TS_TOAST_CDN}/assets/css/toast.min.css`,document.head.appendChild(e)}(),function(){const t="ts-toast-inline-extras";if(document.getElementById(t))return;const e=document.createElement("style");e.id=t,e.textContent="\n /* Ensure center positions exist even if external CSS lacks them */\n .ts-toast-container.top-center { top: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }\n .ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }\n .ts-toast-container.center { top: 50%; left: 50%; transform: translate(-50%, -50%); align-items: center; }\n .ts-toast-overlay.center { align-items: center; justify-content: center; }\n .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; }\n .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; }\n .ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }\n .ts-toast-actions { display: flex; gap: 10px; justify-content: center; margin-top: 12px; }\n .ts-toast-btn { appearance: none; border: 0; padding: 8px 12px; border-radius: 8px; font-weight: 600; cursor: pointer; }\n .ts-toast-btn.cancel { background: #e9ecef; color: #1f2937; }\n .ts-toast-btn.confirm { background: #3b82f6; color: #fff; }\n .ts-toast.ts-toast-error .ts-toast-btn.confirm,\n .ts-toast.ts-toast-warning .ts-toast-btn.confirm { background: #ef4444; color: #fff; }\n .ts-toast.ts-toast-confirm .ts-toast-title { font-weight: 700; font-size: 1.05rem; margin-top: 4px; }\n .ts-toast.ts-toast-confirm .ts-toast-close { position: absolute; top: 8px; right: 8px; width: 28px; height: 28px; border-radius: 999px; border: 0; background: transparent; color: #6b7280; font-size: 20px; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }\n .ts-toast.ts-toast-confirm .ts-toast-close:hover { background: rgba(0,0,0,0.06); }\n .ts-toast.ts-toast-confirm .ts-toast-icon { width: 64px; height: 64px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; }\n .ts-toast.ts-toast-confirm .ts-toast-icon img { width: 36px; height: 36px; }\n .ts-toast.ts-toast-confirm.ts-toast-success .ts-toast-icon { background: #dcfce7; }\n .ts-toast.ts-toast-confirm.ts-toast-info .ts-toast-icon { background: #dbeafe; }\n .ts-toast.ts-toast-confirm.ts-toast-warning .ts-toast-icon { background: #fef3c7; }\n .ts-toast.ts-toast-confirm.ts-toast-error .ts-toast-icon { background: #fee2e2; }\n ",document.head.appendChild(e)}();const toast=function(t,e={}){const{position:o="top-right",animation:s="slide-right",type:n="info",duration:a=3e3,icon:i=null,showLoader:r=!1,mode:l="alert",title:c=null,confirmText:d="Yes",cancelText:u="No",input:m=!1,inputPlaceholder:f="",inputValue:p="",confirmButtonBg:y=null,confirmButtonColor:h=null,cancelButtonBg:g=null,cancelButtonColor:b=null,onConfirm:v=null,onCancel:x=null,onResult:w=null,useOverlay:T=!0,closeOnOverlayClick:C=!0,showClose:E=!1,closeOnEscape:S=!0,allowHtml:L=!0,dismissOnClick:_=!0,onClick:k=null,onShow:A=null,onDismiss:N=null}=e,O="confirm"===l||"swal"===l,M=tsToastReducedMotion(),$="string"==typeof e.animation&&e.animation.trim()?{"slide-top":"ts-toast-slide-top","slide-bottom":"ts-toast-slide-bottom","slide-left":"ts-toast-slide-left","slide-right":"ts-toast-slide-right","zoom-in":"ts-toast-zoom-in","zoom-out":"ts-toast-zoom-out",flip:"ts-toast-flip"}[B=e.animation.trim()]||B:O||"center"===o?"ts-toast-zoom-in":o.startsWith("top")?"ts-toast-slide-top":o.startsWith("bottom")?"ts-toast-slide-bottom":o.endsWith("left")?"ts-toast-slide-left":"ts-toast-slide-right";var B;const R=(t,e)=>{const o=t.dataset&&t.dataset.anim?t.dataset.anim:t.style.animation||"";let s="";o.includes("ts-toast-slide-top")?s="translateY(-100%)":o.includes("ts-toast-slide-bottom")?s="translateY(100%)":(o.includes("ts-toast-slide-left")||o.includes("ts-toast-slide-right"))&&(s="translateX(100%)"),t.classList.add("ts-toast-slide-out"),t.classList.remove("ts-toast-show"),t.style.animation="",s&&(t.style.transform=s),t.style.opacity="0",setTimeout((()=>{t.classList.remove("ts-toast-slide-out"),t.parentNode&&t.parentNode.removeChild(t),"function"==typeof e&&e()}),M?0:500)},D=document.createElement("div");D.className=`ts-toast ts-toast-${n}${O?" ts-toast-confirm":""}`,D.dataset.anim=$,M||(D.style.animation=`${$} 0.5s ease`);const q="ts-toast-"+ ++tsToastIdCounter;O?(D.setAttribute("role","dialog"),D.setAttribute("aria-modal","true"),D.tabIndex=-1):"error"!==n&&"warning"!==n||D.setAttribute("role","alert"),O||(D.style.flexDirection="row-reverse",D.style.justifyContent="flex-end");const j=document.createElement("span");if(j.className="ts-toast-icon",j.style.display="flex",i)j.textContent=i;else{const t=document.createElement("img");t.alt="",t.setAttribute("aria-hidden","true"),t.style.width="30px",t.style.height="30px",t.style.objectFit="contain";const e={success:"success.gif",error:"error.gif",info:"info.gif",warning:"warning.gif"}[n];e&&(t.src=`${TS_TOAST_CDN}/assets/img/${e}`),j.appendChild(t)}const P=document.createElement("div");P.className="ts-toast-body",P.id=`${q}-body`,L?P.innerHTML=t:P.textContent=t;let z=null;if(O){if(z=document.createElement("div"),z.className="ts-toast-content",z.appendChild(j),c){const t=document.createElement("div");t.className="ts-toast-title",t.id=`${q}-title`,t.textContent=c,z.appendChild(t),D.setAttribute("aria-labelledby",t.id)}z.appendChild(P),D.setAttribute("aria-describedby",P.id),D.appendChild(z)}else D.appendChild(P);let I=null;O&&m&&("textarea"===m?(I=document.createElement("textarea"),I.rows=3):(I=document.createElement("input"),I.type="text"===m||"email"===m||"password"===m||"number"===m?m:"text"),I.className="ts-toast-input",I.placeholder=f,I.value=p,"textarea"!==m&&I.addEventListener("keydown",(t=>{"Enter"===t.key&&(t.preventDefault(),Y(!0))})),D.appendChild(I));let F=null,X=null,Y=null,H=()=>{};if(O){F=document.createElement("div"),F.className="ts-toast-actions";const t=document.createElement("button");t.className="ts-toast-btn cancel",t.textContent=u;const e=document.createElement("button");e.className="ts-toast-btn confirm",e.textContent=d,g&&(t.style.background=g),b&&(t.style.color=b),y&&(e.style.background=y),h&&(e.style.color=h),F.appendChild(t),F.appendChild(e),D.appendChild(F),D.result=new Promise((t=>{X=t}));let o=!1;Y=t=>{if(o)return;o=!0;const e=t?!I||I.value:!!I&&null;X&&X(e),t&&"function"==typeof v&&v(e,D),t||"function"!=typeof x||x(D),"function"==typeof w&&w(e,D),H(),R(D,(()=>{N&&"function"==typeof N&&N(D),W&&W.parentNode&&W.parentNode.removeChild(W)}))},t.addEventListener("click",(t=>{t.stopPropagation(),Y(!1)})),e.addEventListener("click",(t=>{t.stopPropagation(),Y(!0)}))}let K=null;r&&(K=document.createElement("div"),K.className="ts-toast-loader",D.appendChild(K));let W=null;if(O&&T){if(W=document.createElement("div"),W.className="ts-toast-overlay"+(e.position?` ${o}`:""),document.body.appendChild(W),W.appendChild(D),E){const t=document.createElement("button");t.className="ts-toast-close",t.setAttribute("aria-label","Close"),t.innerHTML="&times;",t.addEventListener("click",(t=>{t.stopPropagation(),Y(!1)})),D.appendChild(t)}if(C){let t=!1;W.addEventListener("pointerdown",(e=>{t=e.target===W})),W.addEventListener("click",(e=>{e.target===W&&t&&Y(!1),t=!1}))}}else{let t=document.querySelector(`.ts-toast-container.${o}`);t||(t=document.createElement("div"),t.className=`ts-toast-container ${o}`,document.body.appendChild(t)),t.hasAttribute("aria-live")||(t.setAttribute("role","status"),t.setAttribute("aria-live","polite"),t.setAttribute("aria-relevant","additions")),t.appendChild(D)}if(O){const t=document.activeElement;tsToastOpenModals+=1,1===tsToastOpenModals&&(tsToastPrevOverflow=document.body.style.overflow,document.body.style.overflow="hidden");const e=()=>{const t=document.querySelectorAll(".ts-toast.ts-toast-confirm");return 0===t.length||t[t.length-1]===D},o=t=>{if(!e())return;if("Escape"===t.key&&S)return t.preventDefault(),void Y(!1);if("Tab"!==t.key)return;const o=tsToastFocusable(D);if(!o.length)return void t.preventDefault();const s=o[0],n=o[o.length-1];D.contains(document.activeElement)?t.shiftKey&&document.activeElement===s?(t.preventDefault(),n.focus()):t.shiftKey||document.activeElement!==n||(t.preventDefault(),s.focus()):(t.preventDefault(),(t.shiftKey?n:s).focus())};document.addEventListener("keydown",o,!0),H=()=>{document.removeEventListener("keydown",o,!0),tsToastOpenModals=Math.max(0,tsToastOpenModals-1),0===tsToastOpenModals&&(document.body.style.overflow=tsToastPrevOverflow),t&&"function"==typeof t.focus&&document.contains(t)&&t.focus()},(I||D.querySelector(".ts-toast-btn.confirm")||D).focus()}if(A&&"function"==typeof A&&A(D),setTimeout((()=>{D.classList.add("ts-toast-show")}),100),r&&K&&setTimeout((()=>{D._managedByLoading||(K.classList.add("done"),K.remove(),D.contains(j)||(O&&z?z.appendChild(j):D.appendChild(j)))}),2e3),r||O||D.contains(j)||D.appendChild(j),!O&&a>0){const t=setTimeout((()=>{R(D,(()=>{N&&"function"==typeof N&&N(D)}))}),a);D._autoRemove=t}if(!O&&_&&D.addEventListener("click",(()=>{D._autoRemove&&clearTimeout(D._autoRemove),k&&"function"==typeof k&&k(D),R(D,(()=>{N&&"function"==typeof N&&N(D)}))})),!O){let t=0,e=0,o=0;D.addEventListener("touchstart",(o=>{t=o.changedTouches[0].screenX,e=o.changedTouches[0].screenY}),{passive:!0}),D.addEventListener("touchend",(s=>{o=s.changedTouches[0].screenX;const n=Math.abs(t-o),a=Math.abs(e-s.changedTouches[0].screenY);n>50&&n>a&&(D._autoRemove&&clearTimeout(D._autoRemove),R(D,(()=>{N&&"function"==typeof N&&N(D)})))}))}return D.close=()=>{D._autoRemove&&clearTimeout(D._autoRemove),O?Y(!1):R(D,(()=>{N&&"function"==typeof N&&N(D)}))},D};toast.success=function(t,e){return toast(t,{...e,type:"success"})},toast.error=function(t,e){return toast(t,{...e,type:"error"})},toast.warning=function(t,e){return toast(t,{...e,type:"warning"})},toast.info=function(t,e){return toast(t,{...e,type:"info"})},toast.update=function(t,e,o={}){const{type:s=null,icon:n=null,showLoader:a=!1,duration:i=3e3,onClick:r=null,onShow:l=null,onDismiss:c=null}=o,d=t.querySelector(".ts-toast-loader"),u=t.querySelector(".ts-toast-icon");d&&d.remove(),s&&(["success","error","info","warning"].forEach((e=>t.classList.remove(`ts-toast-${e}`))),t.classList.add("ts-toast",`ts-toast-${s}`,"ts-toast-show"));const m=t.querySelector(".ts-toast-body");m&&(m.innerHTML=e),u&&u.remove();const f=document.createElement("span");if(f.className="ts-toast-icon",f.style.display="flex",n)f.textContent=n;else{const t=document.createElement("img");t.alt="",t.setAttribute("aria-hidden","true"),t.style.width="30px",t.style.height="30px",t.style.objectFit="contain";const e={success:"success.gif",error:"error.gif",info:"info.gif",warning:"warning.gif"}[s];e&&(t.src=`${TS_TOAST_CDN}/assets/img/${e}`),f.appendChild(t)}if((t.querySelector(".ts-toast-content")||t).appendChild(f),a){const e=document.createElement("div");e.className="ts-toast-loader",t.appendChild(e),setTimeout((()=>{e.classList.add("done")}),2e3)}t._autoRemove&&clearTimeout(t._autoRemove);const p=setTimeout((()=>{var e,o;o=()=>{c&&"function"==typeof c&&c(t)},(e=t).classList.add("ts-toast-slide-out"),e.classList.remove("ts-toast-show"),e.style.animation="",setTimeout((()=>{e.classList.remove("ts-toast-slide-out"),e.parentNode&&e.parentNode.removeChild(e),"function"==typeof o&&o()}),500)}),i);t._autoRemove=p},toast.loading=function(t,e={}){const o=toast(t,{...e,type:e.type||"info",duration:0,showLoader:!0,icon:null});requestAnimationFrame((()=>{o.classList.add("ts-toast-show")}));const s=o.querySelector(".ts-toast-loader");let n=o.querySelector(".ts-toast-icon");return n||(n=document.createElement("span"),n.className="ts-toast-icon",n.style.display="flex",o.appendChild(n)),o._managedByLoading=!0,s&&setTimeout((()=>{o._managedByLoading||s.classList.add("done")}),2e3),{update:(t,e={})=>{o._managedByLoading=!1,toast.update(o,t,{...e,showLoader:!1})},close:()=>{o._managedByLoading=!1,o.close()}}},toast.confirm=function(t,e={}){return new Promise((o=>{toast(t,{...e,mode:"confirm",duration:0,dismissOnClick:!1,onResult:t=>o(t)})}))},toast.dismissAll=function(){document.querySelectorAll(".ts-toast").forEach((t=>{"function"==typeof t.close&&t.close()}))},"undefined"!=typeof window&&(window.toast=toast),"undefined"!=typeof module&&module.exports&&(module.exports=toast);
1
+ "use strict";const TS_TOAST_VERSION="5.6.1",TS_TOAST_CDN="undefined"!=typeof window&&window.TS_TOAST_ASSET_BASE?String(window.TS_TOAST_ASSET_BASE).replace(/\/+$/,""):"https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@5.6.1";let tsToastOpenModals=0,tsToastPrevOverflow="",tsToastPrevPaddingRight="";const tsToastReducedMotion=()=>"undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches,tsToastFocusable=t=>Array.from(t.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter((t=>!t.disabled&&null!==t.offsetParent));let tsToastIdCounter=0;!function(){const t="ts-toast-stylesheet";if("undefined"!=typeof window&&window.TS_TOAST_NO_CSS)return;if(document.getElementById(t))return;const e=document.createElement("link");e.id=t,e.rel="stylesheet",e.href=`${TS_TOAST_CDN}/assets/css/toast.min.css`,document.head.appendChild(e)}(),function(){const t="ts-toast-inline-extras";if(document.getElementById(t))return;const e=document.createElement("style");e.id=t,e.textContent="\n /* Ensure center positions exist even if external CSS lacks them */\n .ts-toast-container.top-center { top: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }\n .ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }\n .ts-toast-container.center { top: 50%; left: 50%; transform: translate(-50%, -50%); align-items: center; }\n .ts-toast-overlay.center { align-items: center; justify-content: center; }\n .ts-toast-overlay.ts-toast-overlay-stacked { background: transparent; }\n @keyframes ts-toast-progress { from { transform: scaleX(1); } to { transform: scaleX(0); } }\n .ts-toast .ts-toast-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; transform-origin: left center; border-radius: 0 0 8px 8px; background: currentColor; opacity: 0.35; pointer-events: none; }\n .ts-toast .ts-toast-action { order: -1; flex: none; appearance: none; border: 0; background: transparent; color: #3b82f6; font: inherit; font-weight: 600; padding: 4px 8px; margin-left: 4px; border-radius: 6px; cursor: pointer; }\n .ts-toast .ts-toast-action:hover { background: rgba(59,130,246,0.12); }\n .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; }\n .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; }\n .ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }\n .ts-toast-actions { display: flex; gap: 10px; justify-content: center; margin-top: 12px; }\n .ts-toast-btn { appearance: none; border: 0; padding: 8px 12px; border-radius: 8px; font-weight: 600; cursor: pointer; }\n .ts-toast-btn.cancel { background: #e9ecef; color: #1f2937; }\n .ts-toast-btn.confirm { background: #3b82f6; color: #fff; }\n .ts-toast.ts-toast-error .ts-toast-btn.confirm,\n .ts-toast.ts-toast-warning .ts-toast-btn.confirm { background: #ef4444; color: #fff; }\n .ts-toast.ts-toast-confirm .ts-toast-title { font-weight: 700; font-size: 1.05rem; margin-top: 4px; }\n .ts-toast.ts-toast-confirm .ts-toast-close { position: absolute; top: 8px; right: 8px; width: 28px; height: 28px; border-radius: 999px; border: 0; background: transparent; color: #6b7280; font-size: 20px; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }\n .ts-toast.ts-toast-confirm .ts-toast-close:hover { background: rgba(0,0,0,0.06); }\n .ts-toast.ts-toast-confirm .ts-toast-icon { width: 64px; height: 64px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; }\n .ts-toast.ts-toast-confirm .ts-toast-icon img { width: 36px; height: 36px; }\n .ts-toast.ts-toast-confirm.ts-toast-success .ts-toast-icon { background: #dcfce7; }\n .ts-toast.ts-toast-confirm.ts-toast-info .ts-toast-icon { background: #dbeafe; }\n .ts-toast.ts-toast-confirm.ts-toast-warning .ts-toast-icon { background: #fef3c7; }\n .ts-toast.ts-toast-confirm.ts-toast-error .ts-toast-icon { background: #fee2e2; }\n ",document.head.appendChild(e)}();const toast=function(t,e={}){e={...toast.defaults||{},...e};const{position:o="top-right",animation:s="slide-right",type:n="info",duration:a=3e3,icon:i=null,showLoader:r=!1,mode:l="alert",title:c=null,confirmText:d="Yes",cancelText:u="No",input:m=!1,inputPlaceholder:p="",inputValue:f="",confirmButtonBg:g=null,confirmButtonColor:y=null,cancelButtonBg:h=null,cancelButtonColor:b=null,onConfirm:x=null,onCancel:v=null,onResult:w=null,useOverlay:T=!0,closeOnOverlayClick:C=!0,showClose:E=!1,pauseOnHover:S=!0,showProgress:k=!1,action:L=null,closeOnEscape:_=!0,allowHtml:A=!0,dismissOnClick:N=!0,onClick:O=null,onShow:D=null,onDismiss:P=null}=e,M="confirm"===l||"swal"===l,$=tsToastReducedMotion(),B="string"==typeof e.animation&&e.animation.trim()?{"slide-top":"ts-toast-slide-top","slide-bottom":"ts-toast-slide-bottom","slide-left":"ts-toast-slide-left","slide-right":"ts-toast-slide-right","zoom-in":"ts-toast-zoom-in","zoom-out":"ts-toast-zoom-out",flip:"ts-toast-flip"}[q=e.animation.trim()]||q:M||"center"===o?"ts-toast-zoom-in":o.startsWith("top")?"ts-toast-slide-top":o.startsWith("bottom")?"ts-toast-slide-bottom":o.endsWith("left")?"ts-toast-slide-left":"ts-toast-slide-right";var q;const R=(t,e)=>{const o=t.dataset&&t.dataset.anim?t.dataset.anim:t.style.animation||"";let s="";o.includes("ts-toast-slide-top")?s="translateY(-100%)":o.includes("ts-toast-slide-bottom")?s="translateY(100%)":(o.includes("ts-toast-slide-left")||o.includes("ts-toast-slide-right"))&&(s="translateX(100%)"),t.classList.add("ts-toast-slide-out"),t.classList.remove("ts-toast-show"),t.style.animation="",s&&(t.style.transform=s),t.style.opacity="0",setTimeout((()=>{t.classList.remove("ts-toast-slide-out"),t.parentNode&&t.parentNode.removeChild(t),"function"==typeof e&&e()}),$?0:500)},j=document.createElement("div");j.className=`ts-toast ts-toast-${n}${M?" ts-toast-confirm":""}`,j.dataset.anim=B,$||(j.style.animation=`${B} 0.5s ease`);const z="ts-toast-"+ ++tsToastIdCounter;M?(j.setAttribute("role","dialog"),j.setAttribute("aria-modal","true"),j.tabIndex=-1):"error"!==n&&"warning"!==n||j.setAttribute("role","alert"),M||(j.style.flexDirection="row-reverse",j.style.justifyContent="flex-end");const X=document.createElement("span");if(X.className="ts-toast-icon",X.style.display="flex",i)X.textContent=i;else{const t=document.createElement("img");t.alt="",t.setAttribute("aria-hidden","true"),t.style.width="30px",t.style.height="30px",t.style.objectFit="contain";const e={success:"success.gif",error:"error.gif",info:"info.gif",warning:"warning.gif"}[n];e&&(t.src=`${TS_TOAST_CDN}/assets/img/${e}`),X.appendChild(t)}const F=document.createElement("div");F.className="ts-toast-body",F.id=`${z}-body`,A?F.innerHTML=t:F.textContent=t;let H=null;if(M){if(H=document.createElement("div"),H.className="ts-toast-content",H.appendChild(X),c){const t=document.createElement("div");t.className="ts-toast-title",t.id=`${z}-title`,t.textContent=c,H.appendChild(t),j.setAttribute("aria-labelledby",t.id)}H.appendChild(F),j.setAttribute("aria-describedby",F.id),j.appendChild(H)}else j.appendChild(F);let I=null;M&&m&&("textarea"===m?(I=document.createElement("textarea"),I.rows=3):(I=document.createElement("input"),I.type="text"===m||"email"===m||"password"===m||"number"===m?m:"text"),I.className="ts-toast-input",I.placeholder=p,I.value=f,"textarea"!==m&&I.addEventListener("keydown",(t=>{"Enter"===t.key&&(t.preventDefault(),K(!0))})),j.appendChild(I));let W=null,Y=null,K=null,V=()=>{};if(M){W=document.createElement("div"),W.className="ts-toast-actions";const t=document.createElement("button");t.className="ts-toast-btn cancel",t.textContent=u;const e=document.createElement("button");e.className="ts-toast-btn confirm",e.textContent=d,h&&(t.style.background=h),b&&(t.style.color=b),g&&(e.style.background=g),y&&(e.style.color=y),W.appendChild(t),W.appendChild(e),j.appendChild(W),j.result=new Promise((t=>{Y=t}));let o=!1;K=t=>{if(o)return;o=!0;const e=t?!I||I.value:!!I&&null;Y&&Y(e),t&&"function"==typeof x&&x(e,j),t||"function"!=typeof v||v(j),"function"==typeof w&&w(e,j),V(),R(j,(()=>{P&&"function"==typeof P&&P(j),Q&&Q.parentNode&&Q.parentNode.removeChild(Q)}))},t.addEventListener("click",(t=>{t.stopPropagation(),K(!1)})),e.addEventListener("click",(t=>{t.stopPropagation(),K(!0)}))}if(!M&&L&&"object"==typeof L&&L.text){const t=document.createElement("button");t.className="ts-toast-action",t.type="button",t.textContent=L.text,t.addEventListener("click",(t=>{t.stopPropagation(),"function"==typeof L.onClick&&L.onClick(j),j._dismiss()})),j.appendChild(t)}let G=null;!M&&k&&a>0&&!$&&(G=document.createElement("div"),G.className="ts-toast-progress",G.style.animation=`ts-toast-progress ${a}ms linear forwards`,G.style.animationPlayState="paused",j.appendChild(G));let J=null;r&&(J=document.createElement("div"),J.className="ts-toast-loader",j.appendChild(J));let Q=null,U=null;if(M&&T){Q=document.createElement("div");const t=tsToastOpenModals>0?" ts-toast-overlay-stacked":"";if(Q.className="ts-toast-overlay"+t+(e.position?` ${o}`:""),document.body.appendChild(Q),Q.appendChild(j),E){const t=document.createElement("button");t.className="ts-toast-close",t.setAttribute("aria-label","Close"),t.innerHTML="&times;",t.addEventListener("click",(t=>{t.stopPropagation(),K(!1)})),j.appendChild(t)}if(C){let t=!1;Q.addEventListener("pointerdown",(e=>{t=e.target===Q})),Q.addEventListener("click",(e=>{e.target===Q&&t&&K(!1),t=!1}))}}else{let t=document.querySelector(`.ts-toast-container.${o}`);t||(t=document.createElement("div"),t.className=`ts-toast-container ${o}`,document.body.appendChild(t)),t.hasAttribute("aria-live")||(t.setAttribute("role","status"),t.setAttribute("aria-live","polite"),t.setAttribute("aria-relevant","additions")),t.appendChild(j),U=t}if(M){const t=document.activeElement;if(tsToastOpenModals+=1,1===tsToastOpenModals){tsToastPrevOverflow=document.body.style.overflow,tsToastPrevPaddingRight=document.body.style.paddingRight;const t=window.innerWidth-document.documentElement.clientWidth;if(t>0){const e=parseFloat(window.getComputedStyle(document.body).paddingRight)||0;document.body.style.paddingRight=`${e+t}px`}document.body.style.overflow="hidden"}const e=()=>{const t=document.querySelectorAll(".ts-toast.ts-toast-confirm");return 0===t.length||t[t.length-1]===j},o=t=>{if(!e())return;if("Escape"===t.key&&_)return t.preventDefault(),void K(!1);if("Tab"!==t.key)return;const o=tsToastFocusable(j);if(!o.length)return void t.preventDefault();const s=o[0],n=o[o.length-1];j.contains(document.activeElement)?t.shiftKey&&document.activeElement===s?(t.preventDefault(),n.focus()):t.shiftKey||document.activeElement!==n||(t.preventDefault(),s.focus()):(t.preventDefault(),(t.shiftKey?n:s).focus())};document.addEventListener("keydown",o,!0),V=()=>{document.removeEventListener("keydown",o,!0),tsToastOpenModals=Math.max(0,tsToastOpenModals-1),0===tsToastOpenModals&&(document.body.style.overflow=tsToastPrevOverflow,document.body.style.paddingRight=tsToastPrevPaddingRight),t&&"function"==typeof t.focus&&document.contains(t)&&t.focus()},(I||j.querySelector(".ts-toast-btn.confirm")||j).focus()}D&&"function"==typeof D&&D(j),setTimeout((()=>{j.classList.add("ts-toast-show")}),100);const Z=a>0?Math.min(2e3,Math.max(0,a-500)):2e3;r&&J&&setTimeout((()=>{j._managedByLoading||(J.classList.add("done"),J.remove(),j.contains(X)||(M&&H?H.appendChild(X):j.appendChild(X)))}),Z),r||M||j.contains(X)||j.appendChild(X),j._onDismiss="function"==typeof P?P:null;let tt=a,et=null,ot=0;const st=()=>{et&&(clearTimeout(et),et=null)},nt=()=>{M||tt<=0||et||(ot=Date.now(),et=setTimeout((()=>{et=null,j._dismiss()}),tt),G&&(G.style.animationPlayState="running"))},at=()=>{et&&(clearTimeout(et),et=null,tt-=Date.now()-ot,G&&(G.style.animationPlayState="paused"))};if(j._dismiss=()=>{j._removing||(j._removing=!0,st(),R(j,(()=>{j._onDismiss&&j._onDismiss(j),U&&!U.children.length&&U.remove()})))},j._setDuration=t=>{st(),tt=t,nt()},nt(),!M&&S&&(j.addEventListener("mouseenter",at),j.addEventListener("mouseleave",nt),j.addEventListener("focusin",at),j.addEventListener("focusout",nt)),!M&&N&&j.addEventListener("click",(()=>{O&&"function"==typeof O&&O(j),j._dismiss()})),!M){let t=0,e=0,o=0;j.addEventListener("touchstart",(o=>{t=o.changedTouches[0].screenX,e=o.changedTouches[0].screenY}),{passive:!0}),j.addEventListener("touchend",(s=>{o=s.changedTouches[0].screenX;const n=Math.abs(t-o),a=Math.abs(e-s.changedTouches[0].screenY);n>50&&n>a&&j._dismiss()}))}return j.close=()=>{M?K(!1):j._dismiss()},j};toast.success=function(t,e){return toast(t,{...e,type:"success"})},toast.error=function(t,e){return toast(t,{...e,type:"error"})},toast.warning=function(t,e){return toast(t,{...e,type:"warning"})},toast.info=function(t,e){return toast(t,{...e,type:"info"})},toast.update=function(t,e,o={}){const{type:s=null,icon:n=null,showLoader:a=!1,duration:i=3e3,allowHtml:r=!0,onDismiss:l=null}=o,c=t.querySelector(".ts-toast-loader"),d=t.querySelector(".ts-toast-icon");c&&c.remove(),s&&(["success","error","info","warning"].forEach((e=>t.classList.remove(`ts-toast-${e}`))),t.classList.add("ts-toast",`ts-toast-${s}`,"ts-toast-show"));const u=t.querySelector(".ts-toast-body");u&&(r?u.innerHTML=e:u.textContent=e),d&&d.remove();const m=document.createElement("span");if(m.className="ts-toast-icon",m.style.display="flex",n)m.textContent=n;else{const t=document.createElement("img");t.alt="",t.setAttribute("aria-hidden","true"),t.style.width="30px",t.style.height="30px",t.style.objectFit="contain";const e={success:"success.gif",error:"error.gif",info:"info.gif",warning:"warning.gif"}[s];e&&(t.src=`${TS_TOAST_CDN}/assets/img/${e}`),m.appendChild(t)}if(n||m.querySelector("img[src]")){(t.querySelector(".ts-toast-content")||t).appendChild(m)}if(a){const e=document.createElement("div");e.className="ts-toast-loader",t.appendChild(e),setTimeout((()=>{e.classList.add("done")}),i>0?Math.min(2e3,Math.max(0,i-500)):2e3)}"function"==typeof l&&(t._onDismiss=l),"function"==typeof t._setDuration&&t._setDuration(i)},toast.loading=function(t,e={}){const o=toast(t,{...e,type:e.type||"info",duration:0,showLoader:!0,icon:null});requestAnimationFrame((()=>{o.classList.add("ts-toast-show")}));const s=o.querySelector(".ts-toast-loader");let n=o.querySelector(".ts-toast-icon");return n||(n=document.createElement("span"),n.className="ts-toast-icon",n.style.display="flex",o.appendChild(n)),o._managedByLoading=!0,s&&setTimeout((()=>{o._managedByLoading||s.classList.add("done")}),2e3),{update:(t,e={})=>{o._managedByLoading=!1,toast.update(o,t,{...e,showLoader:!1})},close:()=>{o._managedByLoading=!1,o.close()}}},toast.confirm=function(t,e={}){return new Promise((o=>{toast(t,{...e,mode:"confirm",duration:0,dismissOnClick:!1,onResult:t=>o(t)})}))},toast.promise=function(t,e={},o={}){const{loading:s="Loading…",success:n="Done",error:a="Something went wrong"}=e,i=toast.loading(s,o),r=(t,e)=>"function"==typeof t?t(e):t;return Promise.resolve(t).then((t=>(i.update(r(n,t),{type:"success",duration:o.duration}),t)),(t=>{throw i.update(r(a,t),{type:"error",duration:o.duration}),t}))},toast.defaults={},toast.dismissAll=function(){document.querySelectorAll(".ts-toast").forEach((t=>{"function"==typeof t.close&&t.close()}))},"undefined"!=typeof window&&(window.toast=toast),"undefined"!=typeof module&&module.exports&&(module.exports=toast);
package/toast.module.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // Single source of truth for the CDN this build points at.
4
4
  // `npm run sync:version` rewrites it from package.json, so it can never go stale.
5
- const TS_TOAST_VERSION = "5.5.0";
5
+ const TS_TOAST_VERSION = "5.6.1";
6
6
  // Point this at your own copy of assets/ to self-host the CSS and icons
7
7
  // (useful offline, behind a strict CSP, or when you don't want a CDN dependency):
8
8
  // window.TS_TOAST_ASSET_BASE = '/vendor/toastnotification';
@@ -14,6 +14,7 @@ const TS_TOAST_CDN = (typeof window !== 'undefined' && window.TS_TOAST_ASSET_BAS
14
14
  // group, so only the last dialog to close may release it.
15
15
  let tsToastOpenModals = 0;
16
16
  let tsToastPrevOverflow = '';
17
+ let tsToastPrevPaddingRight = '';
17
18
 
18
19
  const tsToastReducedMotion = () =>
19
20
  typeof window !== 'undefined' &&
@@ -52,6 +53,11 @@ let tsToastIdCounter = 0;
52
53
  .ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
53
54
  .ts-toast-container.center { top: 50%; left: 50%; transform: translate(-50%, -50%); align-items: center; }
54
55
  .ts-toast-overlay.center { align-items: center; justify-content: center; }
56
+ .ts-toast-overlay.ts-toast-overlay-stacked { background: transparent; }
57
+ @keyframes ts-toast-progress { from { transform: scaleX(1); } to { transform: scaleX(0); } }
58
+ .ts-toast .ts-toast-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; transform-origin: left center; border-radius: 0 0 8px 8px; background: currentColor; opacity: 0.35; pointer-events: none; }
59
+ .ts-toast .ts-toast-action { order: -1; flex: none; appearance: none; border: 0; background: transparent; color: #3b82f6; font: inherit; font-weight: 600; padding: 4px 8px; margin-left: 4px; border-radius: 6px; cursor: pointer; }
60
+ .ts-toast .ts-toast-action:hover { background: rgba(59,130,246,0.12); }
55
61
  .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; }
56
62
  .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; }
57
63
  .ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
@@ -75,6 +81,9 @@ let tsToastIdCounter = 0;
75
81
  })();
76
82
 
77
83
  const toast = function (message, options = {}) {
84
+ // Site-wide defaults, so a page can turn on things like showProgress once
85
+ // instead of repeating them at every call site.
86
+ options = { ...(toast.defaults || {}), ...options };
78
87
  const {
79
88
  position = 'top-right',
80
89
  animation = 'slide-right', // Default fallback animation
@@ -103,6 +112,12 @@ const toast = function (message, options = {}) {
103
112
  useOverlay = true,
104
113
  closeOnOverlayClick = true,
105
114
  showClose = false,
115
+ // Freeze the countdown while the pointer or keyboard focus is on the toast
116
+ pauseOnHover = true,
117
+ // Thin bar counting the remaining time down
118
+ showProgress = false,
119
+ // { text, onClick } renders a button inside the toast, e.g. Undo
120
+ action = null,
106
121
  // Escape cancels a confirm dialog
107
122
  closeOnEscape = true,
108
123
  // `message` is written as HTML for backwards compatibility. Pass false to
@@ -329,6 +344,31 @@ const toast = function (message, options = {}) {
329
344
  confirmBtn.addEventListener('click', (e) => { e.stopPropagation(); resolveAndClose(true); });
330
345
  }
331
346
 
347
+ // Action button (e.g. Undo). Clicking it runs the callback and closes the toast.
348
+ if (!isConfirm && action && typeof action === 'object' && action.text) {
349
+ const actionBtn = document.createElement('button');
350
+ actionBtn.className = 'ts-toast-action';
351
+ actionBtn.type = 'button';
352
+ actionBtn.textContent = action.text;
353
+ actionBtn.addEventListener('click', (e) => {
354
+ e.stopPropagation(); // do not also trigger dismissOnClick
355
+ if (typeof action.onClick === 'function') action.onClick(toastElement);
356
+ toastElement._dismiss();
357
+ });
358
+ toastElement.appendChild(actionBtn);
359
+ }
360
+
361
+ // Progress bar. The width is driven by a CSS animation whose duration is the
362
+ // toast's own, so pausing it is one property and never drifts from the timer.
363
+ let progressBar = null;
364
+ if (!isConfirm && showProgress && duration > 0 && !reducedMotion) {
365
+ progressBar = document.createElement('div');
366
+ progressBar.className = 'ts-toast-progress';
367
+ progressBar.style.animation = `ts-toast-progress ${duration}ms linear forwards`;
368
+ progressBar.style.animationPlayState = 'paused';
369
+ toastElement.appendChild(progressBar);
370
+ }
371
+
332
372
  // Loader Element
333
373
  let loader = null;
334
374
  if (showLoader) {
@@ -339,11 +379,15 @@ const toast = function (message, options = {}) {
339
379
 
340
380
  // Container/Overlay
341
381
  let overlay = null;
382
+ let containerEl = null;
342
383
  if (isConfirm && useOverlay) {
343
384
  overlay = document.createElement('div');
344
385
  // A modal centres by default. The `position` default of 'top-right' is meant
345
386
  // for toasts; applying it here parked the dialog in a corner of the backdrop.
346
- overlay.className = 'ts-toast-overlay' + (options.position ? ` ${position}` : '');
387
+ // Each backdrop paints its own 50% black, so a second dialog opening over a
388
+ // first turned the page almost fully dark. Only the bottom one dims.
389
+ const stacked = tsToastOpenModals > 0 ? ' ts-toast-overlay-stacked' : '';
390
+ overlay.className = 'ts-toast-overlay' + stacked + (options.position ? ` ${position}` : '');
347
391
  document.body.appendChild(overlay);
348
392
  overlay.appendChild(toastElement);
349
393
  if (showClose) {
@@ -387,6 +431,7 @@ const toast = function (message, options = {}) {
387
431
  container.setAttribute('aria-relevant', 'additions');
388
432
  }
389
433
  container.appendChild(toastElement);
434
+ containerEl = container;
390
435
  }
391
436
 
392
437
  if (isConfirm) {
@@ -399,6 +444,15 @@ const toast = function (message, options = {}) {
399
444
  tsToastOpenModals += 1;
400
445
  if (tsToastOpenModals === 1) {
401
446
  tsToastPrevOverflow = document.body.style.overflow;
447
+ tsToastPrevPaddingRight = document.body.style.paddingRight;
448
+ // Hiding the scrollbar makes the page wider, which shifts the whole
449
+ // layout sideways as the dialog opens. Pad by the scrollbar's width to
450
+ // hold it still. A no-op where scrollbars are overlays, as on macOS.
451
+ const scrollbar = window.innerWidth - document.documentElement.clientWidth;
452
+ if (scrollbar > 0) {
453
+ const current = parseFloat(window.getComputedStyle(document.body).paddingRight) || 0;
454
+ document.body.style.paddingRight = `${current + scrollbar}px`;
455
+ }
402
456
  document.body.style.overflow = 'hidden';
403
457
  }
404
458
 
@@ -441,7 +495,10 @@ const toast = function (message, options = {}) {
441
495
  releaseModal = () => {
442
496
  document.removeEventListener('keydown', onKeydown, true);
443
497
  tsToastOpenModals = Math.max(0, tsToastOpenModals - 1);
444
- if (tsToastOpenModals === 0) document.body.style.overflow = tsToastPrevOverflow;
498
+ if (tsToastOpenModals === 0) {
499
+ document.body.style.overflow = tsToastPrevOverflow;
500
+ document.body.style.paddingRight = tsToastPrevPaddingRight;
501
+ }
445
502
  // Hand the keyboard back to whatever opened the dialog.
446
503
  if (previouslyFocused && typeof previouslyFocused.focus === 'function' &&
447
504
  document.contains(previouslyFocused)) {
@@ -463,6 +520,10 @@ const toast = function (message, options = {}) {
463
520
  toastElement.classList.add('ts-toast-show');
464
521
  }, 100);
465
522
 
523
+ // The loader always ran for 2s, so with a shorter duration the toast was gone
524
+ // before the icon it reveals ever appeared.
525
+ const loaderMs = duration > 0 ? Math.min(2000, Math.max(0, duration - 500)) : 2000;
526
+
466
527
  // Handle Loader and Icon
467
528
  if (showLoader && loader) {
468
529
  setTimeout(() => {
@@ -474,7 +535,7 @@ const toast = function (message, options = {}) {
474
535
  if (isConfirm && contentRow) contentRow.appendChild(iconElement);
475
536
  else toastElement.appendChild(iconElement); // Add icon only if not present
476
537
  }
477
- }, 2000); // Simulate a loading period of 2 seconds
538
+ }, loaderMs);
478
539
  }
479
540
  if (!showLoader) {
480
541
  // For confirm, icon already added above inside contentRow; avoid moving it
@@ -483,26 +544,70 @@ const toast = function (message, options = {}) {
483
544
  }
484
545
  }
485
546
 
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;
547
+ // Dismissal state lives on the element so toast.update() can rebind the callback
548
+ // and reuse this exact removal path instead of keeping its own copy of it.
549
+ toastElement._onDismiss = typeof onDismiss === 'function' ? onDismiss : null;
550
+
551
+ // A plain setTimeout cannot be paused, so the countdown is tracked by hand:
552
+ // `remaining` is what is left, and hovering banks it.
553
+ let remaining = duration;
554
+ let timerId = null;
555
+ let startedAt = 0;
556
+
557
+ const stopTimer = () => {
558
+ if (timerId) { clearTimeout(timerId); timerId = null; }
559
+ };
560
+
561
+ const startTimer = () => {
562
+ if (isConfirm || remaining <= 0 || timerId) return;
563
+ startedAt = Date.now();
564
+ timerId = setTimeout(() => { timerId = null; toastElement._dismiss(); }, remaining);
565
+ if (progressBar) progressBar.style.animationPlayState = 'running';
566
+ };
567
+
568
+ const pauseTimer = () => {
569
+ if (!timerId) return;
570
+ clearTimeout(timerId);
571
+ timerId = null;
572
+ remaining -= Date.now() - startedAt;
573
+ if (progressBar) progressBar.style.animationPlayState = 'paused';
574
+ };
575
+
576
+ toastElement._dismiss = () => {
577
+ if (toastElement._removing) return; // never run the exit twice
578
+ toastElement._removing = true;
579
+ stopTimer();
580
+ removeWithAnimation(toastElement, () => {
581
+ if (toastElement._onDismiss) toastElement._onDismiss(toastElement);
582
+ // Containers used to pile up in the DOM, one per position, forever.
583
+ if (containerEl && !containerEl.children.length) containerEl.remove();
584
+ });
585
+ };
586
+
587
+ // Lets toast.update() re-arm the countdown without reaching into internals.
588
+ toastElement._setDuration = (ms) => {
589
+ stopTimer();
590
+ remaining = ms;
591
+ startTimer();
592
+ };
593
+
594
+ startTimer();
595
+
596
+ if (!isConfirm && pauseOnHover) {
597
+ // Give the reader a chance to finish the sentence.
598
+ toastElement.addEventListener('mouseenter', pauseTimer);
599
+ toastElement.addEventListener('mouseleave', startTimer);
600
+ toastElement.addEventListener('focusin', pauseTimer);
601
+ toastElement.addEventListener('focusout', startTimer);
494
602
  }
495
603
 
496
604
  // Add event listener for closing the toast when clicked (disabled in confirm mode)
497
605
  if (!isConfirm && dismissOnClick) {
498
606
  toastElement.addEventListener('click', () => {
499
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove); // Clear the auto-remove timeout
500
607
  // onClick belongs to the click, not to the end of the exit animation,
501
608
  // which is where it used to fire half a second late.
502
609
  if (onClick && typeof onClick === 'function') onClick(toastElement);
503
- removeWithAnimation(toastElement, () => {
504
- if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
505
- });
610
+ toastElement._dismiss();
506
611
  });
507
612
  }
508
613
 
@@ -525,23 +630,15 @@ const toast = function (message, options = {}) {
525
630
  const dy = Math.abs(touchStartY - e.changedTouches[0].screenY);
526
631
  // Only a mostly-horizontal swipe dismisses, so scrolling the page past a
527
632
  // 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
- }
633
+ if (dx > 50 && dx > dy) toastElement._dismiss();
534
634
  });
535
635
  }
536
636
 
537
637
  // Let callers dismiss a toast they are holding, instead of only waiting out
538
638
  // the duration or making the user click it.
539
639
  toastElement.close = () => {
540
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
541
640
  if (isConfirm) { resolveAndClose(false); return; }
542
- removeWithAnimation(toastElement, () => {
543
- if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
544
- });
641
+ toastElement._dismiss();
545
642
  };
546
643
 
547
644
  return toastElement;
@@ -570,10 +667,9 @@ const toast = function (message, options = {}) {
570
667
  type = null,
571
668
  icon = null,
572
669
  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
670
+ duration = 3000, // Default duration (in ms); 0 keeps the toast on screen
671
+ allowHtml = true,
672
+ onDismiss = null // Replaces the callback the toast was created with
577
673
  } = options;
578
674
 
579
675
  // Remove old loader (if any)
@@ -591,7 +687,8 @@ const toast = function (message, options = {}) {
591
687
  }
592
688
  const toastBody = toastElement.querySelector('.ts-toast-body');
593
689
  if (toastBody) {
594
- toastBody.innerHTML = message;
690
+ if (allowHtml) toastBody.innerHTML = message;
691
+ else toastBody.textContent = message;
595
692
  }
596
693
 
597
694
  // Handle Icon update only if it's new or hasn't been set yet
@@ -619,9 +716,12 @@ const toast = function (message, options = {}) {
619
716
  iconElement.appendChild(img);
620
717
  }
621
718
 
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);
719
+ // Only attach an icon we actually have. Without a type and without an explicit
720
+ // icon this used to append an empty <span><img></span>.
721
+ if (icon || iconElement.querySelector('img[src]')) {
722
+ const contentRow = toastElement.querySelector('.ts-toast-content');
723
+ (contentRow || toastElement).appendChild(iconElement);
724
+ }
625
725
 
626
726
  // Handle loader if requested
627
727
  if (showLoader) {
@@ -630,33 +730,19 @@ const toast = function (message, options = {}) {
630
730
  toastElement.appendChild(loader);
631
731
  setTimeout(() => {
632
732
  loader.classList.add('done');
633
- }, 2000); // Simulate loader completion after 2 seconds
733
+ }, duration > 0 ? Math.min(2000, Math.max(0, duration - 500)) : 2000);
634
734
  }
635
735
 
636
- // Clear previous auto-remove timer if needed
637
- if (toastElement._autoRemove) {
638
- clearTimeout(toastElement._autoRemove);
639
- }
736
+ // Rebind the dismiss callback rather than leaving the creation-time one in place,
737
+ // which meant an updated toast fired two different onDismiss handlers.
738
+ if (typeof onDismiss === 'function') toastElement._onDismiss = onDismiss;
640
739
 
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
740
+ // Re-arm the countdown through the toast's own timer, so duration: 0 keeps the
741
+ // toast on screen. This used to schedule setTimeout(..., 0) and remove it at once,
742
+ // which broke every `toast.loading(...).update(msg, { duration: 0 })`.
743
+ if (typeof toastElement._setDuration === 'function') {
744
+ toastElement._setDuration(duration);
745
+ }
660
746
  };
661
747
 
662
748
  toast.loading = function (message, options = {}) {
@@ -730,6 +816,34 @@ const toast = function (message, options = {}) {
730
816
  });
731
817
  };
732
818
 
819
+ // Wraps an async action: one loading toast that becomes the success or error
820
+ // message, instead of hand-rolling loading/update/catch at every call site.
821
+ toast.promise = function (promise, messages = {}, options = {}) {
822
+ const {
823
+ loading = 'Loading…',
824
+ success = 'Done',
825
+ error = 'Something went wrong'
826
+ } = messages;
827
+
828
+ const handle = toast.loading(loading, options);
829
+ // Messages may be functions so they can name what actually came back.
830
+ const text = (msg, value) => (typeof msg === 'function' ? msg(value) : msg);
831
+
832
+ return Promise.resolve(promise).then(
833
+ (value) => {
834
+ handle.update(text(success, value), { type: 'success', duration: options.duration });
835
+ return value;
836
+ },
837
+ (err) => {
838
+ handle.update(text(error, err), { type: 'error', duration: options.duration });
839
+ throw err; // the caller still owns the failure
840
+ }
841
+ );
842
+ };
843
+
844
+ // Options applied to every toast unless the call overrides them.
845
+ toast.defaults = {};
846
+
733
847
  // Close every toast currently on screen. Confirm dialogs settle as a cancel.
734
848
  toast.dismissAll = function () {
735
849
  document.querySelectorAll('.ts-toast').forEach((el) => {