vanilla-jui 1.3.10 → 1.4.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.
Files changed (90) hide show
  1. package/dist/index.d.ts +2927 -0
  2. package/dist/index.js +1 -1
  3. package/dist/index.umd.js +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +4 -3
  6. package/src/components/accordion.ts +522 -0
  7. package/src/components/drop.ts +497 -0
  8. package/src/components/flow.ts +1579 -0
  9. package/src/components/form.ts +953 -0
  10. package/src/components/{icons.js → icons.ts} +24 -11
  11. package/src/components/menu.ts +615 -0
  12. package/src/components/{modal.js → modal.ts} +560 -237
  13. package/src/components/offcanvas.ts +496 -0
  14. package/src/components/{pagination.js → pagination.ts} +251 -76
  15. package/src/components/parabola.ts +335 -0
  16. package/src/components/{sticky.js → sticky.ts} +123 -98
  17. package/src/components/{swiper.js → swiper.ts} +510 -232
  18. package/src/components/tabs.ts +984 -0
  19. package/src/components/theme.ts +437 -0
  20. package/src/components/toast.ts +371 -0
  21. package/src/components/toc.ts +385 -0
  22. package/src/components/tooltip.ts +179 -0
  23. package/src/components/validator.ts +596 -0
  24. package/src/core/{Component.js → Component.ts} +134 -27
  25. package/src/css/accordion.css +2 -2
  26. package/src/css/animation.css +31 -1
  27. package/src/css/breadcrumb.css +1 -1
  28. package/src/css/button.css +5 -5
  29. package/src/css/card.css +7 -7
  30. package/src/css/color.css +865 -1028
  31. package/src/css/color_p3.css +871 -1036
  32. package/src/css/components.css +1 -0
  33. package/src/css/drop.css +1 -1
  34. package/src/css/flow.css +1 -1
  35. package/src/css/form.css +21 -21
  36. package/src/css/gradient.css +17 -4
  37. package/src/css/icon.css +17 -1
  38. package/src/css/index.css +3 -3
  39. package/src/css/layout.css +1 -1
  40. package/src/css/menu.css +27 -27
  41. package/src/css/modal.css +10 -27
  42. package/src/css/offcanvas.css +2 -2
  43. package/src/css/pagination.css +1 -1
  44. package/src/css/popup.css +2 -2
  45. package/src/css/swiper.css +12 -12
  46. package/src/css/tabs.css +3 -2
  47. package/src/css/tag.css +3 -4
  48. package/src/css/theme-palette.css +6 -6
  49. package/src/css/tip.css +1 -1
  50. package/src/css/toast.css +8 -8
  51. package/src/css/tokens.css +197 -226
  52. package/src/css/toolbar.css +3 -3
  53. package/src/css/tooltip.css +1 -1
  54. package/src/index.ts +37 -0
  55. package/src/locales/{index.js → index.ts} +1 -2
  56. package/src/types.d.ts +1 -0
  57. package/src/utilities/browser.ts +131 -0
  58. package/src/utilities/cache.ts +57 -0
  59. package/src/utilities/core.ts +614 -0
  60. package/src/utilities/dom.ts +468 -0
  61. package/src/utilities/events.ts +106 -0
  62. package/src/utilities/http.ts +35 -0
  63. package/src/utilities/storage.ts +46 -0
  64. package/src/components/accordion.js +0 -373
  65. package/src/components/drop.js +0 -437
  66. package/src/components/flow.js +0 -1133
  67. package/src/components/form.js +0 -585
  68. package/src/components/menu.js +0 -422
  69. package/src/components/offcanvas.js +0 -332
  70. package/src/components/parabola.js +0 -295
  71. package/src/components/tabs.js +0 -746
  72. package/src/components/theme.js +0 -353
  73. package/src/components/toast.js +0 -329
  74. package/src/components/toc.js +0 -226
  75. package/src/components/tooltip.js +0 -86
  76. package/src/components/validator.js +0 -586
  77. package/src/css/toast.min.css +0 -1
  78. package/src/index.js +0 -45
  79. package/src/locales/index.min.js +0 -1
  80. package/src/utilities/browser.js +0 -111
  81. package/src/utilities/cache.js +0 -159
  82. package/src/utilities/core.js +0 -502
  83. package/src/utilities/debug.js +0 -103
  84. package/src/utilities/dom.js +0 -313
  85. package/src/utilities/events.js +0 -69
  86. package/src/utilities/http.js +0 -30
  87. package/src/utilities/legacy-dom.js +0 -179
  88. package/src/utilities/storage.js +0 -46
  89. /package/src/{components/components.js → css/themes/index.css} +0 -0
  90. /package/src/icons/{index.js → index.ts} +0 -0
@@ -0,0 +1,497 @@
1
+ import { jsx } from 'vanilla-signal';
2
+
3
+ import {
4
+ type ResolveSchema,
5
+ randomId,
6
+ resolveProps,
7
+ } from '../utilities/core.ts';
8
+ import {
9
+ type DOMReference,
10
+ type RenderableContent,
11
+ isNode,
12
+ isRenderableContent,
13
+ normalizeContentNodes,
14
+ resolveElement,
15
+ } from '../utilities/dom.ts';
16
+ import { createEventManager, type IEventManager } from '../utilities/events.ts';
17
+
18
+ export type DropMode = 'hover' | 'click';
19
+ export type DropPosition =
20
+ | 'auto'
21
+ | 'top-left'
22
+ | 'top-center'
23
+ | 'top-right'
24
+ | 'bottom-left'
25
+ | 'bottom-center'
26
+ | 'bottom-right'
27
+ | 'left'
28
+ | 'right';
29
+
30
+ export interface DropClassNames {
31
+ root: string;
32
+ container: string;
33
+ active: string;
34
+ }
35
+
36
+ export type DropClassNameConfig = Partial<DropClassNames>;
37
+
38
+ export interface DropDelay {
39
+ show?: number;
40
+ hide?: number;
41
+ }
42
+
43
+ export interface DropProps extends Record<string, unknown> {
44
+ name?: string | null;
45
+ mode?: DropMode;
46
+ position?: DropPosition;
47
+ offset?: number;
48
+ content?: RenderableContent<Drop>;
49
+ className?: DropClassNameConfig;
50
+ id?: string | null;
51
+ containerClassName?: string | null;
52
+ delay?: number | DropDelay;
53
+ hoverIntent?: boolean;
54
+ onShown?: ((drop: Drop) => void | Promise<void>) | null;
55
+ onHidden?: ((drop: Drop) => void | Promise<void>) | null;
56
+ }
57
+
58
+ interface ResolvedDropProps extends Record<string, unknown> {
59
+ name: string | null;
60
+ mode: DropMode;
61
+ position: DropPosition;
62
+ offset: number;
63
+ content: RenderableContent<Drop>;
64
+ className: DropClassNames;
65
+ id: string;
66
+ containerClassName: string | null;
67
+ delay: number | DropDelay;
68
+ hoverIntent: boolean;
69
+ onShown: NonNullable<DropProps['onShown']> | null;
70
+ onHidden: NonNullable<DropProps['onHidden']> | null;
71
+ }
72
+
73
+ interface DropTimer {
74
+ show: ReturnType<typeof setTimeout> | null;
75
+ hide: ReturnType<typeof setTimeout> | null;
76
+ }
77
+
78
+ interface DropCleanup {
79
+ events: IEventManager;
80
+ }
81
+
82
+ interface HoverIntentData {
83
+ x: number;
84
+ y: number;
85
+ lastMoveTime: number;
86
+ }
87
+
88
+ const DEFAULT_CLASS_NAMES: DropClassNames = {
89
+ root: 'j-drop',
90
+ container: 'drop-container',
91
+ active: 'is-active',
92
+ };
93
+
94
+ const DROP_PROPS_SCHEMA = {
95
+ name: { default: null, types: ['string', 'null'] },
96
+ mode: { default: 'click', type: 'string', enum: ['hover', 'click'] },
97
+ position: {
98
+ default: 'auto',
99
+ type: 'string',
100
+ enum: [
101
+ 'auto',
102
+ 'top-left',
103
+ 'top-center',
104
+ 'top-right',
105
+ 'bottom-left',
106
+ 'bottom-center',
107
+ 'bottom-right',
108
+ 'left',
109
+ 'right',
110
+ ],
111
+ },
112
+ offset: {
113
+ default: 10,
114
+ type: 'number',
115
+ validate: (value: unknown) => typeof value === 'number' && value >= 0,
116
+ message: 'expects a positive number or 0.',
117
+ },
118
+ content: {
119
+ default: '',
120
+ validate: isRenderableContent,
121
+ message: 'expects string, Node, array, function or null.',
122
+ },
123
+ className: {
124
+ default: DEFAULT_CLASS_NAMES,
125
+ type: 'object',
126
+ normalize: (value: unknown) => ({
127
+ ...DEFAULT_CLASS_NAMES,
128
+ ...(value && typeof value === 'object' ? value : {}),
129
+ }),
130
+ },
131
+ id: {
132
+ default: null,
133
+ types: ['string', 'null'],
134
+ normalize: (value: unknown) => {
135
+ if (typeof value === 'string') {
136
+ const id = value.trim();
137
+ return id || randomId();
138
+ }
139
+ if (value == null) return randomId();
140
+ return value;
141
+ },
142
+ },
143
+ containerClassName: { default: null, types: ['string', 'null'] },
144
+ delay: { default: 0, types: ['number', 'object'] },
145
+ hoverIntent: { default: true, type: 'boolean' },
146
+ onShown: { default: null, types: ['function', 'null'] },
147
+ onHidden: { default: null, types: ['function', 'null'] },
148
+ } satisfies ResolveSchema<DropProps>;
149
+
150
+ function normalizeProps(input: DropProps): ResolvedDropProps {
151
+ const props = resolveProps(input, DROP_PROPS_SCHEMA, 'Drop');
152
+ return {
153
+ name: props.name as string | null,
154
+ mode: props.mode as DropMode,
155
+ position: props.position as DropPosition,
156
+ offset: props.offset as number,
157
+ content: props.content as RenderableContent<Drop>,
158
+ className: props.className as DropClassNames,
159
+ id: props.id as string,
160
+ containerClassName: props.containerClassName as string | null,
161
+ delay: props.delay as number | DropDelay,
162
+ hoverIntent: props.hoverIntent as boolean,
163
+ onShown: props.onShown as ResolvedDropProps['onShown'],
164
+ onHidden: props.onHidden as ResolvedDropProps['onHidden'],
165
+ };
166
+ }
167
+
168
+ function joinClasses(
169
+ ...classes: Array<string | null | undefined | false>
170
+ ): string {
171
+ return classes.filter(Boolean).join(' ');
172
+ }
173
+
174
+ function normalizeDelay(delay: number | DropDelay): Required<DropDelay> {
175
+ if (typeof delay === 'number' && delay >= 0) {
176
+ return { show: delay, hide: delay };
177
+ }
178
+ if (typeof delay === 'object' && delay !== null) {
179
+ return {
180
+ show: Number(delay.show) || 0,
181
+ hide: Number(delay.hide) || 0,
182
+ };
183
+ }
184
+ return { show: 0, hide: 0 };
185
+ }
186
+
187
+ /**
188
+ * 通用浮层组件。
189
+ *
190
+ * 可用于菜单、提示、下拉面板等场景,支持点击或 hover 触发,并自动计算视口内位置。
191
+ */
192
+ export class Drop {
193
+ target: Element | null;
194
+ props: ResolvedDropProps | null;
195
+ root: HTMLElement | null;
196
+ isVisible: boolean;
197
+ cleanup: DropCleanup | null;
198
+ delayShow: number;
199
+ delayHide: number;
200
+ private timer: DropTimer;
201
+ private hoverIntentData: HoverIntentData;
202
+ private lastX: number;
203
+ private lastY: number;
204
+
205
+ constructor(element: DOMReference, options: DropProps = {}) {
206
+ this.target = resolveElement(element);
207
+ this.props = normalizeProps(options);
208
+ this.root = null;
209
+ this.isVisible = false;
210
+ this.cleanup = null;
211
+ this.delayShow = 0;
212
+ this.delayHide = 0;
213
+ this.timer = { show: null, hide: null };
214
+ this.hoverIntentData = { x: 0, y: 0, lastMoveTime: 0 };
215
+ this.lastX = 0;
216
+ this.lastY = 0;
217
+
218
+ this.init(this.props);
219
+ }
220
+
221
+ private init(props: ResolvedDropProps): void {
222
+ this.cleanup = {
223
+ events: createEventManager(),
224
+ };
225
+
226
+ const delay = normalizeDelay(props.delay);
227
+ this.delayShow = delay.show;
228
+ this.delayHide = delay.hide;
229
+
230
+ this.buildDrop(props);
231
+ this.bindEvents(props);
232
+ }
233
+
234
+ private buildDrop(props: ResolvedDropProps): void {
235
+ const { className, content, id, name, containerClassName } = props;
236
+ const wrapper =
237
+ isNode(content) && content.nodeType === Node.ELEMENT_NODE
238
+ ? content
239
+ : jsx('div', {
240
+ className: joinClasses(className.container, containerClassName),
241
+ 'data-drop-container': name || id,
242
+ children: normalizeContentNodes(content, this),
243
+ });
244
+
245
+ this.root = jsx('div', {
246
+ className: className.root,
247
+ id,
248
+ 'data-drop': name || randomId(),
249
+ children: wrapper,
250
+ }) as HTMLElement;
251
+ }
252
+
253
+ private bindEvents(props: ResolvedDropProps): void {
254
+ const { mode, hoverIntent } = props;
255
+ this.unbindEvents();
256
+
257
+ if (!this.target || !this.cleanup) return;
258
+
259
+ if (mode === 'hover') {
260
+ if (hoverIntent) {
261
+ this.cleanup.events.on('target:enter', this.target, 'mouseenter', () =>
262
+ this.startHoverIntent()
263
+ );
264
+ this.cleanup.events.on('target:leave', this.target, 'mouseleave', () =>
265
+ this.cancelHoverIntent()
266
+ );
267
+ } else {
268
+ this.cleanup.events.on('target:enter', this.target, 'mouseenter', () =>
269
+ this.show()
270
+ );
271
+ this.cleanup.events.on('target:leave', this.target, 'mouseleave', () =>
272
+ this.hide()
273
+ );
274
+ }
275
+ } else {
276
+ this.cleanup.events.on('target:click', this.target, 'click', () =>
277
+ this.toggle()
278
+ );
279
+ this.cleanup.events.on('document:click', document, 'click', (event) =>
280
+ this.docClick(event)
281
+ );
282
+ }
283
+ }
284
+
285
+ private bindRootEvents(): void {
286
+ if (!this.root || this.props?.mode !== 'hover' || !this.cleanup) return;
287
+ this.cleanup.events.on('root:enter', this.root, 'mouseenter', () =>
288
+ this.show()
289
+ );
290
+ this.cleanup.events.on('root:leave', this.root, 'mouseleave', () =>
291
+ this.hide()
292
+ );
293
+ }
294
+
295
+ private unbindRootEvents(): void {
296
+ this.cleanup?.events.off('root:enter');
297
+ this.cleanup?.events.off('root:leave');
298
+ }
299
+
300
+ private unbindEvents(): void {
301
+ this.cleanup?.events.clear();
302
+ }
303
+
304
+ private startHoverIntent(): void {
305
+ if (!this.cleanup) return;
306
+
307
+ this.cleanup.events.on(
308
+ 'document:mousemove',
309
+ document,
310
+ 'mousemove',
311
+ (event) => this.onMouseMove(event)
312
+ );
313
+
314
+ if (this.timer.show) clearTimeout(this.timer.show);
315
+ this.timer.show = setTimeout(() => {
316
+ const now = Date.now();
317
+ const dt = now - this.hoverIntentData.lastMoveTime;
318
+ const dx = Math.abs(this.hoverIntentData.x - this.lastX);
319
+ const dy = Math.abs(this.hoverIntentData.y - this.lastY);
320
+ const dist = dx + dy;
321
+ if (dist < 5 || dt > 100) {
322
+ this.show();
323
+ this.cleanup?.events.off('document:mousemove');
324
+ } else {
325
+ this.startHoverIntent();
326
+ }
327
+ }, this.delayShow);
328
+ }
329
+
330
+ private cancelHoverIntent(): void {
331
+ if (this.timer.show) clearTimeout(this.timer.show);
332
+ this.cleanup?.events.off('document:mousemove');
333
+ this.hide();
334
+ }
335
+
336
+ private onMouseMove(event: Event): void {
337
+ if (!(event instanceof MouseEvent)) return;
338
+ this.lastX = event.clientX;
339
+ this.lastY = event.clientY;
340
+ this.hoverIntentData.lastMoveTime = Date.now();
341
+ this.hoverIntentData.x = event.clientX;
342
+ this.hoverIntentData.y = event.clientY;
343
+ }
344
+
345
+ private setPosition(): void {
346
+ if (!this.target || !this.root || !this.props) return;
347
+
348
+ const rect = this.target.getBoundingClientRect();
349
+ const drop = this.root;
350
+ const { offset, position } = this.props;
351
+
352
+ drop.style.visibility = 'hidden';
353
+ drop.style.display = 'block';
354
+ const dropRect = drop.getBoundingClientRect();
355
+ let top = 0;
356
+ let left = 0;
357
+ let pos = position;
358
+
359
+ if (pos === 'auto') {
360
+ const spaceBelow = window.innerHeight - rect.bottom;
361
+ pos =
362
+ spaceBelow > dropRect.height + offset ? 'top-center' : 'bottom-center';
363
+ }
364
+
365
+ switch (pos) {
366
+ case 'top-left':
367
+ top = rect.top - dropRect.height - offset;
368
+ left = rect.left;
369
+ break;
370
+ case 'top-center':
371
+ top = rect.top - dropRect.height - offset;
372
+ left = rect.left + rect.width / 2 - dropRect.width / 2;
373
+ break;
374
+ case 'top-right':
375
+ top = rect.top - dropRect.height - offset;
376
+ left = rect.right - dropRect.width;
377
+ break;
378
+ case 'bottom-left':
379
+ top = rect.bottom + offset;
380
+ left = rect.left;
381
+ break;
382
+ case 'bottom-center':
383
+ top = rect.bottom + offset;
384
+ left = rect.left + rect.width / 2 - dropRect.width / 2;
385
+ break;
386
+ case 'bottom-right':
387
+ top = rect.bottom + offset;
388
+ left = rect.right - dropRect.width;
389
+ break;
390
+ case 'left':
391
+ top = rect.top + rect.height / 2 - dropRect.height / 2;
392
+ left = rect.left - dropRect.width - offset;
393
+ break;
394
+ case 'right':
395
+ top = rect.top + rect.height / 2 - dropRect.height / 2;
396
+ left = rect.right + offset;
397
+ break;
398
+ default:
399
+ break;
400
+ }
401
+
402
+ top = Math.max(8, Math.min(top, window.innerHeight - dropRect.height - 8));
403
+ left = Math.max(8, Math.min(left, window.innerWidth - dropRect.width - 8));
404
+
405
+ drop.style.top = `${top + window.scrollY}px`;
406
+ drop.style.left = `${left + window.scrollX}px`;
407
+ drop.style.visibility = '';
408
+ drop.style.display = '';
409
+ }
410
+
411
+ private docClick(event: Event): void {
412
+ if (!(event.target instanceof Node)) return;
413
+ if (!this.root || !this.target) return;
414
+ if (
415
+ !this.root.contains(event.target) &&
416
+ !this.target.contains(event.target)
417
+ ) {
418
+ this.hide();
419
+ }
420
+ }
421
+
422
+ private exec(visible: boolean): void {
423
+ if (!this.root || !this.props) return;
424
+
425
+ if (visible) {
426
+ if (!this.root.parentNode) document.body.appendChild(this.root);
427
+ this.bindRootEvents();
428
+ this.setPosition();
429
+ } else {
430
+ this.unbindRootEvents();
431
+ this.root.style.top = '';
432
+ this.root.style.left = '';
433
+ this.root.remove();
434
+ }
435
+
436
+ this.root.classList.toggle(this.props.className.active, visible);
437
+ this.isVisible = visible;
438
+ }
439
+
440
+ show(useDelay = true): void {
441
+ if (this.timer.hide) clearTimeout(this.timer.hide);
442
+ if (this.isVisible) return;
443
+
444
+ if (useDelay && this.delayShow > 0) {
445
+ if (this.timer.show) clearTimeout(this.timer.show);
446
+ this.timer.show = setTimeout(() => this.exec(true), this.delayShow);
447
+ } else {
448
+ this.exec(true);
449
+ }
450
+
451
+ void this.props?.onShown?.(this);
452
+ }
453
+
454
+ hide(useDelay = true): void {
455
+ if (this.timer.show) clearTimeout(this.timer.show);
456
+ if (!this.isVisible) return;
457
+
458
+ if (useDelay && this.delayHide > 0) {
459
+ if (this.timer.hide) clearTimeout(this.timer.hide);
460
+ this.timer.hide = setTimeout(() => this.exec(false), this.delayHide);
461
+ } else {
462
+ this.exec(false);
463
+ }
464
+
465
+ void this.props?.onHidden?.(this);
466
+ }
467
+
468
+ toggle(): void {
469
+ if (this.isVisible) this.hide();
470
+ else this.show();
471
+ }
472
+
473
+ destroy(): void {
474
+ if (!this.props) return;
475
+
476
+ if (this.timer.show) clearTimeout(this.timer.show);
477
+ if (this.timer.hide) clearTimeout(this.timer.hide);
478
+
479
+ this.unbindEvents();
480
+ this.root?.remove();
481
+
482
+ this.props = null;
483
+ this.root = null;
484
+ this.target = null;
485
+ this.timer = { show: null, hide: null };
486
+ this.cleanup?.events.clear();
487
+ this.cleanup = null;
488
+ this.isVisible = false;
489
+ }
490
+ }
491
+
492
+ export function createDrop(
493
+ container: DOMReference,
494
+ input: DropProps = {}
495
+ ): Drop {
496
+ return new Drop(container, input);
497
+ }