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