@tsirosgeorge/toastnotification 5.5.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md 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. |
@@ -351,4 +351,50 @@
351
351
  /* Prevent scroll behind confirm overlay */
352
352
  body.ts-toast-no-scroll {
353
353
  overflow: hidden;
354
- }
354
+ }
355
+
356
+ /* Progress bar counting down the remaining time */
357
+ @keyframes ts-toast-progress {
358
+ from { transform: scaleX(1); }
359
+ to { transform: scaleX(0); }
360
+ }
361
+
362
+ .ts-toast-container .ts-toast-progress {
363
+ position: absolute;
364
+ left: 0;
365
+ right: 0;
366
+ bottom: 0;
367
+ height: 3px;
368
+ transform-origin: left center;
369
+ border-radius: 0 0 8px 8px;
370
+ background: var(--toast-progress-color, currentColor);
371
+ opacity: 0.35;
372
+ pointer-events: none;
373
+ }
374
+
375
+ .ts-toast-container .ts-toast-success .ts-toast-progress { background: var(--toast-success-color, #17a35c); }
376
+ .ts-toast-container .ts-toast-error .ts-toast-progress { background: var(--toast-error-color, #ef4444); }
377
+ .ts-toast-container .ts-toast-warning .ts-toast-progress { background: var(--toast-warning-color, #f0a020); }
378
+ .ts-toast-container .ts-toast-info .ts-toast-progress { background: var(--toast-info-color, #3b82f6); }
379
+
380
+ /* Action button inside a toast (e.g. Undo) */
381
+ .ts-toast-container .ts-toast-action {
382
+ /* Toasts lay out row-reverse, so the lowest order sits at the right-hand end:
383
+ icon, message, then the action button. */
384
+ order: -1;
385
+ flex: none;
386
+ appearance: none;
387
+ border: 0;
388
+ background: transparent;
389
+ color: var(--toast-action-color, #3b82f6);
390
+ font: inherit;
391
+ font-weight: 600;
392
+ padding: 4px 8px;
393
+ margin-left: 4px;
394
+ border-radius: 6px;
395
+ cursor: pointer;
396
+ }
397
+
398
+ .ts-toast-container .ts-toast-action:hover {
399
+ background: var(--toast-action-hover, rgba(59, 130, 246, 0.12));
400
+ }
@@ -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-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.0",
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.0";
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,10 @@ 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
+ @keyframes ts-toast-progress { from { transform: scaleX(1); } to { transform: scaleX(0); } }
57
+ .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; }
58
+ .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; }
59
+ .ts-toast .ts-toast-action:hover { background: rgba(59,130,246,0.12); }
55
60
  .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
61
  .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
62
  .ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
@@ -75,6 +80,9 @@ let tsToastIdCounter = 0;
75
80
  })();
76
81
 
77
82
  const toast = function (message, options = {}) {
83
+ // Site-wide defaults, so a page can turn on things like showProgress once
84
+ // instead of repeating them at every call site.
85
+ options = { ...(toast.defaults || {}), ...options };
78
86
  const {
79
87
  position = 'top-right',
80
88
  animation = 'slide-right', // Default fallback animation
@@ -103,6 +111,12 @@ const toast = function (message, options = {}) {
103
111
  useOverlay = true,
104
112
  closeOnOverlayClick = true,
105
113
  showClose = false,
114
+ // Freeze the countdown while the pointer or keyboard focus is on the toast
115
+ pauseOnHover = true,
116
+ // Thin bar counting the remaining time down
117
+ showProgress = false,
118
+ // { text, onClick } renders a button inside the toast, e.g. Undo
119
+ action = null,
106
120
  // Escape cancels a confirm dialog
107
121
  closeOnEscape = true,
108
122
  // `message` is written as HTML for backwards compatibility. Pass false to
@@ -329,6 +343,31 @@ const toast = function (message, options = {}) {
329
343
  confirmBtn.addEventListener('click', (e) => { e.stopPropagation(); resolveAndClose(true); });
330
344
  }
331
345
 
346
+ // Action button (e.g. Undo). Clicking it runs the callback and closes the toast.
347
+ if (!isConfirm && action && typeof action === 'object' && action.text) {
348
+ const actionBtn = document.createElement('button');
349
+ actionBtn.className = 'ts-toast-action';
350
+ actionBtn.type = 'button';
351
+ actionBtn.textContent = action.text;
352
+ actionBtn.addEventListener('click', (e) => {
353
+ e.stopPropagation(); // do not also trigger dismissOnClick
354
+ if (typeof action.onClick === 'function') action.onClick(toastElement);
355
+ toastElement._dismiss();
356
+ });
357
+ toastElement.appendChild(actionBtn);
358
+ }
359
+
360
+ // Progress bar. The width is driven by a CSS animation whose duration is the
361
+ // toast's own, so pausing it is one property and never drifts from the timer.
362
+ let progressBar = null;
363
+ if (!isConfirm && showProgress && duration > 0 && !reducedMotion) {
364
+ progressBar = document.createElement('div');
365
+ progressBar.className = 'ts-toast-progress';
366
+ progressBar.style.animation = `ts-toast-progress ${duration}ms linear forwards`;
367
+ progressBar.style.animationPlayState = 'paused';
368
+ toastElement.appendChild(progressBar);
369
+ }
370
+
332
371
  // Loader Element
333
372
  let loader = null;
334
373
  if (showLoader) {
@@ -339,6 +378,7 @@ const toast = function (message, options = {}) {
339
378
 
340
379
  // Container/Overlay
341
380
  let overlay = null;
381
+ let containerEl = null;
342
382
  if (isConfirm && useOverlay) {
343
383
  overlay = document.createElement('div');
344
384
  // A modal centres by default. The `position` default of 'top-right' is meant
@@ -387,6 +427,7 @@ const toast = function (message, options = {}) {
387
427
  container.setAttribute('aria-relevant', 'additions');
388
428
  }
389
429
  container.appendChild(toastElement);
430
+ containerEl = container;
390
431
  }
391
432
 
392
433
  if (isConfirm) {
@@ -399,6 +440,15 @@ const toast = function (message, options = {}) {
399
440
  tsToastOpenModals += 1;
400
441
  if (tsToastOpenModals === 1) {
401
442
  tsToastPrevOverflow = document.body.style.overflow;
443
+ tsToastPrevPaddingRight = document.body.style.paddingRight;
444
+ // Hiding the scrollbar makes the page wider, which shifts the whole
445
+ // layout sideways as the dialog opens. Pad by the scrollbar's width to
446
+ // hold it still. A no-op where scrollbars are overlays, as on macOS.
447
+ const scrollbar = window.innerWidth - document.documentElement.clientWidth;
448
+ if (scrollbar > 0) {
449
+ const current = parseFloat(window.getComputedStyle(document.body).paddingRight) || 0;
450
+ document.body.style.paddingRight = `${current + scrollbar}px`;
451
+ }
402
452
  document.body.style.overflow = 'hidden';
403
453
  }
404
454
 
@@ -441,7 +491,10 @@ const toast = function (message, options = {}) {
441
491
  releaseModal = () => {
442
492
  document.removeEventListener('keydown', onKeydown, true);
443
493
  tsToastOpenModals = Math.max(0, tsToastOpenModals - 1);
444
- if (tsToastOpenModals === 0) document.body.style.overflow = tsToastPrevOverflow;
494
+ if (tsToastOpenModals === 0) {
495
+ document.body.style.overflow = tsToastPrevOverflow;
496
+ document.body.style.paddingRight = tsToastPrevPaddingRight;
497
+ }
445
498
  // Hand the keyboard back to whatever opened the dialog.
446
499
  if (previouslyFocused && typeof previouslyFocused.focus === 'function' &&
447
500
  document.contains(previouslyFocused)) {
@@ -463,6 +516,10 @@ const toast = function (message, options = {}) {
463
516
  toastElement.classList.add('ts-toast-show');
464
517
  }, 100);
465
518
 
519
+ // The loader always ran for 2s, so with a shorter duration the toast was gone
520
+ // before the icon it reveals ever appeared.
521
+ const loaderMs = duration > 0 ? Math.min(2000, Math.max(0, duration - 500)) : 2000;
522
+
466
523
  // Handle Loader and Icon
467
524
  if (showLoader && loader) {
468
525
  setTimeout(() => {
@@ -474,7 +531,7 @@ const toast = function (message, options = {}) {
474
531
  if (isConfirm && contentRow) contentRow.appendChild(iconElement);
475
532
  else toastElement.appendChild(iconElement); // Add icon only if not present
476
533
  }
477
- }, 2000); // Simulate a loading period of 2 seconds
534
+ }, loaderMs);
478
535
  }
479
536
  if (!showLoader) {
480
537
  // For confirm, icon already added above inside contentRow; avoid moving it
@@ -483,26 +540,70 @@ const toast = function (message, options = {}) {
483
540
  }
484
541
  }
485
542
 
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;
543
+ // Dismissal state lives on the element so toast.update() can rebind the callback
544
+ // and reuse this exact removal path instead of keeping its own copy of it.
545
+ toastElement._onDismiss = typeof onDismiss === 'function' ? onDismiss : null;
546
+
547
+ // A plain setTimeout cannot be paused, so the countdown is tracked by hand:
548
+ // `remaining` is what is left, and hovering banks it.
549
+ let remaining = duration;
550
+ let timerId = null;
551
+ let startedAt = 0;
552
+
553
+ const stopTimer = () => {
554
+ if (timerId) { clearTimeout(timerId); timerId = null; }
555
+ };
556
+
557
+ const startTimer = () => {
558
+ if (isConfirm || remaining <= 0 || timerId) return;
559
+ startedAt = Date.now();
560
+ timerId = setTimeout(() => { timerId = null; toastElement._dismiss(); }, remaining);
561
+ if (progressBar) progressBar.style.animationPlayState = 'running';
562
+ };
563
+
564
+ const pauseTimer = () => {
565
+ if (!timerId) return;
566
+ clearTimeout(timerId);
567
+ timerId = null;
568
+ remaining -= Date.now() - startedAt;
569
+ if (progressBar) progressBar.style.animationPlayState = 'paused';
570
+ };
571
+
572
+ toastElement._dismiss = () => {
573
+ if (toastElement._removing) return; // never run the exit twice
574
+ toastElement._removing = true;
575
+ stopTimer();
576
+ removeWithAnimation(toastElement, () => {
577
+ if (toastElement._onDismiss) toastElement._onDismiss(toastElement);
578
+ // Containers used to pile up in the DOM, one per position, forever.
579
+ if (containerEl && !containerEl.children.length) containerEl.remove();
580
+ });
581
+ };
582
+
583
+ // Lets toast.update() re-arm the countdown without reaching into internals.
584
+ toastElement._setDuration = (ms) => {
585
+ stopTimer();
586
+ remaining = ms;
587
+ startTimer();
588
+ };
589
+
590
+ startTimer();
591
+
592
+ if (!isConfirm && pauseOnHover) {
593
+ // Give the reader a chance to finish the sentence.
594
+ toastElement.addEventListener('mouseenter', pauseTimer);
595
+ toastElement.addEventListener('mouseleave', startTimer);
596
+ toastElement.addEventListener('focusin', pauseTimer);
597
+ toastElement.addEventListener('focusout', startTimer);
494
598
  }
495
599
 
496
600
  // Add event listener for closing the toast when clicked (disabled in confirm mode)
497
601
  if (!isConfirm && dismissOnClick) {
498
602
  toastElement.addEventListener('click', () => {
499
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove); // Clear the auto-remove timeout
500
603
  // onClick belongs to the click, not to the end of the exit animation,
501
604
  // which is where it used to fire half a second late.
502
605
  if (onClick && typeof onClick === 'function') onClick(toastElement);
503
- removeWithAnimation(toastElement, () => {
504
- if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
505
- });
606
+ toastElement._dismiss();
506
607
  });
507
608
  }
508
609
 
@@ -525,23 +626,15 @@ const toast = function (message, options = {}) {
525
626
  const dy = Math.abs(touchStartY - e.changedTouches[0].screenY);
526
627
  // Only a mostly-horizontal swipe dismisses, so scrolling the page past a
527
628
  // 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
- }
629
+ if (dx > 50 && dx > dy) toastElement._dismiss();
534
630
  });
535
631
  }
536
632
 
537
633
  // Let callers dismiss a toast they are holding, instead of only waiting out
538
634
  // the duration or making the user click it.
539
635
  toastElement.close = () => {
540
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
541
636
  if (isConfirm) { resolveAndClose(false); return; }
542
- removeWithAnimation(toastElement, () => {
543
- if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
544
- });
637
+ toastElement._dismiss();
545
638
  };
546
639
 
547
640
  return toastElement;
@@ -570,10 +663,9 @@ const toast = function (message, options = {}) {
570
663
  type = null,
571
664
  icon = null,
572
665
  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
666
+ duration = 3000, // Default duration (in ms); 0 keeps the toast on screen
667
+ allowHtml = true,
668
+ onDismiss = null // Replaces the callback the toast was created with
577
669
  } = options;
578
670
 
579
671
  // Remove old loader (if any)
@@ -591,7 +683,8 @@ const toast = function (message, options = {}) {
591
683
  }
592
684
  const toastBody = toastElement.querySelector('.ts-toast-body');
593
685
  if (toastBody) {
594
- toastBody.innerHTML = message;
686
+ if (allowHtml) toastBody.innerHTML = message;
687
+ else toastBody.textContent = message;
595
688
  }
596
689
 
597
690
  // Handle Icon update only if it's new or hasn't been set yet
@@ -619,9 +712,12 @@ const toast = function (message, options = {}) {
619
712
  iconElement.appendChild(img);
620
713
  }
621
714
 
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);
715
+ // Only attach an icon we actually have. Without a type and without an explicit
716
+ // icon this used to append an empty <span><img></span>.
717
+ if (icon || iconElement.querySelector('img[src]')) {
718
+ const contentRow = toastElement.querySelector('.ts-toast-content');
719
+ (contentRow || toastElement).appendChild(iconElement);
720
+ }
625
721
 
626
722
  // Handle loader if requested
627
723
  if (showLoader) {
@@ -630,33 +726,19 @@ const toast = function (message, options = {}) {
630
726
  toastElement.appendChild(loader);
631
727
  setTimeout(() => {
632
728
  loader.classList.add('done');
633
- }, 2000); // Simulate loader completion after 2 seconds
729
+ }, duration > 0 ? Math.min(2000, Math.max(0, duration - 500)) : 2000);
634
730
  }
635
731
 
636
- // Clear previous auto-remove timer if needed
637
- if (toastElement._autoRemove) {
638
- clearTimeout(toastElement._autoRemove);
639
- }
732
+ // Rebind the dismiss callback rather than leaving the creation-time one in place,
733
+ // which meant an updated toast fired two different onDismiss handlers.
734
+ if (typeof onDismiss === 'function') toastElement._onDismiss = onDismiss;
640
735
 
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
736
+ // Re-arm the countdown through the toast's own timer, so duration: 0 keeps the
737
+ // toast on screen. This used to schedule setTimeout(..., 0) and remove it at once,
738
+ // which broke every `toast.loading(...).update(msg, { duration: 0 })`.
739
+ if (typeof toastElement._setDuration === 'function') {
740
+ toastElement._setDuration(duration);
741
+ }
660
742
  };
661
743
 
662
744
  toast.loading = function (message, options = {}) {
@@ -730,6 +812,34 @@ const toast = function (message, options = {}) {
730
812
  });
731
813
  };
732
814
 
815
+ // Wraps an async action: one loading toast that becomes the success or error
816
+ // message, instead of hand-rolling loading/update/catch at every call site.
817
+ toast.promise = function (promise, messages = {}, options = {}) {
818
+ const {
819
+ loading = 'Loading…',
820
+ success = 'Done',
821
+ error = 'Something went wrong'
822
+ } = messages;
823
+
824
+ const handle = toast.loading(loading, options);
825
+ // Messages may be functions so they can name what actually came back.
826
+ const text = (msg, value) => (typeof msg === 'function' ? msg(value) : msg);
827
+
828
+ return Promise.resolve(promise).then(
829
+ (value) => {
830
+ handle.update(text(success, value), { type: 'success', duration: options.duration });
831
+ return value;
832
+ },
833
+ (err) => {
834
+ handle.update(text(error, err), { type: 'error', duration: options.duration });
835
+ throw err; // the caller still owns the failure
836
+ }
837
+ );
838
+ };
839
+
840
+ // Options applied to every toast unless the call overrides them.
841
+ toast.defaults = {};
842
+
733
843
  // Close every toast currently on screen. Confirm dialogs settle as a cancel.
734
844
  toast.dismissAll = function () {
735
845
  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.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.6.0";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 @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:h=null,cancelButtonBg:y=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:L=!1,action:_=null,closeOnEscape:k=!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,y&&(t.style.background=y),b&&(t.style.color=b),g&&(e.style.background=g),h&&(e.style.color=h),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&&_&&"object"==typeof _&&_.text){const t=document.createElement("button");t.className="ts-toast-action",t.type="button",t.textContent=_.text,t.addEventListener("click",(t=>{t.stopPropagation(),"function"==typeof _.onClick&&_.onClick(j),j._dismiss()})),j.appendChild(t)}let G=null;!M&&L&&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){if(Q=document.createElement("div"),Q.className="ts-toast-overlay"+(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&&k)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.0";
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,10 @@ 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
+ @keyframes ts-toast-progress { from { transform: scaleX(1); } to { transform: scaleX(0); } }
57
+ .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; }
58
+ .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; }
59
+ .ts-toast .ts-toast-action:hover { background: rgba(59,130,246,0.12); }
55
60
  .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
61
  .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
62
  .ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
@@ -75,6 +80,9 @@ let tsToastIdCounter = 0;
75
80
  })();
76
81
 
77
82
  const toast = function (message, options = {}) {
83
+ // Site-wide defaults, so a page can turn on things like showProgress once
84
+ // instead of repeating them at every call site.
85
+ options = { ...(toast.defaults || {}), ...options };
78
86
  const {
79
87
  position = 'top-right',
80
88
  animation = 'slide-right', // Default fallback animation
@@ -103,6 +111,12 @@ const toast = function (message, options = {}) {
103
111
  useOverlay = true,
104
112
  closeOnOverlayClick = true,
105
113
  showClose = false,
114
+ // Freeze the countdown while the pointer or keyboard focus is on the toast
115
+ pauseOnHover = true,
116
+ // Thin bar counting the remaining time down
117
+ showProgress = false,
118
+ // { text, onClick } renders a button inside the toast, e.g. Undo
119
+ action = null,
106
120
  // Escape cancels a confirm dialog
107
121
  closeOnEscape = true,
108
122
  // `message` is written as HTML for backwards compatibility. Pass false to
@@ -329,6 +343,31 @@ const toast = function (message, options = {}) {
329
343
  confirmBtn.addEventListener('click', (e) => { e.stopPropagation(); resolveAndClose(true); });
330
344
  }
331
345
 
346
+ // Action button (e.g. Undo). Clicking it runs the callback and closes the toast.
347
+ if (!isConfirm && action && typeof action === 'object' && action.text) {
348
+ const actionBtn = document.createElement('button');
349
+ actionBtn.className = 'ts-toast-action';
350
+ actionBtn.type = 'button';
351
+ actionBtn.textContent = action.text;
352
+ actionBtn.addEventListener('click', (e) => {
353
+ e.stopPropagation(); // do not also trigger dismissOnClick
354
+ if (typeof action.onClick === 'function') action.onClick(toastElement);
355
+ toastElement._dismiss();
356
+ });
357
+ toastElement.appendChild(actionBtn);
358
+ }
359
+
360
+ // Progress bar. The width is driven by a CSS animation whose duration is the
361
+ // toast's own, so pausing it is one property and never drifts from the timer.
362
+ let progressBar = null;
363
+ if (!isConfirm && showProgress && duration > 0 && !reducedMotion) {
364
+ progressBar = document.createElement('div');
365
+ progressBar.className = 'ts-toast-progress';
366
+ progressBar.style.animation = `ts-toast-progress ${duration}ms linear forwards`;
367
+ progressBar.style.animationPlayState = 'paused';
368
+ toastElement.appendChild(progressBar);
369
+ }
370
+
332
371
  // Loader Element
333
372
  let loader = null;
334
373
  if (showLoader) {
@@ -339,6 +378,7 @@ const toast = function (message, options = {}) {
339
378
 
340
379
  // Container/Overlay
341
380
  let overlay = null;
381
+ let containerEl = null;
342
382
  if (isConfirm && useOverlay) {
343
383
  overlay = document.createElement('div');
344
384
  // A modal centres by default. The `position` default of 'top-right' is meant
@@ -387,6 +427,7 @@ const toast = function (message, options = {}) {
387
427
  container.setAttribute('aria-relevant', 'additions');
388
428
  }
389
429
  container.appendChild(toastElement);
430
+ containerEl = container;
390
431
  }
391
432
 
392
433
  if (isConfirm) {
@@ -399,6 +440,15 @@ const toast = function (message, options = {}) {
399
440
  tsToastOpenModals += 1;
400
441
  if (tsToastOpenModals === 1) {
401
442
  tsToastPrevOverflow = document.body.style.overflow;
443
+ tsToastPrevPaddingRight = document.body.style.paddingRight;
444
+ // Hiding the scrollbar makes the page wider, which shifts the whole
445
+ // layout sideways as the dialog opens. Pad by the scrollbar's width to
446
+ // hold it still. A no-op where scrollbars are overlays, as on macOS.
447
+ const scrollbar = window.innerWidth - document.documentElement.clientWidth;
448
+ if (scrollbar > 0) {
449
+ const current = parseFloat(window.getComputedStyle(document.body).paddingRight) || 0;
450
+ document.body.style.paddingRight = `${current + scrollbar}px`;
451
+ }
402
452
  document.body.style.overflow = 'hidden';
403
453
  }
404
454
 
@@ -441,7 +491,10 @@ const toast = function (message, options = {}) {
441
491
  releaseModal = () => {
442
492
  document.removeEventListener('keydown', onKeydown, true);
443
493
  tsToastOpenModals = Math.max(0, tsToastOpenModals - 1);
444
- if (tsToastOpenModals === 0) document.body.style.overflow = tsToastPrevOverflow;
494
+ if (tsToastOpenModals === 0) {
495
+ document.body.style.overflow = tsToastPrevOverflow;
496
+ document.body.style.paddingRight = tsToastPrevPaddingRight;
497
+ }
445
498
  // Hand the keyboard back to whatever opened the dialog.
446
499
  if (previouslyFocused && typeof previouslyFocused.focus === 'function' &&
447
500
  document.contains(previouslyFocused)) {
@@ -463,6 +516,10 @@ const toast = function (message, options = {}) {
463
516
  toastElement.classList.add('ts-toast-show');
464
517
  }, 100);
465
518
 
519
+ // The loader always ran for 2s, so with a shorter duration the toast was gone
520
+ // before the icon it reveals ever appeared.
521
+ const loaderMs = duration > 0 ? Math.min(2000, Math.max(0, duration - 500)) : 2000;
522
+
466
523
  // Handle Loader and Icon
467
524
  if (showLoader && loader) {
468
525
  setTimeout(() => {
@@ -474,7 +531,7 @@ const toast = function (message, options = {}) {
474
531
  if (isConfirm && contentRow) contentRow.appendChild(iconElement);
475
532
  else toastElement.appendChild(iconElement); // Add icon only if not present
476
533
  }
477
- }, 2000); // Simulate a loading period of 2 seconds
534
+ }, loaderMs);
478
535
  }
479
536
  if (!showLoader) {
480
537
  // For confirm, icon already added above inside contentRow; avoid moving it
@@ -483,26 +540,70 @@ const toast = function (message, options = {}) {
483
540
  }
484
541
  }
485
542
 
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;
543
+ // Dismissal state lives on the element so toast.update() can rebind the callback
544
+ // and reuse this exact removal path instead of keeping its own copy of it.
545
+ toastElement._onDismiss = typeof onDismiss === 'function' ? onDismiss : null;
546
+
547
+ // A plain setTimeout cannot be paused, so the countdown is tracked by hand:
548
+ // `remaining` is what is left, and hovering banks it.
549
+ let remaining = duration;
550
+ let timerId = null;
551
+ let startedAt = 0;
552
+
553
+ const stopTimer = () => {
554
+ if (timerId) { clearTimeout(timerId); timerId = null; }
555
+ };
556
+
557
+ const startTimer = () => {
558
+ if (isConfirm || remaining <= 0 || timerId) return;
559
+ startedAt = Date.now();
560
+ timerId = setTimeout(() => { timerId = null; toastElement._dismiss(); }, remaining);
561
+ if (progressBar) progressBar.style.animationPlayState = 'running';
562
+ };
563
+
564
+ const pauseTimer = () => {
565
+ if (!timerId) return;
566
+ clearTimeout(timerId);
567
+ timerId = null;
568
+ remaining -= Date.now() - startedAt;
569
+ if (progressBar) progressBar.style.animationPlayState = 'paused';
570
+ };
571
+
572
+ toastElement._dismiss = () => {
573
+ if (toastElement._removing) return; // never run the exit twice
574
+ toastElement._removing = true;
575
+ stopTimer();
576
+ removeWithAnimation(toastElement, () => {
577
+ if (toastElement._onDismiss) toastElement._onDismiss(toastElement);
578
+ // Containers used to pile up in the DOM, one per position, forever.
579
+ if (containerEl && !containerEl.children.length) containerEl.remove();
580
+ });
581
+ };
582
+
583
+ // Lets toast.update() re-arm the countdown without reaching into internals.
584
+ toastElement._setDuration = (ms) => {
585
+ stopTimer();
586
+ remaining = ms;
587
+ startTimer();
588
+ };
589
+
590
+ startTimer();
591
+
592
+ if (!isConfirm && pauseOnHover) {
593
+ // Give the reader a chance to finish the sentence.
594
+ toastElement.addEventListener('mouseenter', pauseTimer);
595
+ toastElement.addEventListener('mouseleave', startTimer);
596
+ toastElement.addEventListener('focusin', pauseTimer);
597
+ toastElement.addEventListener('focusout', startTimer);
494
598
  }
495
599
 
496
600
  // Add event listener for closing the toast when clicked (disabled in confirm mode)
497
601
  if (!isConfirm && dismissOnClick) {
498
602
  toastElement.addEventListener('click', () => {
499
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove); // Clear the auto-remove timeout
500
603
  // onClick belongs to the click, not to the end of the exit animation,
501
604
  // which is where it used to fire half a second late.
502
605
  if (onClick && typeof onClick === 'function') onClick(toastElement);
503
- removeWithAnimation(toastElement, () => {
504
- if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
505
- });
606
+ toastElement._dismiss();
506
607
  });
507
608
  }
508
609
 
@@ -525,23 +626,15 @@ const toast = function (message, options = {}) {
525
626
  const dy = Math.abs(touchStartY - e.changedTouches[0].screenY);
526
627
  // Only a mostly-horizontal swipe dismisses, so scrolling the page past a
527
628
  // 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
- }
629
+ if (dx > 50 && dx > dy) toastElement._dismiss();
534
630
  });
535
631
  }
536
632
 
537
633
  // Let callers dismiss a toast they are holding, instead of only waiting out
538
634
  // the duration or making the user click it.
539
635
  toastElement.close = () => {
540
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
541
636
  if (isConfirm) { resolveAndClose(false); return; }
542
- removeWithAnimation(toastElement, () => {
543
- if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
544
- });
637
+ toastElement._dismiss();
545
638
  };
546
639
 
547
640
  return toastElement;
@@ -570,10 +663,9 @@ const toast = function (message, options = {}) {
570
663
  type = null,
571
664
  icon = null,
572
665
  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
666
+ duration = 3000, // Default duration (in ms); 0 keeps the toast on screen
667
+ allowHtml = true,
668
+ onDismiss = null // Replaces the callback the toast was created with
577
669
  } = options;
578
670
 
579
671
  // Remove old loader (if any)
@@ -591,7 +683,8 @@ const toast = function (message, options = {}) {
591
683
  }
592
684
  const toastBody = toastElement.querySelector('.ts-toast-body');
593
685
  if (toastBody) {
594
- toastBody.innerHTML = message;
686
+ if (allowHtml) toastBody.innerHTML = message;
687
+ else toastBody.textContent = message;
595
688
  }
596
689
 
597
690
  // Handle Icon update only if it's new or hasn't been set yet
@@ -619,9 +712,12 @@ const toast = function (message, options = {}) {
619
712
  iconElement.appendChild(img);
620
713
  }
621
714
 
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);
715
+ // Only attach an icon we actually have. Without a type and without an explicit
716
+ // icon this used to append an empty <span><img></span>.
717
+ if (icon || iconElement.querySelector('img[src]')) {
718
+ const contentRow = toastElement.querySelector('.ts-toast-content');
719
+ (contentRow || toastElement).appendChild(iconElement);
720
+ }
625
721
 
626
722
  // Handle loader if requested
627
723
  if (showLoader) {
@@ -630,33 +726,19 @@ const toast = function (message, options = {}) {
630
726
  toastElement.appendChild(loader);
631
727
  setTimeout(() => {
632
728
  loader.classList.add('done');
633
- }, 2000); // Simulate loader completion after 2 seconds
729
+ }, duration > 0 ? Math.min(2000, Math.max(0, duration - 500)) : 2000);
634
730
  }
635
731
 
636
- // Clear previous auto-remove timer if needed
637
- if (toastElement._autoRemove) {
638
- clearTimeout(toastElement._autoRemove);
639
- }
732
+ // Rebind the dismiss callback rather than leaving the creation-time one in place,
733
+ // which meant an updated toast fired two different onDismiss handlers.
734
+ if (typeof onDismiss === 'function') toastElement._onDismiss = onDismiss;
640
735
 
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
736
+ // Re-arm the countdown through the toast's own timer, so duration: 0 keeps the
737
+ // toast on screen. This used to schedule setTimeout(..., 0) and remove it at once,
738
+ // which broke every `toast.loading(...).update(msg, { duration: 0 })`.
739
+ if (typeof toastElement._setDuration === 'function') {
740
+ toastElement._setDuration(duration);
741
+ }
660
742
  };
661
743
 
662
744
  toast.loading = function (message, options = {}) {
@@ -730,6 +812,34 @@ const toast = function (message, options = {}) {
730
812
  });
731
813
  };
732
814
 
815
+ // Wraps an async action: one loading toast that becomes the success or error
816
+ // message, instead of hand-rolling loading/update/catch at every call site.
817
+ toast.promise = function (promise, messages = {}, options = {}) {
818
+ const {
819
+ loading = 'Loading…',
820
+ success = 'Done',
821
+ error = 'Something went wrong'
822
+ } = messages;
823
+
824
+ const handle = toast.loading(loading, options);
825
+ // Messages may be functions so they can name what actually came back.
826
+ const text = (msg, value) => (typeof msg === 'function' ? msg(value) : msg);
827
+
828
+ return Promise.resolve(promise).then(
829
+ (value) => {
830
+ handle.update(text(success, value), { type: 'success', duration: options.duration });
831
+ return value;
832
+ },
833
+ (err) => {
834
+ handle.update(text(error, err), { type: 'error', duration: options.duration });
835
+ throw err; // the caller still owns the failure
836
+ }
837
+ );
838
+ };
839
+
840
+ // Options applied to every toast unless the call overrides them.
841
+ toast.defaults = {};
842
+
733
843
  // Close every toast currently on screen. Confirm dialogs settle as a cancel.
734
844
  toast.dismissAll = function () {
735
845
  document.querySelectorAll('.ts-toast').forEach((el) => {