vanilla-jui 1.3.11 → 1.4.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 (106) hide show
  1. package/dist/index.d.ts +2921 -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 +457 -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 +0 -45
  26. package/src/css/animation.css +31 -1
  27. package/src/css/avatar.css +7 -33
  28. package/src/css/badge.css +11 -59
  29. package/src/css/breadcrumb.css +9 -32
  30. package/src/css/button.css +23 -224
  31. package/src/css/card.css +1 -1
  32. package/src/css/color.css +863 -1013
  33. package/src/css/color_p3.css +822 -975
  34. package/src/css/components.css +1 -0
  35. package/src/css/drop.css +7 -7
  36. package/src/css/flow.css +1 -1
  37. package/src/css/form.css +1 -1
  38. package/src/css/icon.css +7 -12
  39. package/src/css/index.css +5 -2
  40. package/src/css/loading.css +36 -20
  41. package/src/css/menu.css +1 -1
  42. package/src/css/modal.css +15 -44
  43. package/src/css/offcanvas.css +14 -40
  44. package/src/css/popup.css +4 -4
  45. package/src/css/status.css +0 -2
  46. package/src/css/swiper.css +35 -50
  47. package/src/css/tabs.css +9 -31
  48. package/src/css/tag.css +16 -77
  49. package/src/css/themes/default/accordion.css +62 -0
  50. package/src/css/themes/default/avatar.css +39 -0
  51. package/src/css/themes/default/badge.css +63 -0
  52. package/src/css/themes/default/breadcrumb.css +28 -0
  53. package/src/css/themes/default/button.css +204 -0
  54. package/src/css/themes/default/drop.css +11 -0
  55. package/src/css/themes/default/index.css +16 -0
  56. package/src/css/themes/default/loading.css +23 -0
  57. package/src/css/themes/default/modal.css +40 -0
  58. package/src/css/themes/default/offcanvas.css +13 -0
  59. package/src/css/themes/default/popup.css +7 -0
  60. package/src/css/themes/default/swiper.css +35 -0
  61. package/src/css/themes/default/tabs.css +37 -0
  62. package/src/css/themes/default/tag.css +85 -0
  63. package/src/css/themes/default/tip.css +49 -0
  64. package/src/css/themes/default/toast.css +70 -0
  65. package/src/css/themes/default/tooltip.css +14 -0
  66. package/src/css/themes/index.css +1 -0
  67. package/src/css/tip.css +20 -39
  68. package/src/css/toast.css +18 -80
  69. package/src/css/tokens.css +8 -3
  70. package/src/css/tooltip.css +10 -10
  71. package/src/index.ts +37 -0
  72. package/src/locales/{index.js → index.ts} +1 -2
  73. package/src/types.d.ts +1 -0
  74. package/src/utilities/browser.ts +131 -0
  75. package/src/utilities/cache.ts +57 -0
  76. package/src/utilities/core.ts +614 -0
  77. package/src/utilities/dom.ts +468 -0
  78. package/src/utilities/events.ts +106 -0
  79. package/src/utilities/http.ts +35 -0
  80. package/src/utilities/storage.ts +46 -0
  81. package/src/components/accordion.js +0 -373
  82. package/src/components/components.js +0 -0
  83. package/src/components/drop.js +0 -437
  84. package/src/components/flow.js +0 -1133
  85. package/src/components/form.js +0 -585
  86. package/src/components/menu.js +0 -422
  87. package/src/components/offcanvas.js +0 -332
  88. package/src/components/parabola.js +0 -295
  89. package/src/components/tabs.js +0 -746
  90. package/src/components/theme.js +0 -353
  91. package/src/components/toast.js +0 -329
  92. package/src/components/toc.js +0 -226
  93. package/src/components/tooltip.js +0 -86
  94. package/src/components/validator.js +0 -586
  95. package/src/index.js +0 -45
  96. package/src/locales/index.min.js +0 -1
  97. package/src/utilities/browser.js +0 -111
  98. package/src/utilities/cache.js +0 -159
  99. package/src/utilities/core.js +0 -502
  100. package/src/utilities/debug.js +0 -103
  101. package/src/utilities/dom.js +0 -313
  102. package/src/utilities/events.js +0 -69
  103. package/src/utilities/http.js +0 -30
  104. package/src/utilities/legacy-dom.js +0 -179
  105. package/src/utilities/storage.js +0 -46
  106. /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
+ }