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.
- package/README.md +10 -4
- package/dist/index.d.ts +402 -977
- 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 +467 -349
- package/src/components/flow.ts +779 -1133
- package/src/components/form.ts +666 -509
- package/src/components/menu.ts +199 -326
- package/src/components/modal.ts +510 -747
- package/src/components/offcanvas.ts +281 -361
- package/src/components/pagination.ts +207 -374
- package/src/components/swiper.ts +688 -1168
- package/src/components/tabs.ts +468 -788
- 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/color.css +4 -5
- package/src/css/color_p3.css +6 -5
- package/src/css/content.css +18 -24
- package/src/css/drop.css +4 -1
- package/src/css/form.css +8 -0
- package/src/css/gradient.css +64 -131
- package/src/css/layout.css +9 -23
- package/src/css/menu.css +10 -5
- package/src/css/modal.css +2 -1
- package/src/css/offcanvas.css +1 -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/tokens.css +18 -35
- package/src/css/tooltip.css +2 -1
- package/src/index.ts +8 -3
- 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/utilities/scheduler.ts +0 -156
package/src/components/tabs.ts
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
For,
|
|
3
3
|
createDeepStore,
|
|
4
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} 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';
|
|
19
20
|
import { randomId } from '../utilities/id.ts';
|
|
20
|
-
import {
|
|
21
|
-
|
|
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
|
|
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
|
|
70
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
133
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
content: 'renderable',
|
|
176
|
+
cache: ['boolean', 'undefined'],
|
|
177
|
+
ttl: ['number', 'undefined'],
|
|
190
178
|
},
|
|
191
179
|
};
|
|
192
180
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
204
|
-
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
233
|
-
return
|
|
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:
|
|
226
|
+
data: normalizeData(props.data, 'Tabs.props.data'),
|
|
245
227
|
className: props.className as TabsClassNames,
|
|
246
228
|
};
|
|
247
229
|
}
|
|
248
230
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
303
|
+
state.draggable = next;
|
|
497
304
|
});
|
|
498
305
|
}
|
|
306
|
+
};
|
|
307
|
+
const refreshTask = createScheduledTask(refreshDrag);
|
|
499
308
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
if (!
|
|
584
|
-
|
|
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
|
-
|
|
351
|
+
state.dragging = true;
|
|
587
352
|
});
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
384
|
+
state.dragging = false;
|
|
596
385
|
});
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
606
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
795
|
-
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
);
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
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
|
-
|
|
979
|
-
return new TabsComponent(input);
|
|
659
|
+
return instance;
|
|
980
660
|
}
|