vanilla-jui 1.4.7 → 1.5.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 (52) hide show
  1. package/README.md +10 -4
  2. package/dist/index.d.ts +418 -970
  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 +466 -346
  8. package/src/components/flow.ts +779 -1133
  9. package/src/components/form.ts +666 -509
  10. package/src/components/menu.ts +198 -328
  11. package/src/components/modal.ts +510 -747
  12. package/src/components/offcanvas.ts +273 -367
  13. package/src/components/pagination.ts +201 -375
  14. package/src/components/swiper.ts +689 -1107
  15. package/src/components/tabs.ts +467 -778
  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/content.css +18 -24
  24. package/src/css/drop.css +4 -1
  25. package/src/css/form.css +8 -0
  26. package/src/css/menu.css +10 -5
  27. package/src/css/modal.css +2 -1
  28. package/src/css/offcanvas.css +5 -17
  29. package/src/css/swiper.css +2 -1
  30. package/src/css/tabs.css +8 -17
  31. package/src/css/themes/default/accordion.css +0 -48
  32. package/src/css/themes/default/swiper.css +1 -1
  33. package/src/css/themes/default/tabs.css +4 -6
  34. package/src/css/toast.css +1 -17
  35. package/src/css/tooltip.css +2 -1
  36. package/src/index.ts +8 -2
  37. package/src/locales/index.ts +3 -0
  38. package/src/primitives/drop.ts +253 -262
  39. package/src/primitives/loading.ts +22 -4
  40. package/src/primitives/parabola.ts +123 -164
  41. package/src/primitives/sticky.ts +297 -0
  42. package/src/primitives/theme.ts +164 -222
  43. package/src/primitives/toast.ts +309 -284
  44. package/src/primitives/tooltip.ts +65 -66
  45. package/src/utilities/browser.ts +38 -0
  46. package/src/utilities/dom.ts +0 -54
  47. package/src/utilities/refs.ts +61 -0
  48. package/src/utilities/state.ts +73 -0
  49. package/src/utilities/timer.ts +11 -31
  50. package/src/validation/validator.ts +289 -389
  51. package/src/components/sticky.ts +0 -297
  52. package/src/core/Component.ts +0 -377
@@ -1,24 +1,25 @@
1
1
  import {
2
- bindAttr,
2
+ For,
3
3
  createDeepStore,
4
4
  createEffect,
5
- createRoot,
5
+ createMemo,
6
+ createSignal,
6
7
  flushSync,
7
8
  jsx,
8
- untrack,
9
+ onCleanup,
9
10
  } from 'vanilla-signal';
10
11
 
11
- import Component, {
12
- type ComponentDOM,
13
- type ComponentRuntime,
14
- } from '../core/Component.ts';
15
- import { createLoading } from '../primitives/loading.ts';
16
12
  import {
17
- type RenderableContent,
18
- normalizeContentNodes,
19
- q,
20
- } from '../utilities/dom.ts';
13
+ type FunctionalComponent,
14
+ defineComponent,
15
+ } from '../core/component.ts';
16
+ import { createLoading } from '../primitives/loading.ts';
17
+ import { joinClasses } from '../utilities/class-name.ts';
18
+ import { type RenderableContent } from '../utilities/dom.ts';
19
+ import { createEventManager } from '../utilities/events.ts';
21
20
  import { randomId } from '../utilities/id.ts';
21
+ import { createElementRef, createKeyedElementRefs } from '../utilities/refs.ts';
22
+ import { createScheduledTask } from '../core/scheduler.ts';
22
23
  import {
23
24
  type ResolveSchema,
24
25
  resolveProps,
@@ -36,7 +37,6 @@ export interface TabsClassNames {
36
37
  tab: string;
37
38
  panelWrap: string;
38
39
  panel: string;
39
- disabled: string;
40
40
  dragging: string;
41
41
  }
42
42
 
@@ -49,12 +49,7 @@ export interface TabsPanelContext {
49
49
  name: string | number;
50
50
  }
51
51
 
52
- export interface TabTitleContext {
53
- tabs: Tabs;
54
- item: TabItem;
55
- }
56
-
57
- export type TabPanel =
52
+ export type TabContent =
58
53
  | RenderableContent<TabsPanelContext>
59
54
  | ((context: TabsPanelContext) => RenderableContent<TabsPanelContext>)
60
55
  | ((
@@ -63,8 +58,8 @@ export type TabPanel =
63
58
 
64
59
  export interface TabItem extends Record<string, unknown> {
65
60
  name?: string;
66
- title: RenderableContent<TabTitleContext>;
67
- panel: TabPanel;
61
+ title: Exclude<RenderableContent, (...args: never[]) => unknown>;
62
+ content: TabContent;
68
63
  cache?: boolean;
69
64
  ttl?: number;
70
65
  }
@@ -100,25 +95,14 @@ interface TabsState extends Record<string, unknown> {
100
95
  data: TabItem[];
101
96
  active: TabsValue;
102
97
  disabled: TabsDisabled;
103
- direction: TabsDirection;
104
- current: {
105
- index: number;
106
- name: string | null;
107
- };
108
- isVertical: boolean;
109
98
  draggable: boolean;
99
+ dragging: boolean;
110
100
  loading: boolean;
111
101
  }
112
102
 
113
- interface TabsDisabledState {
114
- names: string[];
115
- indexes: number[];
116
- }
117
-
118
- interface TabsDOM extends ComponentDOM {
119
- root: HTMLElement | null;
120
- tabs: HTMLElement[];
121
- panels: HTMLElement[];
103
+ interface TabsCurrent {
104
+ index: number;
105
+ name: string | null;
122
106
  }
123
107
 
124
108
  interface TabsPanelCacheEntry {
@@ -126,14 +110,23 @@ interface TabsPanelCacheEntry {
126
110
  updatedAt: number;
127
111
  }
128
112
 
129
- interface TabsRuntime extends ComponentRuntime {
130
- built: boolean;
131
- cache: {
132
- panels: Map<string, TabsPanelCacheEntry>;
133
- };
134
- panelLoadId: number;
113
+ interface TabsActions {
114
+ activate(value: TabsValue): Promise<void>;
135
115
  }
136
116
 
117
+ type TabsBase = FunctionalComponent<
118
+ ResolvedTabsProps,
119
+ TabsState,
120
+ HTMLElement,
121
+ TabsActions
122
+ >;
123
+
124
+ export type Tabs = TabsBase & {
125
+ readonly current: TabsCurrent;
126
+ readonly activeIndex: number;
127
+ readonly disabledNames: string[];
128
+ };
129
+
137
130
  type PointerDragEvent = MouseEvent | TouchEvent;
138
131
 
139
132
  const DEFAULT_CLASS_NAMES: TabsClassNames = {
@@ -141,9 +134,8 @@ const DEFAULT_CLASS_NAMES: TabsClassNames = {
141
134
  wrap: 'tab-wrap',
142
135
  list: 'tab-list',
143
136
  tab: 'tab-item',
144
- panelWrap: 'tab-panel',
137
+ panelWrap: 'panel-list',
145
138
  panel: 'panel-item',
146
- disabled: 'is-disabled',
147
139
  dragging: 'dragging',
148
140
  };
149
141
 
@@ -152,12 +144,8 @@ const TABS_PROPS_SCHEMA = {
152
144
  default: null,
153
145
  types: ['string', 'null'],
154
146
  normalize: (value: unknown) => {
155
- if (typeof value === 'string') {
156
- const id = value.trim();
157
- return id || randomId();
158
- }
159
- if (value == null) return randomId();
160
- return value;
147
+ if (typeof value === 'string') return value.trim() || randomId();
148
+ return value == null ? randomId() : value;
161
149
  },
162
150
  },
163
151
  direction: {
@@ -182,790 +170,491 @@ const TABS_PROPS_SCHEMA = {
182
170
  const TAB_CONFIG_RULE = {
183
171
  type: 'plainObject',
184
172
  shape: {
173
+ name: ['string', 'null', 'undefined'],
185
174
  title: 'renderable',
186
- panel: 'renderable',
175
+ content: 'renderable',
176
+ cache: ['boolean', 'undefined'],
177
+ ttl: ['number', 'undefined'],
187
178
  },
188
179
  };
189
180
 
190
- function cloneTabItems(tabs: unknown): TabItem[] {
191
- return Array.isArray(tabs)
192
- ? tabs.map((item) => {
193
- const nextItem = { ...(item as TabItem) };
194
- nextItem.name = nextItem.name || randomId();
195
- return nextItem;
196
- })
197
- : [];
198
- }
181
+ const TABS_STATE_SCHEMA = {
182
+ data: TABS_PROPS_SCHEMA.data,
183
+ active: TABS_PROPS_SCHEMA.active,
184
+ disabled: TABS_PROPS_SCHEMA.disabled,
185
+ };
199
186
 
200
- function cloneDisabled(disabled: TabsDisabled): TabsDisabled {
201
- return Array.isArray(disabled) ? disabled.slice() : disabled;
187
+ function normalizeTabItem(
188
+ item: TabItem,
189
+ index: number,
190
+ namespace = 'Tabs.data'
191
+ ): TabItem {
192
+ validateParam(String(index), item, TAB_CONFIG_RULE, namespace);
193
+ const name = typeof item.name === 'string' ? item.name.trim() : '';
194
+ const resolvedName = name || randomId();
195
+ return { ...item, name: resolvedName };
202
196
  }
203
197
 
204
- function resolveDisabledState(
205
- disabled: TabsDisabled,
206
- data: TabItem[]
207
- ): TabsDisabledState {
208
- const toName = (val: TabsValue): string | null => {
209
- if (typeof val === 'number') return data[val]?.name || null;
210
- if (typeof val === 'string') return val;
211
- return null;
212
- };
213
- const names = Array.isArray(disabled)
214
- ? disabled.map(toName).filter((name): name is string => !!name)
215
- : (() => {
216
- const name = toName(disabled);
217
- return name ? [name] : [];
218
- })();
219
- const uniqNames = Array.from(new Set(names));
220
-
221
- return {
222
- names: uniqNames,
223
- indexes: uniqNames
224
- .map((name) => data.findIndex((tab) => tab.name === name))
225
- .filter((index) => index >= 0),
226
- };
198
+ function normalizeData(data: unknown, namespace = 'Tabs.data'): TabItem[] {
199
+ validateParam('data', data, TABS_STATE_SCHEMA.data, namespace);
200
+ const names = new Set<string>();
201
+ return (data as TabItem[]).map((item, index) => {
202
+ const next = normalizeTabItem(item, index, namespace);
203
+ const resolvedName = next.name as string;
204
+ if (names.has(resolvedName)) {
205
+ throw new Error(
206
+ `${namespace}: item name "${resolvedName}" must be unique.`
207
+ );
208
+ }
209
+ names.add(resolvedName);
210
+ return next;
211
+ });
227
212
  }
228
213
 
229
- function normalizeTtl(ttl: unknown): number {
230
- return typeof ttl === 'number' && ttl > 0 ? ttl : 0;
214
+ function normalizeDisabled(disabled: TabsDisabled): TabsDisabled {
215
+ return Array.isArray(disabled) ? [...disabled] : disabled;
231
216
  }
232
217
 
233
218
  function normalizeProps(input: TabsProps): ResolvedTabsProps {
234
- const props = resolveProps(input, TABS_PROPS_SCHEMA, 'Tabs');
219
+ const props = resolveProps(input, TABS_PROPS_SCHEMA, 'Tabs.props');
235
220
  return {
236
221
  id: props.id as string,
237
222
  direction: props.direction as TabsDirection,
238
223
  active: props.active as TabsValue,
239
- disabled: props.disabled as TabsDisabled,
224
+ disabled: normalizeDisabled(props.disabled as TabsDisabled),
240
225
  onChange: props.onChange as ResolvedTabsProps['onChange'],
241
- data: cloneTabItems(props.data),
226
+ data: normalizeData(props.data, 'Tabs.props.data'),
242
227
  className: props.className as TabsClassNames,
243
228
  };
244
229
  }
245
230
 
246
- const TABS_STATE_SCHEMA = {
247
- data: TABS_PROPS_SCHEMA.data,
248
- active: TABS_PROPS_SCHEMA.active,
249
- disabled: TABS_PROPS_SCHEMA.disabled,
250
- direction: TABS_PROPS_SCHEMA.direction,
251
- };
252
-
253
- /**
254
- * 标签页组件,继承 Component。
255
- *
256
- * DOM 创建一次,通过 createEffect 细粒度更新 class/ARIA。
257
- */
258
- class TabsComponent extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
259
- declare runtime: TabsRuntime;
260
- declare state: TabsState;
261
- private bindingsDispose: (() => void) | null;
262
- private stateDispose: (() => void) | null;
263
- private isDragging: boolean;
264
- private raf: number;
265
- private resizeRaf: number;
266
- private velocity: number;
267
-
268
- /**
269
- * @param {object} [input={}] 标签页配置。
270
- */
271
- constructor(input: TabsProps = {}) {
272
- const props = normalizeProps(input);
273
- super(props);
274
-
275
- this.dom.tabs = [];
276
- this.dom.panels = [];
277
- this.bindingsDispose = null;
278
- this.stateDispose = null;
279
- this.isDragging = false;
280
- this.raf = 0;
281
- this.resizeRaf = 0;
282
- this.velocity = 0;
283
-
284
- this.state = createDeepStore({
285
- data: cloneTabItems(props.data),
286
- active: props.active,
287
- disabled: cloneDisabled(props.disabled),
288
- direction: props.direction,
289
- current: {
290
- index: -1,
291
- name: null,
292
- },
293
- isVertical: props.direction === 'left' || props.direction === 'right',
294
- draggable: false,
295
- loading: false,
296
- }) as TabsState;
297
-
298
- this.runtime.cache = { panels: new Map() };
299
- this.runtime.built = false;
300
- this.runtime.panelLoadId = 0;
301
- }
302
-
303
- protected onInit(props: ResolvedTabsProps): void {
304
- this.dom.root = this.buildRoot(props);
305
- this.renderItems();
306
- void this.activateInternal(this.state.active, false);
307
- this.bindEvents();
308
- this.initDrag();
309
- this.bindState();
310
- }
311
-
312
- private buildRoot(props: ResolvedTabsProps): HTMLElement {
313
- const { id, className } = props;
314
- const nav = jsx('nav', {
315
- className: className.list,
316
- 'data-tabs-list': '',
317
- }) as HTMLElement;
318
- const wrap = jsx('div', {
319
- className: className.wrap,
320
- 'data-tabs-wrap': '',
321
- children: nav,
322
- }) as HTMLElement;
323
- const panelWrapper = jsx('div', {
324
- className: className.panelWrap,
325
- 'data-tabs-panel-wrap': '',
326
- }) as HTMLElement;
327
-
328
- return jsx('div', {
329
- className: className.root,
330
- id,
331
- 'data-tabs': 'root',
332
- 'data-tabs-direction': this.state.direction,
333
- children: [wrap, panelWrapper],
334
- }) as HTMLElement;
335
- }
336
-
337
- private renderItems(): void {
338
- if (!this.dom.root) return;
339
- const tabList = q<HTMLElement>('[data-tabs-list]', this.dom.root);
340
- const panelWrapper = q<HTMLElement>(
341
- '[data-tabs-panel-wrap]',
342
- this.dom.root
343
- );
344
- if (!tabList || !panelWrapper) return;
345
-
346
- tabList.textContent = '';
347
- panelWrapper.textContent = '';
348
-
349
- this.dom.tabs = [];
350
- this.dom.panels = [];
351
-
352
- const tabFragment = document.createDocumentFragment();
353
- const panelFragment = document.createDocumentFragment();
354
-
355
- this.state.data.forEach((item, index) => {
356
- const name = item.name || randomId();
357
-
358
- const tab = jsx('div', {
359
- className: this.props.className.tab,
360
- 'data-tabs-tab': name,
361
- role: 'tab',
362
- children: normalizeContentNodes(item.title, { tabs: this, item }),
363
- }) as HTMLElement;
364
-
365
- this.dom.tabs.push(tab);
366
- const panel = jsx('div', {
367
- className: this.props.className.panel,
368
- 'data-tabs-panel': name,
369
- role: 'tabpanel',
370
- }) as HTMLElement;
371
- this.dom.panels.push(panel);
372
-
373
- if (typeof item.panel !== 'function') {
374
- panel.append(
375
- ...normalizeContentNodes(item.panel, {
376
- tabs: this,
377
- item,
378
- index,
379
- name,
380
- })
381
- );
382
- }
383
- tabFragment.append(tab);
384
- panelFragment.append(panel);
385
- });
386
-
387
- tabList.append(tabFragment);
388
- panelWrapper.append(panelFragment);
389
-
390
- this.bindingsDispose?.();
391
- this.bindingsDispose = createRoot((dispose) => {
392
- this.dom.tabs.forEach((tab, index) => {
393
- const name = tab.dataset.tabsTab || '';
394
- bindAttr(tab, 'aria-selected', () =>
395
- this.state.current.index === index ? 'true' : 'false'
396
- );
397
- bindAttr(tab, 'aria-disabled', () =>
398
- this.isDisabledName(name) ? 'true' : 'false'
399
- );
400
- });
401
- this.dom.panels.forEach((panel, index) => {
402
- bindAttr(panel, 'aria-hidden', () =>
403
- this.state.current.index !== index ? 'true' : 'false'
404
- );
405
- });
406
- return dispose;
407
- });
408
- }
409
-
410
- private bindState(): void {
411
- if (this.stateDispose) return;
412
-
413
- this.stateDispose = createRoot((dispose) => {
414
- let initialized = false;
415
- let previousData = this.state.data;
416
- let previousDirection = this.state.direction;
417
- let previousActive = this.state.active;
418
- createEffect(() => {
419
- const data = this.state.data;
420
- const direction = this.state.direction;
421
- const disabled = this.state.disabled;
422
- const active = this.state.active;
423
- if (!initialized) {
424
- initialized = true;
425
- return;
426
- }
427
-
428
- const dataChanged = data !== previousData;
429
- const directionChanged = direction !== previousDirection;
430
- const activeChanged = active !== previousActive;
431
- previousData = data;
432
- previousDirection = direction;
433
- previousActive = active;
231
+ function normalizeTtl(ttl: unknown): number {
232
+ return typeof ttl === 'number' && ttl > 0 ? ttl : 0;
233
+ }
434
234
 
435
- void untrack(() =>
436
- this.syncStateView({ dataChanged, directionChanged, activeChanged })
437
- );
438
- void disabled;
439
- });
440
- return dispose;
441
- });
442
- }
443
-
444
- private async syncStateView({
445
- dataChanged,
446
- directionChanged,
447
- activeChanged,
448
- }: {
449
- dataChanged: boolean;
450
- directionChanged: boolean;
451
- activeChanged: boolean;
452
- }): Promise<void> {
453
- if (!this.runtime.built || !this.dom.root) return;
454
- this.validateData(this.state.data);
455
- validateParam(
456
- 'direction',
457
- this.state.direction,
458
- TABS_STATE_SCHEMA.direction,
459
- 'Tabs.state'
460
- );
461
- validateParam(
462
- 'disabled',
463
- this.state.disabled,
464
- TABS_STATE_SCHEMA.disabled,
465
- 'Tabs.state'
466
- );
467
- validateParam(
468
- 'active',
469
- this.state.active,
470
- TABS_STATE_SCHEMA.active,
471
- 'Tabs.state'
235
+ export function createTabs(input: TabsProps = {}): Tabs {
236
+ const props = normalizeProps(input);
237
+ const state = createDeepStore({
238
+ data: props.data,
239
+ active: props.active,
240
+ disabled: props.disabled,
241
+ draggable: false,
242
+ dragging: false,
243
+ loading: false,
244
+ }) as TabsState;
245
+ const events = createEventManager();
246
+ const wrapRef = createElementRef<HTMLElement>();
247
+ const listRef = createElementRef<HTMLElement>();
248
+ const tabs = createKeyedElementRefs<string, HTMLElement>();
249
+ const panels = createKeyedElementRefs<string, HTMLElement>();
250
+ const cache = new Map<string, TabsPanelCacheEntry>();
251
+ const loaders = new Map<string, () => Promise<void>>();
252
+ let instance!: Tabs;
253
+ let loadId = 0;
254
+ let pointerDragging = false;
255
+ let inertiaFrame = 0;
256
+ let resizeFrame = 0;
257
+ let velocity = 0;
258
+
259
+ const getIndex = (value: TabsValue): number =>
260
+ typeof value === 'number'
261
+ ? value
262
+ : state.data.findIndex((item) => item.name === value);
263
+ const current = (): TabsCurrent => {
264
+ const index = getIndex(state.active);
265
+ const item = state.data[index];
266
+ return item
267
+ ? { index, name: item.name as string }
268
+ : { index: -1, name: null };
269
+ };
270
+ const activeName = (): string | null => {
271
+ if (typeof state.active === 'string') return state.active;
272
+ const item = state.data[state.active];
273
+ return item ? (item.name as string) : null;
274
+ };
275
+ const disabledNames = (): string[] => {
276
+ const values = Array.isArray(state.disabled)
277
+ ? state.disabled
278
+ : [state.disabled];
279
+ return Array.from(
280
+ new Set(
281
+ values
282
+ .map((value) =>
283
+ typeof value === 'number' ? state.data[value]?.name : value
284
+ )
285
+ .filter((name): name is string => typeof name === 'string')
286
+ )
472
287
  );
473
-
474
- if (directionChanged) {
475
- this.dom.root.setAttribute('data-tabs-direction', this.state.direction);
476
- flushSync(() => {
477
- this.state.isVertical =
478
- this.state.direction === 'left' || this.state.direction === 'right';
479
- });
480
- }
481
-
482
- if (dataChanged) {
483
- this.runtime.cache.panels.clear();
484
- this.renderItems();
485
- this.bindEvents();
288
+ };
289
+ const isDisabled = (name: string): boolean => disabledNames().includes(name);
290
+ const isActive = (name: string): boolean => activeName() === name;
291
+ const renderableData = createMemo(() => state.data);
292
+
293
+ const refreshDrag = (): void => {
294
+ const wrap = wrapRef.current;
295
+ const list = listRef.current;
296
+ if (!wrap || !list) return;
297
+ const vertical = props.direction === 'left' || props.direction === 'right';
298
+ const next = vertical
299
+ ? list.scrollHeight > wrap.clientHeight + 5
300
+ : list.scrollWidth > wrap.clientWidth + 5;
301
+ if (state.draggable !== next) {
486
302
  flushSync(() => {
487
- this.syncCurrent(-1);
303
+ state.draggable = next;
488
304
  });
489
305
  }
306
+ };
307
+ const refreshTask = createScheduledTask(refreshDrag);
490
308
 
491
- if (dataChanged || activeChanged) {
492
- await this.activateInternal(this.state.active, activeChanged);
493
- }
494
-
495
- this.refreshDrag();
496
- }
497
-
498
- private get disabledState(): TabsDisabledState {
499
- return resolveDisabledState(this.state.disabled, this.state.data);
500
- }
501
-
502
- private isDisabledName(name: string): boolean {
503
- return this.disabledState.names.includes(name);
504
- }
505
-
506
- private syncCurrent(index: number): void {
507
- this.state.current = {
508
- index,
509
- name:
510
- index >= 0 && index < this.dom.tabs.length
511
- ? this.dom.tabs[index]?.dataset.tabsTab || null
512
- : null,
309
+ const startInertia = (): void => {
310
+ const wrap = wrapRef.current;
311
+ if (!wrap) return;
312
+ let speed = velocity;
313
+ let last = performance.now();
314
+ const vertical = props.direction === 'left' || props.direction === 'right';
315
+ const step = (now: number): void => {
316
+ const elapsed = now - last;
317
+ last = now;
318
+ speed *= 0.92;
319
+ if (Math.abs(speed) < 0.3) return;
320
+ if (vertical) wrap.scrollTop += speed * elapsed * 0.05;
321
+ else wrap.scrollLeft += speed * elapsed * 0.05;
322
+ inertiaFrame = requestAnimationFrame(step);
513
323
  };
514
- }
515
-
516
- private getPanelKey(item: TabItem, index: number): string {
517
- return item.name || this.dom.tabs[index]?.dataset.tabsTab || String(index);
518
- }
519
-
520
- private getCachedPanel(
521
- item: TabItem,
522
- index: number
523
- ): TabsPanelCacheEntry | null {
524
- if (!item.cache) return null;
525
-
526
- const key = this.getPanelKey(item, index);
527
- const entry = this.runtime.cache.panels.get(key);
528
- if (!entry) return null;
529
-
530
- const ttl = normalizeTtl(item.ttl);
531
- if (ttl && Date.now() - entry.updatedAt > ttl) {
532
- this.runtime.cache.panels.delete(key);
533
- return null;
534
- }
535
-
536
- return entry;
537
- }
538
-
539
- private setCachedPanel(
540
- item: TabItem,
541
- index: number,
542
- content: RenderableContent<TabsPanelContext>
543
- ): void {
544
- if (!item.cache) return;
545
-
546
- this.runtime.cache.panels.set(this.getPanelKey(item, index), {
547
- content,
548
- updatedAt: Date.now(),
549
- });
550
- }
551
-
552
- private renderPanelContent(
553
- index: number,
554
- content: RenderableContent<TabsPanelContext>
555
- ): void {
556
- const panel = this.dom.panels[index];
557
- const item = this.state.data[index];
558
- if (!panel || !item) return;
559
-
560
- panel.textContent = '';
561
- panel.append(
562
- ...normalizeContentNodes(content, {
563
- tabs: this,
564
- item,
565
- index,
566
- name: this.dom.tabs[index]?.dataset.tabsTab || item.name || index,
567
- })
568
- );
569
- }
324
+ inertiaFrame = requestAnimationFrame(step);
325
+ };
570
326
 
571
- private async loadPanel(index: number): Promise<void> {
572
- const item = this.state.data[index];
573
- const panel = this.dom.panels[index];
574
- if (!item || !panel || typeof item.panel !== 'function') {
575
- this.runtime.panelLoadId += 1;
327
+ const bindDrag = (): void => {
328
+ const wrap = wrapRef.current;
329
+ const list = listRef.current;
330
+ if (!wrap || !list) return;
331
+ let start = 0;
332
+ let scrollStart = 0;
333
+ let last = 0;
334
+ let pendingFrame = false;
335
+ let distance = 0;
336
+ const position = (event: PointerDragEvent): number => {
337
+ const vertical =
338
+ props.direction === 'left' || props.direction === 'right';
339
+ if ('touches' in event) {
340
+ const touch = event.touches[0] || event.changedTouches[0];
341
+ return vertical ? touch.pageY : touch.pageX;
342
+ }
343
+ return vertical ? event.pageY : event.pageX;
344
+ };
345
+ const startDrag = (event: Event): void => {
346
+ if (!state.draggable) return;
347
+ if (!(event instanceof MouseEvent) && !(event instanceof TouchEvent))
348
+ return;
349
+ pointerDragging = true;
576
350
  flushSync(() => {
577
- this.state.loading = false;
351
+ state.dragging = true;
578
352
  });
579
- return;
580
- }
581
-
582
- const cached = this.getCachedPanel(item, index);
583
- if (cached) {
584
- this.runtime.panelLoadId += 1;
353
+ start = position(event);
354
+ last = start;
355
+ const vertical =
356
+ props.direction === 'left' || props.direction === 'right';
357
+ scrollStart = vertical ? wrap.scrollTop : wrap.scrollLeft;
358
+ velocity = 0;
359
+ cancelAnimationFrame(inertiaFrame);
360
+ };
361
+ const moveDrag = (event: Event): void => {
362
+ if (!pointerDragging) return;
363
+ if (!(event instanceof MouseEvent) && !(event instanceof TouchEvent))
364
+ return;
365
+ event.preventDefault();
366
+ const next = position(event);
367
+ distance = start - next;
368
+ velocity = last - next;
369
+ last = next;
370
+ if (pendingFrame) return;
371
+ pendingFrame = true;
372
+ requestAnimationFrame(() => {
373
+ pendingFrame = false;
374
+ const vertical =
375
+ props.direction === 'left' || props.direction === 'right';
376
+ if (vertical) wrap.scrollTop = scrollStart + distance;
377
+ else wrap.scrollLeft = scrollStart + distance;
378
+ });
379
+ };
380
+ const endDrag = (): void => {
381
+ if (!pointerDragging) return;
382
+ pointerDragging = false;
585
383
  flushSync(() => {
586
- this.state.loading = false;
384
+ state.dragging = false;
587
385
  });
588
- this.renderPanelContent(index, cached.content);
589
- return;
590
- }
591
-
592
- const loadId = ++this.runtime.panelLoadId;
593
- flushSync(() => {
594
- this.state.loading = true;
386
+ startInertia();
387
+ };
388
+ events.on('drag:mousedown', list, 'mousedown', startDrag);
389
+ events.on('drag:touchstart', list, 'touchstart', startDrag, {
390
+ passive: true,
595
391
  });
596
- panel.setAttribute('aria-live', 'polite');
597
- panel.setAttribute('aria-busy', 'true');
598
- panel.textContent = '';
599
- panel.appendChild(createLoading());
600
-
601
- try {
602
- const content = await Promise.resolve(
603
- item.panel({
604
- tabs: this,
605
- item,
606
- index,
607
- name: this.dom.tabs[index]?.dataset.tabsTab || item.name || index,
608
- })
609
- );
610
-
611
- if (this.runtime.destroyed || loadId !== this.runtime.panelLoadId) return;
612
-
613
- this.setCachedPanel(item, index, content);
614
- this.renderPanelContent(index, content);
615
- } finally {
616
- if (!this.runtime.destroyed && loadId === this.runtime.panelLoadId) {
617
- flushSync(() => {
618
- this.state.loading = false;
619
- });
620
- panel.setAttribute('aria-busy', 'false');
621
- }
622
- }
623
- }
624
-
625
- get activeIndex(): number {
626
- return this.state.current.index;
627
- }
628
-
629
- get disabledNames(): string[] {
630
- return this.disabledState.names;
631
- }
632
-
633
- private bindEvents(): void {
634
- this.unbindEvents();
635
-
636
- if (!this.dom.root) return;
637
- const tabList = q<HTMLElement>('[data-tabs-list]', this.dom.root);
638
- if (!tabList) return;
639
-
640
- this.cleanup.events.on('tabClick', tabList, 'click', (event) => {
641
- if (!(event.target instanceof Element)) return;
642
- const tab = event.target.closest<HTMLElement>('[data-tabs-tab]');
643
- if (!tab || !tabList.contains(tab)) return;
644
- const name = tab.dataset.tabsTab;
645
- if (name && !this.isDisabledName(name)) {
646
- void this.activate(name);
647
- }
392
+ events.on('drag:mousemove', window, 'mousemove', moveDrag, {
393
+ passive: false,
648
394
  });
649
- }
650
-
651
- private unbindEvents(): void {
652
- this.cleanup.events.clear();
653
- }
654
-
655
- private assertActive(method: string): void {
656
- if (this.runtime.destroyed) {
657
- throw new Error(`Tabs.${method}: instance has been destroyed.`);
658
- }
659
- if (!this.runtime.built) {
660
- throw new Error(`Tabs.${method}: call build() first.`);
661
- }
662
- }
663
-
664
- private getIndex(val: TabsValue | undefined | null): number {
665
- if (typeof val === 'number') return val;
666
- if (typeof val === 'string') {
667
- return this.dom.tabs.findIndex((tab) => tab.dataset.tabsTab === val);
668
- }
669
- return -1;
670
- }
671
-
672
- private async activateInternal(
673
- val: TabsValue,
674
- fireEvent = true
675
- ): Promise<void> {
676
- const index = this.getIndex(val);
677
- const name = this.dom.tabs[index]?.dataset.tabsTab || '';
678
- if (
679
- index < 0 ||
680
- index >= this.dom.tabs.length ||
681
- this.isDisabledName(name) ||
682
- this.state.current.index === index
683
- ) {
684
- return;
685
- }
686
-
687
- flushSync(() => {
688
- this.syncCurrent(index);
395
+ events.on('drag:touchmove', window, 'touchmove', moveDrag, {
396
+ passive: false,
689
397
  });
398
+ events.on('drag:mouseup', window, 'mouseup', endDrag);
399
+ events.on('drag:touchend', window, 'touchend', endDrag);
400
+ };
690
401
 
691
- await this.loadPanel(index);
692
-
693
- if (fireEvent && this.props.onChange) {
694
- const tabEl = this.dom.tabs[index];
695
- const panelEl = this.dom.panels[index];
696
- await Promise.resolve(
697
- this.props.onChange(
698
- index,
699
- tabEl?.dataset.tabsTab || index,
700
- tabEl,
701
- panelEl
702
- )
703
- );
704
- }
705
- }
706
-
707
- /**
708
- * 激活指定标签。
709
- * @param {number|string} val 标签索引或名称。
710
- */
711
- async activate(val: TabsValue): Promise<void> {
712
- this.assertActive('activate');
713
- this.state.active = val;
714
- await this.activateInternal(val, true);
715
- }
716
-
717
- /**
718
- * 构建 Tabs DOM。
719
- */
720
- build(): this {
721
- if (this.runtime.destroyed) {
722
- throw new Error('Tabs.build: instance has been destroyed.');
723
- }
724
- if (this.runtime.built) return this;
725
- try {
726
- this.runtime.built = true;
727
- this.onInit(this.props);
728
- } catch (error) {
729
- this.destroy();
730
- throw error;
731
- }
732
- this.emit('init', this.props);
733
- return this;
734
- }
735
-
736
- refresh(): this {
737
- this.assertActive('refresh');
738
- this.refreshDrag();
739
- return this;
740
- }
741
-
742
- private get dragContainer(): HTMLElement | null {
743
- return this.dom.root
744
- ? q<HTMLElement>('[data-tabs-wrap]', this.dom.root)
745
- : null;
746
- }
747
-
748
- private get dragInner(): HTMLElement | null {
749
- return this.dom.root
750
- ? q<HTMLElement>('[data-tabs-list]', this.dom.root)
751
- : null;
752
- }
753
-
754
- private initDrag(): void {
755
- const direction = this.state.direction;
756
- const container = this.dragContainer;
757
- const inner = this.dragInner;
758
- if (!container || !inner) return;
759
-
760
- const isVertical = direction === 'left' || direction === 'right';
761
- const draggable = isVertical
762
- ? inner.scrollHeight > container.clientHeight + 5
763
- : inner.scrollWidth > container.clientWidth + 5;
764
-
402
+ const activate = async (value: TabsValue): Promise<void> => {
403
+ if (!instance.runtime.built)
404
+ throw new Error('Tabs.activate: call build() first.');
405
+ const index = getIndex(value);
406
+ const item = state.data[index];
407
+ if (!item) return;
408
+ const name = item.name as string;
409
+ if (isDisabled(name) || isActive(name)) return;
765
410
  flushSync(() => {
766
- this.state.isVertical = isVertical;
767
- this.state.draggable = draggable;
411
+ state.active = value;
768
412
  });
413
+ await loaders.get(name)?.();
414
+ await Promise.resolve(
415
+ props.onChange?.(index, name, tabs.get(name), panels.get(name))
416
+ );
417
+ };
769
418
 
770
- if (!draggable) {
771
- this.removeDragEvents();
772
- return;
773
- }
774
-
775
- this.bindDragEvents();
776
-
777
- this.cleanup.events.on('resize', window, 'resize', () => {
778
- cancelAnimationFrame(this.resizeRaf);
779
- this.resizeRaf = requestAnimationFrame(() => {
780
- this.refreshDrag();
781
- });
419
+ const panelView = (item: () => TabItem, index: () => number): HTMLElement => {
420
+ const name = item().name as string;
421
+ const initial = item().content;
422
+ const [content, setContent] = createSignal<
423
+ RenderableContent<TabsPanelContext>
424
+ >(typeof initial === 'function' ? null : initial);
425
+ const [loading, setLoading] = createSignal(false);
426
+ let source: TabContent = initial;
427
+ let localLoadId = 0;
428
+ let pending: Promise<void> | null = null;
429
+
430
+ const context = (): TabsPanelContext => ({
431
+ tabs: instance,
432
+ item: item(),
433
+ index: index(),
434
+ name,
782
435
  });
783
- }
784
-
785
- private bindDragEvents(): void {
786
- this.removeDragEvents();
787
-
788
- const container = this.dragContainer;
789
- const inner = this.dragInner;
790
- if (!container || !inner) return;
791
- const isVertical = this.state.isVertical;
792
-
793
- let posStart = 0;
794
- let scrollStart = 0;
795
- let lastPos = 0;
796
- let frameRequested = false;
797
-
798
- const getPos = (event: PointerDragEvent): number => {
799
- if ('touches' in event) {
800
- const touch = event.touches[0] || event.changedTouches[0];
801
- return isVertical ? touch.pageY : touch.pageX;
802
- }
803
- return isVertical ? event.pageY : event.pageX;
804
- };
805
-
806
- const onDragStart = (event: Event): void => {
807
- if (!(event instanceof MouseEvent) && !(event instanceof TouchEvent)) {
436
+ const load = async (): Promise<void> => {
437
+ const current = item();
438
+ const source = current.content;
439
+ if (typeof source !== 'function') {
440
+ setContent(source);
808
441
  return;
809
442
  }
810
- this.isDragging = true;
811
- inner.classList.add(this.props.className.dragging);
812
- posStart = getPos(event);
813
- lastPos = posStart;
814
- scrollStart = isVertical ? container.scrollTop : container.scrollLeft;
815
- this.velocity = 0;
816
- cancelAnimationFrame(this.raf);
817
- };
818
-
819
- const onDragMove = (event: Event): void => {
820
- if (!this.isDragging) return;
821
- if (!(event instanceof MouseEvent) && !(event instanceof TouchEvent)) {
443
+ const cached = current.cache ? cache.get(name) : null;
444
+ const ttlValue = normalizeTtl(current.ttl);
445
+ if (cached && (!ttlValue || Date.now() - cached.updatedAt <= ttlValue)) {
446
+ setContent(cached.content);
822
447
  return;
823
448
  }
824
- event.preventDefault();
825
- const current = getPos(event);
826
- const dist = posStart - current;
827
- this.velocity = lastPos - current;
828
- lastPos = current;
829
-
830
- if (!frameRequested) {
831
- frameRequested = true;
832
- requestAnimationFrame(() => {
833
- frameRequested = false;
834
- if (isVertical) container.scrollTop = scrollStart + dist;
835
- else container.scrollLeft = scrollStart + dist;
449
+ if (pending) return pending;
450
+ const request = ++localLoadId;
451
+ loadId += 1;
452
+ const activeLoad = loadId;
453
+ setLoading(true);
454
+ flushSync(() => {
455
+ state.loading = true;
456
+ });
457
+ const task = Promise.resolve()
458
+ .then(() =>
459
+ (source as (context: TabsPanelContext) => unknown)(context())
460
+ )
461
+ .then((result) => {
462
+ if (request !== localLoadId || instance.runtime.destroyed) return;
463
+ if (item().cache) {
464
+ cache.set(name, {
465
+ content: result as RenderableContent<TabsPanelContext>,
466
+ updatedAt: Date.now(),
467
+ });
468
+ }
469
+ setContent(result as RenderableContent<TabsPanelContext>);
470
+ })
471
+ .finally(() => {
472
+ if (request === localLoadId) setLoading(false);
473
+ if (activeLoad === loadId && !instance.runtime.destroyed) {
474
+ flushSync(() => {
475
+ state.loading = false;
476
+ });
477
+ }
478
+ if (pending === task) pending = null;
836
479
  });
837
- }
838
- };
839
-
840
- const onDragEnd = (): void => {
841
- if (!this.isDragging) return;
842
- this.isDragging = false;
843
- inner.classList.remove(this.props.className.dragging);
844
- this.startInertiaScroll();
480
+ pending = task;
481
+ return task;
845
482
  };
483
+ loaders.set(name, load);
484
+ onCleanup(() => {
485
+ cache.delete(name);
486
+ loaders.delete(name);
487
+ });
846
488
 
847
- this.cleanup.events.on('drag:mousedown', inner, 'mousedown', onDragStart);
848
- this.cleanup.events.on(
849
- 'drag:touchstart',
850
- inner,
851
- 'touchstart',
852
- onDragStart,
853
- {
854
- passive: true,
489
+ createEffect(() => {
490
+ const nextContent = item().content;
491
+ if (nextContent !== source) {
492
+ source = nextContent;
493
+ localLoadId += 1;
494
+ pending = null;
495
+ cache.delete(name);
496
+ setContent(typeof nextContent === 'function' ? null : nextContent);
855
497
  }
856
- );
857
- this.cleanup.events.on('drag:mousemove', window, 'mousemove', onDragMove, {
858
- passive: false,
498
+ if (isActive(name)) void load();
859
499
  });
860
- this.cleanup.events.on('drag:touchmove', window, 'touchmove', onDragMove, {
861
- passive: false,
862
- });
863
- this.cleanup.events.on('drag:mouseup', window, 'mouseup', onDragEnd);
864
- this.cleanup.events.on('drag:touchend', window, 'touchend', onDragEnd);
865
- }
866
-
867
- private startInertiaScroll(): void {
868
- const container = this.dragContainer;
869
- if (!container) return;
870
- let v = this.velocity;
871
- const isVertical = this.state.isVertical;
872
- let last = performance.now();
873
500
 
874
- const step = (now: number): void => {
875
- const dt = now - last;
876
- last = now;
877
- v *= 0.92;
878
- if (Math.abs(v) < 0.3) return;
879
- if (isVertical) container.scrollTop += v * dt * 0.05;
880
- else container.scrollLeft += v * dt * 0.05;
881
- this.raf = requestAnimationFrame(step);
501
+ const panelContent = (): RenderableContent<TabsPanelContext> => {
502
+ if (loading()) return createLoading('flex-start', 'flex-start');
503
+ const value = content();
504
+ if (typeof value === 'function') return value(context());
505
+ return value;
882
506
  };
883
507
 
884
- this.raf = requestAnimationFrame(step);
885
- }
886
-
887
- private removeDragEvents(): void {
888
- this.cleanup.events.off('drag:mousedown');
889
- this.cleanup.events.off('drag:touchstart');
890
- this.cleanup.events.off('drag:mousemove');
891
- this.cleanup.events.off('drag:touchmove');
892
- this.cleanup.events.off('drag:mouseup');
893
- this.cleanup.events.off('drag:touchend');
894
- }
895
-
896
- private refreshDrag(): void {
897
- this.initDrag();
898
- }
899
-
900
- protected onDestroy(): void {
901
- this.unbindEvents();
902
- this.removeDragEvents();
903
- this.bindingsDispose?.();
904
- this.bindingsDispose = null;
905
- this.stateDispose?.();
906
- this.stateDispose = null;
907
- cancelAnimationFrame(this.raf);
908
- cancelAnimationFrame(this.resizeRaf);
909
- this.cleanup.events.off('resize');
910
-
911
- this.dom.root?.remove();
912
- this.runtime.built = false;
913
- }
914
-
915
- private validateData(data: TabItem[]): void {
916
- validateParam('data', data, TABS_STATE_SCHEMA.data, 'Tabs.state');
917
- data.forEach((item, index) => {
918
- validateParam(String(index), item, TAB_CONFIG_RULE, 'Tabs.state.data');
919
- });
920
- }
921
-
922
- protected override normalizeStatePatch(
923
- patch: Partial<TabsState>
924
- ): Partial<TabsState> {
925
- const nextPatch = { ...patch };
926
- if (Object.hasOwn(nextPatch, 'data') && Array.isArray(nextPatch.data)) {
927
- nextPatch.data = cloneTabItems(nextPatch.data);
928
- }
929
- if (
930
- Object.hasOwn(nextPatch, 'disabled') &&
931
- Array.isArray(nextPatch.disabled)
932
- ) {
933
- nextPatch.disabled = cloneDisabled(nextPatch.disabled);
934
- }
935
- return nextPatch;
936
- }
937
-
938
- protected override validateStatePatch(patch: Partial<TabsState>): void {
939
- validateParam(
940
- 'state',
941
- patch,
942
- {
943
- type: 'plainObject',
944
- },
945
- 'Tabs.setState'
946
- );
508
+ return jsx('div', {
509
+ className: props.className.panel,
510
+ 'data-tabs-panel': name,
511
+ role: 'tabpanel',
512
+ 'aria-hidden': () => (isActive(name) ? 'false' : 'true'),
513
+ 'aria-live': () =>
514
+ typeof item().content === 'function' ? 'polite' : null,
515
+ 'aria-busy': () => String(loading()),
516
+ hidden: () => !isActive(name),
517
+ ref: panels.bind(name),
518
+ children: panelContent,
519
+ }) as HTMLElement;
520
+ };
521
+
522
+ const tabView = (item: () => TabItem, _index: () => number): HTMLElement => {
523
+ const name = item().name as string;
524
+ return jsx('div', {
525
+ className: props.className.tab,
526
+ 'data-tabs-tab': name,
527
+ role: 'tab',
528
+ 'aria-selected': () => (isActive(name) ? 'true' : 'false'),
529
+ 'aria-disabled': () => (isDisabled(name) ? 'true' : 'false'),
530
+ ref: tabs.bind(name),
531
+ children: item().title,
532
+ }) as HTMLElement;
533
+ };
947
534
 
948
- for (const key of Object.keys(patch)) {
949
- if (!Object.hasOwn(TABS_STATE_SCHEMA, key)) {
950
- throw new Error(
951
- `Tabs.setState: "${key}" is not a supported state key.`
535
+ instance = defineComponent<
536
+ ResolvedTabsProps,
537
+ TabsState,
538
+ HTMLElement,
539
+ TabsActions
540
+ >({
541
+ name: 'Tabs',
542
+ props,
543
+ state,
544
+ actions: {
545
+ activate,
546
+ },
547
+ normalizeStatePatch(patch) {
548
+ return {
549
+ ...patch,
550
+ ...(Object.hasOwn(patch, 'data')
551
+ ? { data: normalizeData(patch.data, 'Tabs.setState.data') }
552
+ : {}),
553
+ ...(Object.hasOwn(patch, 'disabled') && Array.isArray(patch.disabled)
554
+ ? { disabled: normalizeDisabled(patch.disabled) }
555
+ : {}),
556
+ };
557
+ },
558
+ validateStatePatch(patch) {
559
+ validateParam('state', patch, { type: 'plainObject' }, 'Tabs.setState');
560
+ for (const key of Object.keys(patch)) {
561
+ if (!Object.hasOwn(TABS_STATE_SCHEMA, key)) {
562
+ throw new Error(
563
+ `Tabs.setState: "${key}" is not a supported state key.`
564
+ );
565
+ }
566
+ const stateKey = key as keyof typeof TABS_STATE_SCHEMA;
567
+ validateParam(
568
+ key,
569
+ patch[key as keyof TabsState],
570
+ TABS_STATE_SCHEMA[stateKey],
571
+ 'Tabs.setState'
952
572
  );
953
573
  }
954
- const stateKey = key as keyof typeof TABS_STATE_SCHEMA;
955
- validateParam(
956
- key,
957
- patch[key as keyof TabsState],
958
- TABS_STATE_SCHEMA[stateKey],
959
- 'Tabs.setState'
960
- );
961
- }
962
-
963
- if (Array.isArray(patch.data)) this.validateData(patch.data);
964
- }
965
- }
574
+ },
575
+ view: () => {
576
+ createEffect(() => {
577
+ const dataLength = state.data.length;
578
+ refreshTask.schedule();
579
+ return dataLength;
580
+ });
581
+ return jsx('div', {
582
+ className: props.className.root,
583
+ id: props.id,
584
+ 'data-tabs': 'root',
585
+ 'data-tabs-direction': props.direction,
586
+ children: [
587
+ jsx('div', {
588
+ className: props.className.wrap,
589
+ 'data-tabs-wrap': '',
590
+ ref: wrapRef.set,
591
+ children: jsx('nav', {
592
+ className: () =>
593
+ joinClasses(
594
+ props.className.list,
595
+ state.dragging && props.className.dragging
596
+ ),
597
+ 'data-tabs-list': '',
598
+ ref: listRef.set,
599
+ children: For({
600
+ each: renderableData,
601
+ key: (item: TabItem) => item.name,
602
+ children: (item: () => TabItem, index: () => number) =>
603
+ tabView(item, index),
604
+ }),
605
+ }),
606
+ }),
607
+ jsx('div', {
608
+ className: props.className.panelWrap,
609
+ 'data-tabs-panel-wrap': '',
610
+ children: For({
611
+ each: renderableData,
612
+ key: (item: TabItem) => item.name,
613
+ children: (item: () => TabItem, index: () => number) =>
614
+ panelView(item, index),
615
+ }),
616
+ }),
617
+ ],
618
+ }) as HTMLElement;
619
+ },
620
+ onBuild(context) {
621
+ const root = context.element;
622
+ if (!root) return;
623
+ events.on('click', root, 'click', (event) => {
624
+ if (!(event.target instanceof Element)) return;
625
+ const tab = event.target.closest<HTMLElement>('[data-tabs-tab]');
626
+ const name = tab?.dataset.tabsTab;
627
+ if (name && !isDisabled(name)) void activate(name);
628
+ });
629
+ events.on('resize', window, 'resize', () => {
630
+ cancelAnimationFrame(resizeFrame);
631
+ resizeFrame = requestAnimationFrame(refreshDrag);
632
+ });
633
+ bindDrag();
634
+ context.own(() => events.clear());
635
+ refreshTask.schedule();
636
+ void loaders.get(current().name || '')?.();
637
+ },
638
+ onMount() {
639
+ refreshTask.schedule();
640
+ },
641
+ onDestroy() {
642
+ loadId += 1;
643
+ refreshTask.dispose();
644
+ cancelAnimationFrame(inertiaFrame);
645
+ cancelAnimationFrame(resizeFrame);
646
+ cache.clear();
647
+ loaders.clear();
648
+ tabs.clear();
649
+ panels.clear();
650
+ },
651
+ }) as Tabs;
966
652
 
967
- export type Tabs = TabsComponent;
653
+ Object.defineProperties(instance, {
654
+ current: { enumerable: true, get: current },
655
+ activeIndex: { enumerable: true, get: () => current().index },
656
+ disabledNames: { enumerable: true, get: disabledNames },
657
+ });
968
658
 
969
- export function createTabs(input: TabsProps = {}): Tabs {
970
- return new TabsComponent(input);
659
+ return instance;
971
660
  }