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
|
@@ -1,34 +1,32 @@
|
|
|
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 { icon } from '../primitives/icons.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
|
-
isRenderableContent,
|
|
24
18
|
normalizeContentNodes,
|
|
25
|
-
requireContainer,
|
|
26
19
|
} from '../utilities/dom.ts';
|
|
27
|
-
import {
|
|
20
|
+
import { randomId } from '../utilities/id.ts';
|
|
21
|
+
import {
|
|
22
|
+
type ResolveSchema,
|
|
23
|
+
resolveProps,
|
|
24
|
+
validateParam,
|
|
25
|
+
} from '../utilities/types.ts';
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
type AccordionActive = number | string | Array<number | string> | null;
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
interface AccordionClassNames {
|
|
32
30
|
root: string;
|
|
33
31
|
header: string;
|
|
34
32
|
title: string;
|
|
@@ -37,28 +35,28 @@ export interface AccordionClassNames {
|
|
|
37
35
|
content: string;
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
type AccordionClassNameConfig = Partial<AccordionClassNames>;
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
interface AccordionItem extends Record<string, unknown> {
|
|
43
41
|
name?: string;
|
|
44
42
|
title: RenderableContent<AccordionContentContext>;
|
|
45
43
|
content: RenderableContent<AccordionContentContext>;
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
interface AccordionProps extends Record<string, unknown> {
|
|
49
47
|
id?: string | null;
|
|
50
48
|
active?: AccordionActive;
|
|
51
49
|
collapsible?: boolean;
|
|
52
50
|
multiple?: boolean;
|
|
53
51
|
className?: AccordionClassNameConfig;
|
|
54
|
-
items
|
|
52
|
+
items: AccordionItem[];
|
|
55
53
|
onChange?:
|
|
56
54
|
| ((
|
|
57
55
|
index: number,
|
|
58
56
|
name: string,
|
|
59
57
|
header: HTMLElement,
|
|
60
58
|
panel: HTMLElement,
|
|
61
|
-
accordion:
|
|
59
|
+
accordion: AccordionInstance
|
|
62
60
|
) => void | Promise<void>)
|
|
63
61
|
| null;
|
|
64
62
|
}
|
|
@@ -73,31 +71,48 @@ interface ResolvedAccordionProps extends Record<string, unknown> {
|
|
|
73
71
|
onChange: NonNullable<AccordionProps['onChange']> | null;
|
|
74
72
|
}
|
|
75
73
|
|
|
74
|
+
interface AccordionCurrent {
|
|
75
|
+
index: number | null;
|
|
76
|
+
name: string | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
76
79
|
interface AccordionState extends Record<string, unknown> {
|
|
80
|
+
items: AccordionItem[];
|
|
77
81
|
activeNames: string[];
|
|
78
|
-
current:
|
|
79
|
-
index: number | null;
|
|
80
|
-
name: string | null;
|
|
81
|
-
};
|
|
82
|
+
current: AccordionCurrent;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
interface AccordionDOM extends ComponentDOM {
|
|
85
86
|
root: HTMLElement | null;
|
|
86
|
-
container: Element;
|
|
87
87
|
headers: HTMLElement[];
|
|
88
88
|
panels: HTMLElement[];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
interface AccordionRuntime extends ComponentRuntime {
|
|
91
|
+
interface AccordionRuntime extends ComponentRuntime {
|
|
92
|
+
built: boolean;
|
|
93
|
+
}
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
accordion:
|
|
95
|
+
interface AccordionContentContext {
|
|
96
|
+
accordion: AccordionInstance;
|
|
95
97
|
item: AccordionItem;
|
|
96
98
|
index: number;
|
|
97
99
|
type: 'title' | 'content';
|
|
98
100
|
active: boolean;
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
type AccordionInstance = Component<
|
|
104
|
+
ResolvedAccordionProps,
|
|
105
|
+
AccordionState,
|
|
106
|
+
AccordionDOM
|
|
107
|
+
> & {
|
|
108
|
+
runtime: AccordionRuntime;
|
|
109
|
+
state: AccordionState;
|
|
110
|
+
build(): AccordionInstance;
|
|
111
|
+
isActive(name: string): boolean;
|
|
112
|
+
getIndex(value: number | string | undefined | null): number;
|
|
113
|
+
activate(value: number | string | undefined): Promise<void>;
|
|
114
|
+
};
|
|
115
|
+
|
|
101
116
|
const DEFAULT_CLASS_NAMES: AccordionClassNames = {
|
|
102
117
|
root: 'j-accordion',
|
|
103
118
|
header: 'accordion-header',
|
|
@@ -107,25 +122,29 @@ const DEFAULT_CLASS_NAMES: AccordionClassNames = {
|
|
|
107
122
|
content: 'panel-content',
|
|
108
123
|
};
|
|
109
124
|
|
|
125
|
+
const ACCORDION_ITEM_RULE = {
|
|
126
|
+
type: 'plainObject',
|
|
127
|
+
shape: {
|
|
128
|
+
name: ['string', 'null', 'undefined'],
|
|
129
|
+
title: 'renderable',
|
|
130
|
+
content: 'renderable',
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
110
134
|
const ACCORDION_ITEMS_RULE = {
|
|
111
135
|
type: 'array',
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
nonEmpty: true,
|
|
137
|
+
items: ACCORDION_ITEM_RULE,
|
|
114
138
|
};
|
|
115
139
|
|
|
116
140
|
const ACCORDION_ACTIVE_RULE = {
|
|
117
141
|
types: ['number', 'string', 'array', 'null'],
|
|
118
142
|
validate: (value: unknown) => {
|
|
119
143
|
if (value == null) return true;
|
|
120
|
-
if (Array.isArray(value))
|
|
121
|
-
|
|
122
|
-
(item) => typeof item === 'number' || typeof item === 'string'
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
if (typeof value === 'number') return Number.isInteger(value) && value >= 0;
|
|
126
|
-
return typeof value === 'string' && value.trim().length > 0;
|
|
144
|
+
if (Array.isArray(value)) return value.every(isActiveValue);
|
|
145
|
+
return isActiveValue(value);
|
|
127
146
|
},
|
|
128
|
-
message: 'expects a
|
|
147
|
+
message: 'expects a non-negative integer, non-empty string, array or null.',
|
|
129
148
|
};
|
|
130
149
|
|
|
131
150
|
const ACCORDION_PROPS_SCHEMA = {
|
|
@@ -153,58 +172,116 @@ const ACCORDION_PROPS_SCHEMA = {
|
|
|
153
172
|
}),
|
|
154
173
|
},
|
|
155
174
|
onChange: { default: null, types: ['function', 'null'] },
|
|
156
|
-
items: { default: [],
|
|
175
|
+
items: { default: [], ...ACCORDION_ITEMS_RULE },
|
|
157
176
|
} satisfies ResolveSchema<AccordionProps>;
|
|
158
177
|
|
|
159
|
-
function
|
|
160
|
-
return
|
|
161
|
-
|
|
162
|
-
: [];
|
|
178
|
+
function isActiveValue(value: unknown): value is number | string {
|
|
179
|
+
if (typeof value === 'number') return Number.isInteger(value) && value >= 0;
|
|
180
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
function normalizeProps(
|
|
166
|
-
const
|
|
183
|
+
function normalizeProps(props: AccordionProps): ResolvedAccordionProps {
|
|
184
|
+
const resolved = resolveProps(
|
|
185
|
+
props,
|
|
186
|
+
ACCORDION_PROPS_SCHEMA,
|
|
187
|
+
'Accordion.props'
|
|
188
|
+
);
|
|
167
189
|
return {
|
|
168
|
-
id:
|
|
169
|
-
active:
|
|
170
|
-
collapsible:
|
|
171
|
-
multiple:
|
|
172
|
-
className:
|
|
173
|
-
items:
|
|
174
|
-
onChange:
|
|
190
|
+
id: resolved.id as string,
|
|
191
|
+
active: resolved.active as AccordionActive,
|
|
192
|
+
collapsible: resolved.collapsible as boolean,
|
|
193
|
+
multiple: resolved.multiple as boolean,
|
|
194
|
+
className: resolved.className as AccordionClassNames,
|
|
195
|
+
items: normalizeItems(resolved.items, false),
|
|
196
|
+
onChange: resolved.onChange as ResolvedAccordionProps['onChange'],
|
|
175
197
|
};
|
|
176
198
|
}
|
|
177
199
|
|
|
178
|
-
function normalizeItems(items:
|
|
179
|
-
|
|
200
|
+
function normalizeItems(items: unknown, validate = true): AccordionItem[] {
|
|
201
|
+
if (validate) {
|
|
202
|
+
validateParam('items', items, ACCORDION_ITEMS_RULE, 'Accordion');
|
|
203
|
+
}
|
|
180
204
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
if (!isRenderableContent(item.content)) {
|
|
194
|
-
throw new Error(
|
|
195
|
-
'Accordion: item content expects string, Node, array, function or null.'
|
|
196
|
-
);
|
|
205
|
+
const names = new Set<string>();
|
|
206
|
+
|
|
207
|
+
return (items as unknown[]).map((item) => {
|
|
208
|
+
const source = item as AccordionItem;
|
|
209
|
+
const name = typeof source.name === 'string' ? source.name.trim() : '';
|
|
210
|
+
if (name) {
|
|
211
|
+
if (names.has(name)) {
|
|
212
|
+
throw new Error(`Accordion: item name "${name}" must be unique.`);
|
|
213
|
+
}
|
|
214
|
+
names.add(name);
|
|
197
215
|
}
|
|
198
|
-
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
...source,
|
|
219
|
+
name: name || randomId(),
|
|
220
|
+
};
|
|
199
221
|
});
|
|
200
222
|
}
|
|
201
223
|
|
|
224
|
+
function needsItemsSync(
|
|
225
|
+
source: AccordionItem[],
|
|
226
|
+
normalized: AccordionItem[]
|
|
227
|
+
): boolean {
|
|
228
|
+
return normalized.some((item, index) => source[index]?.name !== item.name);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveActiveItemNames(
|
|
232
|
+
active: AccordionActive,
|
|
233
|
+
items: AccordionItem[],
|
|
234
|
+
multiple: boolean
|
|
235
|
+
): string[] {
|
|
236
|
+
if (active == null) return [];
|
|
237
|
+
|
|
238
|
+
const values = Array.isArray(active) ? active : [active];
|
|
239
|
+
const names: string[] = [];
|
|
240
|
+
|
|
241
|
+
for (const value of values) {
|
|
242
|
+
const index =
|
|
243
|
+
typeof value === 'number'
|
|
244
|
+
? value
|
|
245
|
+
: items.findIndex((item) => item.name === value);
|
|
246
|
+
const name = items[index]?.name;
|
|
247
|
+
|
|
248
|
+
if (!name) continue;
|
|
249
|
+
|
|
250
|
+
names.push(name);
|
|
251
|
+
if (!multiple) break;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return Array.from(new Set(names));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function reconcileActiveNames(
|
|
258
|
+
activeNames: string[],
|
|
259
|
+
items: AccordionItem[],
|
|
260
|
+
multiple: boolean
|
|
261
|
+
): string[] {
|
|
262
|
+
const names = new Set(items.map((item) => item.name).filter(Boolean));
|
|
263
|
+
const nextNames = activeNames.filter((name) => names.has(name));
|
|
264
|
+
return multiple ? nextNames : nextNames.slice(0, 1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function createCurrentState(
|
|
268
|
+
activeNames: string[],
|
|
269
|
+
items: AccordionItem[]
|
|
270
|
+
): AccordionCurrent {
|
|
271
|
+
const name = activeNames[0] || null;
|
|
272
|
+
if (!name) return { index: null, name: null };
|
|
273
|
+
|
|
274
|
+
const index = items.findIndex((item) => item.name === name);
|
|
275
|
+
return index >= 0 ? { index, name } : { index: null, name: null };
|
|
276
|
+
}
|
|
277
|
+
|
|
202
278
|
/**
|
|
203
279
|
* 轻量手风琴组件,继承 Component。
|
|
204
280
|
*
|
|
205
|
-
*
|
|
281
|
+
* 构造器只验证和保存初始配置;调用 build() 后才创建 DOM 和绑定交互。
|
|
282
|
+
* items 属于响应式 state,变更后自动重建面板结构。
|
|
206
283
|
*/
|
|
207
|
-
|
|
284
|
+
class Accordion extends Component<
|
|
208
285
|
ResolvedAccordionProps,
|
|
209
286
|
AccordionState,
|
|
210
287
|
AccordionDOM
|
|
@@ -212,38 +289,33 @@ export class Accordion extends Component<
|
|
|
212
289
|
declare runtime: AccordionRuntime;
|
|
213
290
|
declare state: AccordionState;
|
|
214
291
|
private bindingsDispose: (() => void) | null;
|
|
292
|
+
private itemsDispose: (() => void) | null;
|
|
215
293
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
294
|
+
constructor(props: AccordionProps) {
|
|
295
|
+
const resolvedProps = normalizeProps(props);
|
|
296
|
+
super(resolvedProps);
|
|
297
|
+
|
|
298
|
+
const activeNames = resolveActiveItemNames(
|
|
299
|
+
resolvedProps.active,
|
|
300
|
+
resolvedProps.items,
|
|
301
|
+
resolvedProps.multiple
|
|
302
|
+
);
|
|
224
303
|
|
|
225
|
-
this.dom.container = el;
|
|
226
304
|
this.dom.headers = [];
|
|
227
305
|
this.dom.panels = [];
|
|
228
306
|
this.bindingsDispose = null;
|
|
229
|
-
|
|
307
|
+
this.itemsDispose = null;
|
|
308
|
+
this.runtime.built = false;
|
|
230
309
|
this.state = createDeepStore({
|
|
231
|
-
|
|
232
|
-
|
|
310
|
+
items: resolvedProps.items,
|
|
311
|
+
activeNames,
|
|
312
|
+
current: createCurrentState(activeNames, resolvedProps.items),
|
|
233
313
|
}) as AccordionState;
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
this.onInit(props);
|
|
237
|
-
} catch (error) {
|
|
238
|
-
this.destroy();
|
|
239
|
-
throw error;
|
|
240
|
-
}
|
|
241
314
|
}
|
|
242
315
|
|
|
243
316
|
protected onInit(props: ResolvedAccordionProps): void {
|
|
244
|
-
this.root = this.buildRoot(props);
|
|
245
|
-
this.
|
|
246
|
-
this.syncActiveNames(this.resolveActiveNames(props.active));
|
|
317
|
+
this.dom.root = this.buildRoot(props);
|
|
318
|
+
this.bindItems();
|
|
247
319
|
this.bindEvents();
|
|
248
320
|
}
|
|
249
321
|
|
|
@@ -255,33 +327,64 @@ export class Accordion extends Component<
|
|
|
255
327
|
}) as HTMLElement;
|
|
256
328
|
}
|
|
257
329
|
|
|
258
|
-
private
|
|
259
|
-
|
|
260
|
-
|
|
330
|
+
private bindItems(): void {
|
|
331
|
+
this.itemsDispose?.();
|
|
332
|
+
this.itemsDispose = createRoot((dispose) => {
|
|
333
|
+
createEffect(() => {
|
|
334
|
+
const sourceItems = this.state.items;
|
|
335
|
+
const items = normalizeItems(sourceItems);
|
|
336
|
+
|
|
337
|
+
untrack(() => {
|
|
338
|
+
if (needsItemsSync(sourceItems, items)) {
|
|
339
|
+
this.state.items = items;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const activeNames = reconcileActiveNames(
|
|
343
|
+
this.state.activeNames,
|
|
344
|
+
items,
|
|
345
|
+
this.props.multiple
|
|
346
|
+
);
|
|
347
|
+
this.buildItems(items, activeNames);
|
|
348
|
+
this.syncActiveNames(activeNames, items);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return dispose;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
261
355
|
|
|
356
|
+
private buildItems(items: AccordionItem[], activeNames: string[]): void {
|
|
357
|
+
if (!this.dom.root) return;
|
|
358
|
+
|
|
359
|
+
const fragment = document.createDocumentFragment();
|
|
360
|
+
this.bindingsDispose?.();
|
|
361
|
+
this.bindingsDispose = null;
|
|
362
|
+
this.dom.root.textContent = '';
|
|
262
363
|
this.dom.headers = [];
|
|
263
364
|
this.dom.panels = [];
|
|
264
365
|
|
|
265
366
|
items.forEach((item, index) => {
|
|
266
367
|
const name = item.name || randomId();
|
|
267
|
-
const
|
|
268
|
-
const
|
|
368
|
+
const active = activeNames.includes(name);
|
|
369
|
+
const headerId = `${this.props.id}_header_${index}`;
|
|
370
|
+
const panelId = `${this.props.id}_panel_${index}`;
|
|
269
371
|
|
|
270
372
|
const header = jsx('div', {
|
|
271
|
-
className: props.className.header,
|
|
272
|
-
'data-accordion-header': name,
|
|
373
|
+
className: this.props.className.header,
|
|
273
374
|
id: headerId,
|
|
375
|
+
'data-accordion-header': name,
|
|
274
376
|
role: 'button',
|
|
275
377
|
tabindex: '0',
|
|
276
378
|
'aria-controls': panelId,
|
|
277
379
|
children: [
|
|
278
380
|
jsx('span', {
|
|
279
|
-
className: props.className.title,
|
|
381
|
+
className: this.props.className.title,
|
|
280
382
|
'data-accordion-title': name,
|
|
281
|
-
|
|
383
|
+
role: 'heading',
|
|
384
|
+
children: this.contentView(item, index, 'title', active),
|
|
282
385
|
}),
|
|
283
386
|
jsx('span', {
|
|
284
|
-
className: props.className.arrow,
|
|
387
|
+
className: this.props.className.arrow,
|
|
285
388
|
'data-accordion-arrow': name,
|
|
286
389
|
'aria-hidden': 'true',
|
|
287
390
|
children: icon('arrow-down'),
|
|
@@ -290,15 +393,15 @@ export class Accordion extends Component<
|
|
|
290
393
|
}) as HTMLElement;
|
|
291
394
|
|
|
292
395
|
const panel = jsx('div', {
|
|
293
|
-
className: props.className.panel,
|
|
396
|
+
className: this.props.className.panel,
|
|
294
397
|
'data-accordion-panel': name,
|
|
295
398
|
id: panelId,
|
|
296
399
|
role: 'region',
|
|
297
400
|
'aria-labelledby': headerId,
|
|
298
401
|
children: jsx('div', {
|
|
299
|
-
className: props.className.content,
|
|
402
|
+
className: this.props.className.content,
|
|
300
403
|
'data-accordion-content': name,
|
|
301
|
-
children: this.contentView(item, index, 'content'),
|
|
404
|
+
children: this.contentView(item, index, 'content', active),
|
|
302
405
|
}),
|
|
303
406
|
}) as HTMLElement;
|
|
304
407
|
|
|
@@ -307,29 +410,27 @@ export class Accordion extends Component<
|
|
|
307
410
|
fragment.append(header, panel);
|
|
308
411
|
});
|
|
309
412
|
|
|
310
|
-
this.root
|
|
413
|
+
this.dom.root.append(fragment);
|
|
414
|
+
this.bindItemState();
|
|
415
|
+
}
|
|
311
416
|
|
|
312
|
-
|
|
417
|
+
private bindItemState(): void {
|
|
313
418
|
this.bindingsDispose = createRoot((dispose) => {
|
|
314
419
|
this.dom.headers.forEach((header) => {
|
|
315
420
|
const name = header.dataset.accordionHeader || '';
|
|
316
|
-
// bindClass(header, this.props.className.active, () =>
|
|
317
|
-
// this.state.activeNames.includes(name)
|
|
318
|
-
// );
|
|
319
421
|
bindAttr(header, 'aria-expanded', () =>
|
|
320
422
|
this.state.activeNames.includes(name) ? 'true' : 'false'
|
|
321
423
|
);
|
|
322
424
|
});
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// this.state.activeNames.includes(name)
|
|
327
|
-
// );
|
|
425
|
+
|
|
426
|
+
this.dom.panels.forEach((panel, index) => {
|
|
427
|
+
const name = this.dom.headers[index]?.dataset.accordionHeader || '';
|
|
328
428
|
bindAttr(panel, 'aria-hidden', () =>
|
|
329
429
|
this.state.activeNames.includes(name) ? 'false' : 'true'
|
|
330
430
|
);
|
|
331
431
|
bindAttr(panel, 'hidden', () => !this.state.activeNames.includes(name));
|
|
332
432
|
});
|
|
433
|
+
|
|
333
434
|
return dispose;
|
|
334
435
|
});
|
|
335
436
|
}
|
|
@@ -337,64 +438,48 @@ export class Accordion extends Component<
|
|
|
337
438
|
private contentView(
|
|
338
439
|
item: AccordionItem,
|
|
339
440
|
index: number,
|
|
340
|
-
type: 'title' | 'content'
|
|
441
|
+
type: 'title' | 'content',
|
|
442
|
+
active: boolean
|
|
341
443
|
): Node[] {
|
|
342
444
|
return normalizeContentNodes(type === 'title' ? item.title : item.content, {
|
|
343
445
|
accordion: this,
|
|
344
446
|
item,
|
|
345
447
|
index,
|
|
346
448
|
type,
|
|
347
|
-
active
|
|
449
|
+
active,
|
|
348
450
|
});
|
|
349
451
|
}
|
|
350
452
|
|
|
351
|
-
private
|
|
352
|
-
if (active == null) return [];
|
|
353
|
-
const values = Array.isArray(active) ? active : [active];
|
|
354
|
-
const names: string[] = [];
|
|
355
|
-
for (const value of values) {
|
|
356
|
-
const index = this.getIndex(value);
|
|
357
|
-
if (index < 0 || index >= this.dom.headers.length) continue;
|
|
358
|
-
names.push(
|
|
359
|
-
this.dom.headers[index].dataset.accordionHeader || String(index)
|
|
360
|
-
);
|
|
361
|
-
if (!this.props.multiple) break;
|
|
362
|
-
}
|
|
363
|
-
return Array.from(new Set(names));
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
private syncActiveNames(names: string[]): void {
|
|
367
|
-
const firstName = names[0] || null;
|
|
368
|
-
const index = firstName ? this.getIndex(firstName) : null;
|
|
453
|
+
private syncActiveNames(names: string[], items = this.state.items): void {
|
|
369
454
|
flushSync(() => {
|
|
370
455
|
this.state.activeNames = names;
|
|
371
|
-
this.state.current =
|
|
456
|
+
this.state.current = createCurrentState(names, items);
|
|
372
457
|
});
|
|
373
458
|
}
|
|
374
459
|
|
|
375
460
|
private bindEvents(): void {
|
|
376
461
|
this.unbindEvents();
|
|
377
|
-
if (!this.root) return;
|
|
462
|
+
if (!this.dom.root) return;
|
|
378
463
|
|
|
379
|
-
this.cleanup.events.on('click', this.root, 'click', (event) => {
|
|
464
|
+
this.cleanup.events.on('click', this.dom.root, 'click', (event) => {
|
|
380
465
|
if (!(event.target instanceof Element)) return;
|
|
381
466
|
const header = event.target.closest<HTMLElement>(
|
|
382
467
|
'[data-accordion-header]'
|
|
383
468
|
);
|
|
384
|
-
if (!header || !this.root?.contains(header)) return;
|
|
385
|
-
void this.
|
|
469
|
+
if (!header || !this.dom.root?.contains(header)) return;
|
|
470
|
+
void this.activate(header.dataset.accordionHeader);
|
|
386
471
|
});
|
|
387
472
|
|
|
388
|
-
this.cleanup.events.on('keydown', this.root, 'keydown', (event) => {
|
|
473
|
+
this.cleanup.events.on('keydown', this.dom.root, 'keydown', (event) => {
|
|
389
474
|
if (!(event instanceof KeyboardEvent)) return;
|
|
390
475
|
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
391
476
|
if (!(event.target instanceof Element)) return;
|
|
392
477
|
const header = event.target.closest<HTMLElement>(
|
|
393
478
|
'[data-accordion-header]'
|
|
394
479
|
);
|
|
395
|
-
if (!header || !this.root?.contains(header)) return;
|
|
480
|
+
if (!header || !this.dom.root?.contains(header)) return;
|
|
396
481
|
event.preventDefault();
|
|
397
|
-
void this.
|
|
482
|
+
void this.activate(header.dataset.accordionHeader);
|
|
398
483
|
});
|
|
399
484
|
}
|
|
400
485
|
|
|
@@ -406,114 +491,110 @@ export class Accordion extends Component<
|
|
|
406
491
|
return this.state.activeNames.includes(name);
|
|
407
492
|
}
|
|
408
493
|
|
|
409
|
-
|
|
410
|
-
if (
|
|
411
|
-
|
|
494
|
+
getIndex(val: number | string | undefined | null): number {
|
|
495
|
+
if (typeof val === 'number') return val;
|
|
496
|
+
if (typeof val === 'string') {
|
|
497
|
+
return this.dom.headers.findIndex(
|
|
498
|
+
(header) => header.dataset.accordionHeader === val
|
|
499
|
+
);
|
|
412
500
|
}
|
|
501
|
+
return -1;
|
|
413
502
|
}
|
|
414
503
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const index = this.getIndex(
|
|
504
|
+
async activate(value: number | string | undefined): Promise<void> {
|
|
505
|
+
this.assertActive('activate');
|
|
506
|
+
this.assertBuilt('activate');
|
|
507
|
+
|
|
508
|
+
const index = this.getIndex(value);
|
|
420
509
|
if (index < 0 || index >= this.dom.headers.length) return;
|
|
421
510
|
|
|
422
511
|
const headerEl = this.dom.headers[index];
|
|
423
512
|
const panelEl = this.dom.panels[index];
|
|
424
513
|
const name = headerEl.dataset.accordionHeader || String(index);
|
|
425
|
-
const
|
|
514
|
+
const isActive = this.isActive(name);
|
|
426
515
|
|
|
427
|
-
if (
|
|
516
|
+
if (isActive && !this.props.multiple && !this.props.collapsible) return;
|
|
428
517
|
|
|
429
|
-
let
|
|
518
|
+
let activeNames: string[];
|
|
430
519
|
if (this.props.multiple) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
520
|
+
if (isActive) {
|
|
521
|
+
const nextNames = this.state.activeNames.filter(
|
|
522
|
+
(item) => item !== name
|
|
523
|
+
);
|
|
524
|
+
activeNames =
|
|
525
|
+
!this.props.collapsible && nextNames.length === 0
|
|
526
|
+
? this.state.activeNames
|
|
527
|
+
: nextNames;
|
|
528
|
+
} else {
|
|
529
|
+
activeNames = [...this.state.activeNames, name];
|
|
530
|
+
}
|
|
531
|
+
} else if (isActive) {
|
|
532
|
+
activeNames = this.props.collapsible ? [] : this.state.activeNames;
|
|
436
533
|
} else {
|
|
437
|
-
|
|
534
|
+
activeNames = [name];
|
|
438
535
|
}
|
|
439
536
|
|
|
440
|
-
this.syncActiveNames(
|
|
537
|
+
this.syncActiveNames(activeNames);
|
|
441
538
|
|
|
442
|
-
if (
|
|
539
|
+
if (this.props.onChange) {
|
|
443
540
|
await Promise.resolve(
|
|
444
541
|
this.props.onChange(index, name, headerEl, panelEl, this)
|
|
445
542
|
);
|
|
446
543
|
}
|
|
447
544
|
}
|
|
448
545
|
|
|
449
|
-
getIndex(val: number | string | undefined | null): number {
|
|
450
|
-
if (typeof val === 'number') return val;
|
|
451
|
-
if (typeof val === 'string') {
|
|
452
|
-
return this.dom.headers.findIndex(
|
|
453
|
-
(header) => header.dataset.accordionHeader === val
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
return -1;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* 激活指定面板。
|
|
461
|
-
* @param {number|string} val 面板索引或名称。
|
|
462
|
-
*/
|
|
463
|
-
async active(val: number | string | undefined): Promise<void> {
|
|
464
|
-
this.assertActive('active');
|
|
465
|
-
await this.activateItem(val, true);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* 将组件挂载到构造器指定的容器中。
|
|
470
|
-
*/
|
|
471
546
|
build(): this {
|
|
472
547
|
this.assertActive('build');
|
|
473
|
-
|
|
548
|
+
if (this.runtime.built) return this;
|
|
549
|
+
|
|
550
|
+
this.runtime.built = true;
|
|
551
|
+
try {
|
|
552
|
+
this.init(this.props);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
this.runtime.built = false;
|
|
555
|
+
this.destroy();
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
|
|
474
559
|
return this;
|
|
475
560
|
}
|
|
476
561
|
|
|
477
|
-
|
|
478
|
-
|
|
562
|
+
protected normalizeStatePatch(
|
|
563
|
+
patch: Partial<AccordionState>
|
|
564
|
+
): Partial<AccordionState> {
|
|
565
|
+
return {
|
|
566
|
+
...patch,
|
|
567
|
+
...(Object.hasOwn(patch, 'items')
|
|
568
|
+
? { items: normalizeItems(patch.items) }
|
|
569
|
+
: {}),
|
|
570
|
+
};
|
|
479
571
|
}
|
|
480
572
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
setItems(items: AccordionItem[], active: AccordionActive = 0): this {
|
|
487
|
-
this.assertActive('setItems');
|
|
488
|
-
validateParam('items', items, ACCORDION_ITEMS_RULE, 'Accordion.setItems');
|
|
489
|
-
validateParam(
|
|
490
|
-
'active',
|
|
491
|
-
active,
|
|
492
|
-
ACCORDION_ACTIVE_RULE,
|
|
493
|
-
'Accordion.setItems'
|
|
494
|
-
);
|
|
495
|
-
|
|
496
|
-
this.props.items = cloneItems(normalizeItems(items));
|
|
497
|
-
this.props.active = active;
|
|
573
|
+
private assertActive(method: string): void {
|
|
574
|
+
if (this.runtime.destroyed) {
|
|
575
|
+
throw new Error(`Accordion.${method}: instance has been destroyed.`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
498
578
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return this;
|
|
579
|
+
private assertBuilt(method: string): void {
|
|
580
|
+
if (!this.runtime.built) {
|
|
581
|
+
throw new Error(`Accordion.${method}: call build() first.`);
|
|
582
|
+
}
|
|
504
583
|
}
|
|
505
584
|
|
|
506
585
|
protected onDestroy(): void {
|
|
507
586
|
this.unbindEvents();
|
|
587
|
+
this.itemsDispose?.();
|
|
508
588
|
this.bindingsDispose?.();
|
|
589
|
+
this.itemsDispose = null;
|
|
509
590
|
this.bindingsDispose = null;
|
|
510
|
-
if (this.root?.parentNode)
|
|
591
|
+
if (this.dom.root?.parentNode) {
|
|
592
|
+
this.dom.root.parentNode.removeChild(this.dom.root);
|
|
593
|
+
}
|
|
594
|
+
this.runtime.built = false;
|
|
511
595
|
}
|
|
512
596
|
}
|
|
513
597
|
|
|
514
|
-
export function createAccordion(
|
|
515
|
-
|
|
516
|
-
input: AccordionProps = {}
|
|
517
|
-
): Accordion {
|
|
518
|
-
return new Accordion(container, input);
|
|
598
|
+
export function createAccordion(props: AccordionProps): AccordionInstance {
|
|
599
|
+
return new Accordion(props);
|
|
519
600
|
}
|