vanilla-jui 1.3.11 → 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 (71) 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/animation.css +31 -1
  26. package/src/css/card.css +1 -1
  27. package/src/css/color.css +863 -1013
  28. package/src/css/color_p3.css +822 -975
  29. package/src/css/components.css +1 -0
  30. package/src/css/form.css +1 -1
  31. package/src/css/icon.css +17 -1
  32. package/src/css/index.css +2 -2
  33. package/src/css/menu.css +1 -1
  34. package/src/css/tabs.css +1 -0
  35. package/src/css/tokens.css +1 -1
  36. package/src/index.ts +37 -0
  37. package/src/locales/{index.js → index.ts} +1 -2
  38. package/src/types.d.ts +1 -0
  39. package/src/utilities/browser.ts +131 -0
  40. package/src/utilities/cache.ts +57 -0
  41. package/src/utilities/core.ts +614 -0
  42. package/src/utilities/dom.ts +468 -0
  43. package/src/utilities/events.ts +106 -0
  44. package/src/utilities/http.ts +35 -0
  45. package/src/utilities/storage.ts +46 -0
  46. package/src/components/accordion.js +0 -373
  47. package/src/components/drop.js +0 -437
  48. package/src/components/flow.js +0 -1133
  49. package/src/components/form.js +0 -585
  50. package/src/components/menu.js +0 -422
  51. package/src/components/offcanvas.js +0 -332
  52. package/src/components/parabola.js +0 -295
  53. package/src/components/tabs.js +0 -746
  54. package/src/components/theme.js +0 -353
  55. package/src/components/toast.js +0 -329
  56. package/src/components/toc.js +0 -226
  57. package/src/components/tooltip.js +0 -86
  58. package/src/components/validator.js +0 -586
  59. package/src/index.js +0 -45
  60. package/src/locales/index.min.js +0 -1
  61. package/src/utilities/browser.js +0 -111
  62. package/src/utilities/cache.js +0 -159
  63. package/src/utilities/core.js +0 -502
  64. package/src/utilities/debug.js +0 -103
  65. package/src/utilities/dom.js +0 -313
  66. package/src/utilities/events.js +0 -69
  67. package/src/utilities/http.js +0 -30
  68. package/src/utilities/legacy-dom.js +0 -179
  69. package/src/utilities/storage.js +0 -46
  70. /package/src/{components/components.js → css/themes/index.css} +0 -0
  71. /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
+ }