vanilla-jui 1.4.8 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/index.d.ts +402 -977
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -3
- package/src/components/accordion.ts +467 -349
- package/src/components/flow.ts +779 -1133
- package/src/components/form.ts +666 -509
- package/src/components/menu.ts +199 -326
- package/src/components/modal.ts +510 -747
- package/src/components/offcanvas.ts +281 -361
- package/src/components/pagination.ts +207 -374
- package/src/components/swiper.ts +688 -1168
- package/src/components/tabs.ts +468 -788
- package/src/components/toc.ts +191 -244
- package/src/core/component.ts +332 -0
- package/src/core/motion.ts +298 -0
- package/src/core/presence.ts +233 -0
- package/src/core/scheduler.ts +91 -0
- package/src/core/view.ts +31 -0
- package/src/css/accordion.css +94 -3
- package/src/css/color.css +4 -5
- package/src/css/color_p3.css +6 -5
- package/src/css/content.css +18 -24
- package/src/css/drop.css +4 -1
- package/src/css/form.css +8 -0
- package/src/css/gradient.css +64 -131
- package/src/css/layout.css +9 -23
- package/src/css/menu.css +10 -5
- package/src/css/modal.css +2 -1
- package/src/css/offcanvas.css +1 -17
- package/src/css/swiper.css +2 -1
- package/src/css/tabs.css +8 -17
- package/src/css/themes/default/accordion.css +0 -48
- package/src/css/themes/default/swiper.css +1 -1
- package/src/css/themes/default/tabs.css +4 -6
- package/src/css/toast.css +1 -17
- package/src/css/tokens.css +18 -35
- package/src/css/tooltip.css +2 -1
- package/src/index.ts +8 -3
- package/src/locales/index.ts +3 -0
- package/src/primitives/drop.ts +253 -262
- package/src/primitives/loading.ts +22 -4
- package/src/primitives/parabola.ts +123 -164
- package/src/primitives/sticky.ts +297 -0
- package/src/primitives/theme.ts +164 -222
- package/src/primitives/toast.ts +309 -284
- package/src/primitives/tooltip.ts +65 -66
- package/src/utilities/browser.ts +38 -0
- package/src/utilities/dom.ts +0 -54
- package/src/utilities/refs.ts +61 -0
- package/src/utilities/state.ts +73 -0
- package/src/utilities/timer.ts +11 -31
- package/src/validation/validator.ts +289 -389
- package/src/components/sticky.ts +0 -297
- package/src/core/Component.ts +0 -377
- package/src/utilities/scheduler.ts +0 -156
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
For,
|
|
3
3
|
createDeepStore,
|
|
4
|
-
|
|
4
|
+
createEffect,
|
|
5
|
+
createSignal,
|
|
5
6
|
flushSync,
|
|
6
7
|
jsx,
|
|
8
|
+
onCleanup,
|
|
7
9
|
} from 'vanilla-signal';
|
|
8
10
|
|
|
9
|
-
import Component, {
|
|
10
|
-
type ComponentDOM,
|
|
11
|
-
type ComponentRuntime,
|
|
12
|
-
} from '../core/Component.ts';
|
|
13
|
-
import { icon } from '../primitives/icons.ts';
|
|
14
11
|
import {
|
|
15
|
-
type
|
|
16
|
-
|
|
17
|
-
} from '../
|
|
12
|
+
type FunctionalComponent,
|
|
13
|
+
defineComponent,
|
|
14
|
+
} from '../core/component.ts';
|
|
15
|
+
import { createLoading } from '../primitives/loading.ts';
|
|
16
|
+
import { icon } from '../primitives/icons.ts';
|
|
17
|
+
import { type RenderableContent } from '../utilities/dom.ts';
|
|
18
18
|
import { randomId } from '../utilities/id.ts';
|
|
19
19
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from '../utilities/
|
|
20
|
+
type CollapseMotionController,
|
|
21
|
+
createCollapseTransition,
|
|
22
|
+
} from '../core/motion.ts';
|
|
23
|
+
import { createKeyedElementRefs } from '../utilities/refs.ts';
|
|
24
24
|
import {
|
|
25
25
|
type ResolveSchema,
|
|
26
26
|
resolveProps,
|
|
@@ -28,6 +28,12 @@ import {
|
|
|
28
28
|
} from '../utilities/types.ts';
|
|
29
29
|
|
|
30
30
|
type AccordionActive = number | string | Array<number | string> | null;
|
|
31
|
+
type AccordionDirection = 'vertical' | 'horizontal';
|
|
32
|
+
type AccordionContent =
|
|
33
|
+
| RenderableContent<AccordionContentContext>
|
|
34
|
+
| ((
|
|
35
|
+
context: AccordionContentContext
|
|
36
|
+
) => Promise<RenderableContent<AccordionContentContext>>);
|
|
31
37
|
|
|
32
38
|
interface AccordionClassNames {
|
|
33
39
|
root: string;
|
|
@@ -43,7 +49,9 @@ type AccordionClassNameConfig = Partial<AccordionClassNames>;
|
|
|
43
49
|
interface AccordionItem extends Record<string, unknown> {
|
|
44
50
|
name?: string;
|
|
45
51
|
title: RenderableContent<AccordionContentContext>;
|
|
46
|
-
content:
|
|
52
|
+
content: AccordionContent;
|
|
53
|
+
cache?: boolean;
|
|
54
|
+
ttl?: number;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
interface AccordionProps extends Record<string, unknown> {
|
|
@@ -51,8 +59,9 @@ interface AccordionProps extends Record<string, unknown> {
|
|
|
51
59
|
active?: AccordionActive;
|
|
52
60
|
collapsible?: boolean;
|
|
53
61
|
multiple?: boolean;
|
|
62
|
+
direction?: AccordionDirection;
|
|
54
63
|
className?: AccordionClassNameConfig;
|
|
55
|
-
|
|
64
|
+
data: AccordionItem[];
|
|
56
65
|
onChange?:
|
|
57
66
|
| ((
|
|
58
67
|
index: number,
|
|
@@ -69,8 +78,9 @@ interface ResolvedAccordionProps extends Record<string, unknown> {
|
|
|
69
78
|
active: AccordionActive;
|
|
70
79
|
collapsible: boolean;
|
|
71
80
|
multiple: boolean;
|
|
81
|
+
direction: AccordionDirection;
|
|
72
82
|
className: AccordionClassNames;
|
|
73
|
-
|
|
83
|
+
data: AccordionItem[];
|
|
74
84
|
onChange: NonNullable<AccordionProps['onChange']> | null;
|
|
75
85
|
}
|
|
76
86
|
|
|
@@ -80,19 +90,9 @@ interface AccordionCurrent {
|
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
interface AccordionState extends Record<string, unknown> {
|
|
83
|
-
|
|
93
|
+
data: AccordionItem[];
|
|
84
94
|
activeNames: string[];
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
interface AccordionDOM extends ComponentDOM {
|
|
89
|
-
root: HTMLElement | null;
|
|
90
|
-
headers: HTMLElement[];
|
|
91
|
-
panels: HTMLElement[];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface AccordionRuntime extends ComponentRuntime {
|
|
95
|
-
built: boolean;
|
|
95
|
+
loading: boolean;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
interface AccordionContentContext {
|
|
@@ -103,24 +103,31 @@ interface AccordionContentContext {
|
|
|
103
103
|
active: boolean;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
interface AccordionContentCacheEntry {
|
|
107
|
+
content: RenderableContent<AccordionContentContext>;
|
|
108
|
+
updatedAt: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type AccordionInstance = FunctionalComponent<
|
|
107
112
|
ResolvedAccordionProps,
|
|
108
113
|
AccordionState,
|
|
109
|
-
|
|
114
|
+
HTMLElement,
|
|
115
|
+
AccordionActions
|
|
110
116
|
> & {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
readonly current: AccordionCurrent;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
interface AccordionActions {
|
|
114
121
|
isActive(name: string): boolean;
|
|
115
122
|
getIndex(value: number | string | undefined | null): number;
|
|
116
123
|
activate(value: number | string | undefined): Promise<void>;
|
|
117
|
-
}
|
|
124
|
+
}
|
|
118
125
|
|
|
119
126
|
const DEFAULT_CLASS_NAMES: AccordionClassNames = {
|
|
120
127
|
root: 'j-accordion',
|
|
121
128
|
header: 'accordion-header',
|
|
122
129
|
title: 'header-title',
|
|
123
|
-
arrow: 'header-arrow',
|
|
130
|
+
arrow: 'header-arrow j-button is-icon is-ghost is-sm',
|
|
124
131
|
panel: 'accordion-panel',
|
|
125
132
|
content: 'panel-content',
|
|
126
133
|
};
|
|
@@ -131,10 +138,12 @@ const ACCORDION_ITEM_RULE = {
|
|
|
131
138
|
name: ['string', 'null', 'undefined'],
|
|
132
139
|
title: 'renderable',
|
|
133
140
|
content: 'renderable',
|
|
141
|
+
cache: ['boolean', 'undefined'],
|
|
142
|
+
ttl: ['number', 'undefined'],
|
|
134
143
|
},
|
|
135
144
|
};
|
|
136
145
|
|
|
137
|
-
const
|
|
146
|
+
const ACCORDION_DATA_RULE = {
|
|
138
147
|
type: 'array',
|
|
139
148
|
nonEmpty: true,
|
|
140
149
|
items: ACCORDION_ITEM_RULE,
|
|
@@ -166,6 +175,11 @@ const ACCORDION_PROPS_SCHEMA = {
|
|
|
166
175
|
active: { default: 0, ...ACCORDION_ACTIVE_RULE },
|
|
167
176
|
collapsible: { default: false, type: 'boolean' },
|
|
168
177
|
multiple: { default: false, type: 'boolean' },
|
|
178
|
+
direction: {
|
|
179
|
+
default: 'vertical',
|
|
180
|
+
type: 'string',
|
|
181
|
+
enum: ['vertical', 'horizontal'],
|
|
182
|
+
},
|
|
169
183
|
className: {
|
|
170
184
|
default: DEFAULT_CLASS_NAMES,
|
|
171
185
|
type: 'object',
|
|
@@ -175,7 +189,7 @@ const ACCORDION_PROPS_SCHEMA = {
|
|
|
175
189
|
}),
|
|
176
190
|
},
|
|
177
191
|
onChange: { default: null, types: ['function', 'null'] },
|
|
178
|
-
|
|
192
|
+
data: { default: [], ...ACCORDION_DATA_RULE },
|
|
179
193
|
} satisfies ResolveSchema<AccordionProps>;
|
|
180
194
|
|
|
181
195
|
function isActiveValue(value: unknown): value is number | string {
|
|
@@ -194,46 +208,49 @@ function normalizeProps(props: AccordionProps): ResolvedAccordionProps {
|
|
|
194
208
|
active: resolved.active as AccordionActive,
|
|
195
209
|
collapsible: resolved.collapsible as boolean,
|
|
196
210
|
multiple: resolved.multiple as boolean,
|
|
211
|
+
direction: resolved.direction as AccordionDirection,
|
|
197
212
|
className: resolved.className as AccordionClassNames,
|
|
198
|
-
|
|
213
|
+
data: normalizeData(resolved.data, false),
|
|
199
214
|
onChange: resolved.onChange as ResolvedAccordionProps['onChange'],
|
|
200
215
|
};
|
|
201
216
|
}
|
|
202
217
|
|
|
203
|
-
function
|
|
218
|
+
function normalizeAccordionItem(
|
|
219
|
+
item: AccordionItem,
|
|
220
|
+
index: number,
|
|
221
|
+
validate = true
|
|
222
|
+
): AccordionItem {
|
|
223
|
+
if (validate)
|
|
224
|
+
validateParam(String(index), item, ACCORDION_ITEM_RULE, 'Accordion.data');
|
|
225
|
+
const source = item as AccordionItem;
|
|
226
|
+
const name = typeof source.name === 'string' ? source.name.trim() : '';
|
|
227
|
+
return {
|
|
228
|
+
...source,
|
|
229
|
+
name: name || randomId(),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function normalizeData(data: unknown, validate = true): AccordionItem[] {
|
|
204
234
|
if (validate) {
|
|
205
|
-
validateParam('
|
|
235
|
+
validateParam('data', data, ACCORDION_DATA_RULE, 'Accordion');
|
|
206
236
|
}
|
|
207
237
|
|
|
208
238
|
const names = new Set<string>();
|
|
209
239
|
|
|
210
|
-
return (
|
|
211
|
-
const
|
|
212
|
-
const name =
|
|
213
|
-
if (name) {
|
|
214
|
-
|
|
215
|
-
throw new Error(`Accordion: item name "${name}" must be unique.`);
|
|
216
|
-
}
|
|
217
|
-
names.add(name);
|
|
240
|
+
return (data as unknown[]).map((item, index) => {
|
|
241
|
+
const next = normalizeAccordionItem(item as AccordionItem, index, false);
|
|
242
|
+
const name = next.name as string;
|
|
243
|
+
if (names.has(name)) {
|
|
244
|
+
throw new Error(`Accordion: item name "${name}" must be unique.`);
|
|
218
245
|
}
|
|
219
|
-
|
|
220
|
-
return
|
|
221
|
-
...source,
|
|
222
|
-
name: name || randomId(),
|
|
223
|
-
};
|
|
246
|
+
names.add(name);
|
|
247
|
+
return next;
|
|
224
248
|
});
|
|
225
249
|
}
|
|
226
250
|
|
|
227
|
-
function needsItemsSync(
|
|
228
|
-
source: AccordionItem[],
|
|
229
|
-
normalized: AccordionItem[]
|
|
230
|
-
): boolean {
|
|
231
|
-
return normalized.some((item, index) => source[index]?.name !== item.name);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
251
|
function resolveActiveItemNames(
|
|
235
252
|
active: AccordionActive,
|
|
236
|
-
|
|
253
|
+
data: AccordionItem[],
|
|
237
254
|
multiple: boolean
|
|
238
255
|
): string[] {
|
|
239
256
|
if (active == null) return [];
|
|
@@ -245,8 +262,8 @@ function resolveActiveItemNames(
|
|
|
245
262
|
const index =
|
|
246
263
|
typeof value === 'number'
|
|
247
264
|
? value
|
|
248
|
-
:
|
|
249
|
-
const name =
|
|
265
|
+
: data.findIndex((item) => item.name === value);
|
|
266
|
+
const name = data[index]?.name;
|
|
250
267
|
|
|
251
268
|
if (!name) continue;
|
|
252
269
|
|
|
@@ -259,344 +276,445 @@ function resolveActiveItemNames(
|
|
|
259
276
|
|
|
260
277
|
function reconcileActiveNames(
|
|
261
278
|
activeNames: string[],
|
|
262
|
-
|
|
279
|
+
data: AccordionItem[],
|
|
263
280
|
multiple: boolean
|
|
264
281
|
): string[] {
|
|
265
|
-
const names = new Set(
|
|
282
|
+
const names = new Set(data.map((item) => item.name).filter(Boolean));
|
|
266
283
|
const nextNames = activeNames.filter((name) => names.has(name));
|
|
267
284
|
return multiple ? nextNames : nextNames.slice(0, 1);
|
|
268
285
|
}
|
|
269
286
|
|
|
270
287
|
function createCurrentState(
|
|
271
288
|
activeNames: string[],
|
|
272
|
-
|
|
289
|
+
data: AccordionItem[]
|
|
273
290
|
): AccordionCurrent {
|
|
274
291
|
const name = activeNames[0] || null;
|
|
275
292
|
if (!name) return { index: null, name: null };
|
|
276
293
|
|
|
277
|
-
const index =
|
|
294
|
+
const index = data.findIndex((item) => item.name === name);
|
|
278
295
|
return index >= 0 ? { index, name } : { index: null, name: null };
|
|
279
296
|
}
|
|
280
297
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
298
|
+
function normalizeTtl(ttl: unknown): number {
|
|
299
|
+
return typeof ttl === 'number' && ttl > 0 ? ttl : 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function createAccordion(props: AccordionProps): AccordionInstance {
|
|
303
|
+
const resolvedProps = normalizeProps(props);
|
|
304
|
+
const activeNames = resolveActiveItemNames(
|
|
305
|
+
resolvedProps.active,
|
|
306
|
+
resolvedProps.data,
|
|
307
|
+
resolvedProps.multiple
|
|
308
|
+
);
|
|
309
|
+
const state = createDeepStore({
|
|
310
|
+
data: resolvedProps.data,
|
|
311
|
+
activeNames,
|
|
312
|
+
loading: false,
|
|
313
|
+
}) as AccordionState;
|
|
314
|
+
const generatedNames = new WeakMap<object, string>();
|
|
315
|
+
const headers = createKeyedElementRefs<string, HTMLElement>();
|
|
316
|
+
const panels = createKeyedElementRefs<string, HTMLElement>();
|
|
317
|
+
const panelMotions = new Map<string, CollapseMotionController>();
|
|
318
|
+
const contentCache = new Map<string, AccordionContentCacheEntry>();
|
|
319
|
+
const contentLoaders = new Map<string, () => Promise<void>>();
|
|
320
|
+
let motionActiveNames = new Set(activeNames);
|
|
321
|
+
let motionPromise = Promise.resolve();
|
|
322
|
+
let contentLoadId = 0;
|
|
323
|
+
let loadingCount = 0;
|
|
324
|
+
let loading = false;
|
|
325
|
+
let accordion: AccordionInstance;
|
|
326
|
+
|
|
327
|
+
const itemName = (item: AccordionItem): string => {
|
|
328
|
+
const explicit = typeof item.name === 'string' ? item.name.trim() : '';
|
|
329
|
+
if (explicit) return explicit;
|
|
330
|
+
if (!generatedNames.has(item)) generatedNames.set(item, randomId());
|
|
331
|
+
return generatedNames.get(item) as string;
|
|
332
|
+
};
|
|
333
|
+
const current = (): AccordionCurrent => {
|
|
334
|
+
const data = state.data.map((item) => ({
|
|
335
|
+
...item,
|
|
336
|
+
name: itemName(item),
|
|
337
|
+
}));
|
|
338
|
+
return createCurrentState(
|
|
339
|
+
reconcileActiveNames(state.activeNames, data, resolvedProps.multiple),
|
|
340
|
+
data
|
|
305
341
|
);
|
|
342
|
+
};
|
|
306
343
|
|
|
307
|
-
|
|
308
|
-
this.dom.panels = [];
|
|
309
|
-
this.bindingsDispose = null;
|
|
310
|
-
this.itemsDispose = null;
|
|
311
|
-
this.runtime.built = false;
|
|
312
|
-
this.state = createDeepStore({
|
|
313
|
-
items: resolvedProps.items,
|
|
314
|
-
activeNames,
|
|
315
|
-
current: createCurrentState(activeNames, resolvedProps.items),
|
|
316
|
-
}) as AccordionState;
|
|
317
|
-
}
|
|
344
|
+
const isActive = (name: string): boolean => state.activeNames.includes(name);
|
|
318
345
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
346
|
+
const beginLoading = (): void => {
|
|
347
|
+
loadingCount += 1;
|
|
348
|
+
if (!loading && !accordion.runtime.destroyed) {
|
|
349
|
+
loading = true;
|
|
350
|
+
flushSync(() => {
|
|
351
|
+
state.loading = true;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
};
|
|
324
355
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
356
|
+
const endLoading = (): void => {
|
|
357
|
+
loadingCount = Math.max(0, loadingCount - 1);
|
|
358
|
+
if (loadingCount === 0 && loading && !accordion.runtime.destroyed) {
|
|
359
|
+
loading = false;
|
|
360
|
+
flushSync(() => {
|
|
361
|
+
state.loading = false;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
};
|
|
332
365
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
366
|
+
const getIndex = (value: number | string | undefined | null): number => {
|
|
367
|
+
if (typeof value === 'number') return value;
|
|
368
|
+
if (typeof value !== 'string') return -1;
|
|
369
|
+
return state.data.findIndex((item) => itemName(item) === value);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const transitionPanels = (activeNames: readonly string[]): Promise<void> => {
|
|
373
|
+
const nextActiveNames = new Set(activeNames);
|
|
374
|
+
const names = new Set([...motionActiveNames, ...nextActiveNames]);
|
|
375
|
+
const transitions: Promise<void>[] = [];
|
|
341
376
|
|
|
342
|
-
|
|
343
|
-
|
|
377
|
+
for (const name of names) {
|
|
378
|
+
const wasActive = motionActiveNames.has(name);
|
|
379
|
+
const active = nextActiveNames.has(name);
|
|
380
|
+
if (wasActive === active) continue;
|
|
344
381
|
|
|
345
|
-
|
|
346
|
-
|
|
382
|
+
const panel = panels.get(name);
|
|
383
|
+
if (panel) panel.inert = !active;
|
|
384
|
+
const motion = panelMotions.get(name);
|
|
385
|
+
if (motion) transitions.push(active ? motion.enter() : motion.leave());
|
|
347
386
|
}
|
|
348
387
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
388
|
+
motionActiveNames = nextActiveNames;
|
|
389
|
+
if (transitions.length > 0) {
|
|
390
|
+
motionPromise = Promise.all(transitions).then(() => undefined);
|
|
391
|
+
}
|
|
392
|
+
return motionPromise;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const bindPanel =
|
|
396
|
+
(name: string) =>
|
|
397
|
+
(panel: HTMLElement): void => {
|
|
398
|
+
panels.bind(name)(panel);
|
|
399
|
+
panelMotions.get(name)?.cancel();
|
|
400
|
+
const motion = createCollapseTransition(() => panels.get(name), {
|
|
401
|
+
axis: resolvedProps.direction,
|
|
402
|
+
options: { duration: 250, easing: 'ease' },
|
|
403
|
+
});
|
|
404
|
+
panelMotions.set(name, motion);
|
|
405
|
+
const active = isActive(name);
|
|
406
|
+
panel.inert = !active;
|
|
407
|
+
motion.setExpanded(active);
|
|
408
|
+
onCleanup(() => {
|
|
409
|
+
if (panelMotions.get(name) !== motion) return;
|
|
410
|
+
motion.cancel();
|
|
411
|
+
panelMotions.delete(name);
|
|
412
|
+
motionActiveNames.delete(name);
|
|
413
|
+
if (state.activeNames.includes(name)) {
|
|
414
|
+
flushSync(() => {
|
|
415
|
+
state.activeNames = state.activeNames.filter(
|
|
416
|
+
(activeName) => activeName !== name
|
|
417
|
+
);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
};
|
|
357
422
|
|
|
358
|
-
|
|
359
|
-
|
|
423
|
+
const activate = async (
|
|
424
|
+
value: number | string | undefined
|
|
425
|
+
): Promise<void> => {
|
|
426
|
+
if (accordion.runtime.destroyed) {
|
|
427
|
+
throw new Error('Accordion.activate: instance destroyed');
|
|
428
|
+
}
|
|
429
|
+
if (!accordion.runtime.built) {
|
|
430
|
+
throw new Error('Accordion.activate: call build() first.');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const index = getIndex(value);
|
|
434
|
+
const item = state.data[index];
|
|
435
|
+
if (!item) return;
|
|
436
|
+
const name = itemName(item);
|
|
437
|
+
const active = isActive(name);
|
|
438
|
+
if (active && !resolvedProps.multiple && !resolvedProps.collapsible) return;
|
|
439
|
+
|
|
440
|
+
let next: string[];
|
|
441
|
+
if (resolvedProps.multiple) {
|
|
442
|
+
if (active) {
|
|
443
|
+
const remaining = state.activeNames.filter(
|
|
444
|
+
(itemName) => itemName !== name
|
|
445
|
+
);
|
|
446
|
+
next =
|
|
447
|
+
!resolvedProps.collapsible && remaining.length === 0
|
|
448
|
+
? [...state.activeNames]
|
|
449
|
+
: remaining;
|
|
450
|
+
} else {
|
|
451
|
+
next = [...state.activeNames, name];
|
|
452
|
+
}
|
|
453
|
+
} else if (active) {
|
|
454
|
+
next = resolvedProps.collapsible ? [] : [...state.activeNames];
|
|
455
|
+
} else {
|
|
456
|
+
next = [name];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
flushSync(() => {
|
|
460
|
+
state.activeNames = next;
|
|
461
|
+
});
|
|
462
|
+
const transitioned = transitionPanels(next);
|
|
463
|
+
const loaded = next.includes(name)
|
|
464
|
+
? contentLoaders.get(name)?.() || Promise.resolve()
|
|
465
|
+
: Promise.resolve();
|
|
466
|
+
|
|
467
|
+
const header = headers.get(name);
|
|
468
|
+
const panel = panels.get(name);
|
|
469
|
+
const changed =
|
|
470
|
+
resolvedProps.onChange && header && panel
|
|
471
|
+
? loaded.then(() =>
|
|
472
|
+
resolvedProps.onChange?.(index, name, header, panel, accordion)
|
|
473
|
+
)
|
|
474
|
+
: Promise.resolve();
|
|
475
|
+
await Promise.all([transitioned, changed]);
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const contentContext = (
|
|
479
|
+
item: AccordionItem,
|
|
480
|
+
index: number,
|
|
481
|
+
type: 'title' | 'content'
|
|
482
|
+
): AccordionContentContext => {
|
|
483
|
+
const name = itemName(item);
|
|
484
|
+
return {
|
|
485
|
+
accordion,
|
|
486
|
+
item,
|
|
487
|
+
index,
|
|
488
|
+
type,
|
|
489
|
+
active: isActive(name),
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const titleView = (
|
|
494
|
+
content: RenderableContent<AccordionContentContext>,
|
|
495
|
+
item: AccordionItem,
|
|
496
|
+
index: number
|
|
497
|
+
): RenderableContent<AccordionContentContext> => {
|
|
498
|
+
if (typeof content !== 'function') return content;
|
|
499
|
+
return content(contentContext(item, index, 'title'));
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const createContentRenderer = (
|
|
503
|
+
itemAccessor: () => AccordionItem,
|
|
504
|
+
indexAccessor: () => number,
|
|
505
|
+
name: string
|
|
506
|
+
) => {
|
|
507
|
+
const initial = itemAccessor().content;
|
|
508
|
+
const [content, setContent] = createSignal<
|
|
509
|
+
RenderableContent<AccordionContentContext>
|
|
510
|
+
>(typeof initial === 'function' ? null : initial);
|
|
511
|
+
const [loading, setLoading] = createSignal(false);
|
|
512
|
+
let source: AccordionContent = initial;
|
|
513
|
+
let localLoadId = 0;
|
|
514
|
+
let pending: Promise<void> | null = null;
|
|
515
|
+
|
|
516
|
+
const context = (): AccordionContentContext =>
|
|
517
|
+
contentContext(itemAccessor(), indexAccessor(), 'content');
|
|
518
|
+
|
|
519
|
+
const load = async (): Promise<void> => {
|
|
520
|
+
const current = itemAccessor();
|
|
521
|
+
const source = current.content;
|
|
522
|
+
if (typeof source !== 'function') {
|
|
523
|
+
setContent(source);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const cached = current.cache ? contentCache.get(name) : null;
|
|
527
|
+
const ttlValue = normalizeTtl(current.ttl);
|
|
528
|
+
if (cached && (!ttlValue || Date.now() - cached.updatedAt <= ttlValue)) {
|
|
529
|
+
setContent(cached.content);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (pending) return pending;
|
|
533
|
+
const request = ++localLoadId;
|
|
534
|
+
contentLoadId += 1;
|
|
535
|
+
const activeLoad = contentLoadId;
|
|
536
|
+
setLoading(true);
|
|
537
|
+
beginLoading();
|
|
538
|
+
const task = Promise.resolve()
|
|
539
|
+
.then(() =>
|
|
540
|
+
(source as (context: AccordionContentContext) => unknown)(context())
|
|
541
|
+
)
|
|
542
|
+
.then((result) => {
|
|
543
|
+
if (request !== localLoadId || accordion.runtime.destroyed) return;
|
|
544
|
+
if (itemAccessor().cache) {
|
|
545
|
+
contentCache.set(name, {
|
|
546
|
+
content: result as RenderableContent<AccordionContentContext>,
|
|
547
|
+
updatedAt: Date.now(),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
setContent(result as RenderableContent<AccordionContentContext>);
|
|
551
|
+
})
|
|
552
|
+
.finally(() => {
|
|
553
|
+
if (request === localLoadId && !accordion.runtime.destroyed) {
|
|
554
|
+
setLoading(false);
|
|
555
|
+
}
|
|
556
|
+
if (activeLoad <= contentLoadId) endLoading();
|
|
557
|
+
if (pending === task) pending = null;
|
|
558
|
+
});
|
|
559
|
+
pending = task;
|
|
560
|
+
return task;
|
|
561
|
+
};
|
|
562
|
+
contentLoaders.set(name, load);
|
|
563
|
+
onCleanup(() => {
|
|
564
|
+
localLoadId += 1;
|
|
565
|
+
contentCache.delete(name);
|
|
566
|
+
contentLoaders.delete(name);
|
|
567
|
+
pending = null;
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
createEffect(() => {
|
|
571
|
+
const nextContent = itemAccessor().content;
|
|
572
|
+
if (nextContent !== source) {
|
|
573
|
+
source = nextContent;
|
|
574
|
+
localLoadId += 1;
|
|
575
|
+
pending = null;
|
|
576
|
+
contentCache.delete(name);
|
|
577
|
+
setContent(typeof nextContent === 'function' ? null : nextContent);
|
|
578
|
+
}
|
|
579
|
+
if (isActive(name)) void load();
|
|
580
|
+
});
|
|
360
581
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
582
|
+
return {
|
|
583
|
+
loading,
|
|
584
|
+
isAsync: () => typeof itemAccessor().content === 'function',
|
|
585
|
+
render: (): RenderableContent<AccordionContentContext> => {
|
|
586
|
+
if (loading()) return createLoading('flex-start', 'flex-start');
|
|
587
|
+
const value = content();
|
|
588
|
+
if (typeof value === 'function') return value(context());
|
|
589
|
+
return value;
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
};
|
|
367
593
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
594
|
+
const itemView = (
|
|
595
|
+
itemAccessor: () => AccordionItem,
|
|
596
|
+
indexAccessor: () => number
|
|
597
|
+
): Node[] => {
|
|
598
|
+
const name = itemName(itemAccessor());
|
|
599
|
+
const content = createContentRenderer(itemAccessor, indexAccessor, name);
|
|
600
|
+
const headerId = `${resolvedProps.id}_header_${name}`;
|
|
601
|
+
const panelId = `${resolvedProps.id}_panel_${name}`;
|
|
602
|
+
const handleKeyDown = (event: KeyboardEvent): void => {
|
|
603
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
604
|
+
event.preventDefault();
|
|
605
|
+
void activate(name);
|
|
606
|
+
};
|
|
373
607
|
|
|
374
|
-
|
|
375
|
-
|
|
608
|
+
return [
|
|
609
|
+
jsx('div', {
|
|
610
|
+
className: resolvedProps.className.header,
|
|
376
611
|
id: headerId,
|
|
377
612
|
'data-accordion-header': name,
|
|
378
613
|
role: 'button',
|
|
379
614
|
tabindex: '0',
|
|
380
615
|
'aria-controls': panelId,
|
|
616
|
+
'aria-expanded': () => (isActive(name) ? 'true' : 'false'),
|
|
617
|
+
ref: headers.bind(name),
|
|
618
|
+
onClick: () => void activate(name),
|
|
619
|
+
onKeyDown: handleKeyDown,
|
|
381
620
|
children: [
|
|
382
621
|
jsx('span', {
|
|
383
|
-
className:
|
|
622
|
+
className: resolvedProps.className.title,
|
|
384
623
|
'data-accordion-title': name,
|
|
385
624
|
role: 'heading',
|
|
386
|
-
children:
|
|
625
|
+
children: () => {
|
|
626
|
+
const item = itemAccessor();
|
|
627
|
+
return titleView(item.title, item, indexAccessor());
|
|
628
|
+
},
|
|
387
629
|
}),
|
|
388
630
|
jsx('span', {
|
|
389
|
-
className:
|
|
631
|
+
className: resolvedProps.className.arrow,
|
|
390
632
|
'data-accordion-arrow': name,
|
|
391
633
|
'aria-hidden': 'true',
|
|
392
634
|
children: icon('arrow-down'),
|
|
393
635
|
}),
|
|
394
636
|
],
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
className: this.props.className.panel,
|
|
637
|
+
}),
|
|
638
|
+
jsx('div', {
|
|
639
|
+
className: resolvedProps.className.panel,
|
|
399
640
|
'data-accordion-panel': name,
|
|
400
641
|
id: panelId,
|
|
401
642
|
role: 'region',
|
|
402
643
|
'aria-labelledby': headerId,
|
|
644
|
+
'aria-hidden': () => (isActive(name) ? 'false' : 'true'),
|
|
645
|
+
'aria-live': () => (content.isAsync() ? 'polite' : null),
|
|
646
|
+
'aria-busy': () => String(content.loading()),
|
|
647
|
+
'data-state': () => (isActive(name) ? 'open' : 'closed'),
|
|
648
|
+
ref: bindPanel(name),
|
|
403
649
|
children: jsx('div', {
|
|
404
|
-
className:
|
|
650
|
+
className: resolvedProps.className.content,
|
|
405
651
|
'data-accordion-content': name,
|
|
406
|
-
children:
|
|
652
|
+
children: content.render,
|
|
407
653
|
}),
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this.dom.panels.push(panel);
|
|
412
|
-
fragment.append(header, panel);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
this.dom.root.append(fragment);
|
|
416
|
-
this.bindItemState();
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
private bindItemState(): void {
|
|
420
|
-
this.bindingsDispose = createRoot((dispose) => {
|
|
421
|
-
this.dom.headers.forEach((header) => {
|
|
422
|
-
const name = header.dataset.accordionHeader || '';
|
|
423
|
-
bindAttr(header, 'aria-expanded', () =>
|
|
424
|
-
this.state.activeNames.includes(name) ? 'true' : 'false'
|
|
425
|
-
);
|
|
426
|
-
});
|
|
654
|
+
}),
|
|
655
|
+
];
|
|
656
|
+
};
|
|
427
657
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
658
|
+
accordion = defineComponent<
|
|
659
|
+
ResolvedAccordionProps,
|
|
660
|
+
AccordionState,
|
|
661
|
+
HTMLElement,
|
|
662
|
+
AccordionActions
|
|
663
|
+
>({
|
|
664
|
+
name: 'Accordion',
|
|
665
|
+
props: resolvedProps,
|
|
666
|
+
state,
|
|
667
|
+
actions: {
|
|
668
|
+
isActive,
|
|
669
|
+
getIndex,
|
|
670
|
+
activate,
|
|
671
|
+
},
|
|
672
|
+
normalizeStatePatch(patch) {
|
|
673
|
+
return {
|
|
674
|
+
...patch,
|
|
675
|
+
...(Object.hasOwn(patch, 'data')
|
|
676
|
+
? { data: normalizeData(patch.data) }
|
|
677
|
+
: {}),
|
|
678
|
+
};
|
|
679
|
+
},
|
|
680
|
+
view: () => {
|
|
681
|
+
const root = jsx('div', {
|
|
682
|
+
className: resolvedProps.className.root,
|
|
683
|
+
id: resolvedProps.id,
|
|
684
|
+
'data-accordion': 'root',
|
|
685
|
+
'data-direction': resolvedProps.direction,
|
|
686
|
+
children: For({
|
|
687
|
+
each: () => state.data,
|
|
688
|
+
key: (item: AccordionItem) => itemName(item),
|
|
689
|
+
children: (
|
|
690
|
+
itemAccessor: () => AccordionItem,
|
|
691
|
+
indexAccessor: () => number
|
|
692
|
+
) => itemView(itemAccessor, indexAccessor),
|
|
693
|
+
}),
|
|
694
|
+
}) as HTMLElement;
|
|
695
|
+
motionActiveNames = new Set(state.activeNames);
|
|
696
|
+
createEffect(() => {
|
|
697
|
+
void transitionPanels([...state.activeNames]);
|
|
434
698
|
});
|
|
699
|
+
return root;
|
|
700
|
+
},
|
|
701
|
+
onDestroy() {
|
|
702
|
+
contentLoadId += 1;
|
|
703
|
+
loadingCount = 0;
|
|
704
|
+
loading = false;
|
|
705
|
+
contentCache.clear();
|
|
706
|
+
contentLoaders.clear();
|
|
707
|
+
for (const motion of panelMotions.values()) motion.cancel();
|
|
708
|
+
panelMotions.clear();
|
|
709
|
+
headers.clear();
|
|
710
|
+
panels.clear();
|
|
711
|
+
},
|
|
712
|
+
}) as AccordionInstance;
|
|
435
713
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
private contentView(
|
|
441
|
-
item: AccordionItem,
|
|
442
|
-
index: number,
|
|
443
|
-
type: 'title' | 'content',
|
|
444
|
-
active: boolean
|
|
445
|
-
): Node[] {
|
|
446
|
-
return normalizeContentNodes(type === 'title' ? item.title : item.content, {
|
|
447
|
-
accordion: this,
|
|
448
|
-
item,
|
|
449
|
-
index,
|
|
450
|
-
type,
|
|
451
|
-
active,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
private syncActiveNames(names: string[], items = this.state.items): void {
|
|
456
|
-
flushSync(() => {
|
|
457
|
-
this.state.activeNames = names;
|
|
458
|
-
this.state.current = createCurrentState(names, items);
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
private bindEvents(): void {
|
|
463
|
-
this.unbindEvents();
|
|
464
|
-
if (!this.dom.root) return;
|
|
465
|
-
|
|
466
|
-
this.cleanup.events.on('click', this.dom.root, 'click', (event) => {
|
|
467
|
-
if (!(event.target instanceof Element)) return;
|
|
468
|
-
const header = event.target.closest<HTMLElement>(
|
|
469
|
-
'[data-accordion-header]'
|
|
470
|
-
);
|
|
471
|
-
if (!header || !this.dom.root?.contains(header)) return;
|
|
472
|
-
void this.activate(header.dataset.accordionHeader);
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
this.cleanup.events.on('keydown', this.dom.root, 'keydown', (event) => {
|
|
476
|
-
if (!(event instanceof KeyboardEvent)) return;
|
|
477
|
-
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
478
|
-
if (!(event.target instanceof Element)) return;
|
|
479
|
-
const header = event.target.closest<HTMLElement>(
|
|
480
|
-
'[data-accordion-header]'
|
|
481
|
-
);
|
|
482
|
-
if (!header || !this.dom.root?.contains(header)) return;
|
|
483
|
-
event.preventDefault();
|
|
484
|
-
void this.activate(header.dataset.accordionHeader);
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
private unbindEvents(): void {
|
|
489
|
-
this.cleanup.events.clear();
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
isActive(name: string): boolean {
|
|
493
|
-
return this.state.activeNames.includes(name);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
getIndex(val: number | string | undefined | null): number {
|
|
497
|
-
if (typeof val === 'number') return val;
|
|
498
|
-
if (typeof val === 'string') {
|
|
499
|
-
return this.dom.headers.findIndex(
|
|
500
|
-
(header) => header.dataset.accordionHeader === val
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
return -1;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
async activate(value: number | string | undefined): Promise<void> {
|
|
507
|
-
this.assertActive('activate');
|
|
508
|
-
this.assertBuilt('activate');
|
|
509
|
-
|
|
510
|
-
const index = this.getIndex(value);
|
|
511
|
-
if (index < 0 || index >= this.dom.headers.length) return;
|
|
512
|
-
|
|
513
|
-
const headerEl = this.dom.headers[index];
|
|
514
|
-
const panelEl = this.dom.panels[index];
|
|
515
|
-
const name = headerEl.dataset.accordionHeader || String(index);
|
|
516
|
-
const isActive = this.isActive(name);
|
|
517
|
-
|
|
518
|
-
if (isActive && !this.props.multiple && !this.props.collapsible) return;
|
|
519
|
-
|
|
520
|
-
let activeNames: string[];
|
|
521
|
-
if (this.props.multiple) {
|
|
522
|
-
if (isActive) {
|
|
523
|
-
const nextNames = this.state.activeNames.filter(
|
|
524
|
-
(item) => item !== name
|
|
525
|
-
);
|
|
526
|
-
activeNames =
|
|
527
|
-
!this.props.collapsible && nextNames.length === 0
|
|
528
|
-
? this.state.activeNames
|
|
529
|
-
: nextNames;
|
|
530
|
-
} else {
|
|
531
|
-
activeNames = [...this.state.activeNames, name];
|
|
532
|
-
}
|
|
533
|
-
} else if (isActive) {
|
|
534
|
-
activeNames = this.props.collapsible ? [] : this.state.activeNames;
|
|
535
|
-
} else {
|
|
536
|
-
activeNames = [name];
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
this.syncActiveNames(activeNames);
|
|
540
|
-
|
|
541
|
-
if (this.props.onChange) {
|
|
542
|
-
await Promise.resolve(
|
|
543
|
-
this.props.onChange(index, name, headerEl, panelEl, this)
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
build(): this {
|
|
549
|
-
this.assertActive('build');
|
|
550
|
-
if (this.runtime.built) return this;
|
|
551
|
-
|
|
552
|
-
this.runtime.built = true;
|
|
553
|
-
try {
|
|
554
|
-
this.init(this.props);
|
|
555
|
-
} catch (error) {
|
|
556
|
-
this.runtime.built = false;
|
|
557
|
-
this.destroy();
|
|
558
|
-
throw error;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
return this;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
protected normalizeStatePatch(
|
|
565
|
-
patch: Partial<AccordionState>
|
|
566
|
-
): Partial<AccordionState> {
|
|
567
|
-
return {
|
|
568
|
-
...patch,
|
|
569
|
-
...(Object.hasOwn(patch, 'items')
|
|
570
|
-
? { items: normalizeItems(patch.items) }
|
|
571
|
-
: {}),
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
private assertActive(method: string): void {
|
|
576
|
-
if (this.runtime.destroyed) {
|
|
577
|
-
throw new Error(`Accordion.${method}: instance has been destroyed.`);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
private assertBuilt(method: string): void {
|
|
582
|
-
if (!this.runtime.built) {
|
|
583
|
-
throw new Error(`Accordion.${method}: call build() first.`);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
protected onDestroy(): void {
|
|
588
|
-
this.unbindEvents();
|
|
589
|
-
this.itemsDispose?.();
|
|
590
|
-
this.bindingsDispose?.();
|
|
591
|
-
this.itemsDispose = null;
|
|
592
|
-
this.bindingsDispose = null;
|
|
593
|
-
if (this.dom.root?.parentNode) {
|
|
594
|
-
this.dom.root.parentNode.removeChild(this.dom.root);
|
|
595
|
-
}
|
|
596
|
-
this.runtime.built = false;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
714
|
+
Object.defineProperty(accordion, 'current', {
|
|
715
|
+
enumerable: true,
|
|
716
|
+
get: current,
|
|
717
|
+
});
|
|
599
718
|
|
|
600
|
-
|
|
601
|
-
return new Accordion(props);
|
|
719
|
+
return accordion;
|
|
602
720
|
}
|