vanilla-jui 1.4.7 → 1.5.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.
Files changed (52) hide show
  1. package/README.md +10 -4
  2. package/dist/index.d.ts +418 -970
  3. package/dist/index.js +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -3
  7. package/src/components/accordion.ts +466 -346
  8. package/src/components/flow.ts +779 -1133
  9. package/src/components/form.ts +666 -509
  10. package/src/components/menu.ts +198 -328
  11. package/src/components/modal.ts +510 -747
  12. package/src/components/offcanvas.ts +273 -367
  13. package/src/components/pagination.ts +201 -375
  14. package/src/components/swiper.ts +689 -1107
  15. package/src/components/tabs.ts +467 -778
  16. package/src/components/toc.ts +191 -244
  17. package/src/core/component.ts +332 -0
  18. package/src/core/motion.ts +298 -0
  19. package/src/core/presence.ts +233 -0
  20. package/src/core/scheduler.ts +91 -0
  21. package/src/core/view.ts +31 -0
  22. package/src/css/accordion.css +94 -3
  23. package/src/css/content.css +18 -24
  24. package/src/css/drop.css +4 -1
  25. package/src/css/form.css +8 -0
  26. package/src/css/menu.css +10 -5
  27. package/src/css/modal.css +2 -1
  28. package/src/css/offcanvas.css +5 -17
  29. package/src/css/swiper.css +2 -1
  30. package/src/css/tabs.css +8 -17
  31. package/src/css/themes/default/accordion.css +0 -48
  32. package/src/css/themes/default/swiper.css +1 -1
  33. package/src/css/themes/default/tabs.css +4 -6
  34. package/src/css/toast.css +1 -17
  35. package/src/css/tooltip.css +2 -1
  36. package/src/index.ts +8 -2
  37. package/src/locales/index.ts +3 -0
  38. package/src/primitives/drop.ts +253 -262
  39. package/src/primitives/loading.ts +22 -4
  40. package/src/primitives/parabola.ts +123 -164
  41. package/src/primitives/sticky.ts +297 -0
  42. package/src/primitives/theme.ts +164 -222
  43. package/src/primitives/toast.ts +309 -284
  44. package/src/primitives/tooltip.ts +65 -66
  45. package/src/utilities/browser.ts +38 -0
  46. package/src/utilities/dom.ts +0 -54
  47. package/src/utilities/refs.ts +61 -0
  48. package/src/utilities/state.ts +73 -0
  49. package/src/utilities/timer.ts +11 -31
  50. package/src/validation/validator.ts +289 -389
  51. package/src/components/sticky.ts +0 -297
  52. package/src/core/Component.ts +0 -377
@@ -23,13 +23,9 @@ interface DropDelay {
23
23
  hide?: number;
24
24
  }
25
25
 
26
- interface TooltipDom {
27
- root: HTMLElement | null;
28
- }
29
-
30
- interface TooltipInstance {
31
- dom: TooltipDom;
32
- drop: DropInstance | null;
26
+ export interface TooltipInstance {
27
+ readonly element: HTMLElement | null;
28
+ readonly drop: DropInstance | null;
33
29
  show(useDelay?: boolean): void;
34
30
  hide(useDelay?: boolean): void;
35
31
  toggle(): void;
@@ -68,6 +64,8 @@ interface TooltipProps extends Record<string, unknown> {
68
64
  offset?: number;
69
65
  message?: string;
70
66
  theme?: TooltipTheme;
67
+ cache?: boolean;
68
+ ttl?: number;
71
69
  className?: TooltipClassNameConfig;
72
70
  id?: string | null;
73
71
  delay?: number | DropDelay;
@@ -83,6 +81,8 @@ interface ResolvedTooltipProps extends Record<string, unknown> {
83
81
  offset: number;
84
82
  message: string;
85
83
  theme: TooltipTheme;
84
+ cache: boolean;
85
+ ttl: number;
86
86
  className: TooltipClassNames;
87
87
  id: string | null;
88
88
  delay: number | DropDelay;
@@ -148,6 +148,12 @@ const TOOLTIP_OPTIONS_SCHEMA = {
148
148
  types: ['string', 'boolean'],
149
149
  enum: [false, 'reverse', 'primary', 'success', 'warning', 'error'],
150
150
  },
151
+ cache: { default: false, type: 'boolean' },
152
+ ttl: {
153
+ default: 0,
154
+ type: 'number',
155
+ min: 0,
156
+ },
151
157
  className: {
152
158
  default: DEFAULT_CLASS_NAMES,
153
159
  type: 'object',
@@ -169,6 +175,8 @@ function normalizeProps(input: TooltipProps): ResolvedTooltipProps {
169
175
  offset: props.offset as number,
170
176
  message: props.message as string,
171
177
  theme: props.theme as TooltipTheme,
178
+ cache: props.cache as boolean,
179
+ ttl: props.ttl as number,
172
180
  className: props.className as TooltipClassNames,
173
181
  id: props.id as string | null,
174
182
  delay: props.delay as number | DropDelay,
@@ -183,66 +191,57 @@ function normalizeProps(input: TooltipProps): ResolvedTooltipProps {
183
191
  *
184
192
  * 基于 Drop 实现,提供更轻量的文本提示封装。
185
193
  */
186
- class Tooltip implements TooltipInstance {
187
- dom: TooltipDom;
188
- drop: DropInstance | null;
189
- props: ResolvedTooltipProps | null;
190
-
191
- constructor(element: DOMReference, props: TooltipProps = {}) {
192
- const settings = normalizeProps(props);
193
- this.props = settings;
194
- this.drop = createDrop(element, {
195
- name: settings.name,
196
- mode: settings.mode,
197
- position: settings.position,
198
- offset: settings.offset,
199
- id: settings.id,
200
- delay: settings.delay,
201
- hoverIntent: settings.hoverIntent,
202
- onShown: settings.onShown,
203
- onHidden: settings.onHidden,
204
- content: this.buildContent(settings),
205
- });
206
- this.dom = this.drop.dom;
207
- }
208
-
209
- private buildContent(settings: ResolvedTooltipProps): HTMLElement {
210
- return jsx('div', {
211
- className: joinClasses(
212
- settings.className.container,
213
- settings.theme && settings.className.ui[settings.theme]
214
- ),
215
- 'data-tooltip': settings.name || settings.id || '',
216
- children: jsx('div', {
217
- className: settings.className.message,
218
- 'data-tooltip-message': '',
219
- children: settings.message,
220
- }),
221
- }) as HTMLElement;
222
- }
223
-
224
- show(useDelay = true): void {
225
- this.drop?.show(useDelay);
226
- }
227
-
228
- hide(useDelay = true): void {
229
- this.drop?.hide(useDelay);
230
- }
231
-
232
- toggle(): void {
233
- this.drop?.toggle();
234
- }
235
-
236
- destroy(): void {
237
- this.drop?.destroy();
238
- this.drop = null;
239
- this.props = null;
240
- }
241
- }
242
-
243
194
  export function createTooltip(
244
195
  element: DOMReference,
245
- props: TooltipProps = {}
196
+ input: TooltipProps = {}
246
197
  ): TooltipInstance {
247
- return new Tooltip(element, props);
198
+ const props = normalizeProps(input);
199
+ const content = jsx('div', {
200
+ className: joinClasses(
201
+ props.className.container,
202
+ props.theme && props.className.ui[props.theme]
203
+ ),
204
+ 'data-tooltip': props.name || props.id || '',
205
+ children: jsx('div', {
206
+ className: props.className.message,
207
+ 'data-tooltip-message': '',
208
+ children: props.message,
209
+ }),
210
+ }) as HTMLElement;
211
+ let drop: DropInstance | null = createDrop(element, {
212
+ name: props.name,
213
+ mode: props.mode,
214
+ position: props.position,
215
+ offset: props.offset,
216
+ cache: props.cache,
217
+ ttl: props.ttl,
218
+ id: props.id,
219
+ delay: props.delay,
220
+ hoverIntent: props.hoverIntent,
221
+ onShown: props.onShown,
222
+ onHidden: props.onHidden,
223
+ content,
224
+ });
225
+ const tooltip: TooltipInstance = {
226
+ get element() {
227
+ return drop?.element || null;
228
+ },
229
+ get drop() {
230
+ return drop;
231
+ },
232
+ show(useDelay = true) {
233
+ drop?.show(useDelay);
234
+ },
235
+ hide(useDelay = true) {
236
+ drop?.hide(useDelay);
237
+ },
238
+ toggle() {
239
+ drop?.toggle();
240
+ },
241
+ destroy() {
242
+ drop?.destroy();
243
+ drop = null;
244
+ },
245
+ };
246
+ return tooltip;
248
247
  }
@@ -1,3 +1,7 @@
1
+ import { t } from 'vanilla-signal-i18n';
2
+ import { Toast } from '../primitives/toast';
3
+ import locales from '../locales';
4
+
1
5
  declare global {
2
6
  interface Navigator {
3
7
  userAgentData?: {
@@ -103,3 +107,37 @@ function fallbackCopy(text: string): boolean {
103
107
  }
104
108
  }
105
109
  }
110
+
111
+ export type SupportES2022 = boolean;
112
+ export function isModernBrowser(): SupportES2022 {
113
+ try {
114
+ // Object.hasOwn()
115
+ if (typeof Object.hasOwn !== 'function') return false;
116
+ // Array.prototype.at()
117
+ if (typeof [].at !== 'function') return false;
118
+
119
+ // new Function('class Test { #x = 1; getX() { return this.#x; } }');
120
+ // new Function('const re = /a/d; return re.hasIndices;');
121
+ // const err = new Error('test', { cause: 'reason' });
122
+ // if (err.cause !== 'reason') return false;
123
+
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+ export function checkModernBrowser(): SupportES2022 {
130
+ if (!isModernBrowser()) {
131
+ Toast.action(t('wn', locales), {
132
+ type: 'error',
133
+ onAction: () => {
134
+ window.open('https://www.google.cn/chrome/fallback/', '_blank');
135
+ },
136
+ className: {
137
+ actionBtn: 'j-button is-sm is-danger',
138
+ },
139
+ });
140
+ return false;
141
+ }
142
+ return true;
143
+ }
@@ -104,52 +104,6 @@ function flattenNodeArray(
104
104
  return out;
105
105
  }
106
106
 
107
- /**
108
- * 将可渲染内容归一化为 DOM 节点数组。
109
- *
110
- * 支持的输入类型:
111
- * - **Node/Element**: 包装为单元素数组
112
- * - **string**: 按 HTML 片段解析为节点数组
113
- * - **number/boolean**: 转换为文本节点
114
- * - **Array**: 递归扁平化处理
115
- * - **Function**: 调用后递归处理返回值
116
- * - **null/undefined/false/true**: 返回空数组
117
- *
118
- * @param {RenderableContent<TContext>} content 可渲染内容。
119
- * @param {TContext} [context] 传递给函数类型内容的上下文。
120
- * @returns {Node[]} 归一化后的节点数组。
121
- */
122
- export function normalizeContentNodes<TContext = unknown>(
123
- content: RenderableContent<TContext>,
124
- context?: TContext
125
- ): Node[] {
126
- if (typeof content === 'function') {
127
- return normalizeContentNodes(content(context as TContext), context);
128
- }
129
-
130
- const value = content;
131
-
132
- if (value == null || value === false || value === true) return [];
133
-
134
- if (Array.isArray(value)) {
135
- return value.flatMap((item) => normalizeContentNodes(item, context));
136
- }
137
-
138
- if (isNode(value)) return [value];
139
-
140
- if (typeof value === 'string') {
141
- const template = document.createElement('template');
142
- template.innerHTML = value;
143
- return Array.from(template.content.childNodes);
144
- }
145
-
146
- return [
147
- document.createTextNode(
148
- String(value as string | number | boolean | bigint | symbol)
149
- ),
150
- ];
151
- }
152
-
153
107
  /**
154
108
  * 将 DOM 引用解析为节点列表。
155
109
  *
@@ -299,14 +253,6 @@ export function isRenderableContent(
299
253
  return isRenderableValue(value);
300
254
  }
301
255
 
302
- export function normalizeRenderableContentNodes<TContext>(
303
- content: unknown,
304
- context: TContext
305
- ): Node[] | null {
306
- if (!isRenderableContent(content)) return null;
307
- return normalizeContentNodes(content, context);
308
- }
309
-
310
256
  /**
311
257
  * 根据 CSS 选择器获取第一个匹配的元素。
312
258
  * @param {string} selector CSS 选择器。
@@ -0,0 +1,61 @@
1
+ import { onCleanup } from 'vanilla-signal';
2
+
3
+ export interface ElementRef<TElement extends Element> {
4
+ readonly current: TElement | null;
5
+ set: (element: TElement) => void;
6
+ clear: () => void;
7
+ }
8
+
9
+ export function createElementRef<
10
+ TElement extends Element,
11
+ >(): ElementRef<TElement> {
12
+ let current: TElement | null = null;
13
+
14
+ return {
15
+ get current() {
16
+ return current;
17
+ },
18
+ set(element) {
19
+ current = element;
20
+ },
21
+ clear() {
22
+ current = null;
23
+ },
24
+ };
25
+ }
26
+
27
+ export interface KeyedElementRefs<TKey, TElement extends Element> {
28
+ readonly elements: ReadonlyMap<TKey, TElement>;
29
+ get: (key: TKey) => TElement | undefined;
30
+ bind: (key: TKey) => (element: TElement) => void;
31
+ delete: (key: TKey) => void;
32
+ clear: () => void;
33
+ }
34
+
35
+ export function createKeyedElementRefs<
36
+ TKey,
37
+ TElement extends Element,
38
+ >(): KeyedElementRefs<TKey, TElement> {
39
+ const elements = new Map<TKey, TElement>();
40
+
41
+ return {
42
+ elements,
43
+ get(key) {
44
+ return elements.get(key);
45
+ },
46
+ bind(key) {
47
+ return (element) => {
48
+ elements.set(key, element);
49
+ onCleanup(() => {
50
+ if (elements.get(key) === element) elements.delete(key);
51
+ });
52
+ };
53
+ },
54
+ delete(key) {
55
+ elements.delete(key);
56
+ },
57
+ clear() {
58
+ elements.clear();
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,73 @@
1
+ import { createWatch, untrack, unwrap } from 'vanilla-signal';
2
+
3
+ import { createScheduledTask } from '../core/scheduler.ts';
4
+
5
+ export interface StateSyncOptions {
6
+ deferInitial?: boolean;
7
+ flushInitial?: boolean;
8
+ flush?: 'microtask' | 'sync';
9
+ }
10
+
11
+ export function getStoreVersion(value: unknown): number {
12
+ if (value && typeof value === 'object') {
13
+ const version = (value as { __version__?: unknown }).__version__;
14
+ return typeof version === 'number' ? version : 0;
15
+ }
16
+ return 0;
17
+ }
18
+
19
+ export function trackStoreVersion<T>(value: T): T {
20
+ getStoreVersion(value);
21
+ return value;
22
+ }
23
+
24
+ export function stateSnapshot<T>(value: T): T {
25
+ return unwrap(value) as T;
26
+ }
27
+
28
+ /**
29
+ * Bridges reactive state to an expensive imperative effect. Declarative view
30
+ * bindings should depend on state directly instead of using this helper.
31
+ */
32
+ export function createStateSync<TSnapshot>(
33
+ read: () => TSnapshot,
34
+ sync: (snapshot: TSnapshot) => void | Promise<void>,
35
+ {
36
+ deferInitial = true,
37
+ flushInitial = false,
38
+ flush = 'microtask',
39
+ }: StateSyncOptions = {}
40
+ ): () => void {
41
+ let initialized = false;
42
+ let hasSnapshot = false;
43
+ let latestSnapshot: TSnapshot;
44
+
45
+ const task = createScheduledTask(() => {
46
+ if (!hasSnapshot) return;
47
+ hasSnapshot = false;
48
+ void untrack(() => sync(latestSnapshot));
49
+ });
50
+
51
+ const watcher = createWatch(
52
+ read,
53
+ (snapshot) => {
54
+ const isInitial = !initialized;
55
+ initialized = true;
56
+ latestSnapshot = snapshot as TSnapshot;
57
+ hasSnapshot = true;
58
+
59
+ if ((isInitial && flushInitial) || flush === 'sync') {
60
+ task.flush();
61
+ return;
62
+ }
63
+
64
+ task.schedule();
65
+ },
66
+ { defer: deferInitial }
67
+ );
68
+
69
+ return () => {
70
+ task.dispose();
71
+ watcher.dispose();
72
+ };
73
+ }
@@ -1,38 +1,18 @@
1
- /**
2
- * 定时器管理器
3
- */
4
- export const timer = {
5
- // 存储定时器 { key: timerId }
6
- timers: {} as Record<string, ReturnType<typeof setTimeout>>,
1
+ const timers: Record<string, ReturnType<typeof setTimeout>> = {};
7
2
 
8
- /**
9
- * 注册并开始一个定时器
10
- * @param {string} key - 定时器的唯一标识
11
- * @param {number} duration - 延迟执行的时间(毫秒)
12
- * @param {function} callback - 延迟执行的回调函数
13
- */
14
- start(key: string, duration: number, callback: () => void) {
15
- // 1. 如果该 key 已经存在,先清除旧的定时器,防止重复触发
16
- if (this.timers[key]) {
17
- clearTimeout(this.timers[key]);
18
- }
19
- // 2. 设置新的定时器,并保存其 ID
20
- this.timers[key] = setTimeout(() => {
3
+ export const timer = {
4
+ timers,
5
+ start(key: string, duration: number, callback: () => void): void {
6
+ if (timers[key]) clearTimeout(timers[key]);
7
+ timers[key] = setTimeout(() => {
21
8
  callback();
22
- // 3. 执行完毕后,自动从管理器中注销该定时器
23
- delete this.timers[key];
9
+ delete timers[key];
24
10
  }, duration);
25
11
  },
26
-
27
- /**
28
- * 明确注销一个定时器
29
- * @param {string} key - 需要取消的定时器唯一标识
30
- */
31
- cancel(key: string) {
32
- if (this.timers[key]) {
33
- clearTimeout(this.timers[key]);
34
- // 从管理器中移除该记录
35
- delete this.timers[key];
12
+ cancel(key: string): void {
13
+ if (timers[key]) {
14
+ clearTimeout(timers[key]);
15
+ delete timers[key];
36
16
  }
37
17
  },
38
18
  };