vanilla-jui 1.3.11 → 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/animation.css +31 -1
- package/src/css/card.css +1 -1
- package/src/css/color.css +863 -1013
- package/src/css/color_p3.css +822 -975
- package/src/css/components.css +1 -0
- package/src/css/form.css +1 -1
- package/src/css/icon.css +17 -1
- package/src/css/index.css +2 -2
- package/src/css/menu.css +1 -1
- package/src/css/tabs.css +1 -0
- package/src/css/tokens.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/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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-jui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A lightweight vanilla JavaScript UI plugin library.",
|
|
6
6
|
"keywords": [
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"main": "./dist/index.js",
|
|
38
38
|
"module": "./dist/index.js",
|
|
39
39
|
"browser": "dist/index.umd.js",
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
40
41
|
"unpkg": "dist/index.umd.js",
|
|
41
42
|
"exports": {
|
|
42
43
|
".": "./dist/index.js",
|
|
@@ -56,8 +57,8 @@
|
|
|
56
57
|
"prepublishOnly": "vp run build"
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
59
|
-
"vanilla-signal": "
|
|
60
|
-
"vanilla-signal-i18n": "1.1.
|
|
60
|
+
"vanilla-signal": "latest",
|
|
61
|
+
"vanilla-signal-i18n": "^1.1.1"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"@tsdown/css": "^0.22.2",
|
|
@@ -0,0 +1,522 @@
|
|
|
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
|
+
isRenderableContent,
|
|
25
|
+
normalizeContentNodes,
|
|
26
|
+
requireContainer,
|
|
27
|
+
} from '../utilities/dom.ts';
|
|
28
|
+
import { icon } from './icons.ts';
|
|
29
|
+
|
|
30
|
+
export type AccordionActive = number | string | Array<number | string> | null;
|
|
31
|
+
|
|
32
|
+
export interface AccordionClassNames {
|
|
33
|
+
root: string;
|
|
34
|
+
header: string;
|
|
35
|
+
title: string;
|
|
36
|
+
arrow: string;
|
|
37
|
+
panel: string;
|
|
38
|
+
content: string;
|
|
39
|
+
active: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type AccordionClassNameConfig = Partial<AccordionClassNames>;
|
|
43
|
+
|
|
44
|
+
export interface AccordionItem extends Record<string, unknown> {
|
|
45
|
+
name?: string;
|
|
46
|
+
title: RenderableContent<AccordionContentContext>;
|
|
47
|
+
content: RenderableContent<AccordionContentContext>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AccordionProps extends Record<string, unknown> {
|
|
51
|
+
id?: string | null;
|
|
52
|
+
active?: AccordionActive;
|
|
53
|
+
collapsible?: boolean;
|
|
54
|
+
multiple?: boolean;
|
|
55
|
+
className?: AccordionClassNameConfig;
|
|
56
|
+
items?: AccordionItem[];
|
|
57
|
+
onChange?:
|
|
58
|
+
| ((
|
|
59
|
+
index: number,
|
|
60
|
+
name: string,
|
|
61
|
+
header: HTMLElement,
|
|
62
|
+
panel: HTMLElement,
|
|
63
|
+
accordion: Accordion
|
|
64
|
+
) => void | Promise<void>)
|
|
65
|
+
| null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ResolvedAccordionProps extends Record<string, unknown> {
|
|
69
|
+
id: string;
|
|
70
|
+
active: AccordionActive;
|
|
71
|
+
collapsible: boolean;
|
|
72
|
+
multiple: boolean;
|
|
73
|
+
className: AccordionClassNames;
|
|
74
|
+
items: AccordionItem[];
|
|
75
|
+
onChange: NonNullable<AccordionProps['onChange']> | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface AccordionState extends Record<string, unknown> {
|
|
79
|
+
activeNames: string[];
|
|
80
|
+
current: {
|
|
81
|
+
index: number | null;
|
|
82
|
+
name: string | null;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface AccordionDOM extends ComponentDOM {
|
|
87
|
+
root: HTMLElement | null;
|
|
88
|
+
container: Element;
|
|
89
|
+
headers: HTMLElement[];
|
|
90
|
+
panels: HTMLElement[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface AccordionRuntime extends ComponentRuntime {}
|
|
94
|
+
|
|
95
|
+
export interface AccordionContentContext {
|
|
96
|
+
accordion: Accordion;
|
|
97
|
+
item: AccordionItem;
|
|
98
|
+
index: number;
|
|
99
|
+
type: 'title' | 'content';
|
|
100
|
+
active: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const DEFAULT_CLASS_NAMES: AccordionClassNames = {
|
|
104
|
+
root: 'j-accordion',
|
|
105
|
+
header: 'accordion-header',
|
|
106
|
+
title: 'header-title',
|
|
107
|
+
arrow: 'header-arrow',
|
|
108
|
+
panel: 'accordion-panel',
|
|
109
|
+
content: 'panel-content',
|
|
110
|
+
active: 'is-active',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const ACCORDION_ITEMS_RULE = {
|
|
114
|
+
type: 'array',
|
|
115
|
+
validate: (value: unknown) => Array.isArray(value) && value.length > 0,
|
|
116
|
+
message: 'expects a non-empty array.',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const ACCORDION_ACTIVE_RULE = {
|
|
120
|
+
types: ['number', 'string', 'array', 'null'],
|
|
121
|
+
validate: (value: unknown) => {
|
|
122
|
+
if (value == null) return true;
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
return value.every(
|
|
125
|
+
(item) => typeof item === 'number' || typeof item === 'string'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (typeof value === 'number') return Number.isInteger(value) && value >= 0;
|
|
129
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
130
|
+
},
|
|
131
|
+
message: 'expects a positive number, string, array or null.',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const ACCORDION_PROPS_SCHEMA = {
|
|
135
|
+
id: {
|
|
136
|
+
default: null,
|
|
137
|
+
types: ['string', 'null'],
|
|
138
|
+
normalize: (value: unknown) => {
|
|
139
|
+
if (typeof value === 'string') {
|
|
140
|
+
const id = value.trim();
|
|
141
|
+
return id || randomId();
|
|
142
|
+
}
|
|
143
|
+
if (value == null) return randomId();
|
|
144
|
+
return value;
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
active: { default: 0, ...ACCORDION_ACTIVE_RULE },
|
|
148
|
+
collapsible: { default: false, type: 'boolean' },
|
|
149
|
+
multiple: { default: false, type: 'boolean' },
|
|
150
|
+
className: {
|
|
151
|
+
default: DEFAULT_CLASS_NAMES,
|
|
152
|
+
type: 'object',
|
|
153
|
+
normalize: (value: unknown) => ({
|
|
154
|
+
...DEFAULT_CLASS_NAMES,
|
|
155
|
+
...(value && typeof value === 'object' ? value : {}),
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
onChange: { default: null, types: ['function', 'null'] },
|
|
159
|
+
items: { default: [], type: 'array' },
|
|
160
|
+
} satisfies ResolveSchema<AccordionProps>;
|
|
161
|
+
|
|
162
|
+
function cloneItems(items: unknown): AccordionItem[] {
|
|
163
|
+
return Array.isArray(items)
|
|
164
|
+
? items.map((item) => ({ ...(item as AccordionItem) }))
|
|
165
|
+
: [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeProps(input: AccordionProps): ResolvedAccordionProps {
|
|
169
|
+
const props = resolveProps(input, ACCORDION_PROPS_SCHEMA, 'Accordion');
|
|
170
|
+
return {
|
|
171
|
+
id: props.id as string,
|
|
172
|
+
active: props.active as AccordionActive,
|
|
173
|
+
collapsible: props.collapsible as boolean,
|
|
174
|
+
multiple: props.multiple as boolean,
|
|
175
|
+
className: props.className as AccordionClassNames,
|
|
176
|
+
items: cloneItems(props.items),
|
|
177
|
+
onChange: props.onChange as ResolvedAccordionProps['onChange'],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function normalizeItems(items: AccordionItem[]): AccordionItem[] {
|
|
182
|
+
validateParam('items', items, ACCORDION_ITEMS_RULE, 'Accordion');
|
|
183
|
+
|
|
184
|
+
return items.map((item) => {
|
|
185
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
186
|
+
throw new Error('Accordion: item expects an object.');
|
|
187
|
+
}
|
|
188
|
+
if (item.name != null && typeof item.name !== 'string') {
|
|
189
|
+
throw new Error('Accordion: item name expects a string.');
|
|
190
|
+
}
|
|
191
|
+
if (!isRenderableContent(item.title)) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
'Accordion: item title expects string, Node, array, function or null.'
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (!isRenderableContent(item.content)) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
'Accordion: item content expects string, Node, array, function or null.'
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return { ...item, name: item.name || randomId() };
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 轻量手风琴组件,继承 Component。
|
|
207
|
+
*
|
|
208
|
+
* DOM 创建一次,通过 createEffect 细粒度更新 class/ARIA。
|
|
209
|
+
*/
|
|
210
|
+
export class Accordion extends Component<
|
|
211
|
+
ResolvedAccordionProps,
|
|
212
|
+
AccordionState,
|
|
213
|
+
AccordionDOM
|
|
214
|
+
> {
|
|
215
|
+
declare runtime: AccordionRuntime;
|
|
216
|
+
declare state: AccordionState;
|
|
217
|
+
private bindingsDispose: (() => void) | null;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @param {Element|Node|string|Array} container 挂载容器(元素、选择器或 JSX/h 返回节点)。
|
|
221
|
+
* @param {object} [input={}] 手风琴配置。
|
|
222
|
+
*/
|
|
223
|
+
constructor(container: DOMReference, input: AccordionProps = {}) {
|
|
224
|
+
const el = requireContainer(container, 'Accordion');
|
|
225
|
+
const props = normalizeProps(input);
|
|
226
|
+
super(props);
|
|
227
|
+
|
|
228
|
+
this.dom.container = el;
|
|
229
|
+
this.dom.headers = [];
|
|
230
|
+
this.dom.panels = [];
|
|
231
|
+
this.bindingsDispose = null;
|
|
232
|
+
|
|
233
|
+
this.state = createDeepStore({
|
|
234
|
+
activeNames: [],
|
|
235
|
+
current: { index: null, name: null },
|
|
236
|
+
}) as AccordionState;
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
this.onInit(props);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
this.destroy();
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
protected onInit(props: ResolvedAccordionProps): void {
|
|
247
|
+
this.root = this.buildRoot(props);
|
|
248
|
+
this.buildItems(props);
|
|
249
|
+
this.syncActiveNames(this.resolveActiveNames(props.active));
|
|
250
|
+
this.bindEvents();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private buildRoot(props: ResolvedAccordionProps): HTMLElement {
|
|
254
|
+
return jsx('div', {
|
|
255
|
+
className: props.className.root,
|
|
256
|
+
id: props.id,
|
|
257
|
+
'data-accordion': 'root',
|
|
258
|
+
}) as HTMLElement;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private buildItems(props: ResolvedAccordionProps): void {
|
|
262
|
+
const items = normalizeItems(props.items);
|
|
263
|
+
const fragment = document.createDocumentFragment();
|
|
264
|
+
|
|
265
|
+
this.dom.headers = [];
|
|
266
|
+
this.dom.panels = [];
|
|
267
|
+
|
|
268
|
+
items.forEach((item, index) => {
|
|
269
|
+
const name = item.name || randomId();
|
|
270
|
+
const headerId = `${props.id}_header_${index}`;
|
|
271
|
+
const panelId = `${props.id}_panel_${index}`;
|
|
272
|
+
|
|
273
|
+
const header = jsx('div', {
|
|
274
|
+
className: props.className.header,
|
|
275
|
+
'data-accordion-header': name,
|
|
276
|
+
id: headerId,
|
|
277
|
+
role: 'button',
|
|
278
|
+
tabindex: '0',
|
|
279
|
+
'aria-controls': panelId,
|
|
280
|
+
children: [
|
|
281
|
+
jsx('span', {
|
|
282
|
+
className: props.className.title,
|
|
283
|
+
'data-accordion-title': name,
|
|
284
|
+
children: this.contentView(item, index, 'title'),
|
|
285
|
+
}),
|
|
286
|
+
jsx('span', {
|
|
287
|
+
className: props.className.arrow,
|
|
288
|
+
'data-accordion-arrow': name,
|
|
289
|
+
'aria-hidden': 'true',
|
|
290
|
+
children: icon('arrow-down'),
|
|
291
|
+
}),
|
|
292
|
+
],
|
|
293
|
+
}) as HTMLElement;
|
|
294
|
+
|
|
295
|
+
const panel = jsx('div', {
|
|
296
|
+
className: props.className.panel,
|
|
297
|
+
'data-accordion-panel': name,
|
|
298
|
+
id: panelId,
|
|
299
|
+
role: 'region',
|
|
300
|
+
'aria-labelledby': headerId,
|
|
301
|
+
children: jsx('div', {
|
|
302
|
+
className: props.className.content,
|
|
303
|
+
'data-accordion-content': name,
|
|
304
|
+
children: this.contentView(item, index, 'content'),
|
|
305
|
+
}),
|
|
306
|
+
}) as HTMLElement;
|
|
307
|
+
|
|
308
|
+
this.dom.headers.push(header);
|
|
309
|
+
this.dom.panels.push(panel);
|
|
310
|
+
fragment.append(header, panel);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
this.root?.append(fragment);
|
|
314
|
+
|
|
315
|
+
this.bindingsDispose?.();
|
|
316
|
+
this.bindingsDispose = createRoot((dispose) => {
|
|
317
|
+
this.dom.headers.forEach((header) => {
|
|
318
|
+
const name = header.dataset.accordionHeader || '';
|
|
319
|
+
bindClass(header, this.props.className.active, () =>
|
|
320
|
+
this.state.activeNames.includes(name)
|
|
321
|
+
);
|
|
322
|
+
bindAttr(header, 'aria-expanded', () =>
|
|
323
|
+
this.state.activeNames.includes(name)
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
this.dom.panels.forEach((panel, i) => {
|
|
327
|
+
const name = this.dom.headers[i]?.dataset.accordionHeader || '';
|
|
328
|
+
bindClass(panel, this.props.className.active, () =>
|
|
329
|
+
this.state.activeNames.includes(name)
|
|
330
|
+
);
|
|
331
|
+
bindAttr(panel, 'aria-hidden', () =>
|
|
332
|
+
this.state.activeNames.includes(name) ? 'false' : 'true'
|
|
333
|
+
);
|
|
334
|
+
bindAttr(panel, 'hidden', () => !this.state.activeNames.includes(name));
|
|
335
|
+
});
|
|
336
|
+
return dispose;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private contentView(
|
|
341
|
+
item: AccordionItem,
|
|
342
|
+
index: number,
|
|
343
|
+
type: 'title' | 'content'
|
|
344
|
+
): Node[] {
|
|
345
|
+
return normalizeContentNodes(type === 'title' ? item.title : item.content, {
|
|
346
|
+
accordion: this,
|
|
347
|
+
item,
|
|
348
|
+
index,
|
|
349
|
+
type,
|
|
350
|
+
active: false,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private resolveActiveNames(active: AccordionActive): string[] {
|
|
355
|
+
if (active == null) return [];
|
|
356
|
+
const values = Array.isArray(active) ? active : [active];
|
|
357
|
+
const names: string[] = [];
|
|
358
|
+
for (const value of values) {
|
|
359
|
+
const index = this.getIndex(value);
|
|
360
|
+
if (index < 0 || index >= this.dom.headers.length) continue;
|
|
361
|
+
names.push(
|
|
362
|
+
this.dom.headers[index].dataset.accordionHeader || String(index)
|
|
363
|
+
);
|
|
364
|
+
if (!this.props.multiple) break;
|
|
365
|
+
}
|
|
366
|
+
return Array.from(new Set(names));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private syncActiveNames(names: string[]): void {
|
|
370
|
+
const firstName = names[0] || null;
|
|
371
|
+
const index = firstName ? this.getIndex(firstName) : null;
|
|
372
|
+
flushSync(() => {
|
|
373
|
+
this.state.activeNames = names;
|
|
374
|
+
this.state.current = { index, name: firstName };
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private bindEvents(): void {
|
|
379
|
+
this.unbindEvents();
|
|
380
|
+
if (!this.root) return;
|
|
381
|
+
|
|
382
|
+
this.cleanup.events.on('click', this.root, 'click', (event) => {
|
|
383
|
+
if (!(event.target instanceof Element)) return;
|
|
384
|
+
const header = event.target.closest<HTMLElement>(
|
|
385
|
+
'[data-accordion-header]'
|
|
386
|
+
);
|
|
387
|
+
if (!header || !this.root?.contains(header)) return;
|
|
388
|
+
void this.active(header.dataset.accordionHeader);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
this.cleanup.events.on('keydown', this.root, 'keydown', (event) => {
|
|
392
|
+
if (!(event instanceof KeyboardEvent)) return;
|
|
393
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
394
|
+
if (!(event.target instanceof Element)) return;
|
|
395
|
+
const header = event.target.closest<HTMLElement>(
|
|
396
|
+
'[data-accordion-header]'
|
|
397
|
+
);
|
|
398
|
+
if (!header || !this.root?.contains(header)) return;
|
|
399
|
+
event.preventDefault();
|
|
400
|
+
void this.active(header.dataset.accordionHeader);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private unbindEvents(): void {
|
|
405
|
+
this.cleanup.events.clear();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
isActive(name: string): boolean {
|
|
409
|
+
return this.state.activeNames.includes(name);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private assertActive(method: string): void {
|
|
413
|
+
if (this.runtime.destroyed) {
|
|
414
|
+
throw new Error(`Accordion.${method}: instance has been destroyed.`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private async activateItem(
|
|
419
|
+
val: number | string | undefined,
|
|
420
|
+
fireEvent = true
|
|
421
|
+
): Promise<void> {
|
|
422
|
+
const index = this.getIndex(val);
|
|
423
|
+
if (index < 0 || index >= this.dom.headers.length) return;
|
|
424
|
+
|
|
425
|
+
const headerEl = this.dom.headers[index];
|
|
426
|
+
const panelEl = this.dom.panels[index];
|
|
427
|
+
const name = headerEl.dataset.accordionHeader || String(index);
|
|
428
|
+
const active = this.isActive(name);
|
|
429
|
+
|
|
430
|
+
if (active && !this.props.multiple && !this.props.collapsible) return;
|
|
431
|
+
|
|
432
|
+
let nextNames: string[];
|
|
433
|
+
if (this.props.multiple) {
|
|
434
|
+
nextNames = active
|
|
435
|
+
? this.state.activeNames.filter((n) => n !== name)
|
|
436
|
+
: [...this.state.activeNames, name];
|
|
437
|
+
} else if (active) {
|
|
438
|
+
nextNames = this.props.collapsible ? [] : this.state.activeNames;
|
|
439
|
+
} else {
|
|
440
|
+
nextNames = [name];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
this.syncActiveNames(nextNames);
|
|
444
|
+
|
|
445
|
+
if (fireEvent && this.props.onChange) {
|
|
446
|
+
await Promise.resolve(
|
|
447
|
+
this.props.onChange(index, name, headerEl, panelEl, this)
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
getIndex(val: number | string | undefined | null): number {
|
|
453
|
+
if (typeof val === 'number') return val;
|
|
454
|
+
if (typeof val === 'string') {
|
|
455
|
+
return this.dom.headers.findIndex(
|
|
456
|
+
(header) => header.dataset.accordionHeader === val
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return -1;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* 激活指定面板。
|
|
464
|
+
* @param {number|string} val 面板索引或名称。
|
|
465
|
+
*/
|
|
466
|
+
async active(val: number | string | undefined): Promise<void> {
|
|
467
|
+
this.assertActive('active');
|
|
468
|
+
await this.activateItem(val, true);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* 将组件挂载到构造器指定的容器中。
|
|
473
|
+
*/
|
|
474
|
+
build(): this {
|
|
475
|
+
this.assertActive('build');
|
|
476
|
+
insert(this.dom.container, () => this.root);
|
|
477
|
+
return this;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
render(): this {
|
|
481
|
+
return this.build();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 动态替换全部面板条目。
|
|
486
|
+
* @param {AccordionItem[]} items 新面板配置。
|
|
487
|
+
* @param {number|string|Array<number|string>|null} [active=0] 替换后默认激活项。
|
|
488
|
+
*/
|
|
489
|
+
setItems(items: AccordionItem[], active: AccordionActive = 0): this {
|
|
490
|
+
this.assertActive('setItems');
|
|
491
|
+
validateParam('items', items, ACCORDION_ITEMS_RULE, 'Accordion.setItems');
|
|
492
|
+
validateParam(
|
|
493
|
+
'active',
|
|
494
|
+
active,
|
|
495
|
+
ACCORDION_ACTIVE_RULE,
|
|
496
|
+
'Accordion.setItems'
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
this.props.items = cloneItems(normalizeItems(items));
|
|
500
|
+
this.props.active = active;
|
|
501
|
+
|
|
502
|
+
if (this.root) this.root.textContent = '';
|
|
503
|
+
this.buildItems(this.props);
|
|
504
|
+
this.syncActiveNames(this.resolveActiveNames(active));
|
|
505
|
+
this.bindEvents();
|
|
506
|
+
return this;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
protected onDestroy(): void {
|
|
510
|
+
this.unbindEvents();
|
|
511
|
+
this.bindingsDispose?.();
|
|
512
|
+
this.bindingsDispose = null;
|
|
513
|
+
if (this.root?.parentNode) this.root.parentNode.removeChild(this.root);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function createAccordion(
|
|
518
|
+
container: DOMReference,
|
|
519
|
+
input: AccordionProps = {}
|
|
520
|
+
): Accordion {
|
|
521
|
+
return new Accordion(container, input);
|
|
522
|
+
}
|