@tsirosgeorge/toastnotification 5.4.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +21 -1
- package/assets/css/toast.css +9 -0
- package/assets/css/toast.min.css +1 -1
- package/package.json +3 -2
- package/toast.d.ts +14 -1
- package/toast.js +172 -18
- package/toast.min.js +1 -1
- package/toast.module.js +172 -18
package/Readme.md
CHANGED
|
@@ -103,6 +103,24 @@ toast('Are you sure?', {
|
|
|
103
103
|
});
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
### ⌨️ Keyboard and screen readers
|
|
107
|
+
|
|
108
|
+
Confirm dialogs take focus when they open, keep Tab inside themselves while they are
|
|
109
|
+
open, and hand focus back to whatever opened them when they close. Escape cancels, and
|
|
110
|
+
Enter submits a single-line input. Without this the element that opened the dialog kept
|
|
111
|
+
focus behind the backdrop, so Enter or Space re-triggered it and stacked a second dialog.
|
|
112
|
+
|
|
113
|
+
Toast containers are announced as `aria-live="polite"`; `error` and `warning` toasts get
|
|
114
|
+
`role="alert"`. Everything respects `prefers-reduced-motion`.
|
|
115
|
+
|
|
116
|
+
### ✋ Closing toasts yourself
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
const t = toast('Working…', { duration: 0 });
|
|
120
|
+
t.close(); // dismiss this one
|
|
121
|
+
toast.dismissAll(); // dismiss everything; open dialogs settle as a cancel
|
|
122
|
+
```
|
|
123
|
+
|
|
106
124
|
### ⏳ Loading → Update
|
|
107
125
|
```javascript
|
|
108
126
|
const t = toast.loading('Uploading…', { type: 'info' });
|
|
@@ -114,12 +132,14 @@ t.update('Done!', { type: 'success', duration: 2000 });
|
|
|
114
132
|
|
|
115
133
|
| Option | Type | Default | Description |
|
|
116
134
|
|--------------|------------|---------------|-----------------------------------------------------------------------------------------------|
|
|
117
|
-
| `position` | `string` | `'top-right'` | Container position. Values: `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'`, `'top-center'`, `'bottom-center'`. |
|
|
135
|
+
| `position` | `string` | `'top-right'` | Container position. Values: `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'`, `'top-center'`, `'bottom-center'`, `'center'`. `'center'` places it dead centre of the viewport and defaults to the `zoom-in` animation; it works for plain toasts and for confirm dialogs. |
|
|
118
136
|
| `animation` | `string` | `'slide-right'` | Show animation. Examples: `'slide-right'`, `'slide-left'`, `'slide-top'`, `'slide-bottom'`, `'zoom-in'`. Hide uses reverse automatically. |
|
|
119
137
|
| `type` | `string` | `'info'` | Type of toast, controlling icon and styling. Possible values: `'info'`, `'success'`, `'error'`, `'warning'`. |
|
|
120
138
|
| `duration` | `number` | `3000` | Duration in milliseconds before the toast automatically dismisses. |
|
|
121
139
|
| `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`. |
|
|
122
140
|
| `showLoader` | `boolean` | `false` | Whether to show a loader/progress bar animation on the toast during its visible duration. |
|
|
141
|
+
| `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
|
+
| `closeOnEscape` | `boolean` | `true` | Confirm dialogs only. Escape cancels the dialog. |
|
|
123
143
|
| `onClick` | `function` or `null` | `null` | Callback function executed when the toast is clicked. |
|
|
124
144
|
| `onShow` | `function` or `null` | `null` | Callback function executed when the toast appears (after it's added to the DOM and shown). |
|
|
125
145
|
| `onDismiss` | `function` or `null` | `null` | Callback function executed when the toast is dismissed and removed from the DOM. |
|
package/assets/css/toast.css
CHANGED
|
@@ -52,6 +52,14 @@
|
|
|
52
52
|
align-items: center;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/* Dead centre of the viewport, for alerts that should not sit in a corner. */
|
|
56
|
+
.ts-toast-container.center {
|
|
57
|
+
top: 50%;
|
|
58
|
+
left: 50%;
|
|
59
|
+
transform: translate(-50%, -50%);
|
|
60
|
+
align-items: center;
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
/* General Toast styles */
|
|
56
64
|
.ts-toast-container .ts-toast {
|
|
57
65
|
display: flex;
|
|
@@ -334,6 +342,7 @@
|
|
|
334
342
|
.ts-toast-overlay.bottom-left { align-items: flex-end; justify-content: flex-start; padding: 1rem; }
|
|
335
343
|
.ts-toast-overlay.bottom-center { align-items: flex-end; justify-content: center; padding: 1rem; }
|
|
336
344
|
.ts-toast-overlay.bottom-right { align-items: flex-end; justify-content: flex-end; padding: 1rem; }
|
|
345
|
+
.ts-toast-overlay.center { align-items: center; justify-content: center; padding: 1rem; }
|
|
337
346
|
|
|
338
347
|
/* Tiny utility: flex icon container */
|
|
339
348
|
/* Scoped utility to avoid conflicts */
|
package/assets/css/toast.min.css
CHANGED
|
@@ -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 .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-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}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsirosgeorge/toastnotification",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"description": "a toast notification plugin",
|
|
5
5
|
"main": "toast.min.js",
|
|
6
6
|
"module": "toast.module.js",
|
|
@@ -21,8 +21,9 @@
|
|
|
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
25
|
"prepublishOnly": "npm run build",
|
|
25
|
-
"release": "npm version ${V:-patch} &&
|
|
26
|
+
"release": "npm version ${V:-patch} && git push origin main --follow-tags"
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"toast.js",
|
package/toast.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ export type ToastType = 'info' | 'success' | 'warning' | 'error';
|
|
|
2
2
|
|
|
3
3
|
export type ToastPosition =
|
|
4
4
|
| 'top-left' | 'top-right' | 'top-center'
|
|
5
|
-
| 'bottom-left' | 'bottom-right' | 'bottom-center'
|
|
5
|
+
| 'bottom-left' | 'bottom-right' | 'bottom-center'
|
|
6
|
+
/** Dead centre of the viewport. Defaults to the zoom-in animation. */
|
|
7
|
+
| 'center';
|
|
6
8
|
|
|
7
9
|
export type ToastAnimation =
|
|
8
10
|
| 'slide-top' | 'slide-bottom' | 'slide-left' | 'slide-right'
|
|
@@ -16,6 +18,8 @@ export type ConfirmResult = boolean | string | null;
|
|
|
16
18
|
export interface ToastElement extends HTMLDivElement {
|
|
17
19
|
/** Present in confirm mode: resolves when the user confirms, cancels or dismisses. */
|
|
18
20
|
result?: Promise<ConfirmResult>;
|
|
21
|
+
/** Dismiss this toast now. On a confirm dialog this settles as a cancel. */
|
|
22
|
+
close(): void;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export interface ToastOptions {
|
|
@@ -27,6 +31,11 @@ export interface ToastOptions {
|
|
|
27
31
|
/** Emoji or text used instead of the bundled animated icon. */
|
|
28
32
|
icon?: string | null;
|
|
29
33
|
showLoader?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* `message` is written as HTML by default, for backwards compatibility.
|
|
36
|
+
* Pass `false` to render it as plain text — do that for anything user-supplied.
|
|
37
|
+
*/
|
|
38
|
+
allowHtml?: boolean;
|
|
30
39
|
/** `'confirm'` (alias `'swal'`) renders a modal dialog instead of a toast. */
|
|
31
40
|
mode?: 'alert' | 'confirm' | 'swal';
|
|
32
41
|
dismissOnClick?: boolean;
|
|
@@ -48,6 +57,8 @@ export interface ConfirmOptions extends ToastOptions {
|
|
|
48
57
|
cancelButtonColor?: string | null;
|
|
49
58
|
useOverlay?: boolean;
|
|
50
59
|
closeOnOverlayClick?: boolean;
|
|
60
|
+
/** Escape cancels the dialog. Defaults to true. */
|
|
61
|
+
closeOnEscape?: boolean;
|
|
51
62
|
showClose?: boolean;
|
|
52
63
|
/** Receives the input's value when `input` is set, otherwise `true`. */
|
|
53
64
|
onConfirm?: (value: string | true, el: ToastElement) => void;
|
|
@@ -74,6 +85,8 @@ export interface Toast {
|
|
|
74
85
|
* and `null` on cancel/dismiss — so an empty submission is not read as a cancel.
|
|
75
86
|
*/
|
|
76
87
|
confirm(message: string, options?: ConfirmOptions): Promise<ConfirmResult>;
|
|
88
|
+
/** Dismiss every toast on screen; open confirm dialogs settle as a cancel. */
|
|
89
|
+
dismissAll(): void;
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
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
|
+
const TS_TOAST_VERSION = "5.5.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';
|
|
@@ -10,6 +10,23 @@ const TS_TOAST_CDN = (typeof window !== 'undefined' && window.TS_TOAST_ASSET_BAS
|
|
|
10
10
|
? String(window.TS_TOAST_ASSET_BASE).replace(/\/+$/, '')
|
|
11
11
|
: `https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@${TS_TOAST_VERSION}`;
|
|
12
12
|
|
|
13
|
+
// How many confirm dialogs are currently open. The body scroll lock belongs to the
|
|
14
|
+
// group, so only the last dialog to close may release it.
|
|
15
|
+
let tsToastOpenModals = 0;
|
|
16
|
+
let tsToastPrevOverflow = '';
|
|
17
|
+
|
|
18
|
+
const tsToastReducedMotion = () =>
|
|
19
|
+
typeof window !== 'undefined' &&
|
|
20
|
+
typeof window.matchMedia === 'function' &&
|
|
21
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
22
|
+
|
|
23
|
+
// Everything inside the dialog a keyboard can reach, in DOM order.
|
|
24
|
+
const tsToastFocusable = (root) => Array.from(
|
|
25
|
+
root.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
|
26
|
+
).filter((el) => !el.disabled && el.offsetParent !== null);
|
|
27
|
+
|
|
28
|
+
let tsToastIdCounter = 0;
|
|
29
|
+
|
|
13
30
|
// Load the stylesheet from the CDN, unless the page opted out by importing it itself
|
|
14
31
|
// (set window.TS_TOAST_NO_CSS = true before loading, or ship assets/css/toast.css yourself).
|
|
15
32
|
(function loadStylesheet() {
|
|
@@ -33,6 +50,8 @@ const TS_TOAST_CDN = (typeof window !== 'undefined' && window.TS_TOAST_ASSET_BAS
|
|
|
33
50
|
/* Ensure center positions exist even if external CSS lacks them */
|
|
34
51
|
.ts-toast-container.top-center { top: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
|
|
35
52
|
.ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
|
|
53
|
+
.ts-toast-container.center { top: 50%; left: 50%; transform: translate(-50%, -50%); align-items: center; }
|
|
54
|
+
.ts-toast-overlay.center { align-items: center; justify-content: center; }
|
|
36
55
|
.ts-toast-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 2147483646; }
|
|
37
56
|
.ts-toast.ts-toast-confirm { max-width: min(92vw, 440px); width: max(320px, 60%); flex-direction: column; gap: 12px; padding: 16px 20px; background: var(--toast-bg, #fff); color: var(--toast-color, #000); border: 1px solid var(--toast-border, #e5e7eb); border-radius: 12px; box-shadow: var(--toast-shadow, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)); text-align: center; }
|
|
38
57
|
.ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
|
|
@@ -84,6 +103,11 @@ const toast = function (message, options = {}) {
|
|
|
84
103
|
useOverlay = true,
|
|
85
104
|
closeOnOverlayClick = true,
|
|
86
105
|
showClose = false,
|
|
106
|
+
// Escape cancels a confirm dialog
|
|
107
|
+
closeOnEscape = true,
|
|
108
|
+
// `message` is written as HTML for backwards compatibility. Pass false to
|
|
109
|
+
// render it as plain text, which is what you want for anything user-supplied.
|
|
110
|
+
allowHtml = true,
|
|
87
111
|
// interactions
|
|
88
112
|
dismissOnClick = true, // ignored if confirm-mode
|
|
89
113
|
onClick = null, // Custom onClick event listener
|
|
@@ -92,6 +116,7 @@ const toast = function (message, options = {}) {
|
|
|
92
116
|
} = options;
|
|
93
117
|
|
|
94
118
|
const isConfirm = (mode === 'confirm' || mode === 'swal');
|
|
119
|
+
const reducedMotion = tsToastReducedMotion();
|
|
95
120
|
|
|
96
121
|
// Pick an animation intelligently when one wasn't explicitly provided
|
|
97
122
|
const resolvedAnimation = (typeof options.animation === 'string' && options.animation.trim())
|
|
@@ -107,10 +132,12 @@ const toast = function (message, options = {}) {
|
|
|
107
132
|
};
|
|
108
133
|
return m[a] || a;
|
|
109
134
|
})(options.animation.trim())
|
|
110
|
-
|
|
135
|
+
// 'center' has no edge to slide in from, so it zooms like a dialog.
|
|
136
|
+
: (isConfirm || position === 'center' ? 'ts-toast-zoom-in'
|
|
137
|
+
: position.startsWith('top') ? 'ts-toast-slide-top'
|
|
111
138
|
: position.startsWith('bottom') ? 'ts-toast-slide-bottom'
|
|
112
139
|
: position.endsWith('left') ? 'ts-toast-slide-left'
|
|
113
|
-
: 'ts-toast-slide-right')
|
|
140
|
+
: 'ts-toast-slide-right');
|
|
114
141
|
|
|
115
142
|
// helper: remove with smooth CSS transition and cleanup (used by alerts)
|
|
116
143
|
const removeWithAnimation = (el, callback) => {
|
|
@@ -143,13 +170,24 @@ const toast = function (message, options = {}) {
|
|
|
143
170
|
el.classList.remove('ts-toast-slide-out');
|
|
144
171
|
if (el.parentNode) el.parentNode.removeChild(el);
|
|
145
172
|
if (typeof callback === 'function') callback();
|
|
146
|
-
}, 500);
|
|
173
|
+
}, reducedMotion ? 0 : 500);
|
|
147
174
|
};
|
|
148
175
|
|
|
149
176
|
const toastElement = document.createElement('div');
|
|
150
177
|
toastElement.className = `ts-toast ts-toast-${type}${isConfirm ? ' ts-toast-confirm' : ''}`;
|
|
151
178
|
toastElement.dataset.anim = resolvedAnimation;
|
|
152
|
-
toastElement.style.animation = `${resolvedAnimation} 0.5s ease`;
|
|
179
|
+
if (!reducedMotion) toastElement.style.animation = `${resolvedAnimation} 0.5s ease`;
|
|
180
|
+
|
|
181
|
+
const uid = `ts-toast-${++tsToastIdCounter}`;
|
|
182
|
+
if (isConfirm) {
|
|
183
|
+
// Without these a screen reader announces nothing, and without tabindex the
|
|
184
|
+
// dialog cannot take focus away from whatever opened it.
|
|
185
|
+
toastElement.setAttribute('role', 'dialog');
|
|
186
|
+
toastElement.setAttribute('aria-modal', 'true');
|
|
187
|
+
toastElement.tabIndex = -1;
|
|
188
|
+
} else if (type === 'error' || type === 'warning') {
|
|
189
|
+
toastElement.setAttribute('role', 'alert');
|
|
190
|
+
}
|
|
153
191
|
// In confirm mode, we stack content vertically; in alert mode keep original layout
|
|
154
192
|
if (!isConfirm) {
|
|
155
193
|
toastElement.style.flexDirection = 'row-reverse';
|
|
@@ -181,7 +219,11 @@ const toast = function (message, options = {}) {
|
|
|
181
219
|
// Create Body
|
|
182
220
|
const toastBody = document.createElement('div');
|
|
183
221
|
toastBody.className = 'ts-toast-body';
|
|
184
|
-
toastBody.
|
|
222
|
+
toastBody.id = `${uid}-body`;
|
|
223
|
+
// HTML by default for backwards compatibility; pass allowHtml: false for
|
|
224
|
+
// anything that came from a user.
|
|
225
|
+
if (allowHtml) toastBody.innerHTML = message;
|
|
226
|
+
else toastBody.textContent = message;
|
|
185
227
|
|
|
186
228
|
// Content row for confirm (icon + text side-by-side)
|
|
187
229
|
let contentRow = null;
|
|
@@ -192,10 +234,13 @@ const toast = function (message, options = {}) {
|
|
|
192
234
|
if (title) {
|
|
193
235
|
const titleEl = document.createElement('div');
|
|
194
236
|
titleEl.className = 'ts-toast-title';
|
|
237
|
+
titleEl.id = `${uid}-title`;
|
|
195
238
|
titleEl.textContent = title;
|
|
196
239
|
contentRow.appendChild(titleEl);
|
|
240
|
+
toastElement.setAttribute('aria-labelledby', titleEl.id);
|
|
197
241
|
}
|
|
198
242
|
contentRow.appendChild(toastBody);
|
|
243
|
+
toastElement.setAttribute('aria-describedby', toastBody.id);
|
|
199
244
|
toastElement.appendChild(contentRow);
|
|
200
245
|
} else {
|
|
201
246
|
toastElement.appendChild(toastBody);
|
|
@@ -214,6 +259,13 @@ const toast = function (message, options = {}) {
|
|
|
214
259
|
inputElement.className = 'ts-toast-input';
|
|
215
260
|
inputElement.placeholder = inputPlaceholder;
|
|
216
261
|
inputElement.value = inputValue;
|
|
262
|
+
// Enter submits a single-line field, the way a native prompt does.
|
|
263
|
+
// A textarea keeps Enter for newlines.
|
|
264
|
+
if (input !== 'textarea') {
|
|
265
|
+
inputElement.addEventListener('keydown', (e) => {
|
|
266
|
+
if (e.key === 'Enter') { e.preventDefault(); resolveAndClose(true); }
|
|
267
|
+
});
|
|
268
|
+
}
|
|
217
269
|
toastElement.appendChild(inputElement);
|
|
218
270
|
}
|
|
219
271
|
|
|
@@ -223,6 +275,8 @@ const toast = function (message, options = {}) {
|
|
|
223
275
|
// Assigned in confirm mode; the overlay and (x) handlers below call it so that
|
|
224
276
|
// *every* way of dismissing the dialog settles the promise and the callbacks.
|
|
225
277
|
let resolveAndClose = null;
|
|
278
|
+
// Releases the scroll lock, the key handler and the focus this dialog took.
|
|
279
|
+
let releaseModal = () => {};
|
|
226
280
|
if (isConfirm) {
|
|
227
281
|
actionsContainer = document.createElement('div');
|
|
228
282
|
actionsContainer.className = 'ts-toast-actions';
|
|
@@ -262,6 +316,7 @@ const toast = function (message, options = {}) {
|
|
|
262
316
|
if (confirmed && typeof onConfirm === 'function') onConfirm(result, toastElement);
|
|
263
317
|
if (!confirmed && typeof onCancel === 'function') onCancel(toastElement);
|
|
264
318
|
if (typeof onResult === 'function') onResult(result, toastElement);
|
|
319
|
+
releaseModal();
|
|
265
320
|
// Use the same slide+fade removal as alerts
|
|
266
321
|
removeWithAnimation(toastElement, () => {
|
|
267
322
|
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
@@ -304,11 +359,17 @@ const toast = function (message, options = {}) {
|
|
|
304
359
|
toastElement.appendChild(closeBtn);
|
|
305
360
|
}
|
|
306
361
|
if (closeOnOverlayClick) {
|
|
362
|
+
// The cancel must both start and end on the backdrop. Selecting text in
|
|
363
|
+
// the input and releasing the mouse outside the card produced a click
|
|
364
|
+
// whose target was the overlay, which threw the dialog away mid-edit.
|
|
365
|
+
let pressedOnBackdrop = false;
|
|
366
|
+
overlay.addEventListener('pointerdown', (e) => { pressedOnBackdrop = e.target === overlay; });
|
|
307
367
|
overlay.addEventListener('click', (e) => {
|
|
308
368
|
// Backdrop click is a cancel: settle the promise and onCancel/onResult,
|
|
309
369
|
// then close. Previously this resolved only the internal el.result,
|
|
310
370
|
// so `await toast.confirm(...)` hung forever.
|
|
311
|
-
if (e.target === overlay) resolveAndClose(false);
|
|
371
|
+
if (e.target === overlay && pressedOnBackdrop) resolveAndClose(false);
|
|
372
|
+
pressedOnBackdrop = false;
|
|
312
373
|
});
|
|
313
374
|
}
|
|
314
375
|
} else {
|
|
@@ -319,9 +380,79 @@ const toast = function (message, options = {}) {
|
|
|
319
380
|
container.className = `ts-toast-container ${position}`;
|
|
320
381
|
document.body.appendChild(container);
|
|
321
382
|
}
|
|
383
|
+
if (!container.hasAttribute('aria-live')) {
|
|
384
|
+
// Without this a toast is invisible to a screen reader.
|
|
385
|
+
container.setAttribute('role', 'status');
|
|
386
|
+
container.setAttribute('aria-live', 'polite');
|
|
387
|
+
container.setAttribute('aria-relevant', 'additions');
|
|
388
|
+
}
|
|
322
389
|
container.appendChild(toastElement);
|
|
323
390
|
}
|
|
324
391
|
|
|
392
|
+
if (isConfirm) {
|
|
393
|
+
// The dialog has to own the keyboard while it is open. Without this the
|
|
394
|
+
// element that opened it keeps focus, so pressing Enter or Space activates
|
|
395
|
+
// it again and stacks a second dialog on top of the first — and Tab walks
|
|
396
|
+
// through the page behind the backdrop.
|
|
397
|
+
const previouslyFocused = document.activeElement;
|
|
398
|
+
|
|
399
|
+
tsToastOpenModals += 1;
|
|
400
|
+
if (tsToastOpenModals === 1) {
|
|
401
|
+
tsToastPrevOverflow = document.body.style.overflow;
|
|
402
|
+
document.body.style.overflow = 'hidden';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// With dialogs stacked, only the top one should answer the keyboard.
|
|
406
|
+
const isTopmost = () => {
|
|
407
|
+
const open = document.querySelectorAll('.ts-toast.ts-toast-confirm');
|
|
408
|
+
return open.length === 0 || open[open.length - 1] === toastElement;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const onKeydown = (e) => {
|
|
412
|
+
if (!isTopmost()) return;
|
|
413
|
+
|
|
414
|
+
if (e.key === 'Escape' && closeOnEscape) {
|
|
415
|
+
e.preventDefault();
|
|
416
|
+
resolveAndClose(false);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (e.key !== 'Tab') return;
|
|
420
|
+
|
|
421
|
+
const focusables = tsToastFocusable(toastElement);
|
|
422
|
+
if (!focusables.length) { e.preventDefault(); return; }
|
|
423
|
+
|
|
424
|
+
const first = focusables[0];
|
|
425
|
+
const last = focusables[focusables.length - 1];
|
|
426
|
+
|
|
427
|
+
// Focus can start outside the dialog (the trigger button); pull it back.
|
|
428
|
+
if (!toastElement.contains(document.activeElement)) {
|
|
429
|
+
e.preventDefault();
|
|
430
|
+
(e.shiftKey ? last : first).focus();
|
|
431
|
+
} else if (e.shiftKey && document.activeElement === first) {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
last.focus();
|
|
434
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
first.focus();
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
document.addEventListener('keydown', onKeydown, true);
|
|
440
|
+
|
|
441
|
+
releaseModal = () => {
|
|
442
|
+
document.removeEventListener('keydown', onKeydown, true);
|
|
443
|
+
tsToastOpenModals = Math.max(0, tsToastOpenModals - 1);
|
|
444
|
+
if (tsToastOpenModals === 0) document.body.style.overflow = tsToastPrevOverflow;
|
|
445
|
+
// Hand the keyboard back to whatever opened the dialog.
|
|
446
|
+
if (previouslyFocused && typeof previouslyFocused.focus === 'function' &&
|
|
447
|
+
document.contains(previouslyFocused)) {
|
|
448
|
+
previouslyFocused.focus();
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// Land on the input when there is one, otherwise the confirm button.
|
|
453
|
+
(inputElement || toastElement.querySelector('.ts-toast-btn.confirm') || toastElement).focus();
|
|
454
|
+
}
|
|
455
|
+
|
|
325
456
|
// Trigger the onShow event if provided
|
|
326
457
|
if (onShow && typeof onShow === 'function') {
|
|
327
458
|
onShow(toastElement);
|
|
@@ -366,8 +497,10 @@ const toast = function (message, options = {}) {
|
|
|
366
497
|
if (!isConfirm && dismissOnClick) {
|
|
367
498
|
toastElement.addEventListener('click', () => {
|
|
368
499
|
if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove); // Clear the auto-remove timeout
|
|
500
|
+
// onClick belongs to the click, not to the end of the exit animation,
|
|
501
|
+
// which is where it used to fire half a second late.
|
|
502
|
+
if (onClick && typeof onClick === 'function') onClick(toastElement);
|
|
369
503
|
removeWithAnimation(toastElement, () => {
|
|
370
|
-
if (onClick && typeof onClick === 'function') onClick(toastElement);
|
|
371
504
|
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
372
505
|
});
|
|
373
506
|
});
|
|
@@ -376,15 +509,23 @@ const toast = function (message, options = {}) {
|
|
|
376
509
|
// Add swipe event listeners for mobile dismissal
|
|
377
510
|
if (!isConfirm) {
|
|
378
511
|
let touchStartX = 0;
|
|
512
|
+
let touchStartY = 0;
|
|
379
513
|
let touchEndX = 0;
|
|
380
514
|
|
|
515
|
+
// Passive: these never preventDefault, and a non-passive touchstart blocks
|
|
516
|
+
// scrolling on the whole toast.
|
|
381
517
|
toastElement.addEventListener('touchstart', (e) => {
|
|
382
518
|
touchStartX = e.changedTouches[0].screenX;
|
|
383
|
-
|
|
519
|
+
touchStartY = e.changedTouches[0].screenY;
|
|
520
|
+
}, { passive: true });
|
|
384
521
|
|
|
385
522
|
toastElement.addEventListener('touchend', (e) => {
|
|
386
523
|
touchEndX = e.changedTouches[0].screenX;
|
|
387
|
-
|
|
524
|
+
const dx = Math.abs(touchStartX - touchEndX);
|
|
525
|
+
const dy = Math.abs(touchStartY - e.changedTouches[0].screenY);
|
|
526
|
+
// Only a mostly-horizontal swipe dismisses, so scrolling the page past a
|
|
527
|
+
// toast no longer throws it away on a bit of sideways drift.
|
|
528
|
+
if (dx > 50 && dx > dy) {
|
|
388
529
|
if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
|
|
389
530
|
removeWithAnimation(toastElement, () => {
|
|
390
531
|
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
@@ -393,6 +534,16 @@ const toast = function (message, options = {}) {
|
|
|
393
534
|
});
|
|
394
535
|
}
|
|
395
536
|
|
|
537
|
+
// Let callers dismiss a toast they are holding, instead of only waiting out
|
|
538
|
+
// the duration or making the user click it.
|
|
539
|
+
toastElement.close = () => {
|
|
540
|
+
if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
|
|
541
|
+
if (isConfirm) { resolveAndClose(false); return; }
|
|
542
|
+
removeWithAnimation(toastElement, () => {
|
|
543
|
+
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
|
|
396
547
|
return toastElement;
|
|
397
548
|
};
|
|
398
549
|
|
|
@@ -553,15 +704,11 @@ const toast = function (message, options = {}) {
|
|
|
553
704
|
showLoader: false // Disable loader when updating the message
|
|
554
705
|
});
|
|
555
706
|
},
|
|
707
|
+
// Reuse the element's own close so onDismiss fires, which this
|
|
708
|
+
// hand-rolled copy of the removal never did.
|
|
556
709
|
close: () => {
|
|
557
|
-
|
|
558
|
-
toastElement.
|
|
559
|
-
toastElement.classList.remove('ts-toast-show');
|
|
560
|
-
toastElement.style.animation = '';
|
|
561
|
-
setTimeout(() => {
|
|
562
|
-
toastElement.classList.remove('ts-toast-slide-out');
|
|
563
|
-
if (toastElement.parentNode) toastElement.parentNode.removeChild(toastElement);
|
|
564
|
-
}, 500);
|
|
710
|
+
toastElement._managedByLoading = false;
|
|
711
|
+
toastElement.close();
|
|
565
712
|
}
|
|
566
713
|
};
|
|
567
714
|
};
|
|
@@ -583,6 +730,13 @@ const toast = function (message, options = {}) {
|
|
|
583
730
|
});
|
|
584
731
|
};
|
|
585
732
|
|
|
733
|
+
// Close every toast currently on screen. Confirm dialogs settle as a cancel.
|
|
734
|
+
toast.dismissAll = function () {
|
|
735
|
+
document.querySelectorAll('.ts-toast').forEach((el) => {
|
|
736
|
+
if (typeof el.close === 'function') el.close();
|
|
737
|
+
});
|
|
738
|
+
};
|
|
739
|
+
|
|
586
740
|
// Expose globally for CDN / browser usage
|
|
587
741
|
if (typeof window !== 'undefined') {
|
|
588
742
|
window.toast = toast;
|
package/toast.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const TS_TOAST_VERSION="5.4.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.4.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-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:l=!1,mode:r="alert",title:c=null,confirmText:d="Yes",cancelText:u="No",input:m=!1,inputPlaceholder:p="",inputValue:f="",confirmButtonBg:h=null,confirmButtonColor:g=null,cancelButtonBg:y=null,cancelButtonColor:x=null,onConfirm:b=null,onCancel:v=null,onResult:w=null,useOverlay:C=!0,closeOnOverlayClick:T=!0,showClose:E=!1,dismissOnClick:S=!0,onClick:L=null,onShow:_=null,onDismiss:N=null}=e,k="confirm"===r||"swal"===r,A="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"}[O=e.animation.trim()]||O:k?"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 O;const B=(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()}),500)},$=document.createElement("div");$.className=`ts-toast ts-toast-${n}${k?" ts-toast-confirm":""}`,$.dataset.anim=A,$.style.animation=`${A} 0.5s ease`,k||($.style.flexDirection="row-reverse",$.style.justifyContent="flex-end");const R=document.createElement("span");if(R.className="ts-toast-icon",R.style.display="flex",i)R.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}`),R.appendChild(t)}const j=document.createElement("div");j.className="ts-toast-body",j.innerHTML=t;let q=null;if(k){if(q=document.createElement("div"),q.className="ts-toast-content",q.appendChild(R),c){const t=document.createElement("div");t.className="ts-toast-title",t.textContent=c,q.appendChild(t)}q.appendChild(j),$.appendChild(q)}else $.appendChild(j);let z=null;k&&m&&("textarea"===m?(z=document.createElement("textarea"),z.rows=3):(z=document.createElement("input"),z.type="text"===m||"email"===m||"password"===m||"number"===m?m:"text"),z.className="ts-toast-input",z.placeholder=p,z.value=f,$.appendChild(z));let D=null,P=null,X=null;if(k){D=document.createElement("div"),D.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),x&&(t.style.color=x),h&&(e.style.background=h),g&&(e.style.color=g),D.appendChild(t),D.appendChild(e),$.appendChild(D),$.result=new Promise((t=>{P=t}));let o=!1;X=t=>{if(o)return;o=!0;const e=t?!z||z.value:!!z&&null;P&&P(e),t&&"function"==typeof b&&b(e,$),t||"function"!=typeof v||v($),"function"==typeof w&&w(e,$),B($,(()=>{N&&"function"==typeof N&&N($),F&&F.parentNode&&F.parentNode.removeChild(F)}))},t.addEventListener("click",(t=>{t.stopPropagation(),X(!1)})),e.addEventListener("click",(t=>{t.stopPropagation(),X(!0)}))}let M=null;l&&(M=document.createElement("div"),M.className="ts-toast-loader",$.appendChild(M));let F=null;if(k&&C){if(F=document.createElement("div"),F.className="ts-toast-overlay"+(e.position?` ${o}`:""),document.body.appendChild(F),F.appendChild($),E){const t=document.createElement("button");t.className="ts-toast-close",t.setAttribute("aria-label","Close"),t.innerHTML="×",t.addEventListener("click",(t=>{t.stopPropagation(),X(!1)})),$.appendChild(t)}T&&F.addEventListener("click",(t=>{t.target===F&&X(!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.appendChild($)}if(_&&"function"==typeof _&&_($),setTimeout((()=>{$.classList.add("ts-toast-show")}),100),l&&M&&setTimeout((()=>{$._managedByLoading||(M.classList.add("done"),M.remove(),$.contains(R)||(k&&q?q.appendChild(R):$.appendChild(R)))}),2e3),l||k||$.contains(R)||$.appendChild(R),!k&&a>0){const t=setTimeout((()=>{B($,(()=>{N&&"function"==typeof N&&N($)}))}),a);$._autoRemove=t}if(!k&&S&&$.addEventListener("click",(()=>{$._autoRemove&&clearTimeout($._autoRemove),B($,(()=>{L&&"function"==typeof L&&L($),N&&"function"==typeof N&&N($)}))})),!k){let t=0,e=0;$.addEventListener("touchstart",(e=>{t=e.changedTouches[0].screenX})),$.addEventListener("touchend",(o=>{e=o.changedTouches[0].screenX,Math.abs(t-e)>50&&($._autoRemove&&clearTimeout($._autoRemove),B($,(()=>{N&&"function"==typeof N&&N($)})))}))}return $};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:l=null,onShow:r=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 p=document.createElement("span");if(p.className="ts-toast-icon",p.style.display="flex",n)p.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}`),p.appendChild(t)}if((t.querySelector(".ts-toast-content")||t).appendChild(p),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 f=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=f},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._autoRemove&&clearTimeout(o._autoRemove),o.classList.add("ts-toast-slide-out"),o.classList.remove("ts-toast-show"),o.style.animation="",setTimeout((()=>{o.classList.remove("ts-toast-slide-out"),o.parentNode&&o.parentNode.removeChild(o)}),500)}}},toast.confirm=function(t,e={}){return new Promise((o=>{toast(t,{...e,mode:"confirm",duration:0,dismissOnClick:!1,onResult:t=>o(t)})}))},"undefined"!=typeof window&&(window.toast=toast),"undefined"!=typeof module&&module.exports&&(module.exports=toast);
|
|
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="×",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);
|
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
|
+
const TS_TOAST_VERSION = "5.5.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';
|
|
@@ -10,6 +10,23 @@ const TS_TOAST_CDN = (typeof window !== 'undefined' && window.TS_TOAST_ASSET_BAS
|
|
|
10
10
|
? String(window.TS_TOAST_ASSET_BASE).replace(/\/+$/, '')
|
|
11
11
|
: `https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@${TS_TOAST_VERSION}`;
|
|
12
12
|
|
|
13
|
+
// How many confirm dialogs are currently open. The body scroll lock belongs to the
|
|
14
|
+
// group, so only the last dialog to close may release it.
|
|
15
|
+
let tsToastOpenModals = 0;
|
|
16
|
+
let tsToastPrevOverflow = '';
|
|
17
|
+
|
|
18
|
+
const tsToastReducedMotion = () =>
|
|
19
|
+
typeof window !== 'undefined' &&
|
|
20
|
+
typeof window.matchMedia === 'function' &&
|
|
21
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
22
|
+
|
|
23
|
+
// Everything inside the dialog a keyboard can reach, in DOM order.
|
|
24
|
+
const tsToastFocusable = (root) => Array.from(
|
|
25
|
+
root.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
|
26
|
+
).filter((el) => !el.disabled && el.offsetParent !== null);
|
|
27
|
+
|
|
28
|
+
let tsToastIdCounter = 0;
|
|
29
|
+
|
|
13
30
|
// Load the stylesheet from the CDN, unless the page opted out by importing it itself
|
|
14
31
|
// (set window.TS_TOAST_NO_CSS = true before loading, or ship assets/css/toast.css yourself).
|
|
15
32
|
(function loadStylesheet() {
|
|
@@ -33,6 +50,8 @@ const TS_TOAST_CDN = (typeof window !== 'undefined' && window.TS_TOAST_ASSET_BAS
|
|
|
33
50
|
/* Ensure center positions exist even if external CSS lacks them */
|
|
34
51
|
.ts-toast-container.top-center { top: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
|
|
35
52
|
.ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
|
|
53
|
+
.ts-toast-container.center { top: 50%; left: 50%; transform: translate(-50%, -50%); align-items: center; }
|
|
54
|
+
.ts-toast-overlay.center { align-items: center; justify-content: center; }
|
|
36
55
|
.ts-toast-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 2147483646; }
|
|
37
56
|
.ts-toast.ts-toast-confirm { max-width: min(92vw, 440px); width: max(320px, 60%); flex-direction: column; gap: 12px; padding: 16px 20px; background: var(--toast-bg, #fff); color: var(--toast-color, #000); border: 1px solid var(--toast-border, #e5e7eb); border-radius: 12px; box-shadow: var(--toast-shadow, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)); text-align: center; }
|
|
38
57
|
.ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
|
|
@@ -84,6 +103,11 @@ const toast = function (message, options = {}) {
|
|
|
84
103
|
useOverlay = true,
|
|
85
104
|
closeOnOverlayClick = true,
|
|
86
105
|
showClose = false,
|
|
106
|
+
// Escape cancels a confirm dialog
|
|
107
|
+
closeOnEscape = true,
|
|
108
|
+
// `message` is written as HTML for backwards compatibility. Pass false to
|
|
109
|
+
// render it as plain text, which is what you want for anything user-supplied.
|
|
110
|
+
allowHtml = true,
|
|
87
111
|
// interactions
|
|
88
112
|
dismissOnClick = true, // ignored if confirm-mode
|
|
89
113
|
onClick = null, // Custom onClick event listener
|
|
@@ -92,6 +116,7 @@ const toast = function (message, options = {}) {
|
|
|
92
116
|
} = options;
|
|
93
117
|
|
|
94
118
|
const isConfirm = (mode === 'confirm' || mode === 'swal');
|
|
119
|
+
const reducedMotion = tsToastReducedMotion();
|
|
95
120
|
|
|
96
121
|
// Pick an animation intelligently when one wasn't explicitly provided
|
|
97
122
|
const resolvedAnimation = (typeof options.animation === 'string' && options.animation.trim())
|
|
@@ -107,10 +132,12 @@ const toast = function (message, options = {}) {
|
|
|
107
132
|
};
|
|
108
133
|
return m[a] || a;
|
|
109
134
|
})(options.animation.trim())
|
|
110
|
-
|
|
135
|
+
// 'center' has no edge to slide in from, so it zooms like a dialog.
|
|
136
|
+
: (isConfirm || position === 'center' ? 'ts-toast-zoom-in'
|
|
137
|
+
: position.startsWith('top') ? 'ts-toast-slide-top'
|
|
111
138
|
: position.startsWith('bottom') ? 'ts-toast-slide-bottom'
|
|
112
139
|
: position.endsWith('left') ? 'ts-toast-slide-left'
|
|
113
|
-
: 'ts-toast-slide-right')
|
|
140
|
+
: 'ts-toast-slide-right');
|
|
114
141
|
|
|
115
142
|
// helper: remove with smooth CSS transition and cleanup (used by alerts)
|
|
116
143
|
const removeWithAnimation = (el, callback) => {
|
|
@@ -143,13 +170,24 @@ const toast = function (message, options = {}) {
|
|
|
143
170
|
el.classList.remove('ts-toast-slide-out');
|
|
144
171
|
if (el.parentNode) el.parentNode.removeChild(el);
|
|
145
172
|
if (typeof callback === 'function') callback();
|
|
146
|
-
}, 500);
|
|
173
|
+
}, reducedMotion ? 0 : 500);
|
|
147
174
|
};
|
|
148
175
|
|
|
149
176
|
const toastElement = document.createElement('div');
|
|
150
177
|
toastElement.className = `ts-toast ts-toast-${type}${isConfirm ? ' ts-toast-confirm' : ''}`;
|
|
151
178
|
toastElement.dataset.anim = resolvedAnimation;
|
|
152
|
-
toastElement.style.animation = `${resolvedAnimation} 0.5s ease`;
|
|
179
|
+
if (!reducedMotion) toastElement.style.animation = `${resolvedAnimation} 0.5s ease`;
|
|
180
|
+
|
|
181
|
+
const uid = `ts-toast-${++tsToastIdCounter}`;
|
|
182
|
+
if (isConfirm) {
|
|
183
|
+
// Without these a screen reader announces nothing, and without tabindex the
|
|
184
|
+
// dialog cannot take focus away from whatever opened it.
|
|
185
|
+
toastElement.setAttribute('role', 'dialog');
|
|
186
|
+
toastElement.setAttribute('aria-modal', 'true');
|
|
187
|
+
toastElement.tabIndex = -1;
|
|
188
|
+
} else if (type === 'error' || type === 'warning') {
|
|
189
|
+
toastElement.setAttribute('role', 'alert');
|
|
190
|
+
}
|
|
153
191
|
// In confirm mode, we stack content vertically; in alert mode keep original layout
|
|
154
192
|
if (!isConfirm) {
|
|
155
193
|
toastElement.style.flexDirection = 'row-reverse';
|
|
@@ -181,7 +219,11 @@ const toast = function (message, options = {}) {
|
|
|
181
219
|
// Create Body
|
|
182
220
|
const toastBody = document.createElement('div');
|
|
183
221
|
toastBody.className = 'ts-toast-body';
|
|
184
|
-
toastBody.
|
|
222
|
+
toastBody.id = `${uid}-body`;
|
|
223
|
+
// HTML by default for backwards compatibility; pass allowHtml: false for
|
|
224
|
+
// anything that came from a user.
|
|
225
|
+
if (allowHtml) toastBody.innerHTML = message;
|
|
226
|
+
else toastBody.textContent = message;
|
|
185
227
|
|
|
186
228
|
// Content row for confirm (icon + text side-by-side)
|
|
187
229
|
let contentRow = null;
|
|
@@ -192,10 +234,13 @@ const toast = function (message, options = {}) {
|
|
|
192
234
|
if (title) {
|
|
193
235
|
const titleEl = document.createElement('div');
|
|
194
236
|
titleEl.className = 'ts-toast-title';
|
|
237
|
+
titleEl.id = `${uid}-title`;
|
|
195
238
|
titleEl.textContent = title;
|
|
196
239
|
contentRow.appendChild(titleEl);
|
|
240
|
+
toastElement.setAttribute('aria-labelledby', titleEl.id);
|
|
197
241
|
}
|
|
198
242
|
contentRow.appendChild(toastBody);
|
|
243
|
+
toastElement.setAttribute('aria-describedby', toastBody.id);
|
|
199
244
|
toastElement.appendChild(contentRow);
|
|
200
245
|
} else {
|
|
201
246
|
toastElement.appendChild(toastBody);
|
|
@@ -214,6 +259,13 @@ const toast = function (message, options = {}) {
|
|
|
214
259
|
inputElement.className = 'ts-toast-input';
|
|
215
260
|
inputElement.placeholder = inputPlaceholder;
|
|
216
261
|
inputElement.value = inputValue;
|
|
262
|
+
// Enter submits a single-line field, the way a native prompt does.
|
|
263
|
+
// A textarea keeps Enter for newlines.
|
|
264
|
+
if (input !== 'textarea') {
|
|
265
|
+
inputElement.addEventListener('keydown', (e) => {
|
|
266
|
+
if (e.key === 'Enter') { e.preventDefault(); resolveAndClose(true); }
|
|
267
|
+
});
|
|
268
|
+
}
|
|
217
269
|
toastElement.appendChild(inputElement);
|
|
218
270
|
}
|
|
219
271
|
|
|
@@ -223,6 +275,8 @@ const toast = function (message, options = {}) {
|
|
|
223
275
|
// Assigned in confirm mode; the overlay and (x) handlers below call it so that
|
|
224
276
|
// *every* way of dismissing the dialog settles the promise and the callbacks.
|
|
225
277
|
let resolveAndClose = null;
|
|
278
|
+
// Releases the scroll lock, the key handler and the focus this dialog took.
|
|
279
|
+
let releaseModal = () => {};
|
|
226
280
|
if (isConfirm) {
|
|
227
281
|
actionsContainer = document.createElement('div');
|
|
228
282
|
actionsContainer.className = 'ts-toast-actions';
|
|
@@ -262,6 +316,7 @@ const toast = function (message, options = {}) {
|
|
|
262
316
|
if (confirmed && typeof onConfirm === 'function') onConfirm(result, toastElement);
|
|
263
317
|
if (!confirmed && typeof onCancel === 'function') onCancel(toastElement);
|
|
264
318
|
if (typeof onResult === 'function') onResult(result, toastElement);
|
|
319
|
+
releaseModal();
|
|
265
320
|
// Use the same slide+fade removal as alerts
|
|
266
321
|
removeWithAnimation(toastElement, () => {
|
|
267
322
|
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
@@ -304,11 +359,17 @@ const toast = function (message, options = {}) {
|
|
|
304
359
|
toastElement.appendChild(closeBtn);
|
|
305
360
|
}
|
|
306
361
|
if (closeOnOverlayClick) {
|
|
362
|
+
// The cancel must both start and end on the backdrop. Selecting text in
|
|
363
|
+
// the input and releasing the mouse outside the card produced a click
|
|
364
|
+
// whose target was the overlay, which threw the dialog away mid-edit.
|
|
365
|
+
let pressedOnBackdrop = false;
|
|
366
|
+
overlay.addEventListener('pointerdown', (e) => { pressedOnBackdrop = e.target === overlay; });
|
|
307
367
|
overlay.addEventListener('click', (e) => {
|
|
308
368
|
// Backdrop click is a cancel: settle the promise and onCancel/onResult,
|
|
309
369
|
// then close. Previously this resolved only the internal el.result,
|
|
310
370
|
// so `await toast.confirm(...)` hung forever.
|
|
311
|
-
if (e.target === overlay) resolveAndClose(false);
|
|
371
|
+
if (e.target === overlay && pressedOnBackdrop) resolveAndClose(false);
|
|
372
|
+
pressedOnBackdrop = false;
|
|
312
373
|
});
|
|
313
374
|
}
|
|
314
375
|
} else {
|
|
@@ -319,9 +380,79 @@ const toast = function (message, options = {}) {
|
|
|
319
380
|
container.className = `ts-toast-container ${position}`;
|
|
320
381
|
document.body.appendChild(container);
|
|
321
382
|
}
|
|
383
|
+
if (!container.hasAttribute('aria-live')) {
|
|
384
|
+
// Without this a toast is invisible to a screen reader.
|
|
385
|
+
container.setAttribute('role', 'status');
|
|
386
|
+
container.setAttribute('aria-live', 'polite');
|
|
387
|
+
container.setAttribute('aria-relevant', 'additions');
|
|
388
|
+
}
|
|
322
389
|
container.appendChild(toastElement);
|
|
323
390
|
}
|
|
324
391
|
|
|
392
|
+
if (isConfirm) {
|
|
393
|
+
// The dialog has to own the keyboard while it is open. Without this the
|
|
394
|
+
// element that opened it keeps focus, so pressing Enter or Space activates
|
|
395
|
+
// it again and stacks a second dialog on top of the first — and Tab walks
|
|
396
|
+
// through the page behind the backdrop.
|
|
397
|
+
const previouslyFocused = document.activeElement;
|
|
398
|
+
|
|
399
|
+
tsToastOpenModals += 1;
|
|
400
|
+
if (tsToastOpenModals === 1) {
|
|
401
|
+
tsToastPrevOverflow = document.body.style.overflow;
|
|
402
|
+
document.body.style.overflow = 'hidden';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// With dialogs stacked, only the top one should answer the keyboard.
|
|
406
|
+
const isTopmost = () => {
|
|
407
|
+
const open = document.querySelectorAll('.ts-toast.ts-toast-confirm');
|
|
408
|
+
return open.length === 0 || open[open.length - 1] === toastElement;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const onKeydown = (e) => {
|
|
412
|
+
if (!isTopmost()) return;
|
|
413
|
+
|
|
414
|
+
if (e.key === 'Escape' && closeOnEscape) {
|
|
415
|
+
e.preventDefault();
|
|
416
|
+
resolveAndClose(false);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (e.key !== 'Tab') return;
|
|
420
|
+
|
|
421
|
+
const focusables = tsToastFocusable(toastElement);
|
|
422
|
+
if (!focusables.length) { e.preventDefault(); return; }
|
|
423
|
+
|
|
424
|
+
const first = focusables[0];
|
|
425
|
+
const last = focusables[focusables.length - 1];
|
|
426
|
+
|
|
427
|
+
// Focus can start outside the dialog (the trigger button); pull it back.
|
|
428
|
+
if (!toastElement.contains(document.activeElement)) {
|
|
429
|
+
e.preventDefault();
|
|
430
|
+
(e.shiftKey ? last : first).focus();
|
|
431
|
+
} else if (e.shiftKey && document.activeElement === first) {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
last.focus();
|
|
434
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
first.focus();
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
document.addEventListener('keydown', onKeydown, true);
|
|
440
|
+
|
|
441
|
+
releaseModal = () => {
|
|
442
|
+
document.removeEventListener('keydown', onKeydown, true);
|
|
443
|
+
tsToastOpenModals = Math.max(0, tsToastOpenModals - 1);
|
|
444
|
+
if (tsToastOpenModals === 0) document.body.style.overflow = tsToastPrevOverflow;
|
|
445
|
+
// Hand the keyboard back to whatever opened the dialog.
|
|
446
|
+
if (previouslyFocused && typeof previouslyFocused.focus === 'function' &&
|
|
447
|
+
document.contains(previouslyFocused)) {
|
|
448
|
+
previouslyFocused.focus();
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// Land on the input when there is one, otherwise the confirm button.
|
|
453
|
+
(inputElement || toastElement.querySelector('.ts-toast-btn.confirm') || toastElement).focus();
|
|
454
|
+
}
|
|
455
|
+
|
|
325
456
|
// Trigger the onShow event if provided
|
|
326
457
|
if (onShow && typeof onShow === 'function') {
|
|
327
458
|
onShow(toastElement);
|
|
@@ -366,8 +497,10 @@ const toast = function (message, options = {}) {
|
|
|
366
497
|
if (!isConfirm && dismissOnClick) {
|
|
367
498
|
toastElement.addEventListener('click', () => {
|
|
368
499
|
if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove); // Clear the auto-remove timeout
|
|
500
|
+
// onClick belongs to the click, not to the end of the exit animation,
|
|
501
|
+
// which is where it used to fire half a second late.
|
|
502
|
+
if (onClick && typeof onClick === 'function') onClick(toastElement);
|
|
369
503
|
removeWithAnimation(toastElement, () => {
|
|
370
|
-
if (onClick && typeof onClick === 'function') onClick(toastElement);
|
|
371
504
|
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
372
505
|
});
|
|
373
506
|
});
|
|
@@ -376,15 +509,23 @@ const toast = function (message, options = {}) {
|
|
|
376
509
|
// Add swipe event listeners for mobile dismissal
|
|
377
510
|
if (!isConfirm) {
|
|
378
511
|
let touchStartX = 0;
|
|
512
|
+
let touchStartY = 0;
|
|
379
513
|
let touchEndX = 0;
|
|
380
514
|
|
|
515
|
+
// Passive: these never preventDefault, and a non-passive touchstart blocks
|
|
516
|
+
// scrolling on the whole toast.
|
|
381
517
|
toastElement.addEventListener('touchstart', (e) => {
|
|
382
518
|
touchStartX = e.changedTouches[0].screenX;
|
|
383
|
-
|
|
519
|
+
touchStartY = e.changedTouches[0].screenY;
|
|
520
|
+
}, { passive: true });
|
|
384
521
|
|
|
385
522
|
toastElement.addEventListener('touchend', (e) => {
|
|
386
523
|
touchEndX = e.changedTouches[0].screenX;
|
|
387
|
-
|
|
524
|
+
const dx = Math.abs(touchStartX - touchEndX);
|
|
525
|
+
const dy = Math.abs(touchStartY - e.changedTouches[0].screenY);
|
|
526
|
+
// Only a mostly-horizontal swipe dismisses, so scrolling the page past a
|
|
527
|
+
// toast no longer throws it away on a bit of sideways drift.
|
|
528
|
+
if (dx > 50 && dx > dy) {
|
|
388
529
|
if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
|
|
389
530
|
removeWithAnimation(toastElement, () => {
|
|
390
531
|
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
@@ -393,6 +534,16 @@ const toast = function (message, options = {}) {
|
|
|
393
534
|
});
|
|
394
535
|
}
|
|
395
536
|
|
|
537
|
+
// Let callers dismiss a toast they are holding, instead of only waiting out
|
|
538
|
+
// the duration or making the user click it.
|
|
539
|
+
toastElement.close = () => {
|
|
540
|
+
if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
|
|
541
|
+
if (isConfirm) { resolveAndClose(false); return; }
|
|
542
|
+
removeWithAnimation(toastElement, () => {
|
|
543
|
+
if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
|
|
396
547
|
return toastElement;
|
|
397
548
|
};
|
|
398
549
|
|
|
@@ -553,15 +704,11 @@ const toast = function (message, options = {}) {
|
|
|
553
704
|
showLoader: false // Disable loader when updating the message
|
|
554
705
|
});
|
|
555
706
|
},
|
|
707
|
+
// Reuse the element's own close so onDismiss fires, which this
|
|
708
|
+
// hand-rolled copy of the removal never did.
|
|
556
709
|
close: () => {
|
|
557
|
-
|
|
558
|
-
toastElement.
|
|
559
|
-
toastElement.classList.remove('ts-toast-show');
|
|
560
|
-
toastElement.style.animation = '';
|
|
561
|
-
setTimeout(() => {
|
|
562
|
-
toastElement.classList.remove('ts-toast-slide-out');
|
|
563
|
-
if (toastElement.parentNode) toastElement.parentNode.removeChild(toastElement);
|
|
564
|
-
}, 500);
|
|
710
|
+
toastElement._managedByLoading = false;
|
|
711
|
+
toastElement.close();
|
|
565
712
|
}
|
|
566
713
|
};
|
|
567
714
|
};
|
|
@@ -583,6 +730,13 @@ const toast = function (message, options = {}) {
|
|
|
583
730
|
});
|
|
584
731
|
};
|
|
585
732
|
|
|
733
|
+
// Close every toast currently on screen. Confirm dialogs settle as a cancel.
|
|
734
|
+
toast.dismissAll = function () {
|
|
735
|
+
document.querySelectorAll('.ts-toast').forEach((el) => {
|
|
736
|
+
if (typeof el.close === 'function') el.close();
|
|
737
|
+
});
|
|
738
|
+
};
|
|
739
|
+
|
|
586
740
|
// Expose globally for CDN / browser usage
|
|
587
741
|
if (typeof window !== 'undefined') {
|
|
588
742
|
window.toast = toast;
|