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,385 @@
1
+ import { createDeepStore, jsx } from 'vanilla-signal';
2
+
3
+ import Component, {
4
+ type ComponentDOM,
5
+ type ComponentRuntime,
6
+ } from '../core/Component.ts';
7
+ import {
8
+ type ResolveSchema,
9
+ randomId,
10
+ resolveProps,
11
+ } from '../utilities/core.ts';
12
+ import { type DOMReference, all, requireContainer } from '../utilities/dom.ts';
13
+
14
+ export interface TocClassNames {
15
+ toc: string;
16
+ list: string;
17
+ link: string;
18
+ active: string;
19
+ levelPrefix: string;
20
+ }
21
+
22
+ export type TocClassNameConfig = Partial<TocClassNames>;
23
+
24
+ export interface TocItem {
25
+ id: string;
26
+ text: string;
27
+ level: number;
28
+ element: HTMLHeadingElement;
29
+ }
30
+
31
+ export interface TocCurrent {
32
+ index: number;
33
+ item: TocItem | null;
34
+ }
35
+
36
+ export interface TocProps extends Record<string, unknown> {
37
+ container?: DOMReference;
38
+ target?: DOMReference;
39
+ headings?: string;
40
+ offset?: number;
41
+ className?: TocClassNameConfig;
42
+ onUpdate?: ((item: TocItem | null, index: number, toc: Toc) => void) | null;
43
+ }
44
+
45
+ interface ResolvedTocProps extends Record<string, unknown> {
46
+ container: DOMReference;
47
+ target: DOMReference;
48
+ headings: string;
49
+ offset: number;
50
+ className: TocClassNames;
51
+ onUpdate: ((item: TocItem | null, index: number, toc: Toc) => void) | null;
52
+ }
53
+
54
+ interface TocState extends Record<string, unknown> {
55
+ items: TocItem[];
56
+ current: TocCurrent;
57
+ }
58
+
59
+ interface TocDOM extends ComponentDOM {
60
+ root: HTMLElement | null;
61
+ container: Element | null;
62
+ target: Element | null;
63
+ list: HTMLElement | null;
64
+ headings: HTMLHeadingElement[];
65
+ links: HTMLAnchorElement[];
66
+ }
67
+
68
+ interface TocRuntime extends ComponentRuntime {
69
+ built: boolean;
70
+ ticking: boolean;
71
+ }
72
+
73
+ interface TocScrollOptions {
74
+ activeIndex?: number;
75
+ updateHash?: boolean;
76
+ }
77
+
78
+ const DEFAULT_CLASS_NAMES: TocClassNames = {
79
+ toc: 'j-toc',
80
+ list: 'toc-list',
81
+ link: 'toc-link',
82
+ active: 'is-active',
83
+ levelPrefix: 'is-level-',
84
+ };
85
+
86
+ const TOC_PROPS_SCHEMA = {
87
+ container: { default: null },
88
+ target: { default: '.j-content' },
89
+ headings: { default: 'h2, h3', type: 'string' },
90
+ offset: {
91
+ default: 80,
92
+ type: 'number',
93
+ validate: (value) => typeof value === 'number' && value >= 0,
94
+ message: 'expects a positive number or 0.',
95
+ },
96
+ className: {
97
+ default: DEFAULT_CLASS_NAMES,
98
+ type: 'object',
99
+ normalize: (value: unknown) => ({
100
+ ...DEFAULT_CLASS_NAMES,
101
+ ...(value && typeof value === 'object' ? value : {}),
102
+ }),
103
+ },
104
+ onUpdate: { default: null, types: ['function', 'null'] },
105
+ } satisfies ResolveSchema<TocProps>;
106
+
107
+ const ACTIVE_OFFSET_TOLERANCE = 1;
108
+
109
+ function normalizeProps(input: TocProps): ResolvedTocProps {
110
+ const props = resolveProps(input, TOC_PROPS_SCHEMA, 'Toc.props');
111
+ return {
112
+ container: props.container as DOMReference,
113
+ target: props.target as DOMReference,
114
+ headings: props.headings as string,
115
+ offset: props.offset as number,
116
+ className: props.className as TocClassNames,
117
+ onUpdate: props.onUpdate as ResolvedTocProps['onUpdate'],
118
+ };
119
+ }
120
+
121
+ function resolveHeadingLevel(element: Element): number {
122
+ const match = /^H([1-6])$/.exec(element.tagName);
123
+ return match ? Number(match[1]) : 1;
124
+ }
125
+
126
+ function normalizeHeading(element: HTMLHeadingElement, index: number): TocItem {
127
+ if (!element.id) element.id = `toc-${randomId()}-${index}`;
128
+ return {
129
+ id: element.id,
130
+ text: element.textContent || '',
131
+ level: resolveHeadingLevel(element),
132
+ element,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * 页面目录组件。
138
+ *
139
+ * 扫描内容区域内的标题,生成锚点列表,并随页面滚动更新 active 状态。
140
+ */
141
+ export class Toc extends Component<ResolvedTocProps, TocState, TocDOM> {
142
+ declare runtime: TocRuntime;
143
+
144
+ /**
145
+ * 创建 Toc 实例。
146
+ * @param {object} [input={}] Toc 配置。
147
+ */
148
+ constructor(input: TocProps = {}) {
149
+ const props = normalizeProps(input);
150
+ super(props);
151
+
152
+ this.dom.container = null;
153
+ this.dom.target = null;
154
+ this.dom.list = null;
155
+ this.dom.headings = [];
156
+ this.dom.links = [];
157
+
158
+ this.runtime.built = false;
159
+ this.runtime.ticking = false;
160
+
161
+ this.state = createDeepStore({
162
+ items: [],
163
+ current: {
164
+ index: -1,
165
+ item: null,
166
+ },
167
+ });
168
+ }
169
+
170
+ /**
171
+ * 构建 Toc DOM 和滚动监听。
172
+ * @returns {Toc} 当前实例。
173
+ */
174
+ build(): this {
175
+ if (this.runtime.destroyed)
176
+ throw new Error('Toc.build: instance destroyed');
177
+ if (this.runtime.built) return this;
178
+
179
+ this.init(this.props);
180
+ this.dom.container = requireContainer(
181
+ this.props.container,
182
+ 'Toc.container'
183
+ );
184
+ this.dom.target = requireContainer(this.props.target, 'Toc.target');
185
+ const root = document.createElement('nav');
186
+ root.className = this.props.className.toc;
187
+ root.dataset.toc = 'root';
188
+ this.root = root;
189
+ this.dom.list = document.createElement('div');
190
+ this.dom.list.className = this.props.className.list;
191
+ this.dom.list.dataset.tocList = 'root';
192
+ root.appendChild(this.dom.list);
193
+ this.dom.container.innerHTML = '';
194
+ this.dom.container.appendChild(root);
195
+
196
+ this.runtime.built = true;
197
+ this.refresh();
198
+ return this;
199
+ }
200
+
201
+ private bindEvents(): void {
202
+ if (!this.dom.list) return;
203
+ this.cleanup.events.on('scroll', window, 'scroll', () => this.onScroll(), {
204
+ passive: true,
205
+ });
206
+ this.cleanup.events.on('click', this.dom.list, 'click', (event) =>
207
+ this.onClick(event)
208
+ );
209
+ }
210
+
211
+ private onScroll(): void {
212
+ if (this.runtime.ticking) return;
213
+
214
+ requestAnimationFrame(() => {
215
+ this.updateActive();
216
+ this.runtime.ticking = false;
217
+ });
218
+ this.runtime.ticking = true;
219
+ }
220
+
221
+ private onClick(event: Event): void {
222
+ if (!(event.target instanceof Element) || !this.dom.list) return;
223
+ const link = event.target.closest<HTMLElement>('[data-toc-index]');
224
+ if (!link || !this.dom.list.contains(link)) return;
225
+
226
+ const index = Number(link.dataset.tocIndex);
227
+ const item = this.state?.items[index];
228
+ if (!item) return;
229
+
230
+ event.preventDefault();
231
+ this.scrollToItem(item, { activeIndex: index, updateHash: true });
232
+ }
233
+
234
+ private scrollToItem(
235
+ item: TocItem,
236
+ { activeIndex = -1, updateHash = false }: TocScrollOptions = {}
237
+ ): void {
238
+ const scrollY = window.scrollY || window.pageYOffset || 0;
239
+ const top = Math.max(
240
+ 0,
241
+ item.element.getBoundingClientRect().top + scrollY - this.props.offset
242
+ );
243
+
244
+ window.scrollTo({
245
+ top,
246
+ behavior: 'smooth',
247
+ });
248
+
249
+ if (activeIndex >= 0) this.setActive(activeIndex);
250
+
251
+ if (updateHash && window.history?.pushState) {
252
+ window.history.pushState(null, '', `#${item.id}`);
253
+ }
254
+ }
255
+
256
+ private linkClassName(item: TocItem, active = false): string {
257
+ const classes = [
258
+ this.props.className.link,
259
+ `${this.props.className.levelPrefix}${item.level}`,
260
+ ];
261
+ if (active) classes.push(this.props.className.active);
262
+ return classes.filter(Boolean).join(' ');
263
+ }
264
+
265
+ private buildLink(item: TocItem, index: number): HTMLAnchorElement {
266
+ return jsx('a', {
267
+ className: this.linkClassName(item),
268
+ href: `#${item.id}`,
269
+ 'data-toc-index': String(index),
270
+ 'data-toc-target': item.id,
271
+ children: item.text,
272
+ }) as HTMLAnchorElement;
273
+ }
274
+
275
+ private updateActive(): void {
276
+ if (!this.runtime.built) return;
277
+
278
+ let index = -1;
279
+ const activeOffset = this.props.offset + ACTIVE_OFFSET_TOLERANCE;
280
+ for (let i = this.dom.headings.length - 1; i >= 0; i--) {
281
+ if (this.dom.headings[i].getBoundingClientRect().top <= activeOffset) {
282
+ index = i;
283
+ break;
284
+ }
285
+ }
286
+
287
+ this.setActive(index);
288
+ }
289
+
290
+ private setActive(index: number): void {
291
+ if (!this.state || index === this.state.current.index) return;
292
+
293
+ const current = this.state.items[index] || null;
294
+ this.setState({
295
+ current: { index, item: current },
296
+ });
297
+
298
+ const items = this.state.items;
299
+ this.dom.links.forEach((link, i) => {
300
+ const active = i === index;
301
+ link.dataset.active = active ? '1' : '0';
302
+ link.className = this.linkClassName(items[i], active);
303
+ });
304
+
305
+ if (typeof this.props.onUpdate === 'function') {
306
+ this.props.onUpdate(current, index, this);
307
+ }
308
+ }
309
+
310
+ /**
311
+ * 重新扫描标题并重建目录列表。
312
+ * @returns {Toc} 当前实例。
313
+ */
314
+ refresh(): this {
315
+ if (
316
+ this.runtime.destroyed ||
317
+ !this.runtime.built ||
318
+ !this.dom.target ||
319
+ !this.dom.list
320
+ ) {
321
+ return this;
322
+ }
323
+
324
+ this.cleanup.events.clear();
325
+
326
+ this.dom.headings = all<HTMLHeadingElement>(
327
+ this.props.headings,
328
+ this.dom.target
329
+ );
330
+ const items = this.dom.headings.map(normalizeHeading);
331
+ this.dom.links = items.map((item, index) => this.buildLink(item, index));
332
+
333
+ this.dom.list.innerHTML = '';
334
+ for (const link of this.dom.links) this.dom.list.appendChild(link);
335
+
336
+ this.setState({
337
+ items,
338
+ current: { index: -1, item: null },
339
+ });
340
+
341
+ this.bindEvents();
342
+ this.updateActive();
343
+ return this;
344
+ }
345
+
346
+ /**
347
+ * 激活并滚动到指定目录项。
348
+ * @param {number} index 目录项索引。
349
+ * @returns {Toc} 当前实例。
350
+ */
351
+ activate(index: number): this {
352
+ if (!this.runtime.built || !this.state) return this;
353
+ if (
354
+ !Number.isInteger(index) ||
355
+ index < 0 ||
356
+ index >= this.dom.links.length
357
+ ) {
358
+ return this;
359
+ }
360
+
361
+ this.scrollToItem(this.state.items[index]);
362
+ return this;
363
+ }
364
+
365
+ /**
366
+ * 销毁实例并清空渲染内容。
367
+ * @private
368
+ */
369
+ protected onDestroy(): void {
370
+ this.cleanup.events.clear();
371
+ if (this.dom.container) this.dom.container.innerHTML = '';
372
+
373
+ this.runtime.built = false;
374
+ this.runtime.ticking = false;
375
+ this.dom.container = null;
376
+ this.dom.target = null;
377
+ this.dom.list = null;
378
+ this.dom.headings = [];
379
+ this.dom.links = [];
380
+ }
381
+ }
382
+
383
+ export function createToc(props: TocProps = {}): Toc {
384
+ return new Toc(props);
385
+ }
@@ -0,0 +1,179 @@
1
+ import { jsx } from 'vanilla-signal';
2
+
3
+ import { type ResolveSchema, resolveProps } from '../utilities/core.ts';
4
+ import { type DOMReference } from '../utilities/dom.ts';
5
+ import {
6
+ Drop,
7
+ type DropClassNameConfig,
8
+ type DropDelay,
9
+ type DropMode,
10
+ type DropPosition,
11
+ } from './drop.ts';
12
+
13
+ export interface TooltipClassNames {
14
+ root: string;
15
+ message: string;
16
+ }
17
+
18
+ export type TooltipClassNameConfig = Partial<TooltipClassNames>;
19
+
20
+ export interface TooltipProps extends Record<string, unknown> {
21
+ name?: string | null;
22
+ mode?: DropMode;
23
+ position?: DropPosition;
24
+ offset?: number;
25
+ message?: string;
26
+ className?: TooltipClassNameConfig;
27
+ dropClassName?: DropClassNameConfig;
28
+ id?: string | null;
29
+ delay?: number | DropDelay;
30
+ hoverIntent?: boolean;
31
+ onShown?: ((drop: Drop) => void | Promise<void>) | null;
32
+ onHidden?: ((drop: Drop) => void | Promise<void>) | null;
33
+ }
34
+
35
+ interface ResolvedTooltipProps extends Record<string, unknown> {
36
+ name: string | null;
37
+ mode: DropMode;
38
+ position: DropPosition;
39
+ offset: number;
40
+ message: string;
41
+ className: TooltipClassNames;
42
+ dropClassName: DropClassNameConfig;
43
+ id: string | null;
44
+ delay: number | DropDelay;
45
+ hoverIntent: boolean;
46
+ onShown: NonNullable<TooltipProps['onShown']> | null;
47
+ onHidden: NonNullable<TooltipProps['onHidden']> | null;
48
+ }
49
+
50
+ const DEFAULT_CLASS_NAMES: TooltipClassNames = {
51
+ root: 'j-tooltip',
52
+ message: 'tooltip-message',
53
+ };
54
+
55
+ const TOOLTIP_OPTIONS_SCHEMA = {
56
+ name: { default: null, types: ['string', 'null'] },
57
+ mode: { default: 'hover', type: 'string', enum: ['hover', 'click'] },
58
+ position: {
59
+ default: 'auto',
60
+ type: 'string',
61
+ enum: [
62
+ 'auto',
63
+ 'top-left',
64
+ 'top-center',
65
+ 'top-right',
66
+ 'bottom-left',
67
+ 'bottom-center',
68
+ 'bottom-right',
69
+ 'left',
70
+ 'right',
71
+ ],
72
+ },
73
+ offset: { default: 8, type: 'number' },
74
+ message: {
75
+ default: '',
76
+ type: 'string',
77
+ normalize: (value: unknown) =>
78
+ typeof value === 'string' ? value.trim() : value,
79
+ validate: (value: unknown) => typeof value === 'string' && value.length > 0,
80
+ message: 'expects a non-empty string.',
81
+ },
82
+ className: {
83
+ default: DEFAULT_CLASS_NAMES,
84
+ type: 'object',
85
+ normalize: (value: unknown) => ({
86
+ ...DEFAULT_CLASS_NAMES,
87
+ ...(value && typeof value === 'object' ? value : {}),
88
+ }),
89
+ },
90
+ dropClassName: { default: {}, type: 'object' },
91
+ id: { default: null, types: ['string', 'null'] },
92
+ delay: { default: 100, types: ['number', 'object'] },
93
+ hoverIntent: { default: true, type: 'boolean' },
94
+ onShown: { default: null, types: ['function', 'null'] },
95
+ onHidden: { default: null, types: ['function', 'null'] },
96
+ } satisfies ResolveSchema<TooltipProps>;
97
+
98
+ function normalizeProps(input: TooltipProps): ResolvedTooltipProps {
99
+ const props = resolveProps(input, TOOLTIP_OPTIONS_SCHEMA, 'Tooltip.props');
100
+ return {
101
+ name: props.name as string | null,
102
+ mode: props.mode as DropMode,
103
+ position: props.position as DropPosition,
104
+ offset: props.offset as number,
105
+ message: props.message as string,
106
+ className: props.className as TooltipClassNames,
107
+ dropClassName: props.dropClassName as DropClassNameConfig,
108
+ id: props.id as string | null,
109
+ delay: props.delay as number | DropDelay,
110
+ hoverIntent: props.hoverIntent as boolean,
111
+ onShown: props.onShown as ResolvedTooltipProps['onShown'],
112
+ onHidden: props.onHidden as ResolvedTooltipProps['onHidden'],
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Tooltip 提示组件。
118
+ *
119
+ * 基于 Drop 实现,提供更轻量的文本提示封装。
120
+ */
121
+ export class Tooltip {
122
+ drop: Drop | null;
123
+ props: ResolvedTooltipProps | null;
124
+
125
+ constructor(element: DOMReference, props: TooltipProps = {}) {
126
+ const settings = normalizeProps(props);
127
+ this.props = settings;
128
+ this.drop = new Drop(element, {
129
+ name: settings.name,
130
+ mode: settings.mode,
131
+ position: settings.position,
132
+ offset: settings.offset,
133
+ id: settings.id,
134
+ delay: settings.delay,
135
+ hoverIntent: settings.hoverIntent,
136
+ onShown: settings.onShown,
137
+ onHidden: settings.onHidden,
138
+ className: settings.dropClassName,
139
+ content: this.buildContent(settings),
140
+ });
141
+ }
142
+
143
+ private buildContent(settings: ResolvedTooltipProps): HTMLElement {
144
+ return jsx('div', {
145
+ className: settings.className.root,
146
+ 'data-tooltip': settings.name || settings.id || '',
147
+ children: jsx('div', {
148
+ className: settings.className.message,
149
+ 'data-tooltip-message': '',
150
+ children: settings.message,
151
+ }),
152
+ }) as HTMLElement;
153
+ }
154
+
155
+ show(useDelay = true): void {
156
+ this.drop?.show(useDelay);
157
+ }
158
+
159
+ hide(useDelay = true): void {
160
+ this.drop?.hide(useDelay);
161
+ }
162
+
163
+ toggle(): void {
164
+ this.drop?.toggle();
165
+ }
166
+
167
+ destroy(): void {
168
+ this.drop?.destroy();
169
+ this.drop = null;
170
+ this.props = null;
171
+ }
172
+ }
173
+
174
+ export function createTooltip(
175
+ element: DOMReference,
176
+ props: TooltipProps = {}
177
+ ): Tooltip {
178
+ return new Tooltip(element, props);
179
+ }