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