vanilla-jui 1.7.6 → 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 +24 -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 +403 -62
- package/src/css/components.css +0 -1
- package/src/css/themes/default/form.css +1 -1
- package/src/css/themes/default/toast.css +5 -4
- package/src/css/themes/default/toc.css +15 -12
- package/src/locales/index.ts +2 -0
- package/src/primitives/toast.ts +2 -4
- package/src/utilities/browser.ts +0 -1
- package/src/css/toc.css +0 -10
package/package.json
CHANGED
package/src/components/toc.ts
CHANGED
|
@@ -1,35 +1,44 @@
|
|
|
1
1
|
import {
|
|
2
2
|
For,
|
|
3
|
+
Show,
|
|
3
4
|
createDeepStore,
|
|
5
|
+
createEffect,
|
|
4
6
|
createMemo,
|
|
5
7
|
flushSync,
|
|
8
|
+
insert,
|
|
6
9
|
jsx,
|
|
10
|
+
untrack,
|
|
7
11
|
} from 'vanilla-signal';
|
|
8
12
|
|
|
9
13
|
import {
|
|
10
14
|
type FunctionalComponent,
|
|
11
15
|
defineComponent,
|
|
12
16
|
} from '../core/component.ts';
|
|
13
|
-
import {
|
|
14
|
-
type DOMReference,
|
|
15
|
-
all,
|
|
16
|
-
joinClasses,
|
|
17
|
-
requireContainer,
|
|
18
|
-
} from '../utilities/dom.ts';
|
|
17
|
+
import { type DOMReference, all, requireContainer } from '../utilities/dom.ts';
|
|
19
18
|
import { createEventManager } from '../utilities/events.ts';
|
|
20
|
-
import { randomId } from '../utilities/id.ts';
|
|
21
19
|
import { type ConfigSchema, resolveConfig } from '../utilities/config.ts';
|
|
20
|
+
import { translate } from '../utilities/locale.ts';
|
|
22
21
|
|
|
23
22
|
interface TocClassNames {
|
|
24
23
|
toc: string;
|
|
24
|
+
title: string;
|
|
25
25
|
list: string;
|
|
26
26
|
link: string;
|
|
27
|
-
|
|
28
|
-
levelPrefix: string;
|
|
27
|
+
indicator: string;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
type TocClassNameConfig = Partial<TocClassNames>;
|
|
32
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
|
+
|
|
33
42
|
export interface TocItem {
|
|
34
43
|
id: string;
|
|
35
44
|
text: string;
|
|
@@ -41,11 +50,21 @@ export interface TocCurrent {
|
|
|
41
50
|
item: TocItem | null;
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
export interface TocIndicator {
|
|
54
|
+
visible: boolean;
|
|
55
|
+
top: number;
|
|
56
|
+
height: number;
|
|
57
|
+
duration: number;
|
|
58
|
+
timingFunction: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
44
61
|
interface TocProps extends Record<string, unknown> {
|
|
45
62
|
target?: DOMReference;
|
|
46
63
|
headings?: string;
|
|
47
64
|
offset?: number;
|
|
48
65
|
reactive?: boolean;
|
|
66
|
+
title?: boolean;
|
|
67
|
+
indicator?: TocIndicatorConfigInput;
|
|
49
68
|
className?: TocClassNameConfig;
|
|
50
69
|
onChange?:
|
|
51
70
|
| ((item: TocItem | null, index: number, toc: TocInstance) => void)
|
|
@@ -57,6 +76,8 @@ interface ResolvedTocProps extends Record<string, unknown> {
|
|
|
57
76
|
headings: string;
|
|
58
77
|
offset: number;
|
|
59
78
|
reactive: boolean;
|
|
79
|
+
title: boolean;
|
|
80
|
+
indicator: TocIndicatorConfig;
|
|
60
81
|
className: TocClassNames;
|
|
61
82
|
onChange:
|
|
62
83
|
| ((item: TocItem | null, index: number, toc: TocInstance) => void)
|
|
@@ -66,6 +87,7 @@ interface ResolvedTocProps extends Record<string, unknown> {
|
|
|
66
87
|
interface TocState extends Record<string, unknown> {
|
|
67
88
|
items: TocItem[];
|
|
68
89
|
current: TocCurrent;
|
|
90
|
+
indicator: TocIndicator;
|
|
69
91
|
}
|
|
70
92
|
|
|
71
93
|
interface TocRuntimeExtras {
|
|
@@ -73,6 +95,8 @@ interface TocRuntimeExtras {
|
|
|
73
95
|
frameId: number;
|
|
74
96
|
refreshing: boolean;
|
|
75
97
|
refreshFrameId: number;
|
|
98
|
+
layoutFrameId: number;
|
|
99
|
+
pendingScroll: TocPendingScroll | null;
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
interface TocScrollOptions {
|
|
@@ -80,6 +104,12 @@ interface TocScrollOptions {
|
|
|
80
104
|
updateHash?: boolean;
|
|
81
105
|
}
|
|
82
106
|
|
|
107
|
+
interface TocPendingScroll {
|
|
108
|
+
index: number;
|
|
109
|
+
top: number;
|
|
110
|
+
deadline: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
83
113
|
interface TocActions {
|
|
84
114
|
activate(index: number): TocInstance;
|
|
85
115
|
}
|
|
@@ -93,10 +123,18 @@ type TocInstance = FunctionalComponent<
|
|
|
93
123
|
|
|
94
124
|
const DEFAULT_CLASS_NAMES: TocClassNames = {
|
|
95
125
|
toc: 'j-toc',
|
|
126
|
+
title: 'toc-title',
|
|
96
127
|
list: 'toc-list',
|
|
97
128
|
link: 'toc-link',
|
|
98
|
-
|
|
99
|
-
|
|
129
|
+
indicator: 'toc-indicator',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const DEFAULT_INDICATOR_CONFIG: TocIndicatorConfig = {
|
|
133
|
+
width: '2px',
|
|
134
|
+
heightRatio: 5,
|
|
135
|
+
radius: '2px',
|
|
136
|
+
left: '0',
|
|
137
|
+
color: 'var(--tone-solid)',
|
|
100
138
|
};
|
|
101
139
|
|
|
102
140
|
const TOC_PROPS_SCHEMA = {
|
|
@@ -108,6 +146,24 @@ const TOC_PROPS_SCHEMA = {
|
|
|
108
146
|
min: 0,
|
|
109
147
|
},
|
|
110
148
|
reactive: { default: false, type: 'boolean' },
|
|
149
|
+
title: { default: false, type: 'boolean' },
|
|
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
|
+
},
|
|
166
|
+
},
|
|
111
167
|
className: {
|
|
112
168
|
default: DEFAULT_CLASS_NAMES,
|
|
113
169
|
type: 'plainObject',
|
|
@@ -117,6 +173,11 @@ const TOC_PROPS_SCHEMA = {
|
|
|
117
173
|
} satisfies ConfigSchema<TocProps>;
|
|
118
174
|
|
|
119
175
|
const ACTIVE_OFFSET_TOLERANCE = 1;
|
|
176
|
+
const INDICATOR_TRANSITION_MAX_MS = 1200;
|
|
177
|
+
const INDICATOR_TRANSITION_MIN_MS = 120;
|
|
178
|
+
const INDICATOR_TRANSITION_TIMING = 'ease-in-out';
|
|
179
|
+
const PROGRAMMATIC_SCROLL_TIMEOUT_MS = 2400;
|
|
180
|
+
const SCROLL_REACHED_TOLERANCE = 2;
|
|
120
181
|
|
|
121
182
|
function normalizeProps(input: TocProps): ResolvedTocProps {
|
|
122
183
|
const props = resolveConfig(input, TOC_PROPS_SCHEMA, 'Toc.props');
|
|
@@ -125,6 +186,8 @@ function normalizeProps(input: TocProps): ResolvedTocProps {
|
|
|
125
186
|
headings: props.headings as string,
|
|
126
187
|
offset: props.offset as number,
|
|
127
188
|
reactive: props.reactive as boolean,
|
|
189
|
+
title: props.title as boolean,
|
|
190
|
+
indicator: props.indicator as TocIndicatorConfig,
|
|
128
191
|
className: props.className as TocClassNames,
|
|
129
192
|
onChange: props.onChange as ResolvedTocProps['onChange'],
|
|
130
193
|
};
|
|
@@ -135,45 +198,260 @@ function resolveHeadingLevel(element: Element): number {
|
|
|
135
198
|
return match ? Number(match[1]) : 1;
|
|
136
199
|
}
|
|
137
200
|
|
|
138
|
-
function
|
|
139
|
-
|
|
201
|
+
function normalizeHeadingId(
|
|
202
|
+
element: HTMLHeadingElement,
|
|
203
|
+
index: number,
|
|
204
|
+
usedIds: Set<string>
|
|
205
|
+
): string {
|
|
206
|
+
const textId = (element.textContent || '').trim().replace(/\s+/g, '-');
|
|
207
|
+
const base = element.id || textId || `heading-${index + 1}`;
|
|
208
|
+
let id = base;
|
|
209
|
+
let suffix = 1;
|
|
210
|
+
while (usedIds.has(id)) {
|
|
211
|
+
id = `${base}-${suffix}`;
|
|
212
|
+
suffix += 1;
|
|
213
|
+
}
|
|
214
|
+
element.id = id;
|
|
215
|
+
usedIds.add(id);
|
|
216
|
+
return id;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeHeading(
|
|
220
|
+
element: HTMLHeadingElement,
|
|
221
|
+
index: number,
|
|
222
|
+
usedIds: Set<string>
|
|
223
|
+
): TocItem {
|
|
140
224
|
return {
|
|
141
|
-
id: element
|
|
225
|
+
id: normalizeHeadingId(element, index, usedIds),
|
|
142
226
|
text: element.textContent || '',
|
|
143
227
|
level: resolveHeadingLevel(element),
|
|
144
228
|
};
|
|
145
229
|
}
|
|
146
230
|
|
|
231
|
+
function normalizeHeadings(elements: HTMLHeadingElement[]): TocItem[] {
|
|
232
|
+
const usedIds = new Set<string>();
|
|
233
|
+
return elements.map((element, index) =>
|
|
234
|
+
normalizeHeading(element, index, usedIds)
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
147
238
|
export function createToc(props: TocProps = {}): TocInstance {
|
|
148
239
|
const settings = normalizeProps(props);
|
|
149
240
|
const state = createDeepStore({
|
|
150
241
|
items: [],
|
|
151
242
|
current: { index: -1, item: null },
|
|
243
|
+
indicator: {
|
|
244
|
+
visible: false,
|
|
245
|
+
top: 0,
|
|
246
|
+
height: 0,
|
|
247
|
+
duration: 0,
|
|
248
|
+
timingFunction: INDICATOR_TRANSITION_TIMING,
|
|
249
|
+
},
|
|
152
250
|
}) as TocState;
|
|
153
251
|
const runtime: TocRuntimeExtras = {
|
|
154
252
|
ticking: false,
|
|
155
253
|
frameId: 0,
|
|
156
254
|
refreshing: false,
|
|
157
255
|
refreshFrameId: 0,
|
|
256
|
+
layoutFrameId: 0,
|
|
257
|
+
pendingScroll: null,
|
|
158
258
|
};
|
|
159
259
|
const events = createEventManager();
|
|
160
260
|
let target: Element | null = null;
|
|
161
261
|
let headings: HTMLHeadingElement[] = [];
|
|
162
262
|
let observer: MutationObserver | null = null;
|
|
263
|
+
let listElement: HTMLElement | null = null;
|
|
163
264
|
let toc: TocInstance;
|
|
164
|
-
|
|
265
|
+
let activeLinkId: string | null = null;
|
|
266
|
+
let listHeight = 0;
|
|
267
|
+
const linkRefs = new Map<string, HTMLElement>();
|
|
268
|
+
const getItems = (): TocItem[] =>
|
|
269
|
+
Array.isArray(state.items) ? state.items : [];
|
|
270
|
+
const hasItems = (): boolean => getItems().length > 0;
|
|
271
|
+
const renderableItems = createMemo(getItems);
|
|
272
|
+
|
|
273
|
+
const setIndicator = (indicator: TocIndicator): void => {
|
|
274
|
+
if (
|
|
275
|
+
state.indicator.visible === indicator.visible &&
|
|
276
|
+
state.indicator.top === indicator.top &&
|
|
277
|
+
state.indicator.height === indicator.height &&
|
|
278
|
+
state.indicator.duration === indicator.duration &&
|
|
279
|
+
state.indicator.timingFunction === indicator.timingFunction
|
|
280
|
+
) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
flushSync(() => {
|
|
284
|
+
state.indicator = indicator;
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const hiddenIndicator = (): TocIndicator => ({
|
|
289
|
+
visible: false,
|
|
290
|
+
top: 0,
|
|
291
|
+
height: 0,
|
|
292
|
+
duration: 0,
|
|
293
|
+
timingFunction: INDICATOR_TRANSITION_TIMING,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const pruneLinkRefs = (items: TocItem[]): void => {
|
|
297
|
+
const ids = new Set(items.map((item) => item.id));
|
|
298
|
+
for (const id of linkRefs.keys()) {
|
|
299
|
+
if (!ids.has(id)) linkRefs.delete(id);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const setLinkCurrent = (id: string, active: boolean): void => {
|
|
304
|
+
const link = linkRefs.get(id);
|
|
305
|
+
if (!link) return;
|
|
306
|
+
if (active) {
|
|
307
|
+
if (link.getAttribute('aria-current') !== 'location') {
|
|
308
|
+
link.setAttribute('aria-current', 'location');
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (link.hasAttribute('aria-current')) link.removeAttribute('aria-current');
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const syncActiveMarker = (nextId: string | null): void => {
|
|
316
|
+
if (activeLinkId === nextId) return;
|
|
317
|
+
const previousId = activeLinkId;
|
|
318
|
+
activeLinkId = nextId;
|
|
319
|
+
if (previousId) setLinkCurrent(previousId, false);
|
|
320
|
+
if (nextId) setLinkCurrent(nextId, true);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const updateIndicatorForItem = (item: TocItem | null): number => {
|
|
324
|
+
if (!toc.runtime.built || !listElement || !item) {
|
|
325
|
+
setIndicator(hiddenIndicator());
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
const link = linkRefs.get(item.id);
|
|
329
|
+
if (!link) {
|
|
330
|
+
setIndicator(hiddenIndicator());
|
|
331
|
+
return -1;
|
|
332
|
+
}
|
|
333
|
+
if (listHeight <= 0) {
|
|
334
|
+
const listRect = listElement.getBoundingClientRect();
|
|
335
|
+
listHeight = listElement.offsetHeight || listRect.height;
|
|
336
|
+
}
|
|
337
|
+
const linkRect = link.getBoundingClientRect();
|
|
338
|
+
const linkHeight = link.offsetHeight || linkRect.height;
|
|
339
|
+
const linkTop = link.offsetTop;
|
|
340
|
+
const height = (linkHeight * settings.indicator.heightRatio) / 10;
|
|
341
|
+
const top = linkTop + (linkHeight - height) / 2;
|
|
342
|
+
const distance = state.indicator.visible
|
|
343
|
+
? Math.abs(top - state.indicator.top)
|
|
344
|
+
: 0;
|
|
345
|
+
const duration =
|
|
346
|
+
distance > 0 && listHeight > 0
|
|
347
|
+
? Math.min(
|
|
348
|
+
INDICATOR_TRANSITION_MAX_MS,
|
|
349
|
+
Math.max(
|
|
350
|
+
INDICATOR_TRANSITION_MIN_MS,
|
|
351
|
+
Math.round((distance / listHeight) * INDICATOR_TRANSITION_MAX_MS)
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
: 0;
|
|
355
|
+
setIndicator({
|
|
356
|
+
visible: height > 0,
|
|
357
|
+
top,
|
|
358
|
+
height,
|
|
359
|
+
duration,
|
|
360
|
+
timingFunction: INDICATOR_TRANSITION_TIMING,
|
|
361
|
+
});
|
|
362
|
+
return duration;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const syncListLayout = (): void => {
|
|
366
|
+
const items = getItems();
|
|
367
|
+
if (!toc.runtime.built || !listElement || items.length === 0) {
|
|
368
|
+
listHeight = 0;
|
|
369
|
+
setIndicator(hiddenIndicator());
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
pruneLinkRefs(items);
|
|
373
|
+
const listRect = listElement.getBoundingClientRect();
|
|
374
|
+
listHeight = listElement.offsetHeight || listRect.height;
|
|
375
|
+
const current = items[state.current.index] || null;
|
|
376
|
+
updateIndicatorForItem(current);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const scheduleListLayoutSync = (): void => {
|
|
380
|
+
if (runtime.layoutFrameId) return;
|
|
381
|
+
runtime.layoutFrameId = requestAnimationFrame(() => {
|
|
382
|
+
runtime.layoutFrameId = 0;
|
|
383
|
+
untrack(syncListLayout);
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const bindLinkRef = (item: TocItem, element: HTMLElement): void => {
|
|
388
|
+
linkRefs.set(item.id, element);
|
|
389
|
+
if (item.id === activeLinkId) {
|
|
390
|
+
element.setAttribute('aria-current', 'location');
|
|
391
|
+
} else {
|
|
392
|
+
element.removeAttribute('aria-current');
|
|
393
|
+
}
|
|
394
|
+
scheduleListLayoutSync();
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const getScrollY = (): number => window.scrollY || window.pageYOffset || 0;
|
|
398
|
+
|
|
399
|
+
const getClockTime = (): number => window.performance?.now?.() ?? Date.now();
|
|
400
|
+
|
|
401
|
+
const clearPendingScroll = (): void => {
|
|
402
|
+
runtime.pendingScroll = null;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const beginPendingScroll = (
|
|
406
|
+
index: number,
|
|
407
|
+
top: number,
|
|
408
|
+
duration: number
|
|
409
|
+
): void => {
|
|
410
|
+
if (!Number.isInteger(index) || index < 0) return;
|
|
411
|
+
runtime.pendingScroll = {
|
|
412
|
+
index,
|
|
413
|
+
top,
|
|
414
|
+
deadline:
|
|
415
|
+
getClockTime() + Math.max(PROGRAMMATIC_SCROLL_TIMEOUT_MS, duration),
|
|
416
|
+
};
|
|
417
|
+
};
|
|
165
418
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
419
|
+
const shouldSkipScrollActivation = (): boolean => {
|
|
420
|
+
const pending = runtime.pendingScroll;
|
|
421
|
+
if (!pending) return false;
|
|
422
|
+
if (Math.abs(getScrollY() - pending.top) <= SCROLL_REACHED_TOLERANCE) {
|
|
423
|
+
clearPendingScroll();
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
if (getClockTime() > pending.deadline) {
|
|
427
|
+
clearPendingScroll();
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
return true;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const setActive = (index: number): number => {
|
|
434
|
+
const items = getItems();
|
|
435
|
+
if (index === state.current.index) {
|
|
436
|
+
const current = items[index] || null;
|
|
437
|
+
const duration = updateIndicatorForItem(current);
|
|
438
|
+
if (duration < 0) scheduleListLayoutSync();
|
|
439
|
+
return duration;
|
|
440
|
+
}
|
|
441
|
+
const current = items[index] || null;
|
|
169
442
|
flushSync(() => {
|
|
170
443
|
state.current = { index, item: current };
|
|
171
444
|
});
|
|
445
|
+
syncActiveMarker(current?.id ?? null);
|
|
446
|
+
const duration = updateIndicatorForItem(current);
|
|
447
|
+
if (duration < 0) scheduleListLayoutSync();
|
|
172
448
|
settings.onChange?.(current, index, toc);
|
|
449
|
+
return duration;
|
|
173
450
|
};
|
|
174
451
|
|
|
175
452
|
const updateActive = (): void => {
|
|
176
453
|
if (!toc.runtime.built) return;
|
|
454
|
+
if (shouldSkipScrollActivation()) return;
|
|
177
455
|
let index = -1;
|
|
178
456
|
const activeOffset = settings.offset + ACTIVE_OFFSET_TOLERANCE;
|
|
179
457
|
for (let current = headings.length - 1; current >= 0; current--) {
|
|
@@ -211,13 +489,16 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
211
489
|
): void => {
|
|
212
490
|
const heading = headings.find((element) => element.id === item.id);
|
|
213
491
|
if (!heading) return;
|
|
214
|
-
const scrollY =
|
|
492
|
+
const scrollY = getScrollY();
|
|
215
493
|
const top = Math.max(
|
|
216
494
|
0,
|
|
217
495
|
heading.getBoundingClientRect().top + scrollY - settings.offset
|
|
218
496
|
);
|
|
497
|
+
if (activeIndex >= 0) {
|
|
498
|
+
const duration = setActive(activeIndex);
|
|
499
|
+
beginPendingScroll(activeIndex, top, duration);
|
|
500
|
+
}
|
|
219
501
|
window.scrollTo({ top, behavior: 'smooth' });
|
|
220
|
-
if (activeIndex >= 0) setActive(activeIndex);
|
|
221
502
|
if (updateHash && window.history?.pushState) {
|
|
222
503
|
window.history.pushState(null, '', `#${item.id}`);
|
|
223
504
|
}
|
|
@@ -226,22 +507,26 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
226
507
|
const syncFromTarget = (): void => {
|
|
227
508
|
if (toc.runtime.destroyed || !toc.runtime.built || !target) return;
|
|
228
509
|
headings = all<HTMLHeadingElement>(settings.headings, target);
|
|
229
|
-
|
|
510
|
+
clearPendingScroll();
|
|
511
|
+
const items = normalizeHeadings(headings);
|
|
512
|
+
syncActiveMarker(null);
|
|
230
513
|
flushSync(() => {
|
|
231
514
|
state.items = items;
|
|
232
515
|
state.current = { index: -1, item: null };
|
|
516
|
+
state.indicator = hiddenIndicator();
|
|
233
517
|
});
|
|
234
518
|
updateActive();
|
|
235
519
|
};
|
|
236
520
|
|
|
237
521
|
const activate = (index: number): TocInstance => {
|
|
522
|
+
const items = getItems();
|
|
238
523
|
if (
|
|
239
524
|
toc.runtime.built &&
|
|
240
525
|
Number.isInteger(index) &&
|
|
241
526
|
index >= 0 &&
|
|
242
|
-
index <
|
|
527
|
+
index < items.length
|
|
243
528
|
) {
|
|
244
|
-
scrollToItem(
|
|
529
|
+
scrollToItem(items[index], { activeIndex: index });
|
|
245
530
|
}
|
|
246
531
|
return toc;
|
|
247
532
|
};
|
|
@@ -251,48 +536,98 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
251
536
|
props: settings,
|
|
252
537
|
state,
|
|
253
538
|
actions: { activate },
|
|
254
|
-
view: () =>
|
|
255
|
-
|
|
539
|
+
view: () => {
|
|
540
|
+
createEffect(() => {
|
|
541
|
+
const items = getItems();
|
|
542
|
+
void items.map((item) => item.id).join('\0');
|
|
543
|
+
untrack(() => {
|
|
544
|
+
pruneLinkRefs(items);
|
|
545
|
+
if (items.length === 0) {
|
|
546
|
+
listElement = null;
|
|
547
|
+
listHeight = 0;
|
|
548
|
+
linkRefs.clear();
|
|
549
|
+
syncActiveMarker(null);
|
|
550
|
+
setIndicator(hiddenIndicator());
|
|
551
|
+
}
|
|
552
|
+
scheduleListLayoutSync();
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
const root = jsx('nav', {
|
|
256
556
|
className: settings.className.toc,
|
|
257
557
|
'data-toc': 'root',
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
}),
|
|
292
600
|
}),
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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;
|
|
630
|
+
},
|
|
296
631
|
onBuild(context) {
|
|
297
632
|
target = requireContainer(settings.target, 'Toc.target');
|
|
298
633
|
events.on('scroll', window, 'scroll', onScroll, { passive: true });
|
|
@@ -314,14 +649,20 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
314
649
|
onDestroy() {
|
|
315
650
|
if (runtime.frameId) cancelAnimationFrame(runtime.frameId);
|
|
316
651
|
if (runtime.refreshFrameId) cancelAnimationFrame(runtime.refreshFrameId);
|
|
652
|
+
if (runtime.layoutFrameId) cancelAnimationFrame(runtime.layoutFrameId);
|
|
317
653
|
runtime.frameId = 0;
|
|
318
654
|
runtime.refreshFrameId = 0;
|
|
655
|
+
runtime.layoutFrameId = 0;
|
|
656
|
+
clearPendingScroll();
|
|
319
657
|
runtime.ticking = false;
|
|
320
658
|
runtime.refreshing = false;
|
|
321
659
|
observer?.disconnect();
|
|
322
660
|
observer = null;
|
|
323
661
|
target = null;
|
|
324
662
|
headings = [];
|
|
663
|
+
listElement = null;
|
|
664
|
+
listHeight = 0;
|
|
665
|
+
linkRefs.clear();
|
|
325
666
|
},
|
|
326
667
|
});
|
|
327
668
|
|
package/src/css/components.css
CHANGED