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.
- package/README.md +10 -4
- package/dist/index.d.ts +418 -970
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -3
- package/src/components/accordion.ts +466 -346
- package/src/components/flow.ts +779 -1133
- package/src/components/form.ts +666 -509
- package/src/components/menu.ts +198 -328
- package/src/components/modal.ts +510 -747
- package/src/components/offcanvas.ts +273 -367
- package/src/components/pagination.ts +201 -375
- package/src/components/swiper.ts +689 -1107
- package/src/components/tabs.ts +467 -778
- package/src/components/toc.ts +191 -244
- package/src/core/component.ts +332 -0
- package/src/core/motion.ts +298 -0
- package/src/core/presence.ts +233 -0
- package/src/core/scheduler.ts +91 -0
- package/src/core/view.ts +31 -0
- package/src/css/accordion.css +94 -3
- package/src/css/content.css +18 -24
- package/src/css/drop.css +4 -1
- package/src/css/form.css +8 -0
- package/src/css/menu.css +10 -5
- package/src/css/modal.css +2 -1
- package/src/css/offcanvas.css +5 -17
- package/src/css/swiper.css +2 -1
- package/src/css/tabs.css +8 -17
- package/src/css/themes/default/accordion.css +0 -48
- package/src/css/themes/default/swiper.css +1 -1
- package/src/css/themes/default/tabs.css +4 -6
- package/src/css/toast.css +1 -17
- package/src/css/tooltip.css +2 -1
- package/src/index.ts +8 -2
- package/src/locales/index.ts +3 -0
- package/src/primitives/drop.ts +253 -262
- package/src/primitives/loading.ts +22 -4
- package/src/primitives/parabola.ts +123 -164
- package/src/primitives/sticky.ts +297 -0
- package/src/primitives/theme.ts +164 -222
- package/src/primitives/toast.ts +309 -284
- package/src/primitives/tooltip.ts +65 -66
- package/src/utilities/browser.ts +38 -0
- package/src/utilities/dom.ts +0 -54
- package/src/utilities/refs.ts +61 -0
- package/src/utilities/state.ts +73 -0
- package/src/utilities/timer.ts +11 -31
- package/src/validation/validator.ts +289 -389
- package/src/components/sticky.ts +0 -297
- package/src/core/Component.ts +0 -377
package/src/components/tabs.ts
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
For,
|
|
3
3
|
createDeepStore,
|
|
4
4
|
createEffect,
|
|
5
|
-
|
|
5
|
+
createMemo,
|
|
6
|
+
createSignal,
|
|
6
7
|
flushSync,
|
|
7
8
|
jsx,
|
|
8
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from '../
|
|
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
|
|
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
|
|
67
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
130
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
content: 'renderable',
|
|
176
|
+
cache: ['boolean', 'undefined'],
|
|
177
|
+
ttl: ['number', 'undefined'],
|
|
187
178
|
},
|
|
188
179
|
};
|
|
189
180
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
201
|
-
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
230
|
-
return
|
|
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:
|
|
226
|
+
data: normalizeData(props.data, 'Tabs.props.data'),
|
|
242
227
|
className: props.className as TabsClassNames,
|
|
243
228
|
};
|
|
244
229
|
}
|
|
245
230
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
303
|
+
state.draggable = next;
|
|
488
304
|
});
|
|
489
305
|
}
|
|
306
|
+
};
|
|
307
|
+
const refreshTask = createScheduledTask(refreshDrag);
|
|
490
308
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
572
|
-
const
|
|
573
|
-
const
|
|
574
|
-
if (!
|
|
575
|
-
|
|
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
|
-
|
|
351
|
+
state.dragging = true;
|
|
578
352
|
});
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
384
|
+
state.dragging = false;
|
|
587
385
|
});
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
597
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
786
|
-
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
);
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
|
|
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
|
-
|
|
970
|
-
return new TabsComponent(input);
|
|
659
|
+
return instance;
|
|
971
660
|
}
|