@liwe3/webcomponents 1.0.2 → 1.1.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/dist/AITextEditor.d.ts +173 -0
- package/dist/AITextEditor.d.ts.map +1 -0
- package/dist/ChunkUploader.d.ts +103 -0
- package/dist/ChunkUploader.d.ts.map +1 -0
- package/dist/ChunkUploader.js +614 -0
- package/dist/ChunkUploader.js.map +1 -0
- package/dist/ContainerBox.d.ts +112 -0
- package/dist/ContainerBox.d.ts.map +1 -0
- package/dist/ContainerBox.js +359 -0
- package/dist/ContainerBox.js.map +1 -0
- package/dist/DateSelector.d.ts +103 -0
- package/dist/DateSelector.d.ts.map +1 -0
- package/dist/DateSelector.js +372 -0
- package/dist/DateSelector.js.map +1 -0
- package/dist/Drawer.d.ts +63 -0
- package/dist/Drawer.d.ts.map +1 -0
- package/dist/Drawer.js +340 -0
- package/dist/Drawer.js.map +1 -0
- package/dist/ImageView.d.ts +42 -0
- package/dist/ImageView.d.ts.map +1 -0
- package/dist/ImageView.js +209 -0
- package/dist/ImageView.js.map +1 -0
- package/dist/PopoverMenu.d.ts +103 -0
- package/dist/PopoverMenu.d.ts.map +1 -0
- package/dist/PopoverMenu.js +312 -0
- package/dist/PopoverMenu.js.map +1 -0
- package/dist/SmartSelect.d.ts +99 -0
- package/dist/SmartSelect.d.ts.map +1 -0
- package/dist/SmartSelect.js.map +1 -1
- package/dist/Toast.d.ts +127 -0
- package/dist/Toast.d.ts.map +1 -0
- package/dist/Toast.js +507 -0
- package/dist/Toast.js.map +1 -0
- package/dist/TreeView.d.ts +84 -0
- package/dist/TreeView.d.ts.map +1 -0
- package/dist/TreeView.js +478 -0
- package/dist/TreeView.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -6
- package/dist/index.js.map +1 -1
- package/package.json +43 -3
- package/src/ChunkUploader.ts +921 -0
- package/src/ContainerBox.ts +570 -0
- package/src/DateSelector.ts +550 -0
- package/src/Drawer.ts +435 -0
- package/src/ImageView.ts +265 -0
- package/src/PopoverMenu.ts +595 -0
- package/src/SmartSelect.ts +231 -231
- package/src/Toast.ts +834 -0
- package/src/TreeView.ts +673 -0
- package/src/index.ts +70 -3
package/src/Toast.ts
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast Web Component
|
|
3
|
+
* A customizable toast notification system with multiple types, icons, buttons, and auto-dismiss
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ToastType = 'info' | 'warning' | 'error' | 'success';
|
|
7
|
+
|
|
8
|
+
export type ToastPosition = 'TL' | 'T' | 'TR' | 'BL' | 'B' | 'BR';
|
|
9
|
+
|
|
10
|
+
export type ToastButton = {
|
|
11
|
+
label: string;
|
|
12
|
+
onClick: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ToastConfig = {
|
|
16
|
+
title: string;
|
|
17
|
+
text: string;
|
|
18
|
+
type?: ToastType;
|
|
19
|
+
icon?: string; // URL to icon/image
|
|
20
|
+
buttons?: ToastButton[];
|
|
21
|
+
closable?: boolean; // Show close X button
|
|
22
|
+
duration?: number; // Auto-dismiss after x milliseconds (0 = no auto-dismiss, default: 5000ms)
|
|
23
|
+
position?: ToastPosition; // Toast container position (default: 'TR')
|
|
24
|
+
onClose?: () => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class ToastElement extends HTMLElement {
|
|
28
|
+
declare shadowRoot: ShadowRoot;
|
|
29
|
+
private config: ToastConfig = {
|
|
30
|
+
title: '',
|
|
31
|
+
text: '',
|
|
32
|
+
type: 'info',
|
|
33
|
+
closable: true,
|
|
34
|
+
duration: 5000
|
|
35
|
+
};
|
|
36
|
+
private autoCloseTimer?: number;
|
|
37
|
+
private remainingTime: number = 0;
|
|
38
|
+
private pauseTime: number = 0;
|
|
39
|
+
private progressBar?: HTMLElement;
|
|
40
|
+
|
|
41
|
+
constructor () {
|
|
42
|
+
super();
|
|
43
|
+
this.attachShadow( { mode: 'open' } );
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static get observedAttributes (): string[] {
|
|
47
|
+
return [ 'title', 'text', 'type', 'icon', 'closable', 'duration', 'buttons' ];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
attributeChangedCallback ( _name: string, oldValue: string | null, newValue: string | null ): void {
|
|
51
|
+
if ( oldValue !== newValue ) {
|
|
52
|
+
this.render();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
connectedCallback (): void {
|
|
57
|
+
this.render();
|
|
58
|
+
this.startAutoCloseTimer();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
disconnectedCallback (): void {
|
|
62
|
+
this.clearAutoCloseTimer();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get title (): string {
|
|
66
|
+
const attrTitle = this.getAttribute( 'title' );
|
|
67
|
+
const configTitle = this.config.title;
|
|
68
|
+
|
|
69
|
+
// If no title is provided or empty, use capitalized type
|
|
70
|
+
if ( ( !attrTitle || attrTitle.trim() === '' ) && ( !configTitle || configTitle.trim() === '' ) ) {
|
|
71
|
+
const type = this.type;
|
|
72
|
+
return type.charAt( 0 ).toUpperCase() + type.slice( 1 );
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return attrTitle || configTitle;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
set title ( value: string ) {
|
|
79
|
+
if ( value && value.trim() !== '' ) {
|
|
80
|
+
this.setAttribute( 'title', value );
|
|
81
|
+
this.config.title = value;
|
|
82
|
+
} else {
|
|
83
|
+
this.removeAttribute( 'title' );
|
|
84
|
+
this.config.title = '';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get text (): string {
|
|
89
|
+
return this.getAttribute( 'text' ) || this.config.text;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
set text ( value: string ) {
|
|
93
|
+
this.setAttribute( 'text', value );
|
|
94
|
+
this.config.text = value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get type (): ToastType {
|
|
98
|
+
const attr = this.getAttribute( 'type' );
|
|
99
|
+
return ( attr as ToastType ) || this.config.type || 'info';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
set type ( value: ToastType ) {
|
|
103
|
+
this.setAttribute( 'type', value );
|
|
104
|
+
this.config.type = value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get icon (): string | undefined {
|
|
108
|
+
return this.getAttribute( 'icon' ) || this.config.icon;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
set icon ( value: string | undefined ) {
|
|
112
|
+
if ( value ) {
|
|
113
|
+
this.setAttribute( 'icon', value );
|
|
114
|
+
this.config.icon = value;
|
|
115
|
+
} else {
|
|
116
|
+
this.removeAttribute( 'icon' );
|
|
117
|
+
this.config.icon = undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get closable (): boolean {
|
|
122
|
+
if ( this.hasAttribute( 'closable' ) ) {
|
|
123
|
+
return this.getAttribute( 'closable' ) !== 'false';
|
|
124
|
+
}
|
|
125
|
+
return this.config.closable !== false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
set closable ( value: boolean ) {
|
|
129
|
+
if ( value ) {
|
|
130
|
+
this.setAttribute( 'closable', 'true' );
|
|
131
|
+
} else {
|
|
132
|
+
this.setAttribute( 'closable', 'false' );
|
|
133
|
+
}
|
|
134
|
+
this.config.closable = value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get duration (): number {
|
|
138
|
+
const attr = this.getAttribute( 'duration' );
|
|
139
|
+
if ( attr ) {
|
|
140
|
+
return parseInt( attr, 10 );
|
|
141
|
+
}
|
|
142
|
+
return this.config.duration ?? 5000;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
set duration ( value: number ) {
|
|
146
|
+
this.setAttribute( 'duration', value.toString() );
|
|
147
|
+
this.config.duration = value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get buttons (): ToastButton[] {
|
|
151
|
+
const attr = this.getAttribute( 'buttons' );
|
|
152
|
+
if ( attr ) {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse( attr );
|
|
155
|
+
} catch ( e ) {
|
|
156
|
+
console.error( 'Invalid buttons format:', e );
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return this.config.buttons || [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
set buttons ( value: ToastButton[] ) {
|
|
164
|
+
this.setAttribute( 'buttons', JSON.stringify( value ) );
|
|
165
|
+
this.config.buttons = value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Shows the toast with the given configuration
|
|
170
|
+
*/
|
|
171
|
+
show ( config: ToastConfig ): void {
|
|
172
|
+
this.config = { ...this.config, ...config };
|
|
173
|
+
|
|
174
|
+
// If buttons are present, force duration to 0 (user must interact to close)
|
|
175
|
+
if ( config.buttons && config.buttons.length > 0 ) {
|
|
176
|
+
this.config.duration = 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Sync config to attributes
|
|
180
|
+
if ( config.title && config.title.trim() !== '' ) {
|
|
181
|
+
this.title = config.title;
|
|
182
|
+
} else {
|
|
183
|
+
// Clear title if not provided or empty
|
|
184
|
+
this.removeAttribute( 'title' );
|
|
185
|
+
this.config.title = '';
|
|
186
|
+
}
|
|
187
|
+
this.text = config.text;
|
|
188
|
+
if ( config.type ) this.type = config.type;
|
|
189
|
+
if ( config.icon !== undefined ) this.icon = config.icon;
|
|
190
|
+
if ( config.closable !== undefined ) this.closable = config.closable;
|
|
191
|
+
if ( config.buttons && config.buttons.length > 0 ) {
|
|
192
|
+
// Force duration to 0 when buttons are present
|
|
193
|
+
this.duration = 0;
|
|
194
|
+
} else if ( config.duration !== undefined ) {
|
|
195
|
+
this.duration = config.duration;
|
|
196
|
+
}
|
|
197
|
+
if ( config.buttons ) this.buttons = config.buttons;
|
|
198
|
+
|
|
199
|
+
this.render();
|
|
200
|
+
this.startAutoCloseTimer();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Closes the toast
|
|
205
|
+
*/
|
|
206
|
+
close (): void {
|
|
207
|
+
this.clearAutoCloseTimer();
|
|
208
|
+
|
|
209
|
+
// Add closing animation
|
|
210
|
+
const container = this.shadowRoot.querySelector( '.toast-container' ) as HTMLElement;
|
|
211
|
+
if ( container ) {
|
|
212
|
+
// Use requestAnimationFrame to ensure smooth animation
|
|
213
|
+
requestAnimationFrame( () => {
|
|
214
|
+
container.classList.add( 'closing' );
|
|
215
|
+
} );
|
|
216
|
+
|
|
217
|
+
// Listen for animation end event for smoother transition
|
|
218
|
+
const handleAnimationEnd = () => {
|
|
219
|
+
container.removeEventListener( 'animationend', handleAnimationEnd );
|
|
220
|
+
|
|
221
|
+
// Animate the host element collapsing (height and margin to 0)
|
|
222
|
+
const hostElement = this as unknown as HTMLElement;
|
|
223
|
+
const currentHeight = hostElement.offsetHeight;
|
|
224
|
+
|
|
225
|
+
// Set explicit height for animation
|
|
226
|
+
hostElement.style.height = `${ currentHeight }px`;
|
|
227
|
+
hostElement.style.marginBottom = '12px';
|
|
228
|
+
|
|
229
|
+
// Force reflow
|
|
230
|
+
void hostElement.offsetHeight;
|
|
231
|
+
|
|
232
|
+
// Animate to 0
|
|
233
|
+
hostElement.style.height = '0px';
|
|
234
|
+
hostElement.style.marginBottom = '0px';
|
|
235
|
+
hostElement.style.opacity = '0';
|
|
236
|
+
|
|
237
|
+
// Wait for transition to complete, then remove
|
|
238
|
+
setTimeout( () => {
|
|
239
|
+
this.dispatchEvent( new CustomEvent( 'close' ) );
|
|
240
|
+
if ( this.config.onClose ) {
|
|
241
|
+
this.config.onClose();
|
|
242
|
+
}
|
|
243
|
+
this.remove();
|
|
244
|
+
}, 300 );
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
container.addEventListener( 'animationend', handleAnimationEnd );
|
|
248
|
+
|
|
249
|
+
// Fallback timeout in case animationend doesn't fire
|
|
250
|
+
setTimeout( () => {
|
|
251
|
+
if ( this.isConnected ) {
|
|
252
|
+
handleAnimationEnd();
|
|
253
|
+
}
|
|
254
|
+
}, 350 );
|
|
255
|
+
} else {
|
|
256
|
+
this.dispatchEvent( new CustomEvent( 'close' ) );
|
|
257
|
+
if ( this.config.onClose ) {
|
|
258
|
+
this.config.onClose();
|
|
259
|
+
}
|
|
260
|
+
this.remove();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Starts the auto-close timer if duration is set
|
|
266
|
+
*/
|
|
267
|
+
private startAutoCloseTimer (): void {
|
|
268
|
+
this.clearAutoCloseTimer();
|
|
269
|
+
|
|
270
|
+
if ( this.duration > 0 ) {
|
|
271
|
+
this.remainingTime = this.duration;
|
|
272
|
+
this.pauseTime = Date.now();
|
|
273
|
+
this.autoCloseTimer = window.setTimeout( () => {
|
|
274
|
+
this.close();
|
|
275
|
+
}, this.duration );
|
|
276
|
+
|
|
277
|
+
// Start progress bar animation
|
|
278
|
+
this.startProgressBarAnimation();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Pauses the auto-close timer
|
|
284
|
+
*/
|
|
285
|
+
private pauseAutoCloseTimer (): void {
|
|
286
|
+
if ( this.autoCloseTimer && this.duration > 0 ) {
|
|
287
|
+
clearTimeout( this.autoCloseTimer );
|
|
288
|
+
this.autoCloseTimer = undefined;
|
|
289
|
+
this.remainingTime -= Date.now() - this.pauseTime;
|
|
290
|
+
|
|
291
|
+
// Pause progress bar animation
|
|
292
|
+
this.pauseProgressBarAnimation();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Resumes the auto-close timer
|
|
298
|
+
*/
|
|
299
|
+
private resumeAutoCloseTimer (): void {
|
|
300
|
+
if ( !this.autoCloseTimer && this.remainingTime > 0 ) {
|
|
301
|
+
this.pauseTime = Date.now();
|
|
302
|
+
this.autoCloseTimer = window.setTimeout( () => {
|
|
303
|
+
this.close();
|
|
304
|
+
}, this.remainingTime );
|
|
305
|
+
|
|
306
|
+
// Resume progress bar animation
|
|
307
|
+
this.resumeProgressBarAnimation();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Clears the auto-close timer
|
|
313
|
+
*/
|
|
314
|
+
private clearAutoCloseTimer (): void {
|
|
315
|
+
if ( this.autoCloseTimer ) {
|
|
316
|
+
clearTimeout( this.autoCloseTimer );
|
|
317
|
+
this.autoCloseTimer = undefined;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Starts the progress bar animation
|
|
323
|
+
*/
|
|
324
|
+
private startProgressBarAnimation (): void {
|
|
325
|
+
if ( !this.progressBar || this.duration <= 0 ) return;
|
|
326
|
+
|
|
327
|
+
// Reset and start the animation
|
|
328
|
+
this.progressBar.style.animation = 'none';
|
|
329
|
+
// Force a reflow to reset the animation
|
|
330
|
+
void this.progressBar.offsetWidth;
|
|
331
|
+
this.progressBar.style.animation = `shrinkProgress ${ this.duration }ms linear forwards`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Pauses the progress bar animation
|
|
336
|
+
*/
|
|
337
|
+
private pauseProgressBarAnimation (): void {
|
|
338
|
+
if ( !this.progressBar ) return;
|
|
339
|
+
|
|
340
|
+
// Get the current computed width as a percentage of the container
|
|
341
|
+
const computedStyle = window.getComputedStyle( this.progressBar );
|
|
342
|
+
const currentWidth = computedStyle.width;
|
|
343
|
+
const containerWidth = this.progressBar.parentElement?.offsetWidth || 1;
|
|
344
|
+
const widthPercent = ( parseFloat( currentWidth ) / containerWidth ) * 100;
|
|
345
|
+
|
|
346
|
+
// Stop the animation and set the width directly
|
|
347
|
+
this.progressBar.style.animation = 'none';
|
|
348
|
+
this.progressBar.style.width = `${ widthPercent }%`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Resumes the progress bar animation
|
|
353
|
+
*/
|
|
354
|
+
private resumeProgressBarAnimation (): void {
|
|
355
|
+
if ( !this.progressBar || this.remainingTime <= 0 ) return;
|
|
356
|
+
|
|
357
|
+
// Get current width as starting point
|
|
358
|
+
const computedStyle = window.getComputedStyle( this.progressBar );
|
|
359
|
+
const currentWidth = computedStyle.width;
|
|
360
|
+
const containerWidth = this.progressBar.parentElement?.offsetWidth || 1;
|
|
361
|
+
const currentPercent = ( parseFloat( currentWidth ) / containerWidth ) * 100;
|
|
362
|
+
|
|
363
|
+
// Calculate the duration based on the remaining percentage and remaining time
|
|
364
|
+
// The animation should take exactly remainingTime to go from currentPercent to 0
|
|
365
|
+
const adjustedDuration = this.remainingTime;
|
|
366
|
+
|
|
367
|
+
// Create a new keyframe animation from current position to 0
|
|
368
|
+
const animationName = `shrinkProgress-${ Date.now() }`;
|
|
369
|
+
const styleSheet = this.shadowRoot.styleSheets[ 0 ];
|
|
370
|
+
const keyframes = `
|
|
371
|
+
@keyframes ${ animationName } {
|
|
372
|
+
from {
|
|
373
|
+
width: ${ currentPercent }%;
|
|
374
|
+
}
|
|
375
|
+
to {
|
|
376
|
+
width: 0%;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
`;
|
|
380
|
+
|
|
381
|
+
// Add the new keyframe rule
|
|
382
|
+
if ( styleSheet ) {
|
|
383
|
+
styleSheet.insertRule( keyframes, styleSheet.cssRules.length );
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Apply the animation
|
|
387
|
+
this.progressBar.style.animation = `${ animationName } ${ adjustedDuration }ms linear forwards`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Gets the color scheme for the toast type
|
|
392
|
+
*/
|
|
393
|
+
private getTypeColors (): { background: string; border: string; icon: string } {
|
|
394
|
+
const type = this.type;
|
|
395
|
+
|
|
396
|
+
switch ( type ) {
|
|
397
|
+
case 'success':
|
|
398
|
+
return {
|
|
399
|
+
background: 'var(--toast-success-background, #d4edda)',
|
|
400
|
+
border: 'var(--toast-success-border, #c3e6cb)',
|
|
401
|
+
icon: 'var(--toast-success-icon, #155724)'
|
|
402
|
+
};
|
|
403
|
+
case 'error':
|
|
404
|
+
return {
|
|
405
|
+
background: 'var(--toast-error-background, #f8d7da)',
|
|
406
|
+
border: 'var(--toast-error-border, #f5c6cb)',
|
|
407
|
+
icon: 'var(--toast-error-icon, #721c24)'
|
|
408
|
+
};
|
|
409
|
+
case 'warning':
|
|
410
|
+
return {
|
|
411
|
+
background: 'var(--toast-warning-background, #fff3cd)',
|
|
412
|
+
border: 'var(--toast-warning-border, #ffeaa7)',
|
|
413
|
+
icon: 'var(--toast-warning-icon, #856404)'
|
|
414
|
+
};
|
|
415
|
+
case 'info':
|
|
416
|
+
default:
|
|
417
|
+
return {
|
|
418
|
+
background: 'var(--toast-info-background, #d1ecf1)',
|
|
419
|
+
border: 'var(--toast-info-border, #bee5eb)',
|
|
420
|
+
icon: 'var(--toast-info-icon, #0c5460)'
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Gets the default icon for the toast type
|
|
427
|
+
*/
|
|
428
|
+
private getDefaultIcon (): string {
|
|
429
|
+
const type = this.type;
|
|
430
|
+
|
|
431
|
+
switch ( type ) {
|
|
432
|
+
case 'success':
|
|
433
|
+
return '✓';
|
|
434
|
+
case 'error':
|
|
435
|
+
return '✕';
|
|
436
|
+
case 'warning':
|
|
437
|
+
return '⚠';
|
|
438
|
+
case 'info':
|
|
439
|
+
default:
|
|
440
|
+
return 'ℹ';
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Binds all event listeners
|
|
446
|
+
*/
|
|
447
|
+
private bindEvents (): void {
|
|
448
|
+
// Handle close button click and button clicks
|
|
449
|
+
this.shadowRoot.addEventListener( 'click', ( e ) => {
|
|
450
|
+
const target = e.target as HTMLElement;
|
|
451
|
+
|
|
452
|
+
if ( target.closest( '.close-button' ) ) {
|
|
453
|
+
this.close();
|
|
454
|
+
} else if ( target.closest( '.toast-button' ) ) {
|
|
455
|
+
const buttonIndex = ( target.closest( '.toast-button' ) as HTMLElement ).dataset.index;
|
|
456
|
+
if ( buttonIndex !== undefined ) {
|
|
457
|
+
const button = this.buttons[ parseInt( buttonIndex, 10 ) ];
|
|
458
|
+
if ( button && button.onClick ) {
|
|
459
|
+
button.onClick();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} );
|
|
464
|
+
|
|
465
|
+
// Handle mouse enter/leave to pause/resume timer
|
|
466
|
+
const container = this.shadowRoot.querySelector( '.toast-container' );
|
|
467
|
+
if ( container ) {
|
|
468
|
+
container.addEventListener( 'mouseenter', () => {
|
|
469
|
+
this.pauseAutoCloseTimer();
|
|
470
|
+
} );
|
|
471
|
+
|
|
472
|
+
container.addEventListener( 'mouseleave', () => {
|
|
473
|
+
this.resumeAutoCloseTimer();
|
|
474
|
+
} );
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Renders the component
|
|
480
|
+
*/
|
|
481
|
+
private render (): void {
|
|
482
|
+
const colors = this.getTypeColors();
|
|
483
|
+
const iconContent = this.icon
|
|
484
|
+
? `<img src="${ this.icon }" alt="Toast icon" class="toast-icon-img" />`
|
|
485
|
+
: `<span class="toast-icon-default">${ this.getDefaultIcon() }</span>`;
|
|
486
|
+
|
|
487
|
+
this.shadowRoot.innerHTML = `
|
|
488
|
+
<style>
|
|
489
|
+
:host {
|
|
490
|
+
display: block;
|
|
491
|
+
font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
492
|
+
font-size: var(--font-size, 14px);
|
|
493
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.toast-container {
|
|
497
|
+
display: flex;
|
|
498
|
+
flex-direction: column;
|
|
499
|
+
min-width: 300px;
|
|
500
|
+
max-width: 500px;
|
|
501
|
+
padding: 16px;
|
|
502
|
+
background: ${ colors.background };
|
|
503
|
+
border: 1px solid ${ colors.border };
|
|
504
|
+
border-radius: var(--toast-border-radius, 8px);
|
|
505
|
+
box-shadow: var(--toast-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));
|
|
506
|
+
animation: slideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
|
507
|
+
position: relative;
|
|
508
|
+
will-change: transform, opacity;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.toast-container.closing {
|
|
512
|
+
animation: slideOut 0.3s cubic-bezier(0.4, 0, 1, 1) forwards;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
@keyframes slideIn {
|
|
516
|
+
from {
|
|
517
|
+
opacity: 0;
|
|
518
|
+
transform: translateY(-20px) scale(0.95);
|
|
519
|
+
}
|
|
520
|
+
to {
|
|
521
|
+
opacity: 1;
|
|
522
|
+
transform: translateY(0) scale(1);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
@keyframes slideOut {
|
|
527
|
+
from {
|
|
528
|
+
opacity: 1;
|
|
529
|
+
transform: translateY(0) scale(1);
|
|
530
|
+
}
|
|
531
|
+
to {
|
|
532
|
+
opacity: 0;
|
|
533
|
+
transform: translateY(-20px) scale(0.95);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.toast-header {
|
|
538
|
+
display: flex;
|
|
539
|
+
align-items: flex-start;
|
|
540
|
+
gap: 12px;
|
|
541
|
+
margin-bottom: 8px;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.toast-icon {
|
|
545
|
+
flex-shrink: 0;
|
|
546
|
+
width: 24px;
|
|
547
|
+
height: 24px;
|
|
548
|
+
display: flex;
|
|
549
|
+
align-items: center;
|
|
550
|
+
justify-content: center;
|
|
551
|
+
color: ${ colors.icon };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.toast-icon-img {
|
|
555
|
+
width: 100%;
|
|
556
|
+
height: 100%;
|
|
557
|
+
object-fit: contain;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.toast-icon-default {
|
|
561
|
+
font-size: 20px;
|
|
562
|
+
font-weight: bold;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.toast-content {
|
|
566
|
+
flex: 1;
|
|
567
|
+
min-width: 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.toast-title {
|
|
571
|
+
font-weight: 600;
|
|
572
|
+
font-size: 16px;
|
|
573
|
+
margin: 0 0 4px 0;
|
|
574
|
+
color: var(--toast-title-color, #333);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.toast-text {
|
|
578
|
+
margin: 0;
|
|
579
|
+
color: var(--toast-text-color, #555);
|
|
580
|
+
line-height: 1.5;
|
|
581
|
+
word-wrap: break-word;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.close-button {
|
|
585
|
+
position: absolute;
|
|
586
|
+
top: 8px;
|
|
587
|
+
right: 8px;
|
|
588
|
+
width: 24px;
|
|
589
|
+
height: 24px;
|
|
590
|
+
display: flex;
|
|
591
|
+
align-items: center;
|
|
592
|
+
justify-content: center;
|
|
593
|
+
background: transparent;
|
|
594
|
+
border: none;
|
|
595
|
+
cursor: pointer;
|
|
596
|
+
font-size: 20px;
|
|
597
|
+
color: var(--toast-close-color, #666);
|
|
598
|
+
border-radius: 4px;
|
|
599
|
+
transition: background-color 0.2s, color 0.2s;
|
|
600
|
+
padding: 0;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.close-button:hover {
|
|
604
|
+
background-color: var(--toast-close-hover-background, rgba(0, 0, 0, 0.1));
|
|
605
|
+
color: var(--toast-close-hover-color, #333);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.toast-buttons {
|
|
609
|
+
display: flex;
|
|
610
|
+
gap: 8px;
|
|
611
|
+
justify-content: flex-end;
|
|
612
|
+
margin-top: 12px;
|
|
613
|
+
padding-top: 12px;
|
|
614
|
+
border-top: 1px solid var(--toast-button-border, rgba(0, 0, 0, 0.1));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.toast-button {
|
|
618
|
+
padding: 6px 16px;
|
|
619
|
+
border: 1px solid var(--toast-button-border-color, #ccc);
|
|
620
|
+
border-radius: var(--toast-button-border-radius, 4px);
|
|
621
|
+
background: var(--toast-button-background, white);
|
|
622
|
+
color: var(--toast-button-color, #333);
|
|
623
|
+
font-size: 14px;
|
|
624
|
+
cursor: pointer;
|
|
625
|
+
transition: background-color 0.2s, border-color 0.2s;
|
|
626
|
+
font-family: inherit;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.toast-button:hover {
|
|
630
|
+
background-color: var(--toast-button-hover-background, #f8f9fa);
|
|
631
|
+
border-color: var(--toast-button-hover-border-color, #999);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.toast-button:active {
|
|
635
|
+
background-color: var(--toast-button-active-background, #e9ecef);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.toast-progress-bar {
|
|
639
|
+
position: absolute;
|
|
640
|
+
bottom: 0;
|
|
641
|
+
left: 0;
|
|
642
|
+
height: 4px;
|
|
643
|
+
width: 100%;
|
|
644
|
+
background: var(--toast-progress-bar-color, rgba(0, 0, 0, 0.3));
|
|
645
|
+
border-bottom-left-radius: var(--toast-border-radius, 8px);
|
|
646
|
+
border-bottom-right-radius: var(--toast-border-radius, 8px);
|
|
647
|
+
transform-origin: left;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
@keyframes shrinkProgress {
|
|
651
|
+
from {
|
|
652
|
+
width: 100%;
|
|
653
|
+
}
|
|
654
|
+
to {
|
|
655
|
+
width: 0%;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
</style>
|
|
659
|
+
|
|
660
|
+
<div class="toast-container">
|
|
661
|
+
${ this.closable ? '<button class="close-button" aria-label="Close">×</button>' : '' }
|
|
662
|
+
|
|
663
|
+
<div class="toast-header">
|
|
664
|
+
<div class="toast-icon">
|
|
665
|
+
${ iconContent }
|
|
666
|
+
</div>
|
|
667
|
+
<div class="toast-content">
|
|
668
|
+
<h4 class="toast-title">${ this.title }</h4>
|
|
669
|
+
<p class="toast-text">${ this.text }</p>
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
|
|
673
|
+
${ this.buttons.length > 0 ? `
|
|
674
|
+
<div class="toast-buttons">
|
|
675
|
+
${ this.buttons.map( ( button, index ) => `
|
|
676
|
+
<button class="toast-button" data-index="${ index }">
|
|
677
|
+
${ button.label }
|
|
678
|
+
</button>
|
|
679
|
+
`).join( '' ) }
|
|
680
|
+
</div>
|
|
681
|
+
` : '' }
|
|
682
|
+
|
|
683
|
+
${ this.duration > 0 ? '<div class="toast-progress-bar"></div>' : '' }
|
|
684
|
+
</div>
|
|
685
|
+
`;
|
|
686
|
+
|
|
687
|
+
// Store reference to progress bar
|
|
688
|
+
this.progressBar = this.shadowRoot.querySelector( '.toast-progress-bar' ) as HTMLElement;
|
|
689
|
+
|
|
690
|
+
this.bindEvents();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Conditionally defines the custom element if in a browser environment.
|
|
696
|
+
*/
|
|
697
|
+
const defineToast = ( tagName: string = 'liwe3-toast' ): void => {
|
|
698
|
+
if ( typeof window !== 'undefined' && !window.customElements.get( tagName ) ) {
|
|
699
|
+
customElements.define( tagName, ToastElement );
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// Auto-register with default tag name
|
|
704
|
+
defineToast();
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Base container ID prefix for toast notifications
|
|
708
|
+
*/
|
|
709
|
+
const CONTAINER_ID_PREFIX = 'liwe3-toast-container';
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Gets the container positioning styles based on position
|
|
713
|
+
*/
|
|
714
|
+
const getContainerStyles = ( position: ToastPosition ): { top?: string; bottom?: string; left?: string; right?: string; alignItems: string } => {
|
|
715
|
+
switch ( position ) {
|
|
716
|
+
case 'TL':
|
|
717
|
+
return { top: '20px', left: '20px', alignItems: 'flex-start' };
|
|
718
|
+
case 'T':
|
|
719
|
+
return { top: '20px', left: '50%', alignItems: 'center' };
|
|
720
|
+
case 'TR':
|
|
721
|
+
return { top: '20px', right: '20px', alignItems: 'flex-end' };
|
|
722
|
+
case 'BL':
|
|
723
|
+
return { bottom: '20px', left: '20px', alignItems: 'flex-start' };
|
|
724
|
+
case 'B':
|
|
725
|
+
return { bottom: '20px', left: '50%', alignItems: 'center' };
|
|
726
|
+
case 'BR':
|
|
727
|
+
return { bottom: '20px', right: '20px', alignItems: 'flex-end' };
|
|
728
|
+
default:
|
|
729
|
+
return { top: '20px', right: '20px', alignItems: 'flex-end' };
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Creates or gets the toast container element for the specified position
|
|
735
|
+
*/
|
|
736
|
+
const getToastContainer = ( position: ToastPosition = 'TR' ): HTMLElement => {
|
|
737
|
+
const containerId = `${ CONTAINER_ID_PREFIX }-${ position.toLowerCase() }`;
|
|
738
|
+
let container = document.getElementById( containerId );
|
|
739
|
+
|
|
740
|
+
if ( !container ) {
|
|
741
|
+
container = document.createElement( 'div' );
|
|
742
|
+
container.id = containerId;
|
|
743
|
+
container.style.position = 'fixed';
|
|
744
|
+
container.style.zIndex = '99999';
|
|
745
|
+
container.style.display = 'flex';
|
|
746
|
+
container.style.flexDirection = 'column';
|
|
747
|
+
container.style.maxWidth = '400px';
|
|
748
|
+
container.style.pointerEvents = 'none';
|
|
749
|
+
|
|
750
|
+
// Apply position-specific styles
|
|
751
|
+
const styles = getContainerStyles( position );
|
|
752
|
+
if ( styles.top ) container.style.top = styles.top;
|
|
753
|
+
if ( styles.bottom ) container.style.bottom = styles.bottom;
|
|
754
|
+
if ( styles.left ) container.style.left = styles.left;
|
|
755
|
+
if ( styles.right ) container.style.right = styles.right;
|
|
756
|
+
container.style.alignItems = styles.alignItems;
|
|
757
|
+
|
|
758
|
+
// For centered positions, apply transform to center horizontally
|
|
759
|
+
if ( position === 'T' || position === 'B' ) {
|
|
760
|
+
container.style.transform = 'translateX(-50%)';
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Add media query styles for mobile and smooth transitions
|
|
764
|
+
const styleId = `${ containerId }-styles`;
|
|
765
|
+
if ( !document.getElementById( styleId ) ) {
|
|
766
|
+
const style = document.createElement( 'style' );
|
|
767
|
+
style.id = styleId;
|
|
768
|
+
style.textContent = `
|
|
769
|
+
#${ containerId } > * {
|
|
770
|
+
margin-bottom: 12px;
|
|
771
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
772
|
+
overflow: hidden;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
#${ containerId } > *:last-child {
|
|
776
|
+
margin-bottom: 0;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
@media (max-width: 768px) {
|
|
780
|
+
#${ containerId } {
|
|
781
|
+
left: 20px !important;
|
|
782
|
+
right: 20px !important;
|
|
783
|
+
max-width: none !important;
|
|
784
|
+
transform: none !important;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
`;
|
|
788
|
+
document.head.appendChild( style );
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
document.body.appendChild( container );
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return container;
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Shows a toast notification with the given configuration.
|
|
799
|
+
* This is the recommended way to display toasts.
|
|
800
|
+
*
|
|
801
|
+
* @param config - The toast configuration
|
|
802
|
+
* @returns The toast element instance
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* ```typescript
|
|
806
|
+
* import { toastAdd } from '@liwe3/webcomponents';
|
|
807
|
+
*
|
|
808
|
+
* toastAdd({
|
|
809
|
+
* title: 'Success!',
|
|
810
|
+
* text: 'Your changes have been saved.',
|
|
811
|
+
* type: 'success',
|
|
812
|
+
* duration: 5000,
|
|
813
|
+
* position: 'TR' // Optional: top-right (default)
|
|
814
|
+
* });
|
|
815
|
+
* ```
|
|
816
|
+
*/
|
|
817
|
+
const toastAdd = ( config: ToastConfig ): ToastElement => {
|
|
818
|
+
const position = config.position || 'TR';
|
|
819
|
+
const container = getToastContainer( position );
|
|
820
|
+
const toast = document.createElement( 'liwe3-toast' ) as ToastElement;
|
|
821
|
+
|
|
822
|
+
// Allow pointer events on individual toasts
|
|
823
|
+
toast.style.pointerEvents = 'auto';
|
|
824
|
+
|
|
825
|
+
// Show the toast with the provided config
|
|
826
|
+
toast.show( config );
|
|
827
|
+
|
|
828
|
+
// Add to container
|
|
829
|
+
container.appendChild( toast );
|
|
830
|
+
|
|
831
|
+
return toast;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
export { defineToast, toastAdd };
|