vanilla-jui 1.4.8 → 1.5.2

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 (58) hide show
  1. package/README.md +10 -4
  2. package/dist/index.d.ts +402 -977
  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 +467 -349
  8. package/src/components/flow.ts +779 -1133
  9. package/src/components/form.ts +666 -509
  10. package/src/components/menu.ts +199 -326
  11. package/src/components/modal.ts +510 -747
  12. package/src/components/offcanvas.ts +281 -361
  13. package/src/components/pagination.ts +207 -374
  14. package/src/components/swiper.ts +688 -1168
  15. package/src/components/tabs.ts +468 -788
  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/color.css +4 -5
  24. package/src/css/color_p3.css +6 -5
  25. package/src/css/content.css +18 -24
  26. package/src/css/drop.css +4 -1
  27. package/src/css/form.css +8 -0
  28. package/src/css/gradient.css +64 -131
  29. package/src/css/layout.css +9 -23
  30. package/src/css/menu.css +10 -5
  31. package/src/css/modal.css +2 -1
  32. package/src/css/offcanvas.css +1 -17
  33. package/src/css/swiper.css +2 -1
  34. package/src/css/tabs.css +8 -17
  35. package/src/css/themes/default/accordion.css +0 -48
  36. package/src/css/themes/default/swiper.css +1 -1
  37. package/src/css/themes/default/tabs.css +4 -6
  38. package/src/css/toast.css +1 -17
  39. package/src/css/tokens.css +18 -35
  40. package/src/css/tooltip.css +2 -1
  41. package/src/index.ts +8 -3
  42. package/src/locales/index.ts +3 -0
  43. package/src/primitives/drop.ts +253 -262
  44. package/src/primitives/loading.ts +22 -4
  45. package/src/primitives/parabola.ts +123 -164
  46. package/src/primitives/sticky.ts +297 -0
  47. package/src/primitives/theme.ts +164 -222
  48. package/src/primitives/toast.ts +309 -284
  49. package/src/primitives/tooltip.ts +65 -66
  50. package/src/utilities/browser.ts +38 -0
  51. package/src/utilities/dom.ts +0 -54
  52. package/src/utilities/refs.ts +61 -0
  53. package/src/utilities/state.ts +73 -0
  54. package/src/utilities/timer.ts +11 -31
  55. package/src/validation/validator.ts +289 -389
  56. package/src/components/sticky.ts +0 -297
  57. package/src/core/Component.ts +0 -377
  58. package/src/utilities/scheduler.ts +0 -156
@@ -1,11 +1,18 @@
1
- import { createDeepStore, jsx } from 'vanilla-signal';
2
-
3
- import Component, {
4
- type ComponentDOM,
5
- type ComponentRuntime,
6
- } from '../core/Component.ts';
1
+ import {
2
+ For,
3
+ createMemo,
4
+ createDeepStore,
5
+ flushSync,
6
+ jsx,
7
+ } from 'vanilla-signal';
8
+
9
+ import {
10
+ type FunctionalComponent,
11
+ defineComponent,
12
+ } from '../core/component.ts';
7
13
  import { joinClasses } from '../utilities/class-name.ts';
8
14
  import { type DOMReference, all, requireContainer } from '../utilities/dom.ts';
15
+ import { createEventManager } from '../utilities/events.ts';
9
16
  import { randomId } from '../utilities/id.ts';
10
17
  import { type ResolveSchema, resolveProps } from '../utilities/types.ts';
11
18
 
@@ -19,14 +26,13 @@ interface TocClassNames {
19
26
 
20
27
  type TocClassNameConfig = Partial<TocClassNames>;
21
28
 
22
- interface TocItem {
29
+ export interface TocItem {
23
30
  id: string;
24
31
  text: string;
25
32
  level: number;
26
- element: HTMLHeadingElement;
27
33
  }
28
34
 
29
- interface TocCurrent {
35
+ export interface TocCurrent {
30
36
  index: number;
31
37
  item: TocItem | null;
32
38
  }
@@ -35,6 +41,7 @@ interface TocProps extends Record<string, unknown> {
35
41
  target?: DOMReference;
36
42
  headings?: string;
37
43
  offset?: number;
44
+ reactive?: boolean;
38
45
  className?: TocClassNameConfig;
39
46
  onChange?:
40
47
  | ((item: TocItem | null, index: number, toc: TocInstance) => void)
@@ -45,6 +52,7 @@ interface ResolvedTocProps extends Record<string, unknown> {
45
52
  target: DOMReference;
46
53
  headings: string;
47
54
  offset: number;
55
+ reactive: boolean;
48
56
  className: TocClassNames;
49
57
  onChange:
50
58
  | ((item: TocItem | null, index: number, toc: TocInstance) => void)
@@ -56,17 +64,11 @@ interface TocState extends Record<string, unknown> {
56
64
  current: TocCurrent;
57
65
  }
58
66
 
59
- interface TocDOM extends ComponentDOM {
60
- root: HTMLElement | null;
61
- target: Element | null;
62
- list: HTMLElement | null;
63
- headings: HTMLHeadingElement[];
64
- links: HTMLAnchorElement[];
65
- }
66
-
67
- interface TocRuntime extends ComponentRuntime {
68
- built: boolean;
67
+ interface TocRuntimeExtras {
69
68
  ticking: boolean;
69
+ frameId: number;
70
+ refreshing: boolean;
71
+ refreshFrameId: number;
70
72
  }
71
73
 
72
74
  interface TocScrollOptions {
@@ -74,12 +76,16 @@ interface TocScrollOptions {
74
76
  updateHash?: boolean;
75
77
  }
76
78
 
77
- type TocInstance = Component<ResolvedTocProps, TocState, TocDOM> & {
78
- runtime: TocRuntime;
79
- build(): TocInstance;
80
- refresh(): TocInstance;
79
+ interface TocActions {
81
80
  activate(index: number): TocInstance;
82
- };
81
+ }
82
+
83
+ type TocInstance = FunctionalComponent<
84
+ ResolvedTocProps,
85
+ TocState,
86
+ HTMLElement,
87
+ TocActions
88
+ >;
83
89
 
84
90
  const DEFAULT_CLASS_NAMES: TocClassNames = {
85
91
  toc: 'j-toc',
@@ -97,6 +103,7 @@ const TOC_PROPS_SCHEMA = {
97
103
  type: 'number',
98
104
  min: 0,
99
105
  },
106
+ reactive: { default: false, type: 'boolean' },
100
107
  className: {
101
108
  default: DEFAULT_CLASS_NAMES,
102
109
  type: 'object',
@@ -116,6 +123,7 @@ function normalizeProps(input: TocProps): ResolvedTocProps {
116
123
  target: props.target as DOMReference,
117
124
  headings: props.headings as string,
118
125
  offset: props.offset as number,
126
+ reactive: props.reactive as boolean,
119
127
  className: props.className as TocClassNames,
120
128
  onChange: props.onChange as ResolvedTocProps['onChange'],
121
129
  };
@@ -132,250 +140,189 @@ function normalizeHeading(element: HTMLHeadingElement, index: number): TocItem {
132
140
  id: element.id,
133
141
  text: element.textContent || '',
134
142
  level: resolveHeadingLevel(element),
135
- element,
136
143
  };
137
144
  }
138
145
 
139
- /**
140
- * 页面目录组件。
141
- *
142
- * 扫描内容区域内的标题,生成锚点列表,并随页面滚动更新 active 状态。
143
- */
144
- class Toc extends Component<ResolvedTocProps, TocState, TocDOM> {
145
- declare runtime: TocRuntime;
146
-
147
- /**
148
- * 创建 Toc 实例。
149
- * @param {object} [input={}] Toc 配置。
150
- */
151
- constructor(props: TocProps = {}) {
152
- const settings = normalizeProps(props);
153
- super(settings);
154
-
155
- this.dom.target = null;
156
- this.dom.list = null;
157
- this.dom.headings = [];
158
- this.dom.links = [];
159
-
160
- this.runtime.built = false;
161
- this.runtime.ticking = false;
162
-
163
- this.state = createDeepStore({
164
- items: [],
165
- current: {
166
- index: -1,
167
- item: null,
168
- },
146
+ export function createToc(props: TocProps = {}): TocInstance {
147
+ const settings = normalizeProps(props);
148
+ const state = createDeepStore({
149
+ items: [],
150
+ current: { index: -1, item: null },
151
+ }) as TocState;
152
+ const runtime: TocRuntimeExtras = {
153
+ ticking: false,
154
+ frameId: 0,
155
+ refreshing: false,
156
+ refreshFrameId: 0,
157
+ };
158
+ const events = createEventManager();
159
+ let target: Element | null = null;
160
+ let headings: HTMLHeadingElement[] = [];
161
+ let observer: MutationObserver | null = null;
162
+ let toc: TocInstance;
163
+ const renderableItems = createMemo(() => state.items);
164
+
165
+ const setActive = (index: number): void => {
166
+ if (index === state.current.index) return;
167
+ const current = state.items[index] || null;
168
+ flushSync(() => {
169
+ state.current = { index, item: current };
169
170
  });
170
- }
171
-
172
- /**
173
- * 构建 Toc DOM 和滚动监听。
174
- * @returns {Toc} 当前实例。
175
- */
176
- build(): this {
177
- if (this.runtime.destroyed)
178
- throw new Error('Toc.build: instance destroyed');
179
- if (this.runtime.built) return this;
180
-
181
- this.init(this.props);
182
- this.dom.target = requireContainer(this.props.target, 'Toc.target');
183
- this.dom.list = jsx('div', {
184
- className: this.props.className.list,
185
- 'data-toc-list': 'root',
186
- }) as HTMLElement;
187
- this.dom.root = jsx('nav', {
188
- className: this.props.className.toc,
189
- 'data-toc': 'root',
190
- children: this.dom.list,
191
- }) as HTMLElement;
171
+ settings.onChange?.(current, index, toc);
172
+ };
192
173
 
193
- this.runtime.built = true;
194
- this.refresh();
195
- return this;
196
- }
174
+ const updateActive = (): void => {
175
+ if (!toc.runtime.built) return;
176
+ let index = -1;
177
+ const activeOffset = settings.offset + ACTIVE_OFFSET_TOLERANCE;
178
+ for (let current = headings.length - 1; current >= 0; current--) {
179
+ if (headings[current].getBoundingClientRect().top <= activeOffset) {
180
+ index = current;
181
+ break;
182
+ }
183
+ }
184
+ setActive(index);
185
+ };
197
186
 
198
- private bindEvents(): void {
199
- if (!this.dom.list) return;
200
- this.cleanup.events.on('scroll', window, 'scroll', () => this.onScroll(), {
201
- passive: true,
187
+ const onScroll = (): void => {
188
+ if (runtime.ticking) return;
189
+ runtime.ticking = true;
190
+ runtime.frameId = requestAnimationFrame(() => {
191
+ runtime.ticking = false;
192
+ runtime.frameId = 0;
193
+ updateActive();
202
194
  });
203
- this.cleanup.events.on('click', this.dom.list, 'click', (event) =>
204
- this.onClick(event)
205
- );
206
- }
207
-
208
- private onScroll(): void {
209
- if (this.runtime.ticking) return;
195
+ };
210
196
 
211
- requestAnimationFrame(() => {
212
- this.updateActive();
213
- this.runtime.ticking = false;
197
+ const scheduleRefresh = (): void => {
198
+ if (runtime.refreshing) return;
199
+ runtime.refreshing = true;
200
+ runtime.refreshFrameId = requestAnimationFrame(() => {
201
+ runtime.refreshing = false;
202
+ runtime.refreshFrameId = 0;
203
+ syncFromTarget();
214
204
  });
215
- this.runtime.ticking = true;
216
- }
217
-
218
- private onClick(event: Event): void {
219
- if (!(event.target instanceof Element) || !this.dom.list) return;
220
- const link = event.target.closest<HTMLElement>('[data-toc-index]');
221
- if (!link || !this.dom.list.contains(link)) return;
222
-
223
- const index = Number(link.dataset.tocIndex);
224
- const item = this.state?.items[index];
225
- if (!item) return;
226
-
227
- event.preventDefault();
228
- this.scrollToItem(item, { activeIndex: index, updateHash: true });
229
- }
205
+ };
230
206
 
231
- private scrollToItem(
207
+ const scrollToItem = (
232
208
  item: TocItem,
233
209
  { activeIndex = -1, updateHash = false }: TocScrollOptions = {}
234
- ): void {
210
+ ): void => {
211
+ const heading = headings.find((element) => element.id === item.id);
212
+ if (!heading) return;
235
213
  const scrollY = window.scrollY || window.pageYOffset || 0;
236
214
  const top = Math.max(
237
215
  0,
238
- item.element.getBoundingClientRect().top + scrollY - this.props.offset
216
+ heading.getBoundingClientRect().top + scrollY - settings.offset
239
217
  );
240
-
241
- window.scrollTo({
242
- top,
243
- behavior: 'smooth',
244
- });
245
-
246
- if (activeIndex >= 0) this.setActive(activeIndex);
247
-
218
+ window.scrollTo({ top, behavior: 'smooth' });
219
+ if (activeIndex >= 0) setActive(activeIndex);
248
220
  if (updateHash && window.history?.pushState) {
249
221
  window.history.pushState(null, '', `#${item.id}`);
250
222
  }
251
- }
252
-
253
- private linkClassName(item: TocItem, active = false): string {
254
- const classes = [
255
- this.props.className.link,
256
- `${this.props.className.levelPrefix}${item.level}`,
257
- ];
258
- if (active) classes.push(this.props.className.active);
259
- return joinClasses(...classes);
260
- }
261
-
262
- private buildLink(item: TocItem, index: number): HTMLAnchorElement {
263
- return jsx('a', {
264
- className: this.linkClassName(item),
265
- href: `#${item.id}`,
266
- 'data-toc-index': String(index),
267
- 'data-toc-target': item.id,
268
- children: item.text,
269
- }) as HTMLAnchorElement;
270
- }
271
-
272
- private updateActive(): void {
273
- if (!this.runtime.built) return;
274
-
275
- let index = -1;
276
- const activeOffset = this.props.offset + ACTIVE_OFFSET_TOLERANCE;
277
- for (let i = this.dom.headings.length - 1; i >= 0; i--) {
278
- if (this.dom.headings[i].getBoundingClientRect().top <= activeOffset) {
279
- index = i;
280
- break;
281
- }
282
- }
283
-
284
- this.setActive(index);
285
- }
286
-
287
- private setActive(index: number): void {
288
- if (!this.state || index === this.state.current.index) return;
289
-
290
- const current = this.state.items[index] || null;
291
- this.setState({
292
- current: { index, item: current },
293
- });
294
-
295
- const items = this.state.items;
296
- this.dom.links.forEach((link, i) => {
297
- const active = i === index;
298
- link.dataset.active = active ? '1' : '0';
299
- link.className = this.linkClassName(items[i], active);
300
- });
301
-
302
- if (typeof this.props.onChange === 'function') {
303
- this.props.onChange(current, index, this);
304
- }
305
- }
306
-
307
- /**
308
- * 重新扫描标题并重建目录列表。
309
- * @returns {Toc} 当前实例。
310
- */
311
- refresh(): this {
312
- if (
313
- this.runtime.destroyed ||
314
- !this.runtime.built ||
315
- !this.dom.target ||
316
- !this.dom.list
317
- ) {
318
- return this;
319
- }
320
-
321
- this.cleanup.events.clear();
322
-
323
- this.dom.headings = all<HTMLHeadingElement>(
324
- this.props.headings,
325
- this.dom.target
326
- );
327
- const items = this.dom.headings.map(normalizeHeading);
328
- this.dom.links = items.map((item, index) => this.buildLink(item, index));
329
-
330
- this.dom.list.innerHTML = '';
331
- for (const link of this.dom.links) this.dom.list.appendChild(link);
223
+ };
332
224
 
333
- this.setState({
334
- items,
335
- current: { index: -1, item: null },
225
+ const syncFromTarget = (): void => {
226
+ if (toc.runtime.destroyed || !toc.runtime.built || !target) return;
227
+ headings = all<HTMLHeadingElement>(settings.headings, target);
228
+ const items = headings.map(normalizeHeading);
229
+ flushSync(() => {
230
+ state.items = items;
231
+ state.current = { index: -1, item: null };
336
232
  });
233
+ updateActive();
234
+ };
337
235
 
338
- this.bindEvents();
339
- this.updateActive();
340
- return this;
341
- }
342
-
343
- /**
344
- * 激活并滚动到指定目录项。
345
- * @param {number} index 目录项索引。
346
- * @returns {Toc} 当前实例。
347
- */
348
- activate(index: number): this {
349
- if (!this.runtime.built || !this.state) return this;
236
+ const activate = (index: number): TocInstance => {
350
237
  if (
351
- !Number.isInteger(index) ||
352
- index < 0 ||
353
- index >= this.dom.links.length
238
+ toc.runtime.built &&
239
+ Number.isInteger(index) &&
240
+ index >= 0 &&
241
+ index < state.items.length
354
242
  ) {
355
- return this;
243
+ scrollToItem(state.items[index]);
356
244
  }
245
+ return toc;
246
+ };
357
247
 
358
- this.scrollToItem(this.state.items[index]);
359
- return this;
360
- }
361
-
362
- /**
363
- * 销毁实例并清空渲染内容。
364
- * @private
365
- */
366
- protected onDestroy(): void {
367
- this.cleanup.events.clear();
368
- this.dom.root?.remove();
369
-
370
- this.runtime.built = false;
371
- this.runtime.ticking = false;
372
- this.dom.target = null;
373
- this.dom.list = null;
374
- this.dom.headings = [];
375
- this.dom.links = [];
376
- }
377
- }
378
-
379
- export function createToc(props: TocProps = {}): TocInstance {
380
- return new Toc(props);
248
+ toc = defineComponent({
249
+ name: 'Toc',
250
+ props: settings,
251
+ state,
252
+ actions: { activate },
253
+ view: () =>
254
+ jsx('nav', {
255
+ className: settings.className.toc,
256
+ 'data-toc': 'root',
257
+ children: jsx('div', {
258
+ className: settings.className.list,
259
+ 'data-toc-list': 'root',
260
+ children: For({
261
+ each: renderableItems,
262
+ key: (item: TocItem) => item.id,
263
+ children: (
264
+ itemAccessor: () => TocItem,
265
+ indexAccessor: () => number
266
+ ) =>
267
+ jsx('a', {
268
+ className: () => {
269
+ const item = itemAccessor();
270
+ return joinClasses(
271
+ settings.className.link,
272
+ `${settings.className.levelPrefix}${item.level}`,
273
+ state.current.index === indexAccessor()
274
+ ? settings.className.active
275
+ : ''
276
+ );
277
+ },
278
+ href: () => `#${itemAccessor().id}`,
279
+ 'data-toc-index': () => String(indexAccessor()),
280
+ 'data-toc-target': () => itemAccessor().id,
281
+ 'data-active': () =>
282
+ state.current.index === indexAccessor() ? '1' : '0',
283
+ onClick: (event: Event) => {
284
+ event.preventDefault();
285
+ scrollToItem(itemAccessor(), {
286
+ activeIndex: indexAccessor(),
287
+ updateHash: true,
288
+ });
289
+ },
290
+ children: () => itemAccessor().text,
291
+ }),
292
+ }),
293
+ }),
294
+ }) as HTMLElement,
295
+ onBuild(context) {
296
+ target = requireContainer(settings.target, 'Toc.target');
297
+ events.on('scroll', window, 'scroll', onScroll, { passive: true });
298
+ context.own(() => events.clear());
299
+ syncFromTarget();
300
+ if (settings.reactive) {
301
+ observer = new MutationObserver(scheduleRefresh);
302
+ observer.observe(target, {
303
+ childList: true,
304
+ subtree: true,
305
+ characterData: true,
306
+ });
307
+ context.own(() => {
308
+ observer?.disconnect();
309
+ observer = null;
310
+ });
311
+ }
312
+ },
313
+ onDestroy() {
314
+ if (runtime.frameId) cancelAnimationFrame(runtime.frameId);
315
+ if (runtime.refreshFrameId) cancelAnimationFrame(runtime.refreshFrameId);
316
+ runtime.frameId = 0;
317
+ runtime.refreshFrameId = 0;
318
+ runtime.ticking = false;
319
+ runtime.refreshing = false;
320
+ observer?.disconnect();
321
+ observer = null;
322
+ target = null;
323
+ headings = [];
324
+ },
325
+ });
326
+
327
+ return toc;
381
328
  }