vanilla-jui 1.7.7 → 1.7.8
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 +10 -4
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/toc.ts +134 -86
- package/src/css/themes/default/toc.css +3 -15
package/package.json
CHANGED
package/src/components/toc.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
For,
|
|
3
|
+
Show,
|
|
3
4
|
createDeepStore,
|
|
4
5
|
createEffect,
|
|
5
6
|
createMemo,
|
|
6
7
|
flushSync,
|
|
8
|
+
insert,
|
|
7
9
|
jsx,
|
|
8
10
|
untrack,
|
|
9
11
|
} from 'vanilla-signal';
|
|
@@ -27,6 +29,16 @@ interface TocClassNames {
|
|
|
27
29
|
|
|
28
30
|
type TocClassNameConfig = Partial<TocClassNames>;
|
|
29
31
|
|
|
32
|
+
interface TocIndicatorConfig {
|
|
33
|
+
width: string;
|
|
34
|
+
heightRatio: number;
|
|
35
|
+
radius: string;
|
|
36
|
+
left: string;
|
|
37
|
+
color: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type TocIndicatorConfigInput = Partial<TocIndicatorConfig>;
|
|
41
|
+
|
|
30
42
|
export interface TocItem {
|
|
31
43
|
id: string;
|
|
32
44
|
text: string;
|
|
@@ -52,8 +64,7 @@ interface TocProps extends Record<string, unknown> {
|
|
|
52
64
|
offset?: number;
|
|
53
65
|
reactive?: boolean;
|
|
54
66
|
title?: boolean;
|
|
55
|
-
|
|
56
|
-
indicatorHeightRatio?: number;
|
|
67
|
+
indicator?: TocIndicatorConfigInput;
|
|
57
68
|
className?: TocClassNameConfig;
|
|
58
69
|
onChange?:
|
|
59
70
|
| ((item: TocItem | null, index: number, toc: TocInstance) => void)
|
|
@@ -66,8 +77,7 @@ interface ResolvedTocProps extends Record<string, unknown> {
|
|
|
66
77
|
offset: number;
|
|
67
78
|
reactive: boolean;
|
|
68
79
|
title: boolean;
|
|
69
|
-
|
|
70
|
-
indicatorHeightRatio: number;
|
|
80
|
+
indicator: TocIndicatorConfig;
|
|
71
81
|
className: TocClassNames;
|
|
72
82
|
onChange:
|
|
73
83
|
| ((item: TocItem | null, index: number, toc: TocInstance) => void)
|
|
@@ -119,6 +129,14 @@ const DEFAULT_CLASS_NAMES: TocClassNames = {
|
|
|
119
129
|
indicator: 'toc-indicator',
|
|
120
130
|
};
|
|
121
131
|
|
|
132
|
+
const DEFAULT_INDICATOR_CONFIG: TocIndicatorConfig = {
|
|
133
|
+
width: '2px',
|
|
134
|
+
heightRatio: 5,
|
|
135
|
+
radius: '2px',
|
|
136
|
+
left: '0',
|
|
137
|
+
color: 'var(--tone-solid)',
|
|
138
|
+
};
|
|
139
|
+
|
|
122
140
|
const TOC_PROPS_SCHEMA = {
|
|
123
141
|
target: { default: '.j-content' },
|
|
124
142
|
headings: { default: 'h2, h3', type: 'string' },
|
|
@@ -129,12 +147,22 @@ const TOC_PROPS_SCHEMA = {
|
|
|
129
147
|
},
|
|
130
148
|
reactive: { default: false, type: 'boolean' },
|
|
131
149
|
title: { default: false, type: 'boolean' },
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
150
|
+
indicator: {
|
|
151
|
+
default: DEFAULT_INDICATOR_CONFIG,
|
|
152
|
+
type: 'plainObject',
|
|
153
|
+
merge: 'shallow',
|
|
154
|
+
schema: {
|
|
155
|
+
width: { default: DEFAULT_INDICATOR_CONFIG.width, type: 'string' },
|
|
156
|
+
heightRatio: {
|
|
157
|
+
default: DEFAULT_INDICATOR_CONFIG.heightRatio,
|
|
158
|
+
type: 'number',
|
|
159
|
+
min: 1,
|
|
160
|
+
max: 10,
|
|
161
|
+
},
|
|
162
|
+
radius: { default: DEFAULT_INDICATOR_CONFIG.radius, type: 'string' },
|
|
163
|
+
left: { default: DEFAULT_INDICATOR_CONFIG.left, type: 'string' },
|
|
164
|
+
color: { default: DEFAULT_INDICATOR_CONFIG.color, type: 'string' },
|
|
165
|
+
},
|
|
138
166
|
},
|
|
139
167
|
className: {
|
|
140
168
|
default: DEFAULT_CLASS_NAMES,
|
|
@@ -159,8 +187,7 @@ function normalizeProps(input: TocProps): ResolvedTocProps {
|
|
|
159
187
|
offset: props.offset as number,
|
|
160
188
|
reactive: props.reactive as boolean,
|
|
161
189
|
title: props.title as boolean,
|
|
162
|
-
|
|
163
|
-
indicatorHeightRatio: props.indicatorHeightRatio as number,
|
|
190
|
+
indicator: props.indicator as TocIndicatorConfig,
|
|
164
191
|
className: props.className as TocClassNames,
|
|
165
192
|
onChange: props.onChange as ResolvedTocProps['onChange'],
|
|
166
193
|
};
|
|
@@ -238,7 +265,10 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
238
265
|
let activeLinkId: string | null = null;
|
|
239
266
|
let listHeight = 0;
|
|
240
267
|
const linkRefs = new Map<string, HTMLElement>();
|
|
241
|
-
const
|
|
268
|
+
const getItems = (): TocItem[] =>
|
|
269
|
+
Array.isArray(state.items) ? state.items : [];
|
|
270
|
+
const hasItems = (): boolean => getItems().length > 0;
|
|
271
|
+
const renderableItems = createMemo(getItems);
|
|
242
272
|
|
|
243
273
|
const setIndicator = (indicator: TocIndicator): void => {
|
|
244
274
|
if (
|
|
@@ -307,7 +337,7 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
307
337
|
const linkRect = link.getBoundingClientRect();
|
|
308
338
|
const linkHeight = link.offsetHeight || linkRect.height;
|
|
309
339
|
const linkTop = link.offsetTop;
|
|
310
|
-
const height = (linkHeight * settings.
|
|
340
|
+
const height = (linkHeight * settings.indicator.heightRatio) / 10;
|
|
311
341
|
const top = linkTop + (linkHeight - height) / 2;
|
|
312
342
|
const distance = state.indicator.visible
|
|
313
343
|
? Math.abs(top - state.indicator.top)
|
|
@@ -333,15 +363,16 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
333
363
|
};
|
|
334
364
|
|
|
335
365
|
const syncListLayout = (): void => {
|
|
336
|
-
|
|
366
|
+
const items = getItems();
|
|
367
|
+
if (!toc.runtime.built || !listElement || items.length === 0) {
|
|
337
368
|
listHeight = 0;
|
|
338
369
|
setIndicator(hiddenIndicator());
|
|
339
370
|
return;
|
|
340
371
|
}
|
|
341
|
-
pruneLinkRefs(
|
|
372
|
+
pruneLinkRefs(items);
|
|
342
373
|
const listRect = listElement.getBoundingClientRect();
|
|
343
374
|
listHeight = listElement.offsetHeight || listRect.height;
|
|
344
|
-
const current =
|
|
375
|
+
const current = items[state.current.index] || null;
|
|
345
376
|
updateIndicatorForItem(current);
|
|
346
377
|
};
|
|
347
378
|
|
|
@@ -400,13 +431,14 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
400
431
|
};
|
|
401
432
|
|
|
402
433
|
const setActive = (index: number): number => {
|
|
434
|
+
const items = getItems();
|
|
403
435
|
if (index === state.current.index) {
|
|
404
|
-
const current =
|
|
436
|
+
const current = items[index] || null;
|
|
405
437
|
const duration = updateIndicatorForItem(current);
|
|
406
438
|
if (duration < 0) scheduleListLayoutSync();
|
|
407
439
|
return duration;
|
|
408
440
|
}
|
|
409
|
-
const current =
|
|
441
|
+
const current = items[index] || null;
|
|
410
442
|
flushSync(() => {
|
|
411
443
|
state.current = { index, item: current };
|
|
412
444
|
});
|
|
@@ -487,13 +519,14 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
487
519
|
};
|
|
488
520
|
|
|
489
521
|
const activate = (index: number): TocInstance => {
|
|
522
|
+
const items = getItems();
|
|
490
523
|
if (
|
|
491
524
|
toc.runtime.built &&
|
|
492
525
|
Number.isInteger(index) &&
|
|
493
526
|
index >= 0 &&
|
|
494
|
-
index <
|
|
527
|
+
index < items.length
|
|
495
528
|
) {
|
|
496
|
-
scrollToItem(
|
|
529
|
+
scrollToItem(items[index], { activeIndex: index });
|
|
497
530
|
}
|
|
498
531
|
return toc;
|
|
499
532
|
};
|
|
@@ -505,80 +538,95 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
505
538
|
actions: { activate },
|
|
506
539
|
view: () => {
|
|
507
540
|
createEffect(() => {
|
|
508
|
-
const items =
|
|
541
|
+
const items = getItems();
|
|
509
542
|
void items.map((item) => item.id).join('\0');
|
|
510
543
|
untrack(() => {
|
|
511
544
|
pruneLinkRefs(items);
|
|
545
|
+
if (items.length === 0) {
|
|
546
|
+
listElement = null;
|
|
547
|
+
listHeight = 0;
|
|
548
|
+
linkRefs.clear();
|
|
549
|
+
syncActiveMarker(null);
|
|
550
|
+
setIndicator(hiddenIndicator());
|
|
551
|
+
}
|
|
512
552
|
scheduleListLayoutSync();
|
|
513
553
|
});
|
|
514
554
|
});
|
|
515
|
-
const
|
|
516
|
-
? jsx('div', {
|
|
517
|
-
className: settings.className.title,
|
|
518
|
-
'data-toc-title': '',
|
|
519
|
-
children: translate('toc'),
|
|
520
|
-
})
|
|
521
|
-
: null;
|
|
522
|
-
const listView = jsx('div', {
|
|
523
|
-
ref: (element: HTMLElement) => {
|
|
524
|
-
listElement = element;
|
|
525
|
-
scheduleListLayoutSync();
|
|
526
|
-
},
|
|
527
|
-
className: settings.className.list,
|
|
528
|
-
'data-toc-list': 'root',
|
|
529
|
-
children: [
|
|
530
|
-
For({
|
|
531
|
-
each: renderableItems,
|
|
532
|
-
key: (item: TocItem) => item.id,
|
|
533
|
-
children: (
|
|
534
|
-
itemAccessor: () => TocItem,
|
|
535
|
-
indexAccessor: () => number
|
|
536
|
-
) =>
|
|
537
|
-
jsx('a', {
|
|
538
|
-
ref: (element: HTMLElement) =>
|
|
539
|
-
bindLinkRef(itemAccessor(), element),
|
|
540
|
-
className: settings.className.link,
|
|
541
|
-
href: () => `#${itemAccessor().id}`,
|
|
542
|
-
'data-toc-link': () => String(indexAccessor()),
|
|
543
|
-
'data-toc-target': () => itemAccessor().id,
|
|
544
|
-
'data-toc-level': () => String(itemAccessor().level),
|
|
545
|
-
onClick: (event: Event) => {
|
|
546
|
-
event.preventDefault();
|
|
547
|
-
const item = itemAccessor();
|
|
548
|
-
scrollToItem(item, {
|
|
549
|
-
activeIndex: state.items.findIndex(
|
|
550
|
-
(current) => current.id === item.id
|
|
551
|
-
),
|
|
552
|
-
updateHash: true,
|
|
553
|
-
});
|
|
554
|
-
},
|
|
555
|
-
children: () => itemAccessor().text,
|
|
556
|
-
}),
|
|
557
|
-
}),
|
|
558
|
-
For({
|
|
559
|
-
each: () => (state.items.length > 0 ? [true] : []),
|
|
560
|
-
key: () => 'toc-indicator',
|
|
561
|
-
children: () =>
|
|
562
|
-
jsx('div', {
|
|
563
|
-
className: settings.className.indicator,
|
|
564
|
-
'data-toc-indicator': '',
|
|
565
|
-
hidden: () => !state.indicator.visible,
|
|
566
|
-
style: () => ({
|
|
567
|
-
top: `${state.indicator.top}px`,
|
|
568
|
-
width: settings.indicatorWidth,
|
|
569
|
-
height: `${state.indicator.height}px`,
|
|
570
|
-
transitionDuration: `${state.indicator.duration}ms`,
|
|
571
|
-
transitionTimingFunction: state.indicator.timingFunction,
|
|
572
|
-
}),
|
|
573
|
-
}),
|
|
574
|
-
}),
|
|
575
|
-
],
|
|
576
|
-
});
|
|
577
|
-
return jsx('nav', {
|
|
555
|
+
const root = jsx('nav', {
|
|
578
556
|
className: settings.className.toc,
|
|
579
557
|
'data-toc': 'root',
|
|
580
|
-
children: settings.title ? [titleElement, listView] : listView,
|
|
581
558
|
}) as HTMLElement;
|
|
559
|
+
insert(
|
|
560
|
+
root,
|
|
561
|
+
Show({
|
|
562
|
+
when: hasItems,
|
|
563
|
+
children: () => {
|
|
564
|
+
const titleElement = settings.title
|
|
565
|
+
? jsx('div', {
|
|
566
|
+
className: settings.className.title,
|
|
567
|
+
'data-toc-title': '',
|
|
568
|
+
children: translate('toc'),
|
|
569
|
+
})
|
|
570
|
+
: null;
|
|
571
|
+
const listView = jsx('div', {
|
|
572
|
+
ref: (element: HTMLElement) => {
|
|
573
|
+
listElement = element;
|
|
574
|
+
scheduleListLayoutSync();
|
|
575
|
+
},
|
|
576
|
+
className: settings.className.list,
|
|
577
|
+
'data-toc-list': 'root',
|
|
578
|
+
children: For({
|
|
579
|
+
each: renderableItems,
|
|
580
|
+
key: (item: TocItem) => item.id,
|
|
581
|
+
children: (itemAccessor: () => TocItem) =>
|
|
582
|
+
jsx('a', {
|
|
583
|
+
ref: (element: HTMLElement) =>
|
|
584
|
+
bindLinkRef(itemAccessor(), element),
|
|
585
|
+
className: settings.className.link,
|
|
586
|
+
href: () => `#${itemAccessor().id}`,
|
|
587
|
+
'data-toc-level': () => String(itemAccessor().level),
|
|
588
|
+
onClick: (event: Event) => {
|
|
589
|
+
event.preventDefault();
|
|
590
|
+
const item = itemAccessor();
|
|
591
|
+
scrollToItem(item, {
|
|
592
|
+
activeIndex: getItems().findIndex(
|
|
593
|
+
(current) => current.id === item.id
|
|
594
|
+
),
|
|
595
|
+
updateHash: true,
|
|
596
|
+
});
|
|
597
|
+
},
|
|
598
|
+
children: () => itemAccessor().text,
|
|
599
|
+
}),
|
|
600
|
+
}),
|
|
601
|
+
}) as HTMLElement;
|
|
602
|
+
insert(
|
|
603
|
+
listView,
|
|
604
|
+
Show({
|
|
605
|
+
when: hasItems,
|
|
606
|
+
children: () =>
|
|
607
|
+
jsx('div', {
|
|
608
|
+
className: settings.className.indicator,
|
|
609
|
+
'data-toc-indicator': '',
|
|
610
|
+
hidden: () => !state.indicator.visible,
|
|
611
|
+
style: () => ({
|
|
612
|
+
pointerEvents: 'none',
|
|
613
|
+
position: 'absolute',
|
|
614
|
+
left: settings.indicator.left,
|
|
615
|
+
top: `${state.indicator.top}px`,
|
|
616
|
+
width: settings.indicator.width,
|
|
617
|
+
height: `${state.indicator.height}px`,
|
|
618
|
+
transition: `top ${state.indicator.duration}ms ${state.indicator.timingFunction}`,
|
|
619
|
+
borderRadius: settings.indicator.radius,
|
|
620
|
+
backgroundColor: settings.indicator.color,
|
|
621
|
+
}),
|
|
622
|
+
}),
|
|
623
|
+
})
|
|
624
|
+
);
|
|
625
|
+
return settings.title ? [titleElement, listView] : listView;
|
|
626
|
+
},
|
|
627
|
+
})
|
|
628
|
+
);
|
|
629
|
+
return root;
|
|
582
630
|
},
|
|
583
631
|
onBuild(context) {
|
|
584
632
|
target = requireContainer(settings.target, 'Toc.target');
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
@layer themes {
|
|
2
|
-
.j-toc {
|
|
3
|
-
width: 100%;
|
|
4
|
-
padding-left: 0.75rem;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
2
|
.j-toc .toc-title {
|
|
8
3
|
color: var(--tone-caption);
|
|
9
4
|
margin-bottom: 4px;
|
|
10
5
|
}
|
|
11
6
|
|
|
12
7
|
.j-toc .toc-link {
|
|
13
|
-
padding-
|
|
8
|
+
padding-top: 0.375rem;
|
|
9
|
+
padding-bottom: 0.375rem;
|
|
14
10
|
color: var(--tone-caption);
|
|
15
11
|
font-size: var(--font-size-sm);
|
|
16
12
|
text-decoration: none;
|
|
@@ -22,19 +18,11 @@
|
|
|
22
18
|
color: var(--tone-heading);
|
|
23
19
|
}
|
|
24
20
|
|
|
21
|
+
.j-toc:has(.toc-link),
|
|
25
22
|
.j-toc .toc-link[data-toc-level='3'],
|
|
26
23
|
.j-toc .toc-link[data-toc-level='4'],
|
|
27
24
|
.j-toc .toc-link[data-toc-level='5'],
|
|
28
25
|
.j-toc .toc-link[data-toc-level='6'] {
|
|
29
26
|
padding-left: 0.75rem;
|
|
30
27
|
}
|
|
31
|
-
|
|
32
|
-
.j-toc .toc-indicator {
|
|
33
|
-
position: absolute;
|
|
34
|
-
left: 0;
|
|
35
|
-
background-color: var(--tone-solid);
|
|
36
|
-
transition-property: top;
|
|
37
|
-
pointer-events: none;
|
|
38
|
-
border-radius: 2px;
|
|
39
|
-
}
|
|
40
28
|
}
|