vanilla-jui 1.4.5 → 1.4.7
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 +6 -0
- package/dist/index.d.ts +1273 -1896
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +21 -2
- package/src/components/accordion.ts +297 -216
- package/src/components/flow.ts +222 -258
- package/src/components/form.ts +44 -102
- package/src/components/menu.ts +268 -356
- package/src/components/modal.ts +181 -641
- package/src/components/offcanvas.ts +225 -106
- package/src/components/pagination.ts +296 -326
- package/src/components/sticky.ts +55 -54
- package/src/components/swiper.ts +237 -247
- package/src/components/tabs.ts +278 -271
- package/src/components/toc.ts +42 -46
- package/src/core/Component.ts +69 -47
- package/src/css/accordion.css +2 -2
- package/src/css/animation.css +1 -5
- package/src/css/avatar.css +1 -1
- package/src/css/badge.css +1 -1
- package/src/css/breadcrumb.css +9 -10
- package/src/css/button.css +1 -1
- package/src/css/card.css +2 -2
- package/src/css/color.css +974 -1002
- package/src/css/color_p3.css +974 -1004
- package/src/css/components.css +1 -1
- package/src/css/content.css +72 -63
- package/src/css/drop.css +1 -1
- package/src/css/editor.css +14 -14
- package/src/css/flow.css +4 -14
- package/src/css/font.css +5 -4
- package/src/css/form.css +76 -52
- package/src/css/gradient.css +299 -222
- package/src/css/icon.css +57 -57
- package/src/css/index.css +1 -1
- package/src/css/layout.css +13 -10
- package/src/css/loading.css +60 -12
- package/src/css/menu.css +262 -188
- package/src/css/modal.css +1 -1
- package/src/css/offcanvas.css +9 -13
- package/src/css/pagination.css +5 -1
- package/src/css/popup.css +1 -1
- package/src/css/skeleton.css +3 -2
- package/src/css/status.css +2 -3
- package/src/css/swiper.css +1 -1
- package/src/css/table.css +1 -1
- package/src/css/tabs.css +12 -11
- package/src/css/tag.css +1 -1
- package/src/css/theme-palette.css +19 -5
- package/src/css/themes/default/accordion.css +61 -61
- package/src/css/themes/default/avatar.css +54 -54
- package/src/css/themes/default/badge.css +62 -62
- package/src/css/themes/default/breadcrumb.css +23 -23
- package/src/css/themes/default/button.css +203 -203
- package/src/css/themes/default/content.css +53 -53
- package/src/css/themes/default/drop.css +10 -10
- package/src/css/themes/default/editor.css +9 -9
- package/src/css/themes/default/index.css +1 -1
- package/src/css/themes/default/loading.css +26 -20
- package/src/css/themes/default/menu.css +75 -11
- package/src/css/themes/default/modal.css +32 -36
- package/src/css/themes/default/pagination.css +7 -7
- package/src/css/themes/default/popup.css +6 -6
- package/src/css/themes/default/swiper.css +32 -32
- package/src/css/themes/default/table.css +16 -16
- package/src/css/themes/default/tabs.css +30 -30
- package/src/css/themes/default/tag.css +71 -71
- package/src/css/themes/default/theme-palette.css +11 -11
- package/src/css/themes/default/tip.css +42 -42
- package/src/css/themes/default/toast.css +60 -60
- package/src/css/themes/default/toolbar.css +23 -29
- package/src/css/themes/default/tooltip.css +43 -13
- package/src/css/themes/index.css +1 -1
- package/src/css/tip.css +2 -2
- package/src/css/toast.css +3 -3
- package/src/css/tokens.css +12 -17
- package/src/css/toolbar.css +9 -10
- package/src/css/tooltip.css +3 -2
- package/src/index.ts +22 -19
- package/src/{components → primitives}/drop.ts +58 -49
- package/src/{components → primitives}/icons.ts +1 -1
- package/src/primitives/loading.ts +23 -0
- package/src/primitives/parabola.ts +388 -0
- package/src/primitives/popup.ts +61 -0
- package/src/{components → primitives}/theme.ts +89 -57
- package/src/{components → primitives}/toast.ts +18 -25
- package/src/{components → primitives}/tooltip.ts +100 -25
- package/src/utilities/browser.ts +23 -49
- package/src/utilities/class-name.ts +5 -0
- package/src/utilities/dom.ts +14 -32
- package/src/utilities/function.ts +4 -0
- package/src/utilities/http.ts +7 -7
- package/src/utilities/id.ts +57 -0
- package/src/utilities/object.ts +5 -0
- package/src/utilities/timer.ts +38 -0
- package/src/utilities/types.ts +420 -0
- package/src/{components → validation}/validator.ts +64 -53
- package/src/components/parabola.ts +0 -335
- package/src/utilities/cache.ts +0 -57
- package/src/utilities/core.ts +0 -614
- package/src/utilities/storage.ts +0 -46
package/src/components/tabs.ts
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
2
|
bindAttr,
|
|
3
3
|
createDeepStore,
|
|
4
|
+
createEffect,
|
|
4
5
|
createRoot,
|
|
5
6
|
flushSync,
|
|
6
|
-
insert,
|
|
7
7
|
jsx,
|
|
8
|
+
untrack,
|
|
8
9
|
} from 'vanilla-signal';
|
|
9
10
|
|
|
10
11
|
import Component, {
|
|
11
12
|
type ComponentDOM,
|
|
12
13
|
type ComponentRuntime,
|
|
13
14
|
} from '../core/Component.ts';
|
|
15
|
+
import { createLoading } from '../primitives/loading.ts';
|
|
14
16
|
import {
|
|
15
|
-
type ResolveSchema,
|
|
16
|
-
randomId,
|
|
17
|
-
resolveProps,
|
|
18
|
-
validateParam,
|
|
19
|
-
} from '../utilities/core.ts';
|
|
20
|
-
import {
|
|
21
|
-
type DOMReference,
|
|
22
17
|
type RenderableContent,
|
|
23
|
-
createLoading,
|
|
24
|
-
isRenderableContent,
|
|
25
18
|
normalizeContentNodes,
|
|
26
19
|
q,
|
|
27
|
-
requireContainer,
|
|
28
20
|
} from '../utilities/dom.ts';
|
|
21
|
+
import { randomId } from '../utilities/id.ts';
|
|
22
|
+
import {
|
|
23
|
+
type ResolveSchema,
|
|
24
|
+
resolveProps,
|
|
25
|
+
validateParam,
|
|
26
|
+
} from '../utilities/types.ts';
|
|
29
27
|
|
|
30
28
|
export type TabsDirection = 'top' | 'bottom' | 'left' | 'right';
|
|
31
29
|
export type TabsValue = number | string;
|
|
@@ -33,10 +31,6 @@ export type TabsDisabled = TabsValue | TabsValue[];
|
|
|
33
31
|
|
|
34
32
|
export interface TabsClassNames {
|
|
35
33
|
root: string;
|
|
36
|
-
top: string;
|
|
37
|
-
bottom: string;
|
|
38
|
-
left: string;
|
|
39
|
-
right: string;
|
|
40
34
|
wrap: string;
|
|
41
35
|
list: string;
|
|
42
36
|
tab: string;
|
|
@@ -88,19 +82,8 @@ export interface TabsProps extends Record<string, unknown> {
|
|
|
88
82
|
panel: HTMLElement | undefined
|
|
89
83
|
) => void | Promise<void>)
|
|
90
84
|
| null;
|
|
91
|
-
|
|
85
|
+
data?: TabItem[];
|
|
92
86
|
className?: TabsClassNameConfig;
|
|
93
|
-
onAdd?:
|
|
94
|
-
| ((
|
|
95
|
-
index: number,
|
|
96
|
-
item: TabItem,
|
|
97
|
-
tab: HTMLElement | undefined,
|
|
98
|
-
panel: HTMLElement | undefined
|
|
99
|
-
) => void | Promise<void>)
|
|
100
|
-
| null;
|
|
101
|
-
onRemove?:
|
|
102
|
-
| ((index: number, name: string | undefined) => void | Promise<void>)
|
|
103
|
-
| null;
|
|
104
87
|
}
|
|
105
88
|
|
|
106
89
|
interface ResolvedTabsProps extends Record<string, unknown> {
|
|
@@ -109,29 +92,31 @@ interface ResolvedTabsProps extends Record<string, unknown> {
|
|
|
109
92
|
active: TabsValue;
|
|
110
93
|
disabled: TabsDisabled;
|
|
111
94
|
onChange: NonNullable<TabsProps['onChange']> | null;
|
|
112
|
-
|
|
95
|
+
data: TabItem[];
|
|
113
96
|
className: TabsClassNames;
|
|
114
|
-
onAdd: NonNullable<TabsProps['onAdd']> | null;
|
|
115
|
-
onRemove: NonNullable<TabsProps['onRemove']> | null;
|
|
116
97
|
}
|
|
117
98
|
|
|
118
99
|
interface TabsState extends Record<string, unknown> {
|
|
100
|
+
data: TabItem[];
|
|
101
|
+
active: TabsValue;
|
|
102
|
+
disabled: TabsDisabled;
|
|
103
|
+
direction: TabsDirection;
|
|
119
104
|
current: {
|
|
120
105
|
index: number;
|
|
121
106
|
name: string | null;
|
|
122
107
|
};
|
|
123
|
-
disabled: {
|
|
124
|
-
names: string[];
|
|
125
|
-
indexes: number[];
|
|
126
|
-
};
|
|
127
108
|
isVertical: boolean;
|
|
128
109
|
draggable: boolean;
|
|
129
110
|
loading: boolean;
|
|
130
111
|
}
|
|
131
112
|
|
|
113
|
+
interface TabsDisabledState {
|
|
114
|
+
names: string[];
|
|
115
|
+
indexes: number[];
|
|
116
|
+
}
|
|
117
|
+
|
|
132
118
|
interface TabsDOM extends ComponentDOM {
|
|
133
119
|
root: HTMLElement | null;
|
|
134
|
-
container: Element;
|
|
135
120
|
tabs: HTMLElement[];
|
|
136
121
|
panels: HTMLElement[];
|
|
137
122
|
}
|
|
@@ -142,6 +127,7 @@ interface TabsPanelCacheEntry {
|
|
|
142
127
|
}
|
|
143
128
|
|
|
144
129
|
interface TabsRuntime extends ComponentRuntime {
|
|
130
|
+
built: boolean;
|
|
145
131
|
cache: {
|
|
146
132
|
panels: Map<string, TabsPanelCacheEntry>;
|
|
147
133
|
};
|
|
@@ -152,10 +138,6 @@ type PointerDragEvent = MouseEvent | TouchEvent;
|
|
|
152
138
|
|
|
153
139
|
const DEFAULT_CLASS_NAMES: TabsClassNames = {
|
|
154
140
|
root: 'j-tabs',
|
|
155
|
-
top: 'is-top',
|
|
156
|
-
bottom: 'is-bottom',
|
|
157
|
-
left: 'is-left',
|
|
158
|
-
right: 'is-right',
|
|
159
141
|
wrap: 'tab-wrap',
|
|
160
142
|
list: 'tab-list',
|
|
161
143
|
tab: 'tab-item',
|
|
@@ -186,7 +168,7 @@ const TABS_PROPS_SCHEMA = {
|
|
|
186
168
|
active: { default: 0, types: ['number', 'string'] },
|
|
187
169
|
disabled: { default: [], types: ['number', 'string', 'array'] },
|
|
188
170
|
onChange: { default: null, types: ['function', 'null'] },
|
|
189
|
-
|
|
171
|
+
data: { default: [], type: 'array' },
|
|
190
172
|
className: {
|
|
191
173
|
default: DEFAULT_CLASS_NAMES,
|
|
192
174
|
type: 'object',
|
|
@@ -195,31 +177,55 @@ const TABS_PROPS_SCHEMA = {
|
|
|
195
177
|
...(value && typeof value === 'object' ? value : {}),
|
|
196
178
|
}),
|
|
197
179
|
},
|
|
198
|
-
onAdd: { default: null, types: ['function', 'null'] },
|
|
199
|
-
onRemove: { default: null, types: ['function', 'null'] },
|
|
200
180
|
} satisfies ResolveSchema<TabsProps>;
|
|
201
181
|
|
|
202
182
|
const TAB_CONFIG_RULE = {
|
|
203
|
-
type: '
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
!!item &&
|
|
208
|
-
typeof item === 'object' &&
|
|
209
|
-
isRenderableContent(item.title) &&
|
|
210
|
-
isRenderableContent(item.panel)
|
|
211
|
-
);
|
|
183
|
+
type: 'plainObject',
|
|
184
|
+
shape: {
|
|
185
|
+
title: 'renderable',
|
|
186
|
+
panel: 'renderable',
|
|
212
187
|
},
|
|
213
|
-
message:
|
|
214
|
-
'expects an object with renderable title and panel: string, Node, array, function or null.',
|
|
215
188
|
};
|
|
216
189
|
|
|
217
190
|
function cloneTabItems(tabs: unknown): TabItem[] {
|
|
218
191
|
return Array.isArray(tabs)
|
|
219
|
-
? tabs.map((item) =>
|
|
192
|
+
? tabs.map((item) => {
|
|
193
|
+
const nextItem = { ...(item as TabItem) };
|
|
194
|
+
nextItem.name = nextItem.name || randomId();
|
|
195
|
+
return nextItem;
|
|
196
|
+
})
|
|
220
197
|
: [];
|
|
221
198
|
}
|
|
222
199
|
|
|
200
|
+
function cloneDisabled(disabled: TabsDisabled): TabsDisabled {
|
|
201
|
+
return Array.isArray(disabled) ? disabled.slice() : disabled;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resolveDisabledState(
|
|
205
|
+
disabled: TabsDisabled,
|
|
206
|
+
data: TabItem[]
|
|
207
|
+
): TabsDisabledState {
|
|
208
|
+
const toName = (val: TabsValue): string | null => {
|
|
209
|
+
if (typeof val === 'number') return data[val]?.name || null;
|
|
210
|
+
if (typeof val === 'string') return val;
|
|
211
|
+
return null;
|
|
212
|
+
};
|
|
213
|
+
const names = Array.isArray(disabled)
|
|
214
|
+
? disabled.map(toName).filter((name): name is string => !!name)
|
|
215
|
+
: (() => {
|
|
216
|
+
const name = toName(disabled);
|
|
217
|
+
return name ? [name] : [];
|
|
218
|
+
})();
|
|
219
|
+
const uniqNames = Array.from(new Set(names));
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
names: uniqNames,
|
|
223
|
+
indexes: uniqNames
|
|
224
|
+
.map((name) => data.findIndex((tab) => tab.name === name))
|
|
225
|
+
.filter((index) => index >= 0),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
223
229
|
function normalizeTtl(ttl: unknown): number {
|
|
224
230
|
return typeof ttl === 'number' && ttl > 0 ? ttl : 0;
|
|
225
231
|
}
|
|
@@ -232,83 +238,79 @@ function normalizeProps(input: TabsProps): ResolvedTabsProps {
|
|
|
232
238
|
active: props.active as TabsValue,
|
|
233
239
|
disabled: props.disabled as TabsDisabled,
|
|
234
240
|
onChange: props.onChange as ResolvedTabsProps['onChange'],
|
|
235
|
-
|
|
241
|
+
data: cloneTabItems(props.data),
|
|
236
242
|
className: props.className as TabsClassNames,
|
|
237
|
-
onAdd: props.onAdd as ResolvedTabsProps['onAdd'],
|
|
238
|
-
onRemove: props.onRemove as ResolvedTabsProps['onRemove'],
|
|
239
243
|
};
|
|
240
244
|
}
|
|
241
245
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
246
|
+
const TABS_STATE_SCHEMA = {
|
|
247
|
+
data: TABS_PROPS_SCHEMA.data,
|
|
248
|
+
active: TABS_PROPS_SCHEMA.active,
|
|
249
|
+
disabled: TABS_PROPS_SCHEMA.disabled,
|
|
250
|
+
direction: TABS_PROPS_SCHEMA.direction,
|
|
251
|
+
};
|
|
247
252
|
|
|
248
253
|
/**
|
|
249
254
|
* 标签页组件,继承 Component。
|
|
250
255
|
*
|
|
251
256
|
* DOM 创建一次,通过 createEffect 细粒度更新 class/ARIA。
|
|
252
257
|
*/
|
|
253
|
-
|
|
258
|
+
class TabsComponent extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
254
259
|
declare runtime: TabsRuntime;
|
|
255
260
|
declare state: TabsState;
|
|
256
261
|
private bindingsDispose: (() => void) | null;
|
|
262
|
+
private stateDispose: (() => void) | null;
|
|
257
263
|
private isDragging: boolean;
|
|
258
264
|
private raf: number;
|
|
259
265
|
private resizeRaf: number;
|
|
260
266
|
private velocity: number;
|
|
261
267
|
|
|
262
268
|
/**
|
|
263
|
-
* @param {Element|Node|string|Array} container 挂载容器(元素、选择器或 JSX/h 返回节点)。
|
|
264
269
|
* @param {object} [input={}] 标签页配置。
|
|
265
270
|
*/
|
|
266
|
-
constructor(
|
|
267
|
-
const el = requireContainer(container, 'Tabs');
|
|
271
|
+
constructor(input: TabsProps = {}) {
|
|
268
272
|
const props = normalizeProps(input);
|
|
269
273
|
super(props);
|
|
270
274
|
|
|
271
|
-
this.dom.container = el;
|
|
272
275
|
this.dom.tabs = [];
|
|
273
276
|
this.dom.panels = [];
|
|
274
277
|
this.bindingsDispose = null;
|
|
278
|
+
this.stateDispose = null;
|
|
275
279
|
this.isDragging = false;
|
|
276
280
|
this.raf = 0;
|
|
277
281
|
this.resizeRaf = 0;
|
|
278
282
|
this.velocity = 0;
|
|
279
283
|
|
|
280
284
|
this.state = createDeepStore({
|
|
285
|
+
data: cloneTabItems(props.data),
|
|
286
|
+
active: props.active,
|
|
287
|
+
disabled: cloneDisabled(props.disabled),
|
|
288
|
+
direction: props.direction,
|
|
281
289
|
current: {
|
|
282
290
|
index: -1,
|
|
283
291
|
name: null,
|
|
284
292
|
},
|
|
285
|
-
disabled: this.parseDisabled(props.disabled),
|
|
286
293
|
isVertical: props.direction === 'left' || props.direction === 'right',
|
|
287
294
|
draggable: false,
|
|
288
295
|
loading: false,
|
|
289
296
|
}) as TabsState;
|
|
290
297
|
|
|
291
298
|
this.runtime.cache = { panels: new Map() };
|
|
299
|
+
this.runtime.built = false;
|
|
292
300
|
this.runtime.panelLoadId = 0;
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
this.onInit(props);
|
|
296
|
-
} catch (error) {
|
|
297
|
-
this.destroy();
|
|
298
|
-
throw error;
|
|
299
|
-
}
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
protected onInit(props: ResolvedTabsProps): void {
|
|
303
|
-
this.root = this.buildRoot(props);
|
|
304
|
-
this.
|
|
305
|
-
void this.activateInternal(
|
|
304
|
+
this.dom.root = this.buildRoot(props);
|
|
305
|
+
this.renderItems();
|
|
306
|
+
void this.activateInternal(this.state.active, false);
|
|
306
307
|
this.bindEvents();
|
|
307
308
|
this.initDrag();
|
|
309
|
+
this.bindState();
|
|
308
310
|
}
|
|
309
311
|
|
|
310
312
|
private buildRoot(props: ResolvedTabsProps): HTMLElement {
|
|
311
|
-
const { id,
|
|
313
|
+
const { id, className } = props;
|
|
312
314
|
const nav = jsx('nav', {
|
|
313
315
|
className: className.list,
|
|
314
316
|
'data-tabs-list': '',
|
|
@@ -324,18 +326,21 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
324
326
|
}) as HTMLElement;
|
|
325
327
|
|
|
326
328
|
return jsx('div', {
|
|
327
|
-
className:
|
|
329
|
+
className: className.root,
|
|
328
330
|
id,
|
|
329
331
|
'data-tabs': 'root',
|
|
330
|
-
'data-tabs-direction': direction,
|
|
332
|
+
'data-tabs-direction': this.state.direction,
|
|
331
333
|
children: [wrap, panelWrapper],
|
|
332
334
|
}) as HTMLElement;
|
|
333
335
|
}
|
|
334
336
|
|
|
335
|
-
private
|
|
336
|
-
if (!this.root) return;
|
|
337
|
-
const tabList = q<HTMLElement>('[data-tabs-list]', this.root);
|
|
338
|
-
const panelWrapper = q<HTMLElement>(
|
|
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
|
+
);
|
|
339
344
|
if (!tabList || !panelWrapper) return;
|
|
340
345
|
|
|
341
346
|
tabList.textContent = '';
|
|
@@ -347,7 +352,7 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
347
352
|
const tabFragment = document.createDocumentFragment();
|
|
348
353
|
const panelFragment = document.createDocumentFragment();
|
|
349
354
|
|
|
350
|
-
this.
|
|
355
|
+
this.state.data.forEach((item, index) => {
|
|
351
356
|
const name = item.name || randomId();
|
|
352
357
|
|
|
353
358
|
const tab = jsx('div', {
|
|
@@ -390,7 +395,7 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
390
395
|
this.state.current.index === index ? 'true' : 'false'
|
|
391
396
|
);
|
|
392
397
|
bindAttr(tab, 'aria-disabled', () =>
|
|
393
|
-
this.
|
|
398
|
+
this.isDisabledName(name) ? 'true' : 'false'
|
|
394
399
|
);
|
|
395
400
|
});
|
|
396
401
|
this.dom.panels.forEach((panel, index) => {
|
|
@@ -402,31 +407,100 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
402
407
|
});
|
|
403
408
|
}
|
|
404
409
|
|
|
405
|
-
private
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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;
|
|
434
|
+
|
|
435
|
+
void untrack(() =>
|
|
436
|
+
this.syncStateView({ dataChanged, directionChanged, activeChanged })
|
|
437
|
+
);
|
|
438
|
+
void disabled;
|
|
439
|
+
});
|
|
440
|
+
return dispose;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private async syncStateView({
|
|
445
|
+
dataChanged,
|
|
446
|
+
directionChanged,
|
|
447
|
+
activeChanged,
|
|
448
|
+
}: {
|
|
449
|
+
dataChanged: boolean;
|
|
450
|
+
directionChanged: boolean;
|
|
451
|
+
activeChanged: boolean;
|
|
452
|
+
}): Promise<void> {
|
|
453
|
+
if (!this.runtime.built || !this.dom.root) return;
|
|
454
|
+
this.validateData(this.state.data);
|
|
455
|
+
validateParam(
|
|
456
|
+
'direction',
|
|
457
|
+
this.state.direction,
|
|
458
|
+
TABS_STATE_SCHEMA.direction,
|
|
459
|
+
'Tabs.state'
|
|
460
|
+
);
|
|
461
|
+
validateParam(
|
|
462
|
+
'disabled',
|
|
463
|
+
this.state.disabled,
|
|
464
|
+
TABS_STATE_SCHEMA.disabled,
|
|
465
|
+
'Tabs.state'
|
|
466
|
+
);
|
|
467
|
+
validateParam(
|
|
468
|
+
'active',
|
|
469
|
+
this.state.active,
|
|
470
|
+
TABS_STATE_SCHEMA.active,
|
|
471
|
+
'Tabs.state'
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
if (directionChanged) {
|
|
475
|
+
this.dom.root.setAttribute('data-tabs-direction', this.state.direction);
|
|
476
|
+
flushSync(() => {
|
|
477
|
+
this.state.isVertical =
|
|
478
|
+
this.state.direction === 'left' || this.state.direction === 'right';
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (dataChanged) {
|
|
483
|
+
this.runtime.cache.panels.clear();
|
|
484
|
+
this.renderItems();
|
|
485
|
+
this.bindEvents();
|
|
486
|
+
flushSync(() => {
|
|
487
|
+
this.syncCurrent(-1);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (dataChanged || activeChanged) {
|
|
492
|
+
await this.activateInternal(this.state.active, activeChanged);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
this.refreshDrag();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private get disabledState(): TabsDisabledState {
|
|
499
|
+
return resolveDisabledState(this.state.disabled, this.state.data);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private isDisabledName(name: string): boolean {
|
|
503
|
+
return this.disabledState.names.includes(name);
|
|
430
504
|
}
|
|
431
505
|
|
|
432
506
|
private syncCurrent(index: number): void {
|
|
@@ -480,7 +554,7 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
480
554
|
content: RenderableContent<TabsPanelContext>
|
|
481
555
|
): void {
|
|
482
556
|
const panel = this.dom.panels[index];
|
|
483
|
-
const item = this.
|
|
557
|
+
const item = this.state.data[index];
|
|
484
558
|
if (!panel || !item) return;
|
|
485
559
|
|
|
486
560
|
panel.textContent = '';
|
|
@@ -495,7 +569,7 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
495
569
|
}
|
|
496
570
|
|
|
497
571
|
private async loadPanel(index: number): Promise<void> {
|
|
498
|
-
const item = this.
|
|
572
|
+
const item = this.state.data[index];
|
|
499
573
|
const panel = this.dom.panels[index];
|
|
500
574
|
if (!item || !panel || typeof item.panel !== 'function') {
|
|
501
575
|
this.runtime.panelLoadId += 1;
|
|
@@ -553,14 +627,14 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
553
627
|
}
|
|
554
628
|
|
|
555
629
|
get disabledNames(): string[] {
|
|
556
|
-
return this.
|
|
630
|
+
return this.disabledState.names;
|
|
557
631
|
}
|
|
558
632
|
|
|
559
633
|
private bindEvents(): void {
|
|
560
634
|
this.unbindEvents();
|
|
561
635
|
|
|
562
|
-
if (!this.root) return;
|
|
563
|
-
const tabList = q<HTMLElement>('[data-tabs-list]', this.root);
|
|
636
|
+
if (!this.dom.root) return;
|
|
637
|
+
const tabList = q<HTMLElement>('[data-tabs-list]', this.dom.root);
|
|
564
638
|
if (!tabList) return;
|
|
565
639
|
|
|
566
640
|
this.cleanup.events.on('tabClick', tabList, 'click', (event) => {
|
|
@@ -568,7 +642,7 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
568
642
|
const tab = event.target.closest<HTMLElement>('[data-tabs-tab]');
|
|
569
643
|
if (!tab || !tabList.contains(tab)) return;
|
|
570
644
|
const name = tab.dataset.tabsTab;
|
|
571
|
-
if (name && !this.
|
|
645
|
+
if (name && !this.isDisabledName(name)) {
|
|
572
646
|
void this.activate(name);
|
|
573
647
|
}
|
|
574
648
|
});
|
|
@@ -582,6 +656,9 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
582
656
|
if (this.runtime.destroyed) {
|
|
583
657
|
throw new Error(`Tabs.${method}: instance has been destroyed.`);
|
|
584
658
|
}
|
|
659
|
+
if (!this.runtime.built) {
|
|
660
|
+
throw new Error(`Tabs.${method}: call build() first.`);
|
|
661
|
+
}
|
|
585
662
|
}
|
|
586
663
|
|
|
587
664
|
private getIndex(val: TabsValue | undefined | null): number {
|
|
@@ -601,7 +678,7 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
601
678
|
if (
|
|
602
679
|
index < 0 ||
|
|
603
680
|
index >= this.dom.tabs.length ||
|
|
604
|
-
this.
|
|
681
|
+
this.isDisabledName(name) ||
|
|
605
682
|
this.state.current.index === index
|
|
606
683
|
) {
|
|
607
684
|
return;
|
|
@@ -633,172 +710,49 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
633
710
|
*/
|
|
634
711
|
async activate(val: TabsValue): Promise<void> {
|
|
635
712
|
this.assertActive('activate');
|
|
713
|
+
this.state.active = val;
|
|
636
714
|
await this.activateInternal(val, true);
|
|
637
715
|
}
|
|
638
716
|
|
|
639
717
|
/**
|
|
640
|
-
*
|
|
718
|
+
* 构建 Tabs DOM。
|
|
641
719
|
*/
|
|
642
720
|
build(): this {
|
|
643
|
-
this.
|
|
644
|
-
|
|
645
|
-
return this;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
render(): this {
|
|
649
|
-
return this.build();
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* 动态新增标签。
|
|
654
|
-
* @param {object} tabConfig 标签配置。
|
|
655
|
-
*/
|
|
656
|
-
async add(tabConfig: TabItem): Promise<void> {
|
|
657
|
-
this.assertActive('add');
|
|
658
|
-
validateParam('tabConfig', tabConfig, TAB_CONFIG_RULE, 'Tabs.add');
|
|
659
|
-
|
|
660
|
-
tabConfig.name = tabConfig.name || randomId();
|
|
661
|
-
this.props.tabs = [...cloneTabItems(this.props.tabs), tabConfig];
|
|
662
|
-
this.runtime.cache.panels.clear();
|
|
663
|
-
|
|
664
|
-
this.rebuildItems();
|
|
665
|
-
this.syncActiveNames(this.resolveActiveIndex(this.props.active));
|
|
666
|
-
this.bindEvents();
|
|
667
|
-
this.refreshDrag();
|
|
668
|
-
|
|
669
|
-
const { onAdd } = this.props;
|
|
670
|
-
if (onAdd) {
|
|
671
|
-
const index = this.props.tabs.length - 1;
|
|
672
|
-
await Promise.resolve(
|
|
673
|
-
onAdd(index, tabConfig, this.dom.tabs[index], this.dom.panels[index])
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* 根据索引或名称删除标签。
|
|
680
|
-
* @param {number|string} val 标签索引或名称。
|
|
681
|
-
*/
|
|
682
|
-
async delete(val: TabsValue): Promise<void> {
|
|
683
|
-
this.assertActive('delete');
|
|
684
|
-
if (this.props.tabs.length <= 1) return;
|
|
685
|
-
|
|
686
|
-
const index = this.getIndex(val);
|
|
687
|
-
if (index < 0 || index >= this.props.tabs.length) return;
|
|
688
|
-
|
|
689
|
-
const removedName = this.props.tabs[index].name;
|
|
690
|
-
const { onRemove } = this.props;
|
|
691
|
-
|
|
692
|
-
this.props.tabs = this.props.tabs.filter((_, i) => i !== index);
|
|
693
|
-
if (removedName) this.runtime.cache.panels.delete(removedName);
|
|
694
|
-
|
|
695
|
-
if (this.state.current.index >= this.props.tabs.length) {
|
|
696
|
-
flushSync(() => {
|
|
697
|
-
this.syncCurrent(this.props.tabs.length - 1);
|
|
698
|
-
});
|
|
699
|
-
} else if (this.state.current.index > index) {
|
|
700
|
-
flushSync(() => {
|
|
701
|
-
this.syncCurrent(this.state.current.index - 1);
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
this.rebuildItems();
|
|
706
|
-
this.bindEvents();
|
|
707
|
-
await this.loadPanel(this.state.current.index);
|
|
708
|
-
this.refreshDrag();
|
|
709
|
-
|
|
710
|
-
if (onRemove) await Promise.resolve(onRemove(index, removedName));
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* 根据索引或名称禁用标签。
|
|
715
|
-
* @param {number|string} val 标签索引或名称。
|
|
716
|
-
*/
|
|
717
|
-
disable(val: TabsValue): void {
|
|
718
|
-
this.assertActive('disable');
|
|
719
|
-
const name =
|
|
720
|
-
typeof val === 'number' ? this.dom.tabs[val]?.dataset.tabsTab : val;
|
|
721
|
-
if (name && !this.state.disabled.names.includes(name)) {
|
|
722
|
-
flushSync(() => {
|
|
723
|
-
this.state.disabled = this.createDisabledState([
|
|
724
|
-
...this.state.disabled.names,
|
|
725
|
-
name,
|
|
726
|
-
]);
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
* 根据索引或名称启用标签。
|
|
733
|
-
* @param {number|string} val 标签索引或名称。
|
|
734
|
-
*/
|
|
735
|
-
enable(val: TabsValue): void {
|
|
736
|
-
this.assertActive('enable');
|
|
737
|
-
const name =
|
|
738
|
-
typeof val === 'number' ? this.dom.tabs[val]?.dataset.tabsTab : val;
|
|
739
|
-
if (name) {
|
|
740
|
-
flushSync(() => {
|
|
741
|
-
this.state.disabled = this.createDisabledState(
|
|
742
|
-
this.state.disabled.names.filter((n) => n !== name)
|
|
743
|
-
);
|
|
744
|
-
});
|
|
721
|
+
if (this.runtime.destroyed) {
|
|
722
|
+
throw new Error('Tabs.build: instance has been destroyed.');
|
|
745
723
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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;
|
|
753
731
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
private syncActiveNames(index: number): void {
|
|
758
|
-
flushSync(() => {
|
|
759
|
-
this.syncCurrent(index);
|
|
760
|
-
});
|
|
732
|
+
this.emit('init', this.props);
|
|
733
|
+
return this;
|
|
761
734
|
}
|
|
762
735
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
* @param {object} [patch={}] 需要覆盖的配置。
|
|
766
|
-
*/
|
|
767
|
-
async reInit(patch: TabsProps = {}): Promise<void> {
|
|
768
|
-
this.assertActive('reInit');
|
|
769
|
-
Object.assign(
|
|
770
|
-
this.props,
|
|
771
|
-
normalizeProps({
|
|
772
|
-
...this.props,
|
|
773
|
-
...patch,
|
|
774
|
-
className: {
|
|
775
|
-
...this.props.className,
|
|
776
|
-
...patch.className,
|
|
777
|
-
},
|
|
778
|
-
})
|
|
779
|
-
);
|
|
780
|
-
|
|
781
|
-
flushSync(() => {
|
|
782
|
-
this.state.disabled = this.parseDisabled(this.props.disabled);
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
this.rebuildItems();
|
|
786
|
-
this.syncActiveNames(this.resolveActiveIndex(this.props.active));
|
|
787
|
-
this.bindEvents();
|
|
788
|
-
await this.loadPanel(this.state.current.index);
|
|
736
|
+
refresh(): this {
|
|
737
|
+
this.assertActive('refresh');
|
|
789
738
|
this.refreshDrag();
|
|
739
|
+
return this;
|
|
790
740
|
}
|
|
791
741
|
|
|
792
742
|
private get dragContainer(): HTMLElement | null {
|
|
793
|
-
return this.
|
|
743
|
+
return this.dom.root
|
|
744
|
+
? q<HTMLElement>('[data-tabs-wrap]', this.dom.root)
|
|
745
|
+
: null;
|
|
794
746
|
}
|
|
795
747
|
|
|
796
748
|
private get dragInner(): HTMLElement | null {
|
|
797
|
-
return this.
|
|
749
|
+
return this.dom.root
|
|
750
|
+
? q<HTMLElement>('[data-tabs-list]', this.dom.root)
|
|
751
|
+
: null;
|
|
798
752
|
}
|
|
799
753
|
|
|
800
754
|
private initDrag(): void {
|
|
801
|
-
const
|
|
755
|
+
const direction = this.state.direction;
|
|
802
756
|
const container = this.dragContainer;
|
|
803
757
|
const inner = this.dragInner;
|
|
804
758
|
if (!container || !inner) return;
|
|
@@ -948,17 +902,70 @@ export class Tabs extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
|
|
|
948
902
|
this.removeDragEvents();
|
|
949
903
|
this.bindingsDispose?.();
|
|
950
904
|
this.bindingsDispose = null;
|
|
905
|
+
this.stateDispose?.();
|
|
906
|
+
this.stateDispose = null;
|
|
951
907
|
cancelAnimationFrame(this.raf);
|
|
952
908
|
cancelAnimationFrame(this.resizeRaf);
|
|
953
909
|
this.cleanup.events.off('resize');
|
|
954
910
|
|
|
955
|
-
|
|
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
|
+
);
|
|
947
|
+
|
|
948
|
+
for (const key of Object.keys(patch)) {
|
|
949
|
+
if (!Object.hasOwn(TABS_STATE_SCHEMA, key)) {
|
|
950
|
+
throw new Error(
|
|
951
|
+
`Tabs.setState: "${key}" is not a supported state key.`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
const stateKey = key as keyof typeof TABS_STATE_SCHEMA;
|
|
955
|
+
validateParam(
|
|
956
|
+
key,
|
|
957
|
+
patch[key as keyof TabsState],
|
|
958
|
+
TABS_STATE_SCHEMA[stateKey],
|
|
959
|
+
'Tabs.setState'
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (Array.isArray(patch.data)) this.validateData(patch.data);
|
|
956
964
|
}
|
|
957
965
|
}
|
|
958
966
|
|
|
959
|
-
export
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
return new Tabs(container, input);
|
|
967
|
+
export type Tabs = TabsComponent;
|
|
968
|
+
|
|
969
|
+
export function createTabs(input: TabsProps = {}): Tabs {
|
|
970
|
+
return new TabsComponent(input);
|
|
964
971
|
}
|