vanilla-jui 1.5.7 → 1.5.9

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,87 @@ 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
+ if (isToastLoading(options)) await options.onCancel?.();
160
+ await options.onClose?.();
161
+ hide(toast);
162
+ }
163
+
164
+ function bindToastStatus(
165
+ element: HTMLElement,
166
+ iconElement: HTMLElement,
167
+ messageElement: HTMLElement,
168
+ message: string,
169
+ theme: ToastTheme,
170
+ options: ToastOptions,
171
+ duration = 0
172
+ ): void {
173
+ const id = element.dataset.toast || randomId();
174
+ const dispose = createRoot((dispose) => {
175
+ insert(iconElement, () => toastIconContent(theme, options));
176
+ insert(messageElement, () => toastMessage(message, options));
177
+ createEffect(() => {
178
+ cancelToastTimers(id);
179
+ if (duration > 0 && !isToastLoading(options)) {
180
+ setToastTimer(id, 'hide', () => hide(element), duration);
181
+ }
182
+ });
183
+ return dispose;
184
+ });
185
+ addToastDisposer(element, dispose);
186
+ }
187
+
88
188
  function getOrCreateContainer(names: ToastClassNames): HTMLElement {
89
189
  let container = q<HTMLElement>('[data-toast-container]');
90
190
  if (!container) {
@@ -241,45 +341,48 @@ function hide(toast: HTMLElement | null | undefined): void {
241
341
  });
242
342
  }
243
343
 
244
- function show(
245
- message = '',
246
- duration = 3000,
247
- type: ToastType = 'info',
248
- options: ToastOptions = {}
249
- ): HTMLElement {
344
+ function show(message = '', options: ToastOptions = {}): HTMLElement {
250
345
  validateParam('message', message, 'string', 'Toast.show');
346
+ const duration = options.duration ?? 3000;
347
+ const theme = options.theme || 'info';
251
348
  validateParam('duration', duration, TOAST_DURATION_RULE, 'Toast.show');
252
- validateParam('type', type, TOAST_TYPE_RULE, 'Toast.show');
349
+ validateParam('theme', theme, TOAST_THEME_RULE, 'Toast.show');
253
350
 
254
351
  const names = resolveClassNames(options);
255
352
  const id = randomId();
353
+ const iconElement = jsx('span', {
354
+ className: names.icon,
355
+ 'data-toast-icon': id,
356
+ style: () => toastIconStyle(options),
357
+ }) as HTMLElement;
358
+ const messageElement = jsx('span', {
359
+ className: names.message,
360
+ 'data-toast-message': id,
361
+ }) as HTMLElement;
256
362
  const toast = jsx('div', {
257
- className: joinClasses(names.toast, names[type]),
363
+ className: joinClasses(names.toast, names[theme]),
258
364
  'data-toast': id,
259
365
  role: 'alert',
260
366
  '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
- ],
367
+ children: [iconElement, messageElement],
273
368
  }) as HTMLElement;
369
+ bindToastStatus(
370
+ toast,
371
+ iconElement,
372
+ messageElement,
373
+ message,
374
+ theme,
375
+ options,
376
+ duration
377
+ );
274
378
  mountToast(
275
379
  toast,
276
380
  () => insert(getOrCreateContainer(names), toast),
277
- type === 'error' ? 'assertive' : 'polite'
381
+ theme === 'error' ? 'assertive' : 'polite'
278
382
  );
279
- if (duration > 0) setToastTimer(id, 'hide', () => hide(toast), duration);
280
- disposers.set(
383
+ addToastDisposer(
281
384
  toast,
282
- listen(toast, 'click', () => hide(toast))
385
+ listen(toast, 'click', () => void closeByUser(toast, options))
283
386
  );
284
387
  return toast;
285
388
  }
@@ -287,12 +390,12 @@ function show(
287
390
  function lite(
288
391
  message = '',
289
392
  duration = 2000,
290
- options: ToastOptions = {}
393
+ className?: ToastClassNameConfig
291
394
  ): HTMLElement {
292
395
  validateParam('message', message, 'string', 'Toast.lite');
293
396
  validateParam('duration', duration, LITE_DURATION_RULE, 'Toast.lite');
294
397
 
295
- const names = resolveClassNames(options);
398
+ const names = mergeClassNames({ ...classNames, ...className });
296
399
  const previous = q<HTMLElement>('[data-toast-lite]');
297
400
  if (previous) {
298
401
  cancelToastTimers(previous.dataset.toast || '');
@@ -311,16 +414,18 @@ function lite(
311
414
  return element;
312
415
  }
313
416
 
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);
417
+ function confirm(message = '', props: ToastConfirmProps = {}): HTMLElement {
418
+ validateParam('message', message, 'string', 'Toast.confirm');
419
+ const closeText = props.text?.close || s('Close');
420
+ const confirmText = props.text?.confirm || s('Confirm');
421
+ const theme = props.theme || 'info';
422
+ validateParam('theme', theme, TOAST_THEME_RULE, 'Toast.confirm');
318
423
  const names = resolveClassNames(props);
319
424
  const id = randomId();
320
425
  const element = jsx('div', {
321
- className: joinClasses(names.action, names[props.type || 'info']),
426
+ className: joinClasses(names.confirm, names[theme]),
322
427
  'data-toast': id,
323
- 'data-toast-action': '',
428
+ 'data-toast-confirm': '',
324
429
  children: [
325
430
  jsx('div', {
326
431
  className: names.message,
@@ -328,13 +433,13 @@ function action(message = '', props: ToastActionProps = {}): HTMLElement {
328
433
  children: message,
329
434
  }),
330
435
  jsx('div', {
331
- className: names.actions,
332
- 'data-toast-actions': id,
436
+ className: names.buttons,
437
+ 'data-toast-buttons': id,
333
438
  children: [
334
439
  jsx('button', {
335
440
  className: joinClasses(names.button, names.closeBtn),
336
441
  children: closeText,
337
- 'data-action': 'close',
442
+ 'data-toast-button': 'close',
338
443
  'aria-label': closeText,
339
444
  onClick: async () => {
340
445
  await props.onClose?.();
@@ -342,12 +447,12 @@ function action(message = '', props: ToastActionProps = {}): HTMLElement {
342
447
  },
343
448
  }),
344
449
  jsx('button', {
345
- className: joinClasses(names.button, names.actionBtn),
346
- children: actionText,
347
- 'data-action': 'toast-action',
348
- 'aria-label': actionText,
450
+ className: joinClasses(names.button, names.confirmBtn),
451
+ children: confirmText,
452
+ 'data-toast-button': 'confirm',
453
+ 'aria-label': confirmText,
349
454
  onClick: async () => {
350
- await props.onAction?.();
455
+ await props.onConfirm?.();
351
456
  hide(element);
352
457
  },
353
458
  }),
@@ -360,7 +465,7 @@ function action(message = '', props: ToastActionProps = {}): HTMLElement {
360
465
  () => insert(getOrCreateContainer(names), element),
361
466
  'polite',
362
467
  () => {
363
- q<HTMLButtonElement>('[data-action="toast-action"]', element)?.focus();
468
+ q<HTMLButtonElement>('[data-toast-button="confirm"]', element)?.focus();
364
469
  }
365
470
  );
366
471
  return element;
@@ -381,24 +486,24 @@ function clearAll(): void {
381
486
  export const Toast = {
382
487
  timers,
383
488
  disposers,
384
- configure(options: ToastOptions = {}): ToastOptions {
489
+ configure(options: ToastClassNameOptions = {}): ToastClassNameOptions {
385
490
  classNames = mergeClassNames(options.className);
386
491
  return { className: classNames };
387
492
  },
388
493
  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),
494
+ success: (message = '', options: ToastOptions = {}) =>
495
+ show(message, { ...options, theme: 'success' }),
496
+ info: (message = '', options: ToastOptions = {}) =>
497
+ show(message, { ...options, theme: 'info' }),
498
+ primary: (message = '', options: ToastOptions = {}) =>
499
+ show(message, { ...options, theme: 'primary' }),
500
+ warning: (message = '', options: ToastOptions = {}) =>
501
+ show(message, { ...options, theme: 'warning' }),
502
+ error: (message = '', options: ToastOptions = {}) =>
503
+ show(message, { ...options, theme: 'error' }),
399
504
  hide,
400
505
  lite,
401
- action,
506
+ confirm,
402
507
  clearAll,
403
508
  destroyAll: clearAll,
404
509
  };
@@ -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;