sonner-vanilla 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +264 -0
- package/dist/assets.d.ts +9 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +49 -0
- package/dist/assets.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +7 -0
- package/dist/state.d.ts +60 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +212 -0
- package/dist/state.js.map +1 -0
- package/dist/stimulus.d.ts +171 -0
- package/dist/stimulus.d.ts.map +1 -0
- package/dist/stimulus.js +13 -0
- package/dist/stimulus.js.map +7 -0
- package/dist/styles.css +725 -0
- package/dist/toaster.d.ts +40 -0
- package/dist/toaster.d.ts.map +1 -0
- package/dist/toaster.js +702 -0
- package/dist/toaster.js.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -0
package/dist/toaster.js
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toaster component - DOM manipulation and rendering
|
|
3
|
+
* Vanilla JS port of sonner's index.tsx
|
|
4
|
+
*/
|
|
5
|
+
import { CloseIcon, createLoader, getAsset } from './assets';
|
|
6
|
+
import { ToastState } from './state';
|
|
7
|
+
import { isAction, } from './types';
|
|
8
|
+
// Constants
|
|
9
|
+
const VISIBLE_TOASTS_AMOUNT = 3;
|
|
10
|
+
const VIEWPORT_OFFSET = '24px';
|
|
11
|
+
const MOBILE_VIEWPORT_OFFSET = '16px';
|
|
12
|
+
const TOAST_LIFETIME = 4000;
|
|
13
|
+
const TOAST_WIDTH = 356;
|
|
14
|
+
const GAP = 14;
|
|
15
|
+
const SWIPE_THRESHOLD = 45;
|
|
16
|
+
const TIME_BEFORE_UNMOUNT = 200;
|
|
17
|
+
function cn(...classes) {
|
|
18
|
+
return classes.filter(Boolean).join(' ');
|
|
19
|
+
}
|
|
20
|
+
function getDefaultSwipeDirections(position) {
|
|
21
|
+
const [y, x] = position.split('-');
|
|
22
|
+
const directions = [];
|
|
23
|
+
if (y)
|
|
24
|
+
directions.push(y);
|
|
25
|
+
if (x)
|
|
26
|
+
directions.push(x);
|
|
27
|
+
return directions;
|
|
28
|
+
}
|
|
29
|
+
function getDocumentDirection() {
|
|
30
|
+
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
31
|
+
return 'ltr';
|
|
32
|
+
const dirAttribute = document.documentElement.getAttribute('dir');
|
|
33
|
+
if (dirAttribute === 'auto' || !dirAttribute) {
|
|
34
|
+
return window.getComputedStyle(document.documentElement).direction;
|
|
35
|
+
}
|
|
36
|
+
return dirAttribute;
|
|
37
|
+
}
|
|
38
|
+
function assignOffset(defaultOffset, mobileOffset) {
|
|
39
|
+
const styles = {};
|
|
40
|
+
[defaultOffset, mobileOffset].forEach((offset, index) => {
|
|
41
|
+
const isMobile = index === 1;
|
|
42
|
+
const prefix = isMobile ? '--mobile-offset' : '--offset';
|
|
43
|
+
const defaultValue = isMobile ? MOBILE_VIEWPORT_OFFSET : VIEWPORT_OFFSET;
|
|
44
|
+
function assignAll(offset) {
|
|
45
|
+
['top', 'right', 'bottom', 'left'].forEach((key) => {
|
|
46
|
+
styles[`${prefix}-${key}`] = typeof offset === 'number' ? `${offset}px` : offset;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (typeof offset === 'number' || typeof offset === 'string') {
|
|
50
|
+
assignAll(offset);
|
|
51
|
+
}
|
|
52
|
+
else if (typeof offset === 'object' && offset !== null) {
|
|
53
|
+
['top', 'right', 'bottom', 'left'].forEach((key) => {
|
|
54
|
+
const k = key;
|
|
55
|
+
if (offset[k] === undefined) {
|
|
56
|
+
styles[`${prefix}-${key}`] = defaultValue;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
styles[`${prefix}-${key}`] = typeof offset[k] === 'number' ? `${offset[k]}px` : String(offset[k]);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
assignAll(defaultValue);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return styles;
|
|
68
|
+
}
|
|
69
|
+
export class Toaster {
|
|
70
|
+
constructor(options = {}) {
|
|
71
|
+
this.container = null;
|
|
72
|
+
this.listEl = null;
|
|
73
|
+
this.toastInstances = new Map();
|
|
74
|
+
this.heights = [];
|
|
75
|
+
this.expanded = false;
|
|
76
|
+
this.interacting = false;
|
|
77
|
+
this.unsubscribe = null;
|
|
78
|
+
this.boundListHandlers = null;
|
|
79
|
+
this.actualTheme = 'light';
|
|
80
|
+
this.isDocumentHidden = false;
|
|
81
|
+
this.handleKeyDown = (event) => {
|
|
82
|
+
const { hotkey } = this.options;
|
|
83
|
+
const isHotkeyPressed = hotkey.length > 0 &&
|
|
84
|
+
hotkey.every((key) => event[key] || event.code === key);
|
|
85
|
+
if (isHotkeyPressed) {
|
|
86
|
+
this.setExpanded(true);
|
|
87
|
+
this.listEl?.focus();
|
|
88
|
+
}
|
|
89
|
+
if (event.code === 'Escape' && this.listEl?.contains(document.activeElement)) {
|
|
90
|
+
this.setExpanded(false);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
this.handleVisibilityChange = () => {
|
|
94
|
+
this.isDocumentHidden = document.hidden;
|
|
95
|
+
// Pause/resume timers for all toasts
|
|
96
|
+
this.toastInstances.forEach((instance) => {
|
|
97
|
+
if (this.isDocumentHidden) {
|
|
98
|
+
this.pauseTimer(instance);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
this.startTimer(instance);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
this.options = {
|
|
106
|
+
id: options.id ?? '',
|
|
107
|
+
invert: options.invert ?? false,
|
|
108
|
+
theme: options.theme ?? 'light',
|
|
109
|
+
position: options.position ?? 'bottom-right',
|
|
110
|
+
hotkey: options.hotkey ?? ['altKey', 'KeyT'],
|
|
111
|
+
richColors: options.richColors ?? false,
|
|
112
|
+
expand: options.expand ?? false,
|
|
113
|
+
duration: options.duration ?? TOAST_LIFETIME,
|
|
114
|
+
gap: options.gap ?? GAP,
|
|
115
|
+
visibleToasts: options.visibleToasts ?? VISIBLE_TOASTS_AMOUNT,
|
|
116
|
+
closeButton: options.closeButton ?? false,
|
|
117
|
+
className: options.className ?? '',
|
|
118
|
+
style: options.style ?? {},
|
|
119
|
+
offset: options.offset ?? VIEWPORT_OFFSET,
|
|
120
|
+
mobileOffset: options.mobileOffset ?? MOBILE_VIEWPORT_OFFSET,
|
|
121
|
+
dir: options.dir ?? getDocumentDirection(),
|
|
122
|
+
swipeDirections: options.swipeDirections ?? undefined,
|
|
123
|
+
containerAriaLabel: options.containerAriaLabel ?? 'Notifications',
|
|
124
|
+
toastOptions: options.toastOptions ?? {},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
mount(target = document.body) {
|
|
128
|
+
const targetEl = typeof target === 'string' ? document.querySelector(target) : target;
|
|
129
|
+
if (!targetEl) {
|
|
130
|
+
console.error('Toaster: target element not found');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.setupTheme();
|
|
134
|
+
this.createContainer();
|
|
135
|
+
targetEl.appendChild(this.container);
|
|
136
|
+
this.setupEventListeners();
|
|
137
|
+
this.subscribeToState();
|
|
138
|
+
}
|
|
139
|
+
unmount() {
|
|
140
|
+
document.removeEventListener('keydown', this.handleKeyDown);
|
|
141
|
+
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
142
|
+
// Remove list event listeners
|
|
143
|
+
if (this.listEl && this.boundListHandlers) {
|
|
144
|
+
this.listEl.removeEventListener('mouseenter', this.boundListHandlers.mouseenter);
|
|
145
|
+
this.listEl.removeEventListener('mousemove', this.boundListHandlers.mousemove);
|
|
146
|
+
this.listEl.removeEventListener('mouseleave', this.boundListHandlers.mouseleave);
|
|
147
|
+
this.listEl.removeEventListener('pointerdown', this.boundListHandlers.pointerdown);
|
|
148
|
+
this.listEl.removeEventListener('pointerup', this.boundListHandlers.pointerup);
|
|
149
|
+
}
|
|
150
|
+
this.boundListHandlers = null;
|
|
151
|
+
this.unsubscribe?.();
|
|
152
|
+
this.container?.remove();
|
|
153
|
+
this.container = null;
|
|
154
|
+
this.listEl = null;
|
|
155
|
+
this.toastInstances.clear();
|
|
156
|
+
}
|
|
157
|
+
setupTheme() {
|
|
158
|
+
if (this.options.theme !== 'system') {
|
|
159
|
+
this.actualTheme = this.options.theme;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
163
|
+
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
164
|
+
this.actualTheme = darkQuery.matches ? 'dark' : 'light';
|
|
165
|
+
darkQuery.addEventListener('change', (e) => {
|
|
166
|
+
this.actualTheme = e.matches ? 'dark' : 'light';
|
|
167
|
+
this.listEl?.setAttribute('data-sonner-theme', this.actualTheme);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
createContainer() {
|
|
172
|
+
const [y, x] = this.options.position.split('-');
|
|
173
|
+
// Create section wrapper
|
|
174
|
+
this.container = document.createElement('section');
|
|
175
|
+
const hotkeyLabel = this.options.hotkey.join('+').replace(/Key/g, '').replace(/Digit/g, '');
|
|
176
|
+
this.container.setAttribute('aria-label', `${this.options.containerAriaLabel} ${hotkeyLabel}`);
|
|
177
|
+
this.container.setAttribute('tabindex', '-1');
|
|
178
|
+
this.container.setAttribute('aria-live', 'polite');
|
|
179
|
+
this.container.setAttribute('aria-relevant', 'additions text');
|
|
180
|
+
this.container.setAttribute('aria-atomic', 'false');
|
|
181
|
+
// Create ordered list
|
|
182
|
+
this.listEl = document.createElement('ol');
|
|
183
|
+
this.listEl.setAttribute('data-sonner-toaster', '');
|
|
184
|
+
this.listEl.setAttribute('data-sonner-theme', this.actualTheme);
|
|
185
|
+
this.listEl.setAttribute('data-y-position', y);
|
|
186
|
+
this.listEl.setAttribute('data-x-position', x);
|
|
187
|
+
this.listEl.setAttribute('dir', this.options.dir === 'auto' ? getDocumentDirection() : this.options.dir);
|
|
188
|
+
this.listEl.setAttribute('tabindex', '-1');
|
|
189
|
+
if (this.options.className) {
|
|
190
|
+
this.listEl.className = this.options.className;
|
|
191
|
+
}
|
|
192
|
+
// Apply styles - CSS custom properties must use setProperty
|
|
193
|
+
this.listEl.style.setProperty('--front-toast-height', '0px');
|
|
194
|
+
this.listEl.style.setProperty('--width', `${TOAST_WIDTH}px`);
|
|
195
|
+
this.listEl.style.setProperty('--gap', `${this.options.gap}px`);
|
|
196
|
+
// Apply offset styles (CSS custom properties)
|
|
197
|
+
const offsetStyles = assignOffset(this.options.offset, this.options.mobileOffset);
|
|
198
|
+
Object.entries(offsetStyles).forEach(([key, value]) => {
|
|
199
|
+
this.listEl.style.setProperty(key, value);
|
|
200
|
+
});
|
|
201
|
+
// Apply any additional user styles
|
|
202
|
+
Object.assign(this.listEl.style, this.options.style);
|
|
203
|
+
this.container.appendChild(this.listEl);
|
|
204
|
+
}
|
|
205
|
+
setupEventListeners() {
|
|
206
|
+
// Hotkey listener
|
|
207
|
+
document.addEventListener('keydown', this.handleKeyDown);
|
|
208
|
+
// Document visibility
|
|
209
|
+
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
210
|
+
// List interactions - store references for cleanup
|
|
211
|
+
this.boundListHandlers = {
|
|
212
|
+
mouseenter: () => this.setExpanded(true),
|
|
213
|
+
mousemove: () => this.setExpanded(true),
|
|
214
|
+
mouseleave: () => {
|
|
215
|
+
if (!this.interacting)
|
|
216
|
+
this.setExpanded(false);
|
|
217
|
+
},
|
|
218
|
+
pointerdown: (e) => {
|
|
219
|
+
const target = e.target;
|
|
220
|
+
if (target.dataset.dismissible !== 'false') {
|
|
221
|
+
this.interacting = true;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
pointerup: () => {
|
|
225
|
+
this.interacting = false;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
this.listEl?.addEventListener('mouseenter', this.boundListHandlers.mouseenter);
|
|
229
|
+
this.listEl?.addEventListener('mousemove', this.boundListHandlers.mousemove);
|
|
230
|
+
this.listEl?.addEventListener('mouseleave', this.boundListHandlers.mouseleave);
|
|
231
|
+
this.listEl?.addEventListener('pointerdown', this.boundListHandlers.pointerdown);
|
|
232
|
+
this.listEl?.addEventListener('pointerup', this.boundListHandlers.pointerup);
|
|
233
|
+
}
|
|
234
|
+
setExpanded(expanded) {
|
|
235
|
+
this.expanded = expanded;
|
|
236
|
+
this.toastInstances.forEach((instance) => {
|
|
237
|
+
instance.element.dataset.expanded = String(expanded || this.options.expand);
|
|
238
|
+
});
|
|
239
|
+
// Update positions to recalculate heights for expanded/collapsed state
|
|
240
|
+
this.updatePositions();
|
|
241
|
+
}
|
|
242
|
+
subscribeToState() {
|
|
243
|
+
this.unsubscribe = ToastState.subscribe((toast) => {
|
|
244
|
+
if (toast.dismiss) {
|
|
245
|
+
this.dismissToast(toast.id);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const toastData = toast;
|
|
249
|
+
// Filter by toasterId - only show toasts that match this toaster's id
|
|
250
|
+
// If toaster has an id, only show toasts with matching toasterId
|
|
251
|
+
// If toaster has no id, only show toasts without a toasterId
|
|
252
|
+
if (this.options.id) {
|
|
253
|
+
if (toastData.toasterId !== this.options.id)
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
if (toastData.toasterId)
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.addOrUpdateToast(toastData);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
addOrUpdateToast(toast) {
|
|
265
|
+
const existing = this.toastInstances.get(toast.id);
|
|
266
|
+
if (existing) {
|
|
267
|
+
// Update existing toast
|
|
268
|
+
this.updateToastElement(existing, toast);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// Create new toast
|
|
272
|
+
this.createToast(toast);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
createToast(toast) {
|
|
276
|
+
const [y, x] = (toast.position || this.options.position).split('-');
|
|
277
|
+
const duration = toast.duration ?? this.options.toastOptions?.duration ?? this.options.duration;
|
|
278
|
+
const dismissible = toast.dismissible !== false;
|
|
279
|
+
const closeButton = toast.closeButton ?? this.options.toastOptions?.closeButton ?? this.options.closeButton;
|
|
280
|
+
const toastType = toast.type;
|
|
281
|
+
// Create list item
|
|
282
|
+
const li = document.createElement('li');
|
|
283
|
+
li.setAttribute('tabindex', '0');
|
|
284
|
+
li.setAttribute('data-sonner-toast', '');
|
|
285
|
+
li.setAttribute('data-styled', String(!toast.custom && !toast.unstyled && !this.options.toastOptions?.unstyled));
|
|
286
|
+
li.setAttribute('data-mounted', 'false');
|
|
287
|
+
li.setAttribute('data-promise', String(Boolean(toast.promise)));
|
|
288
|
+
li.setAttribute('data-swiped', 'false');
|
|
289
|
+
li.setAttribute('data-removed', 'false');
|
|
290
|
+
li.setAttribute('data-visible', 'true');
|
|
291
|
+
li.setAttribute('data-y-position', y);
|
|
292
|
+
li.setAttribute('data-x-position', x);
|
|
293
|
+
li.setAttribute('data-front', 'true');
|
|
294
|
+
li.setAttribute('data-swiping', 'false');
|
|
295
|
+
li.setAttribute('data-dismissible', String(dismissible));
|
|
296
|
+
li.setAttribute('data-type', toastType || '');
|
|
297
|
+
li.setAttribute('data-invert', String(toast.invert ?? this.options.invert));
|
|
298
|
+
li.setAttribute('data-swipe-out', 'false');
|
|
299
|
+
li.setAttribute('data-expanded', String(this.expanded || this.options.expand));
|
|
300
|
+
li.setAttribute('data-rich-colors', String(toast.richColors ?? this.options.richColors));
|
|
301
|
+
if (toast.testId) {
|
|
302
|
+
li.setAttribute('data-testid', toast.testId);
|
|
303
|
+
}
|
|
304
|
+
li.className = cn(this.options.toastOptions?.className, toast.className, this.options.toastOptions?.classNames?.toast, toast.classNames?.toast, this.options.toastOptions?.classNames?.[toastType], toast.classNames?.[toastType]);
|
|
305
|
+
// Build toast content
|
|
306
|
+
this.buildToastContent(li, toast, closeButton);
|
|
307
|
+
// Create instance
|
|
308
|
+
const instance = {
|
|
309
|
+
toast,
|
|
310
|
+
element: li,
|
|
311
|
+
mounted: false,
|
|
312
|
+
removed: false,
|
|
313
|
+
height: 0,
|
|
314
|
+
offset: 0,
|
|
315
|
+
remainingTime: duration,
|
|
316
|
+
closeTimerStart: 0,
|
|
317
|
+
swiping: false,
|
|
318
|
+
swipeDirection: null,
|
|
319
|
+
pointerStart: null,
|
|
320
|
+
dragStartTime: null,
|
|
321
|
+
isSwiped: false,
|
|
322
|
+
};
|
|
323
|
+
// Setup pointer events for swipe
|
|
324
|
+
this.setupSwipeHandlers(instance, dismissible);
|
|
325
|
+
// Add to DOM
|
|
326
|
+
this.listEl?.prepend(li);
|
|
327
|
+
this.toastInstances.set(toast.id, instance);
|
|
328
|
+
// Measure height after adding to DOM
|
|
329
|
+
requestAnimationFrame(() => {
|
|
330
|
+
const height = li.getBoundingClientRect().height;
|
|
331
|
+
instance.height = height;
|
|
332
|
+
this.heights.unshift({ toastId: toast.id, height, position: toast.position || this.options.position });
|
|
333
|
+
// Set mounted and update positions
|
|
334
|
+
li.dataset.mounted = 'true';
|
|
335
|
+
this.updatePositions();
|
|
336
|
+
// Start close timer
|
|
337
|
+
if (toastType !== 'loading' && toast.promise === undefined && duration !== Infinity) {
|
|
338
|
+
this.startTimer(instance);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
buildToastContent(li, toast, closeButton) {
|
|
343
|
+
const toastType = toast.type;
|
|
344
|
+
// Handle custom content - replaces the default structure
|
|
345
|
+
if (toast.custom) {
|
|
346
|
+
let customElement;
|
|
347
|
+
if (typeof toast.custom === 'string') {
|
|
348
|
+
// HTML string
|
|
349
|
+
const wrapper = document.createElement('div');
|
|
350
|
+
wrapper.innerHTML = toast.custom;
|
|
351
|
+
customElement = wrapper.firstElementChild || wrapper;
|
|
352
|
+
}
|
|
353
|
+
else if (typeof toast.custom === 'function') {
|
|
354
|
+
// Builder function
|
|
355
|
+
customElement = toast.custom(toast.id);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
// HTMLElement
|
|
359
|
+
customElement = toast.custom;
|
|
360
|
+
}
|
|
361
|
+
li.appendChild(customElement);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// Close button
|
|
365
|
+
if (closeButton && toastType !== 'loading') {
|
|
366
|
+
const closeBtn = document.createElement('button');
|
|
367
|
+
closeBtn.setAttribute('aria-label', 'Close toast');
|
|
368
|
+
closeBtn.setAttribute('data-close-button', '');
|
|
369
|
+
closeBtn.className = cn(this.options.toastOptions?.classNames?.closeButton, toast.classNames?.closeButton);
|
|
370
|
+
closeBtn.innerHTML = CloseIcon;
|
|
371
|
+
closeBtn.addEventListener('click', () => {
|
|
372
|
+
if (toast.dismissible !== false) {
|
|
373
|
+
this.removeToast(toast);
|
|
374
|
+
toast.onDismiss?.(toast);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
li.appendChild(closeBtn);
|
|
378
|
+
}
|
|
379
|
+
// Icon
|
|
380
|
+
if (toastType || toast.icon) {
|
|
381
|
+
const iconWrapper = document.createElement('div');
|
|
382
|
+
iconWrapper.setAttribute('data-icon', '');
|
|
383
|
+
iconWrapper.className = cn(this.options.toastOptions?.classNames?.icon, toast.classNames?.icon);
|
|
384
|
+
if (toast.type === 'loading' && !toast.icon) {
|
|
385
|
+
const loader = createLoader(true, cn(this.options.toastOptions?.classNames?.loader, toast.classNames?.loader));
|
|
386
|
+
iconWrapper.appendChild(loader);
|
|
387
|
+
}
|
|
388
|
+
else if (toast.icon) {
|
|
389
|
+
if (typeof toast.icon === 'string') {
|
|
390
|
+
iconWrapper.innerHTML = toast.icon;
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
iconWrapper.appendChild(toast.icon);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
const asset = getAsset(toastType);
|
|
398
|
+
if (asset) {
|
|
399
|
+
iconWrapper.innerHTML = asset;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
li.appendChild(iconWrapper);
|
|
403
|
+
}
|
|
404
|
+
// Content
|
|
405
|
+
const content = document.createElement('div');
|
|
406
|
+
content.setAttribute('data-content', '');
|
|
407
|
+
content.className = cn(this.options.toastOptions?.classNames?.content, toast.classNames?.content);
|
|
408
|
+
// Title
|
|
409
|
+
const title = document.createElement('div');
|
|
410
|
+
title.setAttribute('data-title', '');
|
|
411
|
+
title.className = cn(this.options.toastOptions?.classNames?.title, toast.classNames?.title);
|
|
412
|
+
title.textContent = toast.title || '';
|
|
413
|
+
content.appendChild(title);
|
|
414
|
+
// Description
|
|
415
|
+
if (toast.description) {
|
|
416
|
+
const desc = document.createElement('div');
|
|
417
|
+
desc.setAttribute('data-description', '');
|
|
418
|
+
desc.className = cn(this.options.toastOptions?.descriptionClassName, toast.descriptionClassName, this.options.toastOptions?.classNames?.description, toast.classNames?.description);
|
|
419
|
+
desc.textContent = toast.description;
|
|
420
|
+
content.appendChild(desc);
|
|
421
|
+
}
|
|
422
|
+
li.appendChild(content);
|
|
423
|
+
// Cancel button
|
|
424
|
+
if (toast.cancel && isAction(toast.cancel)) {
|
|
425
|
+
const cancelBtn = document.createElement('button');
|
|
426
|
+
cancelBtn.setAttribute('data-button', '');
|
|
427
|
+
cancelBtn.setAttribute('data-cancel', '');
|
|
428
|
+
cancelBtn.className = cn(this.options.toastOptions?.classNames?.cancelButton, toast.classNames?.cancelButton);
|
|
429
|
+
cancelBtn.textContent = toast.cancel.label;
|
|
430
|
+
Object.assign(cancelBtn.style, toast.cancelButtonStyle || this.options.toastOptions?.cancelButtonStyle);
|
|
431
|
+
cancelBtn.addEventListener('click', (e) => {
|
|
432
|
+
if (toast.dismissible !== false) {
|
|
433
|
+
toast.cancel.onClick(e);
|
|
434
|
+
this.removeToast(toast);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
li.appendChild(cancelBtn);
|
|
438
|
+
}
|
|
439
|
+
// Action button
|
|
440
|
+
if (toast.action && isAction(toast.action)) {
|
|
441
|
+
const actionBtn = document.createElement('button');
|
|
442
|
+
actionBtn.setAttribute('data-button', '');
|
|
443
|
+
actionBtn.setAttribute('data-action', '');
|
|
444
|
+
actionBtn.className = cn(this.options.toastOptions?.classNames?.actionButton, toast.classNames?.actionButton);
|
|
445
|
+
actionBtn.textContent = toast.action.label;
|
|
446
|
+
Object.assign(actionBtn.style, toast.actionButtonStyle || this.options.toastOptions?.actionButtonStyle);
|
|
447
|
+
actionBtn.addEventListener('click', (e) => {
|
|
448
|
+
toast.action.onClick(e);
|
|
449
|
+
if (!e.defaultPrevented) {
|
|
450
|
+
this.removeToast(toast);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
li.appendChild(actionBtn);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
setupSwipeHandlers(instance, dismissible) {
|
|
457
|
+
const { element: li, toast } = instance;
|
|
458
|
+
const position = toast.position || this.options.position;
|
|
459
|
+
const swipeDirections = this.options.swipeDirections ?? getDefaultSwipeDirections(position);
|
|
460
|
+
li.addEventListener('pointerdown', (e) => {
|
|
461
|
+
if (e.button === 2)
|
|
462
|
+
return; // Right click
|
|
463
|
+
if (toast.type === 'loading' || !dismissible)
|
|
464
|
+
return;
|
|
465
|
+
instance.dragStartTime = new Date();
|
|
466
|
+
instance.pointerStart = { x: e.clientX, y: e.clientY };
|
|
467
|
+
e.target.setPointerCapture(e.pointerId);
|
|
468
|
+
if (e.target.tagName !== 'BUTTON') {
|
|
469
|
+
instance.swiping = true;
|
|
470
|
+
li.dataset.swiping = 'true';
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
li.addEventListener('pointermove', (e) => {
|
|
474
|
+
if (!instance.pointerStart || !dismissible)
|
|
475
|
+
return;
|
|
476
|
+
const isHighlighted = (window.getSelection()?.toString().length ?? 0) > 0;
|
|
477
|
+
if (isHighlighted)
|
|
478
|
+
return;
|
|
479
|
+
const yDelta = e.clientY - instance.pointerStart.y;
|
|
480
|
+
const xDelta = e.clientX - instance.pointerStart.x;
|
|
481
|
+
// Determine swipe direction
|
|
482
|
+
if (!instance.swipeDirection && (Math.abs(xDelta) > 1 || Math.abs(yDelta) > 1)) {
|
|
483
|
+
instance.swipeDirection = Math.abs(xDelta) > Math.abs(yDelta) ? 'x' : 'y';
|
|
484
|
+
}
|
|
485
|
+
let swipeAmount = { x: 0, y: 0 };
|
|
486
|
+
const getDampening = (delta) => {
|
|
487
|
+
const factor = Math.abs(delta) / 20;
|
|
488
|
+
return 1 / (1.5 + factor);
|
|
489
|
+
};
|
|
490
|
+
if (instance.swipeDirection === 'y') {
|
|
491
|
+
if (swipeDirections.includes('top') || swipeDirections.includes('bottom')) {
|
|
492
|
+
if ((swipeDirections.includes('top') && yDelta < 0) || (swipeDirections.includes('bottom') && yDelta > 0)) {
|
|
493
|
+
swipeAmount.y = yDelta;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
const dampenedDelta = yDelta * getDampening(yDelta);
|
|
497
|
+
swipeAmount.y = Math.abs(dampenedDelta) < Math.abs(yDelta) ? dampenedDelta : yDelta;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else if (instance.swipeDirection === 'x') {
|
|
502
|
+
if (swipeDirections.includes('left') || swipeDirections.includes('right')) {
|
|
503
|
+
if ((swipeDirections.includes('left') && xDelta < 0) || (swipeDirections.includes('right') && xDelta > 0)) {
|
|
504
|
+
swipeAmount.x = xDelta;
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
const dampenedDelta = xDelta * getDampening(xDelta);
|
|
508
|
+
swipeAmount.x = Math.abs(dampenedDelta) < Math.abs(xDelta) ? dampenedDelta : xDelta;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (Math.abs(swipeAmount.x) > 0 || Math.abs(swipeAmount.y) > 0) {
|
|
513
|
+
instance.isSwiped = true;
|
|
514
|
+
li.dataset.swiped = 'true';
|
|
515
|
+
}
|
|
516
|
+
li.style.setProperty('--swipe-amount-x', `${swipeAmount.x}px`);
|
|
517
|
+
li.style.setProperty('--swipe-amount-y', `${swipeAmount.y}px`);
|
|
518
|
+
});
|
|
519
|
+
li.addEventListener('pointerup', () => {
|
|
520
|
+
if (!dismissible)
|
|
521
|
+
return;
|
|
522
|
+
const swipeAmountX = parseFloat(li.style.getPropertyValue('--swipe-amount-x') || '0');
|
|
523
|
+
const swipeAmountY = parseFloat(li.style.getPropertyValue('--swipe-amount-y') || '0');
|
|
524
|
+
const timeTaken = instance.dragStartTime ? new Date().getTime() - instance.dragStartTime.getTime() : 1000;
|
|
525
|
+
const swipeAmount = instance.swipeDirection === 'x' ? swipeAmountX : swipeAmountY;
|
|
526
|
+
const velocity = Math.abs(swipeAmount) / timeTaken;
|
|
527
|
+
if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
|
|
528
|
+
toast.onDismiss?.(toast);
|
|
529
|
+
if (instance.swipeDirection === 'x') {
|
|
530
|
+
li.dataset.swipeDirection = swipeAmountX > 0 ? 'right' : 'left';
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
li.dataset.swipeDirection = swipeAmountY > 0 ? 'down' : 'up';
|
|
534
|
+
}
|
|
535
|
+
li.dataset.swipeOut = 'true';
|
|
536
|
+
this.removeToast(toast);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
li.style.setProperty('--swipe-amount-x', '0px');
|
|
540
|
+
li.style.setProperty('--swipe-amount-y', '0px');
|
|
541
|
+
}
|
|
542
|
+
instance.isSwiped = false;
|
|
543
|
+
instance.swiping = false;
|
|
544
|
+
instance.swipeDirection = null;
|
|
545
|
+
instance.pointerStart = null;
|
|
546
|
+
li.dataset.swiped = 'false';
|
|
547
|
+
li.dataset.swiping = 'false';
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
updateToastElement(instance, toast) {
|
|
551
|
+
const { element: li } = instance;
|
|
552
|
+
instance.toast = toast;
|
|
553
|
+
// Update type
|
|
554
|
+
li.dataset.type = toast.type || '';
|
|
555
|
+
// Update title
|
|
556
|
+
const titleEl = li.querySelector('[data-title]');
|
|
557
|
+
if (titleEl) {
|
|
558
|
+
titleEl.textContent = toast.title || '';
|
|
559
|
+
}
|
|
560
|
+
// Update description
|
|
561
|
+
let descEl = li.querySelector('[data-description]');
|
|
562
|
+
if (toast.description) {
|
|
563
|
+
if (!descEl) {
|
|
564
|
+
descEl = document.createElement('div');
|
|
565
|
+
descEl.setAttribute('data-description', '');
|
|
566
|
+
li.querySelector('[data-content]')?.appendChild(descEl);
|
|
567
|
+
}
|
|
568
|
+
descEl.textContent = toast.description;
|
|
569
|
+
}
|
|
570
|
+
else if (descEl) {
|
|
571
|
+
descEl.remove();
|
|
572
|
+
}
|
|
573
|
+
// Update icon if type changed
|
|
574
|
+
const iconEl = li.querySelector('[data-icon]');
|
|
575
|
+
if (iconEl && toast.type && toast.type !== 'loading') {
|
|
576
|
+
const asset = getAsset(toast.type);
|
|
577
|
+
if (asset) {
|
|
578
|
+
iconEl.innerHTML = asset;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// Restart timer for non-loading types
|
|
582
|
+
if (toast.type !== 'loading') {
|
|
583
|
+
instance.remainingTime = toast.duration ?? this.options.duration;
|
|
584
|
+
this.startTimer(instance);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
startTimer(instance) {
|
|
588
|
+
const { toast } = instance;
|
|
589
|
+
if (toast.promise && toast.type === 'loading')
|
|
590
|
+
return;
|
|
591
|
+
if (toast.duration === Infinity || toast.type === 'loading')
|
|
592
|
+
return;
|
|
593
|
+
if (this.expanded || this.interacting || this.isDocumentHidden)
|
|
594
|
+
return;
|
|
595
|
+
clearTimeout(instance.closeTimeout);
|
|
596
|
+
instance.closeTimerStart = Date.now();
|
|
597
|
+
instance.closeTimeout = setTimeout(() => {
|
|
598
|
+
toast.onAutoClose?.(toast);
|
|
599
|
+
this.removeToast(toast);
|
|
600
|
+
}, instance.remainingTime);
|
|
601
|
+
}
|
|
602
|
+
pauseTimer(instance) {
|
|
603
|
+
if (instance.closeTimerStart > 0) {
|
|
604
|
+
const elapsed = Date.now() - instance.closeTimerStart;
|
|
605
|
+
instance.remainingTime = Math.max(0, instance.remainingTime - elapsed);
|
|
606
|
+
}
|
|
607
|
+
clearTimeout(instance.closeTimeout);
|
|
608
|
+
}
|
|
609
|
+
dismissToast(id) {
|
|
610
|
+
const instance = this.toastInstances.get(id);
|
|
611
|
+
if (instance) {
|
|
612
|
+
instance.element.dataset.removed = 'true';
|
|
613
|
+
this.removeToast(instance.toast);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
removeToast(toast) {
|
|
617
|
+
const instance = this.toastInstances.get(toast.id);
|
|
618
|
+
if (!instance || instance.removed)
|
|
619
|
+
return;
|
|
620
|
+
instance.removed = true;
|
|
621
|
+
instance.element.dataset.removed = 'true';
|
|
622
|
+
clearTimeout(instance.closeTimeout);
|
|
623
|
+
// Remove from heights
|
|
624
|
+
this.heights = this.heights.filter((h) => h.toastId !== toast.id);
|
|
625
|
+
setTimeout(() => {
|
|
626
|
+
instance.element.remove();
|
|
627
|
+
this.toastInstances.delete(toast.id);
|
|
628
|
+
ToastState.dismiss(toast.id);
|
|
629
|
+
this.updatePositions();
|
|
630
|
+
}, TIME_BEFORE_UNMOUNT);
|
|
631
|
+
this.updatePositions();
|
|
632
|
+
}
|
|
633
|
+
updatePositions() {
|
|
634
|
+
const { visibleToasts, gap, expand } = this.options;
|
|
635
|
+
// Use heights array order (newest first) to determine positioning
|
|
636
|
+
const orderedToasts = this.heights
|
|
637
|
+
.map((h) => this.toastInstances.get(h.toastId))
|
|
638
|
+
.filter((instance) => instance !== undefined && !instance.removed);
|
|
639
|
+
const newFrontHeight = orderedToasts.length > 0 ? orderedToasts[0].height : 0;
|
|
640
|
+
const isExpanded = this.expanded || expand;
|
|
641
|
+
// FIRST PASS: Lock current heights to enable transitions
|
|
642
|
+
// This captures the current rendered height before we change anything
|
|
643
|
+
// We do NOT change data-front here to avoid triggering opacity changes
|
|
644
|
+
orderedToasts.forEach((instance, index) => {
|
|
645
|
+
const { element: li } = instance;
|
|
646
|
+
const isFront = index === 0;
|
|
647
|
+
if (!isFront && !isExpanded) {
|
|
648
|
+
// Get current computed height and set it explicitly
|
|
649
|
+
// This gives the transition a "from" value
|
|
650
|
+
const currentHeight = li.getBoundingClientRect().height;
|
|
651
|
+
if (currentHeight > 0) {
|
|
652
|
+
li.style.height = `${currentHeight}px`;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
// Force a reflow so the browser registers the current heights
|
|
657
|
+
// before we change them to new values
|
|
658
|
+
if (orderedToasts.length > 1) {
|
|
659
|
+
void this.listEl?.offsetHeight;
|
|
660
|
+
}
|
|
661
|
+
// SECOND PASS: Update heights and positions (but not data-front yet)
|
|
662
|
+
let heightBefore = 0;
|
|
663
|
+
orderedToasts.forEach((instance, index) => {
|
|
664
|
+
const { element: li } = instance;
|
|
665
|
+
const isFront = index === 0;
|
|
666
|
+
const isVisible = index < visibleToasts;
|
|
667
|
+
li.dataset.index = String(index);
|
|
668
|
+
li.dataset.visible = String(isVisible);
|
|
669
|
+
const offset = index * gap + heightBefore;
|
|
670
|
+
instance.offset = offset;
|
|
671
|
+
li.style.setProperty('--index', String(index));
|
|
672
|
+
li.style.setProperty('--toasts-before', String(index));
|
|
673
|
+
li.style.setProperty('--z-index', String(orderedToasts.length - index));
|
|
674
|
+
li.style.setProperty('--offset', `${offset}px`);
|
|
675
|
+
li.style.setProperty('--initial-height', `${instance.height}px`);
|
|
676
|
+
// Set explicit height to enable smooth CSS transitions
|
|
677
|
+
if (isExpanded) {
|
|
678
|
+
li.style.height = `${instance.height}px`;
|
|
679
|
+
}
|
|
680
|
+
else if (isFront) {
|
|
681
|
+
li.style.height = '';
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
li.style.height = `${newFrontHeight}px`;
|
|
685
|
+
}
|
|
686
|
+
heightBefore += instance.height;
|
|
687
|
+
});
|
|
688
|
+
// Update front toast height on the container
|
|
689
|
+
if (this.listEl && orderedToasts.length > 0) {
|
|
690
|
+
this.listEl.style.setProperty('--front-toast-height', `${newFrontHeight}px`);
|
|
691
|
+
}
|
|
692
|
+
// THIRD PASS: Update data-front
|
|
693
|
+
orderedToasts.forEach((instance, index) => {
|
|
694
|
+
instance.element.dataset.front = String(index === 0);
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Factory function for easier usage
|
|
699
|
+
export function createToaster(options) {
|
|
700
|
+
return new Toaster(options);
|
|
701
|
+
}
|
|
702
|
+
//# sourceMappingURL=toaster.js.map
|