vanilla-jui 1.5.8 → 1.5.10

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.
@@ -1,8 +1,16 @@
1
- import { insert, jsx } from 'vanilla-signal';
1
+ import {
2
+ type MaybeAccessor,
3
+ access,
4
+ createEffect,
5
+ createRoot,
6
+ insert,
7
+ jsx,
8
+ } from 'vanilla-signal';
2
9
  import { t } from 'vanilla-signal-i18n';
3
10
 
4
11
  import locales from '../locales/index.ts';
5
12
  import { icon } from '../primitives/icons.ts';
13
+ import { createLoading } from '../primitives/loading.ts';
6
14
  import { joinClasses } from '../utilities/class-name.ts';
7
15
  import { q } from '../utilities/dom.ts';
8
16
  import { listen } from '../utilities/events.ts';
@@ -10,7 +18,9 @@ import { randomId } from '../utilities/id.ts';
10
18
  import { timer } from '../utilities/timer.ts';
11
19
  import { validateParam } from '../utilities/types.ts';
12
20
 
13
- export type ToastType = 'info' | 'success' | 'warning' | 'error' | 'primary';
21
+ const s = (k: string) => t(k, locales);
22
+
23
+ export type ToastTheme = 'info' | 'success' | 'warning' | 'error' | 'primary';
14
24
 
15
25
  export interface ToastClassNames {
16
26
  container: string;
@@ -18,11 +28,11 @@ export interface ToastClassNames {
18
28
  icon: string;
19
29
  message: string;
20
30
  lite: string;
21
- action: string;
22
- actions: string;
31
+ confirm: string;
32
+ buttons: string;
23
33
  button: string;
24
34
  closeBtn: string;
25
- actionBtn: string;
35
+ confirmBtn: string;
26
36
  info: string;
27
37
  success: string;
28
38
  warning: string;
@@ -32,17 +42,30 @@ export interface ToastClassNames {
32
42
 
33
43
  export type ToastClassNameConfig = Partial<ToastClassNames>;
34
44
 
35
- export interface ToastOptions {
45
+ export interface ToastClassNameOptions {
36
46
  className?: ToastClassNameConfig;
37
47
  }
38
48
 
39
- export interface ToastActionProps extends ToastOptions {
40
- type?: ToastType;
49
+ export interface ToastThemeOptions extends ToastClassNameOptions {
50
+ theme?: ToastTheme;
51
+ }
52
+
53
+ export interface ToastOptions extends ToastThemeOptions {
54
+ duration?: number;
55
+ loading?: MaybeAccessor<boolean>;
56
+ text?: {
57
+ loading?: string;
58
+ };
59
+ onClose?: () => void | Promise<void>;
60
+ onCancel?: () => void | Promise<void>;
61
+ }
62
+
63
+ export interface ToastConfirmProps extends ToastThemeOptions {
41
64
  text?: {
42
65
  close?: string;
43
- action?: string;
66
+ confirm?: string;
44
67
  };
45
- onAction?: () => void | Promise<void>;
68
+ onConfirm?: () => void | Promise<void>;
46
69
  onClose?: () => void | Promise<void>;
47
70
  }
48
71
 
@@ -52,11 +75,11 @@ const DEFAULT_CLASS_NAMES: ToastClassNames = {
52
75
  icon: 'el-icon',
53
76
  message: 'el-text',
54
77
  lite: 'j-toast-lite',
55
- action: 'j-toast is-action',
56
- actions: 'toast-actions',
78
+ confirm: 'j-toast is-confirm',
79
+ buttons: 'toast-buttons',
57
80
  button: 'j-button is-sm',
58
81
  closeBtn: 'is-ghost',
59
- actionBtn: 'is-outline',
82
+ confirmBtn: 'is-outline',
60
83
  info: 'is-info',
61
84
  primary: 'is-primary',
62
85
  success: 'is-success',
@@ -64,7 +87,7 @@ const DEFAULT_CLASS_NAMES: ToastClassNames = {
64
87
  error: 'is-error',
65
88
  };
66
89
 
67
- const TOAST_TYPE_RULE = {
90
+ const TOAST_THEME_RULE = {
68
91
  type: 'string',
69
92
  enum: ['info', 'success', 'warning', 'error', 'primary'],
70
93
  };
@@ -81,10 +104,88 @@ function mergeClassNames(value?: ToastClassNameConfig): ToastClassNames {
81
104
  return { ...DEFAULT_CLASS_NAMES, ...value };
82
105
  }
83
106
 
84
- function resolveClassNames(options?: ToastOptions): ToastClassNames {
107
+ function resolveClassNames(options?: ToastClassNameOptions): ToastClassNames {
85
108
  return mergeClassNames({ ...classNames, ...options?.className });
86
109
  }
87
110
 
111
+ function addToastDisposer(element: HTMLElement, dispose: () => void): void {
112
+ const current = disposers.get(element);
113
+ disposers.set(element, () => {
114
+ current?.();
115
+ dispose();
116
+ });
117
+ }
118
+
119
+ function isToastLoading(options: ToastOptions): boolean {
120
+ return !!access(options.loading ?? false);
121
+ }
122
+
123
+ function toastIconName(theme: ToastTheme): ToastTheme {
124
+ return theme === 'primary' ? 'info' : theme;
125
+ }
126
+
127
+ function toastIconStyle(options: ToastOptions): Record<string, string> {
128
+ return isToastLoading(options)
129
+ ? {
130
+ position: 'relative',
131
+ display: 'inline-flex',
132
+ alignItems: 'center',
133
+ justifyContent: 'center',
134
+ width: 'var(--toast-icon-size, 1em)',
135
+ height: 'var(--toast-icon-size, 1em)',
136
+ flex: '0 0 auto',
137
+ }
138
+ : {};
139
+ }
140
+
141
+ function toastIconContent(
142
+ theme: ToastTheme,
143
+ options: ToastOptions
144
+ ): HTMLElement | SVGElement {
145
+ if (isToastLoading(options)) return createLoading();
146
+ return icon(toastIconName(theme));
147
+ }
148
+
149
+ function toastMessage(message: string, options: ToastOptions): string {
150
+ return isToastLoading(options)
151
+ ? options.text?.loading || s('Loading...')
152
+ : message;
153
+ }
154
+
155
+ async function closeByUser(
156
+ toast: HTMLElement,
157
+ options: ToastOptions
158
+ ): Promise<void> {
159
+ const shouldCancel = isToastLoading(options);
160
+ hide(toast);
161
+ if (shouldCancel) await options.onCancel?.();
162
+ await options.onClose?.();
163
+ }
164
+
165
+ function bindToastStatus(
166
+ element: HTMLElement,
167
+ iconElement: HTMLElement,
168
+ messageElement: HTMLElement,
169
+ message: string,
170
+ theme: ToastTheme,
171
+ options: ToastOptions,
172
+ duration = 0
173
+ ): void {
174
+ const id = element.dataset.toast || randomId();
175
+ const dispose = createRoot((dispose) => {
176
+ insert(iconElement, () => toastIconContent(theme, options));
177
+ insert(messageElement, () => toastMessage(message, options));
178
+ createEffect(() => {
179
+ cancelToastTimers(id);
180
+ if (duration > 0 && !isToastLoading(options)) {
181
+ setToastTimer(id, 'hide', () => hide(element), duration);
182
+ }
183
+ });
184
+ return dispose;
185
+ });
186
+ addToastDisposer(element, dispose);
187
+ }
188
+
88
189
  function getOrCreateContainer(names: ToastClassNames): HTMLElement {
89
190
  let container = q<HTMLElement>('[data-toast-container]');
90
191
  if (!container) {
@@ -241,45 +342,48 @@ function hide(toast: HTMLElement | null | undefined): void {
241
342
  });
242
343
  }
243
344
 
244
- function show(
245
- message = '',
246
- duration = 3000,
247
- type: ToastType = 'info',
248
- options: ToastOptions = {}
249
- ): HTMLElement {
345
+ function show(message = '', options: ToastOptions = {}): HTMLElement {
250
346
  validateParam('message', message, 'string', 'Toast.show');
347
+ const duration = options.duration ?? 3000;
348
+ const theme = options.theme || 'info';
251
349
  validateParam('duration', duration, TOAST_DURATION_RULE, 'Toast.show');
252
- validateParam('type', type, TOAST_TYPE_RULE, 'Toast.show');
350
+ validateParam('theme', theme, TOAST_THEME_RULE, 'Toast.show');
253
351
 
254
352
  const names = resolveClassNames(options);
255
353
  const id = randomId();
354
+ const iconElement = jsx('span', {
355
+ className: names.icon,
356
+ 'data-toast-icon': id,
357
+ style: () => toastIconStyle(options),
358
+ }) as HTMLElement;
359
+ const messageElement = jsx('span', {
360
+ className: names.message,
361
+ 'data-toast-message': id,
362
+ }) as HTMLElement;
256
363
  const toast = jsx('div', {
257
- className: joinClasses(names.toast, names[type]),
364
+ className: joinClasses(names.toast, names[theme]),
258
365
  'data-toast': id,
259
366
  role: 'alert',
260
367
  'aria-atomic': 'true',
261
- children: [
262
- jsx('span', {
263
- className: names.icon,
264
- 'data-toast-icon': id,
265
- children: icon(type === 'primary' ? 'info' : type),
266
- }),
267
- jsx('span', {
268
- className: names.message,
269
- 'data-toast-message': id,
270
- children: message,
271
- }),
272
- ],
368
+ children: [iconElement, messageElement],
273
369
  }) as HTMLElement;
370
+ bindToastStatus(
371
+ toast,
372
+ iconElement,
373
+ messageElement,
374
+ message,
375
+ theme,
376
+ options,
377
+ duration
378
+ );
274
379
  mountToast(
275
380
  toast,
276
381
  () => insert(getOrCreateContainer(names), toast),
277
- type === 'error' ? 'assertive' : 'polite'
382
+ theme === 'error' ? 'assertive' : 'polite'
278
383
  );
279
- if (duration > 0) setToastTimer(id, 'hide', () => hide(toast), duration);
280
- disposers.set(
384
+ addToastDisposer(
281
385
  toast,
282
- listen(toast, 'click', () => hide(toast))
386
+ listen(toast, 'click', () => void closeByUser(toast, options))
283
387
  );
284
388
  return toast;
285
389
  }
@@ -287,12 +391,12 @@ function show(
287
391
  function lite(
288
392
  message = '',
289
393
  duration = 2000,
290
- options: ToastOptions = {}
394
+ className?: ToastClassNameConfig
291
395
  ): HTMLElement {
292
396
  validateParam('message', message, 'string', 'Toast.lite');
293
397
  validateParam('duration', duration, LITE_DURATION_RULE, 'Toast.lite');
294
398
 
295
- const names = resolveClassNames(options);
399
+ const names = mergeClassNames({ ...classNames, ...className });
296
400
  const previous = q<HTMLElement>('[data-toast-lite]');
297
401
  if (previous) {
298
402
  cancelToastTimers(previous.dataset.toast || '');
@@ -311,16 +415,18 @@ function lite(
311
415
  return element;
312
416
  }
313
417
 
314
- function action(message = '', props: ToastActionProps = {}): HTMLElement {
315
- validateParam('message', message, 'string', 'Toast.action');
316
- const closeText = props.text?.close || t('Close', locales);
317
- const actionText = props.text?.action || t('Confirm', locales);
418
+ function confirm(message = '', props: ToastConfirmProps = {}): HTMLElement {
419
+ validateParam('message', message, 'string', 'Toast.confirm');
420
+ const closeText = props.text?.close || s('Close');
421
+ const confirmText = props.text?.confirm || s('Confirm');
422
+ const theme = props.theme || 'info';
423
+ validateParam('theme', theme, TOAST_THEME_RULE, 'Toast.confirm');
318
424
  const names = resolveClassNames(props);
319
425
  const id = randomId();
320
426
  const element = jsx('div', {
321
- className: joinClasses(names.action, names[props.type || 'info']),
427
+ className: joinClasses(names.confirm, names[theme]),
322
428
  'data-toast': id,
323
- 'data-toast-action': '',
429
+ 'data-toast-confirm': '',
324
430
  children: [
325
431
  jsx('div', {
326
432
  className: names.message,
@@ -328,13 +434,13 @@ function action(message = '', props: ToastActionProps = {}): HTMLElement {
328
434
  children: message,
329
435
  }),
330
436
  jsx('div', {
331
- className: names.actions,
332
- 'data-toast-actions': id,
437
+ className: names.buttons,
438
+ 'data-toast-buttons': id,
333
439
  children: [
334
440
  jsx('button', {
335
441
  className: joinClasses(names.button, names.closeBtn),
336
442
  children: closeText,
337
- 'data-action': 'close',
443
+ 'data-toast-button': 'close',
338
444
  'aria-label': closeText,
339
445
  onClick: async () => {
340
446
  await props.onClose?.();
@@ -342,12 +448,12 @@ function action(message = '', props: ToastActionProps = {}): HTMLElement {
342
448
  },
343
449
  }),
344
450
  jsx('button', {
345
- className: joinClasses(names.button, names.actionBtn),
346
- children: actionText,
347
- 'data-action': 'toast-action',
348
- 'aria-label': actionText,
451
+ className: joinClasses(names.button, names.confirmBtn),
452
+ children: confirmText,
453
+ 'data-toast-button': 'confirm',
454
+ 'aria-label': confirmText,
349
455
  onClick: async () => {
350
- await props.onAction?.();
456
+ await props.onConfirm?.();
351
457
  hide(element);
352
458
  },
353
459
  }),
@@ -360,7 +466,7 @@ function action(message = '', props: ToastActionProps = {}): HTMLElement {
360
466
  () => insert(getOrCreateContainer(names), element),
361
467
  'polite',
362
468
  () => {
363
- q<HTMLButtonElement>('[data-action="toast-action"]', element)?.focus();
469
+ q<HTMLButtonElement>('[data-toast-button="confirm"]', element)?.focus();
364
470
  }
365
471
  );
366
472
  return element;
@@ -381,24 +487,24 @@ function clearAll(): void {
381
487
  export const Toast = {
382
488
  timers,
383
489
  disposers,
384
- configure(options: ToastOptions = {}): ToastOptions {
490
+ configure(options: ToastClassNameOptions = {}): ToastClassNameOptions {
385
491
  classNames = mergeClassNames(options.className);
386
492
  return { className: classNames };
387
493
  },
388
494
  show,
389
- success: (message = '', duration = 3000, options: ToastOptions = {}) =>
390
- show(message, duration, 'success', options),
391
- info: (message = '', duration = 3000, options: ToastOptions = {}) =>
392
- show(message, duration, 'info', options),
393
- primary: (message = '', duration = 3000, options: ToastOptions = {}) =>
394
- show(message, duration, 'primary', options),
395
- warning: (message = '', duration = 3000, options: ToastOptions = {}) =>
396
- show(message, duration, 'warning', options),
397
- error: (message = '', duration = 3000, options: ToastOptions = {}) =>
398
- show(message, duration, 'error', options),
495
+ success: (message = '', options: ToastOptions = {}) =>
496
+ show(message, { ...options, theme: 'success' }),
497
+ info: (message = '', options: ToastOptions = {}) =>
498
+ show(message, { ...options, theme: 'info' }),
499
+ primary: (message = '', options: ToastOptions = {}) =>
500
+ show(message, { ...options, theme: 'primary' }),
501
+ warning: (message = '', options: ToastOptions = {}) =>
502
+ show(message, { ...options, theme: 'warning' }),
503
+ error: (message = '', options: ToastOptions = {}) =>
504
+ show(message, { ...options, theme: 'error' }),
399
505
  hide,
400
506
  lite,
401
- action,
507
+ confirm,
402
508
  clearAll,
403
509
  destroyAll: clearAll,
404
510
  };
@@ -128,13 +128,13 @@ export function isModernBrowser(): SupportES2022 {
128
128
  }
129
129
  export function checkModernBrowser(): SupportES2022 {
130
130
  if (!isModernBrowser()) {
131
- Toast.action(t('wn', locales), {
132
- type: 'error',
133
- onAction: () => {
131
+ Toast.confirm(t('wn', locales), {
132
+ theme: 'error',
133
+ onConfirm: () => {
134
134
  window.open('https://www.google.cn/chrome/fallback/', '_blank');
135
135
  },
136
136
  className: {
137
- actionBtn: 'j-button is-sm is-danger',
137
+ confirmBtn: 'j-button is-sm is-danger',
138
138
  },
139
139
  });
140
140
  return false;