vanilla-jui 1.7.5 → 1.7.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/dist/index.d.ts +18 -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 +338 -45
- package/src/css/avatar.css +0 -1
- package/src/css/components.css +0 -1
- package/src/css/editor.css +0 -4
- package/src/css/layout.css +2 -22
- package/src/css/skeleton.css +0 -5
- package/src/css/swiper.css +0 -1
- package/src/css/theme-palette.css +1 -5
- package/src/css/themes/default/editor.css +6 -6
- 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 +25 -10
- package/src/css/tip.css +0 -2
- 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,31 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
For,
|
|
3
3
|
createDeepStore,
|
|
4
|
+
createEffect,
|
|
4
5
|
createMemo,
|
|
5
6
|
flushSync,
|
|
6
7
|
jsx,
|
|
8
|
+
untrack,
|
|
7
9
|
} from 'vanilla-signal';
|
|
8
10
|
|
|
9
11
|
import {
|
|
10
12
|
type FunctionalComponent,
|
|
11
13
|
defineComponent,
|
|
12
14
|
} from '../core/component.ts';
|
|
13
|
-
import {
|
|
14
|
-
type DOMReference,
|
|
15
|
-
all,
|
|
16
|
-
joinClasses,
|
|
17
|
-
requireContainer,
|
|
18
|
-
} from '../utilities/dom.ts';
|
|
15
|
+
import { type DOMReference, all, requireContainer } from '../utilities/dom.ts';
|
|
19
16
|
import { createEventManager } from '../utilities/events.ts';
|
|
20
|
-
import { randomId } from '../utilities/id.ts';
|
|
21
17
|
import { type ConfigSchema, resolveConfig } from '../utilities/config.ts';
|
|
18
|
+
import { translate } from '../utilities/locale.ts';
|
|
22
19
|
|
|
23
20
|
interface TocClassNames {
|
|
24
21
|
toc: string;
|
|
22
|
+
title: string;
|
|
25
23
|
list: string;
|
|
26
24
|
link: string;
|
|
27
|
-
|
|
28
|
-
levelPrefix: string;
|
|
25
|
+
indicator: string;
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
type TocClassNameConfig = Partial<TocClassNames>;
|
|
@@ -41,11 +38,22 @@ export interface TocCurrent {
|
|
|
41
38
|
item: TocItem | null;
|
|
42
39
|
}
|
|
43
40
|
|
|
41
|
+
export interface TocIndicator {
|
|
42
|
+
visible: boolean;
|
|
43
|
+
top: number;
|
|
44
|
+
height: number;
|
|
45
|
+
duration: number;
|
|
46
|
+
timingFunction: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
interface TocProps extends Record<string, unknown> {
|
|
45
50
|
target?: DOMReference;
|
|
46
51
|
headings?: string;
|
|
47
52
|
offset?: number;
|
|
48
53
|
reactive?: boolean;
|
|
54
|
+
title?: boolean;
|
|
55
|
+
indicatorWidth?: string;
|
|
56
|
+
indicatorHeightRatio?: number;
|
|
49
57
|
className?: TocClassNameConfig;
|
|
50
58
|
onChange?:
|
|
51
59
|
| ((item: TocItem | null, index: number, toc: TocInstance) => void)
|
|
@@ -57,6 +65,9 @@ interface ResolvedTocProps extends Record<string, unknown> {
|
|
|
57
65
|
headings: string;
|
|
58
66
|
offset: number;
|
|
59
67
|
reactive: boolean;
|
|
68
|
+
title: boolean;
|
|
69
|
+
indicatorWidth: string;
|
|
70
|
+
indicatorHeightRatio: number;
|
|
60
71
|
className: TocClassNames;
|
|
61
72
|
onChange:
|
|
62
73
|
| ((item: TocItem | null, index: number, toc: TocInstance) => void)
|
|
@@ -66,6 +77,7 @@ interface ResolvedTocProps extends Record<string, unknown> {
|
|
|
66
77
|
interface TocState extends Record<string, unknown> {
|
|
67
78
|
items: TocItem[];
|
|
68
79
|
current: TocCurrent;
|
|
80
|
+
indicator: TocIndicator;
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
interface TocRuntimeExtras {
|
|
@@ -73,6 +85,8 @@ interface TocRuntimeExtras {
|
|
|
73
85
|
frameId: number;
|
|
74
86
|
refreshing: boolean;
|
|
75
87
|
refreshFrameId: number;
|
|
88
|
+
layoutFrameId: number;
|
|
89
|
+
pendingScroll: TocPendingScroll | null;
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
interface TocScrollOptions {
|
|
@@ -80,6 +94,12 @@ interface TocScrollOptions {
|
|
|
80
94
|
updateHash?: boolean;
|
|
81
95
|
}
|
|
82
96
|
|
|
97
|
+
interface TocPendingScroll {
|
|
98
|
+
index: number;
|
|
99
|
+
top: number;
|
|
100
|
+
deadline: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
83
103
|
interface TocActions {
|
|
84
104
|
activate(index: number): TocInstance;
|
|
85
105
|
}
|
|
@@ -93,10 +113,10 @@ type TocInstance = FunctionalComponent<
|
|
|
93
113
|
|
|
94
114
|
const DEFAULT_CLASS_NAMES: TocClassNames = {
|
|
95
115
|
toc: 'j-toc',
|
|
116
|
+
title: 'toc-title',
|
|
96
117
|
list: 'toc-list',
|
|
97
118
|
link: 'toc-link',
|
|
98
|
-
|
|
99
|
-
levelPrefix: 'is-level-',
|
|
119
|
+
indicator: 'toc-indicator',
|
|
100
120
|
};
|
|
101
121
|
|
|
102
122
|
const TOC_PROPS_SCHEMA = {
|
|
@@ -108,6 +128,14 @@ const TOC_PROPS_SCHEMA = {
|
|
|
108
128
|
min: 0,
|
|
109
129
|
},
|
|
110
130
|
reactive: { default: false, type: 'boolean' },
|
|
131
|
+
title: { default: false, type: 'boolean' },
|
|
132
|
+
indicatorWidth: { default: '2px', type: 'string' },
|
|
133
|
+
indicatorHeightRatio: {
|
|
134
|
+
default: 5,
|
|
135
|
+
type: 'number',
|
|
136
|
+
min: 1,
|
|
137
|
+
max: 10,
|
|
138
|
+
},
|
|
111
139
|
className: {
|
|
112
140
|
default: DEFAULT_CLASS_NAMES,
|
|
113
141
|
type: 'plainObject',
|
|
@@ -117,6 +145,11 @@ const TOC_PROPS_SCHEMA = {
|
|
|
117
145
|
} satisfies ConfigSchema<TocProps>;
|
|
118
146
|
|
|
119
147
|
const ACTIVE_OFFSET_TOLERANCE = 1;
|
|
148
|
+
const INDICATOR_TRANSITION_MAX_MS = 1200;
|
|
149
|
+
const INDICATOR_TRANSITION_MIN_MS = 120;
|
|
150
|
+
const INDICATOR_TRANSITION_TIMING = 'ease-in-out';
|
|
151
|
+
const PROGRAMMATIC_SCROLL_TIMEOUT_MS = 2400;
|
|
152
|
+
const SCROLL_REACHED_TOLERANCE = 2;
|
|
120
153
|
|
|
121
154
|
function normalizeProps(input: TocProps): ResolvedTocProps {
|
|
122
155
|
const props = resolveConfig(input, TOC_PROPS_SCHEMA, 'Toc.props');
|
|
@@ -125,6 +158,9 @@ function normalizeProps(input: TocProps): ResolvedTocProps {
|
|
|
125
158
|
headings: props.headings as string,
|
|
126
159
|
offset: props.offset as number,
|
|
127
160
|
reactive: props.reactive as boolean,
|
|
161
|
+
title: props.title as boolean,
|
|
162
|
+
indicatorWidth: props.indicatorWidth as string,
|
|
163
|
+
indicatorHeightRatio: props.indicatorHeightRatio as number,
|
|
128
164
|
className: props.className as TocClassNames,
|
|
129
165
|
onChange: props.onChange as ResolvedTocProps['onChange'],
|
|
130
166
|
};
|
|
@@ -135,45 +171,255 @@ function resolveHeadingLevel(element: Element): number {
|
|
|
135
171
|
return match ? Number(match[1]) : 1;
|
|
136
172
|
}
|
|
137
173
|
|
|
138
|
-
function
|
|
139
|
-
|
|
174
|
+
function normalizeHeadingId(
|
|
175
|
+
element: HTMLHeadingElement,
|
|
176
|
+
index: number,
|
|
177
|
+
usedIds: Set<string>
|
|
178
|
+
): string {
|
|
179
|
+
const textId = (element.textContent || '').trim().replace(/\s+/g, '-');
|
|
180
|
+
const base = element.id || textId || `heading-${index + 1}`;
|
|
181
|
+
let id = base;
|
|
182
|
+
let suffix = 1;
|
|
183
|
+
while (usedIds.has(id)) {
|
|
184
|
+
id = `${base}-${suffix}`;
|
|
185
|
+
suffix += 1;
|
|
186
|
+
}
|
|
187
|
+
element.id = id;
|
|
188
|
+
usedIds.add(id);
|
|
189
|
+
return id;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeHeading(
|
|
193
|
+
element: HTMLHeadingElement,
|
|
194
|
+
index: number,
|
|
195
|
+
usedIds: Set<string>
|
|
196
|
+
): TocItem {
|
|
140
197
|
return {
|
|
141
|
-
id: element
|
|
198
|
+
id: normalizeHeadingId(element, index, usedIds),
|
|
142
199
|
text: element.textContent || '',
|
|
143
200
|
level: resolveHeadingLevel(element),
|
|
144
201
|
};
|
|
145
202
|
}
|
|
146
203
|
|
|
204
|
+
function normalizeHeadings(elements: HTMLHeadingElement[]): TocItem[] {
|
|
205
|
+
const usedIds = new Set<string>();
|
|
206
|
+
return elements.map((element, index) =>
|
|
207
|
+
normalizeHeading(element, index, usedIds)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
147
211
|
export function createToc(props: TocProps = {}): TocInstance {
|
|
148
212
|
const settings = normalizeProps(props);
|
|
149
213
|
const state = createDeepStore({
|
|
150
214
|
items: [],
|
|
151
215
|
current: { index: -1, item: null },
|
|
216
|
+
indicator: {
|
|
217
|
+
visible: false,
|
|
218
|
+
top: 0,
|
|
219
|
+
height: 0,
|
|
220
|
+
duration: 0,
|
|
221
|
+
timingFunction: INDICATOR_TRANSITION_TIMING,
|
|
222
|
+
},
|
|
152
223
|
}) as TocState;
|
|
153
224
|
const runtime: TocRuntimeExtras = {
|
|
154
225
|
ticking: false,
|
|
155
226
|
frameId: 0,
|
|
156
227
|
refreshing: false,
|
|
157
228
|
refreshFrameId: 0,
|
|
229
|
+
layoutFrameId: 0,
|
|
230
|
+
pendingScroll: null,
|
|
158
231
|
};
|
|
159
232
|
const events = createEventManager();
|
|
160
233
|
let target: Element | null = null;
|
|
161
234
|
let headings: HTMLHeadingElement[] = [];
|
|
162
235
|
let observer: MutationObserver | null = null;
|
|
236
|
+
let listElement: HTMLElement | null = null;
|
|
163
237
|
let toc: TocInstance;
|
|
238
|
+
let activeLinkId: string | null = null;
|
|
239
|
+
let listHeight = 0;
|
|
240
|
+
const linkRefs = new Map<string, HTMLElement>();
|
|
164
241
|
const renderableItems = createMemo(() => state.items);
|
|
165
242
|
|
|
166
|
-
const
|
|
167
|
-
if (
|
|
243
|
+
const setIndicator = (indicator: TocIndicator): void => {
|
|
244
|
+
if (
|
|
245
|
+
state.indicator.visible === indicator.visible &&
|
|
246
|
+
state.indicator.top === indicator.top &&
|
|
247
|
+
state.indicator.height === indicator.height &&
|
|
248
|
+
state.indicator.duration === indicator.duration &&
|
|
249
|
+
state.indicator.timingFunction === indicator.timingFunction
|
|
250
|
+
) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
flushSync(() => {
|
|
254
|
+
state.indicator = indicator;
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const hiddenIndicator = (): TocIndicator => ({
|
|
259
|
+
visible: false,
|
|
260
|
+
top: 0,
|
|
261
|
+
height: 0,
|
|
262
|
+
duration: 0,
|
|
263
|
+
timingFunction: INDICATOR_TRANSITION_TIMING,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const pruneLinkRefs = (items: TocItem[]): void => {
|
|
267
|
+
const ids = new Set(items.map((item) => item.id));
|
|
268
|
+
for (const id of linkRefs.keys()) {
|
|
269
|
+
if (!ids.has(id)) linkRefs.delete(id);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const setLinkCurrent = (id: string, active: boolean): void => {
|
|
274
|
+
const link = linkRefs.get(id);
|
|
275
|
+
if (!link) return;
|
|
276
|
+
if (active) {
|
|
277
|
+
if (link.getAttribute('aria-current') !== 'location') {
|
|
278
|
+
link.setAttribute('aria-current', 'location');
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (link.hasAttribute('aria-current')) link.removeAttribute('aria-current');
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const syncActiveMarker = (nextId: string | null): void => {
|
|
286
|
+
if (activeLinkId === nextId) return;
|
|
287
|
+
const previousId = activeLinkId;
|
|
288
|
+
activeLinkId = nextId;
|
|
289
|
+
if (previousId) setLinkCurrent(previousId, false);
|
|
290
|
+
if (nextId) setLinkCurrent(nextId, true);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const updateIndicatorForItem = (item: TocItem | null): number => {
|
|
294
|
+
if (!toc.runtime.built || !listElement || !item) {
|
|
295
|
+
setIndicator(hiddenIndicator());
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
const link = linkRefs.get(item.id);
|
|
299
|
+
if (!link) {
|
|
300
|
+
setIndicator(hiddenIndicator());
|
|
301
|
+
return -1;
|
|
302
|
+
}
|
|
303
|
+
if (listHeight <= 0) {
|
|
304
|
+
const listRect = listElement.getBoundingClientRect();
|
|
305
|
+
listHeight = listElement.offsetHeight || listRect.height;
|
|
306
|
+
}
|
|
307
|
+
const linkRect = link.getBoundingClientRect();
|
|
308
|
+
const linkHeight = link.offsetHeight || linkRect.height;
|
|
309
|
+
const linkTop = link.offsetTop;
|
|
310
|
+
const height = (linkHeight * settings.indicatorHeightRatio) / 10;
|
|
311
|
+
const top = linkTop + (linkHeight - height) / 2;
|
|
312
|
+
const distance = state.indicator.visible
|
|
313
|
+
? Math.abs(top - state.indicator.top)
|
|
314
|
+
: 0;
|
|
315
|
+
const duration =
|
|
316
|
+
distance > 0 && listHeight > 0
|
|
317
|
+
? Math.min(
|
|
318
|
+
INDICATOR_TRANSITION_MAX_MS,
|
|
319
|
+
Math.max(
|
|
320
|
+
INDICATOR_TRANSITION_MIN_MS,
|
|
321
|
+
Math.round((distance / listHeight) * INDICATOR_TRANSITION_MAX_MS)
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
: 0;
|
|
325
|
+
setIndicator({
|
|
326
|
+
visible: height > 0,
|
|
327
|
+
top,
|
|
328
|
+
height,
|
|
329
|
+
duration,
|
|
330
|
+
timingFunction: INDICATOR_TRANSITION_TIMING,
|
|
331
|
+
});
|
|
332
|
+
return duration;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const syncListLayout = (): void => {
|
|
336
|
+
if (!toc.runtime.built || !listElement || state.items.length === 0) {
|
|
337
|
+
listHeight = 0;
|
|
338
|
+
setIndicator(hiddenIndicator());
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
pruneLinkRefs(state.items);
|
|
342
|
+
const listRect = listElement.getBoundingClientRect();
|
|
343
|
+
listHeight = listElement.offsetHeight || listRect.height;
|
|
344
|
+
const current = state.items[state.current.index] || null;
|
|
345
|
+
updateIndicatorForItem(current);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const scheduleListLayoutSync = (): void => {
|
|
349
|
+
if (runtime.layoutFrameId) return;
|
|
350
|
+
runtime.layoutFrameId = requestAnimationFrame(() => {
|
|
351
|
+
runtime.layoutFrameId = 0;
|
|
352
|
+
untrack(syncListLayout);
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const bindLinkRef = (item: TocItem, element: HTMLElement): void => {
|
|
357
|
+
linkRefs.set(item.id, element);
|
|
358
|
+
if (item.id === activeLinkId) {
|
|
359
|
+
element.setAttribute('aria-current', 'location');
|
|
360
|
+
} else {
|
|
361
|
+
element.removeAttribute('aria-current');
|
|
362
|
+
}
|
|
363
|
+
scheduleListLayoutSync();
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const getScrollY = (): number => window.scrollY || window.pageYOffset || 0;
|
|
367
|
+
|
|
368
|
+
const getClockTime = (): number => window.performance?.now?.() ?? Date.now();
|
|
369
|
+
|
|
370
|
+
const clearPendingScroll = (): void => {
|
|
371
|
+
runtime.pendingScroll = null;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const beginPendingScroll = (
|
|
375
|
+
index: number,
|
|
376
|
+
top: number,
|
|
377
|
+
duration: number
|
|
378
|
+
): void => {
|
|
379
|
+
if (!Number.isInteger(index) || index < 0) return;
|
|
380
|
+
runtime.pendingScroll = {
|
|
381
|
+
index,
|
|
382
|
+
top,
|
|
383
|
+
deadline:
|
|
384
|
+
getClockTime() + Math.max(PROGRAMMATIC_SCROLL_TIMEOUT_MS, duration),
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const shouldSkipScrollActivation = (): boolean => {
|
|
389
|
+
const pending = runtime.pendingScroll;
|
|
390
|
+
if (!pending) return false;
|
|
391
|
+
if (Math.abs(getScrollY() - pending.top) <= SCROLL_REACHED_TOLERANCE) {
|
|
392
|
+
clearPendingScroll();
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
if (getClockTime() > pending.deadline) {
|
|
396
|
+
clearPendingScroll();
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
return true;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const setActive = (index: number): number => {
|
|
403
|
+
if (index === state.current.index) {
|
|
404
|
+
const current = state.items[index] || null;
|
|
405
|
+
const duration = updateIndicatorForItem(current);
|
|
406
|
+
if (duration < 0) scheduleListLayoutSync();
|
|
407
|
+
return duration;
|
|
408
|
+
}
|
|
168
409
|
const current = state.items[index] || null;
|
|
169
410
|
flushSync(() => {
|
|
170
411
|
state.current = { index, item: current };
|
|
171
412
|
});
|
|
413
|
+
syncActiveMarker(current?.id ?? null);
|
|
414
|
+
const duration = updateIndicatorForItem(current);
|
|
415
|
+
if (duration < 0) scheduleListLayoutSync();
|
|
172
416
|
settings.onChange?.(current, index, toc);
|
|
417
|
+
return duration;
|
|
173
418
|
};
|
|
174
419
|
|
|
175
420
|
const updateActive = (): void => {
|
|
176
421
|
if (!toc.runtime.built) return;
|
|
422
|
+
if (shouldSkipScrollActivation()) return;
|
|
177
423
|
let index = -1;
|
|
178
424
|
const activeOffset = settings.offset + ACTIVE_OFFSET_TOLERANCE;
|
|
179
425
|
for (let current = headings.length - 1; current >= 0; current--) {
|
|
@@ -211,13 +457,16 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
211
457
|
): void => {
|
|
212
458
|
const heading = headings.find((element) => element.id === item.id);
|
|
213
459
|
if (!heading) return;
|
|
214
|
-
const scrollY =
|
|
460
|
+
const scrollY = getScrollY();
|
|
215
461
|
const top = Math.max(
|
|
216
462
|
0,
|
|
217
463
|
heading.getBoundingClientRect().top + scrollY - settings.offset
|
|
218
464
|
);
|
|
465
|
+
if (activeIndex >= 0) {
|
|
466
|
+
const duration = setActive(activeIndex);
|
|
467
|
+
beginPendingScroll(activeIndex, top, duration);
|
|
468
|
+
}
|
|
219
469
|
window.scrollTo({ top, behavior: 'smooth' });
|
|
220
|
-
if (activeIndex >= 0) setActive(activeIndex);
|
|
221
470
|
if (updateHash && window.history?.pushState) {
|
|
222
471
|
window.history.pushState(null, '', `#${item.id}`);
|
|
223
472
|
}
|
|
@@ -226,10 +475,13 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
226
475
|
const syncFromTarget = (): void => {
|
|
227
476
|
if (toc.runtime.destroyed || !toc.runtime.built || !target) return;
|
|
228
477
|
headings = all<HTMLHeadingElement>(settings.headings, target);
|
|
229
|
-
|
|
478
|
+
clearPendingScroll();
|
|
479
|
+
const items = normalizeHeadings(headings);
|
|
480
|
+
syncActiveMarker(null);
|
|
230
481
|
flushSync(() => {
|
|
231
482
|
state.items = items;
|
|
232
483
|
state.current = { index: -1, item: null };
|
|
484
|
+
state.indicator = hiddenIndicator();
|
|
233
485
|
});
|
|
234
486
|
updateActive();
|
|
235
487
|
};
|
|
@@ -241,7 +493,7 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
241
493
|
index >= 0 &&
|
|
242
494
|
index < state.items.length
|
|
243
495
|
) {
|
|
244
|
-
scrollToItem(state.items[index]);
|
|
496
|
+
scrollToItem(state.items[index], { activeIndex: index });
|
|
245
497
|
}
|
|
246
498
|
return toc;
|
|
247
499
|
};
|
|
@@ -251,14 +503,31 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
251
503
|
props: settings,
|
|
252
504
|
state,
|
|
253
505
|
actions: { activate },
|
|
254
|
-
view: () =>
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
506
|
+
view: () => {
|
|
507
|
+
createEffect(() => {
|
|
508
|
+
const items = state.items;
|
|
509
|
+
void items.map((item) => item.id).join('\0');
|
|
510
|
+
untrack(() => {
|
|
511
|
+
pruneLinkRefs(items);
|
|
512
|
+
scheduleListLayoutSync();
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
const titleElement = settings.title
|
|
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({
|
|
262
531
|
each: renderableItems,
|
|
263
532
|
key: (item: TocItem) => item.id,
|
|
264
533
|
children: (
|
|
@@ -266,33 +535,51 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
266
535
|
indexAccessor: () => number
|
|
267
536
|
) =>
|
|
268
537
|
jsx('a', {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
settings.className.link,
|
|
273
|
-
`${settings.className.levelPrefix}${item.level}`,
|
|
274
|
-
state.current.index === indexAccessor()
|
|
275
|
-
? settings.className.active
|
|
276
|
-
: ''
|
|
277
|
-
);
|
|
278
|
-
},
|
|
538
|
+
ref: (element: HTMLElement) =>
|
|
539
|
+
bindLinkRef(itemAccessor(), element),
|
|
540
|
+
className: settings.className.link,
|
|
279
541
|
href: () => `#${itemAccessor().id}`,
|
|
280
|
-
'data-toc-
|
|
542
|
+
'data-toc-link': () => String(indexAccessor()),
|
|
281
543
|
'data-toc-target': () => itemAccessor().id,
|
|
282
|
-
'data-
|
|
283
|
-
state.current.index === indexAccessor() ? '1' : '0',
|
|
544
|
+
'data-toc-level': () => String(itemAccessor().level),
|
|
284
545
|
onClick: (event: Event) => {
|
|
285
546
|
event.preventDefault();
|
|
286
|
-
|
|
287
|
-
|
|
547
|
+
const item = itemAccessor();
|
|
548
|
+
scrollToItem(item, {
|
|
549
|
+
activeIndex: state.items.findIndex(
|
|
550
|
+
(current) => current.id === item.id
|
|
551
|
+
),
|
|
288
552
|
updateHash: true,
|
|
289
553
|
});
|
|
290
554
|
},
|
|
291
555
|
children: () => itemAccessor().text,
|
|
292
556
|
}),
|
|
293
557
|
}),
|
|
294
|
-
|
|
295
|
-
|
|
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', {
|
|
578
|
+
className: settings.className.toc,
|
|
579
|
+
'data-toc': 'root',
|
|
580
|
+
children: settings.title ? [titleElement, listView] : listView,
|
|
581
|
+
}) as HTMLElement;
|
|
582
|
+
},
|
|
296
583
|
onBuild(context) {
|
|
297
584
|
target = requireContainer(settings.target, 'Toc.target');
|
|
298
585
|
events.on('scroll', window, 'scroll', onScroll, { passive: true });
|
|
@@ -314,14 +601,20 @@ export function createToc(props: TocProps = {}): TocInstance {
|
|
|
314
601
|
onDestroy() {
|
|
315
602
|
if (runtime.frameId) cancelAnimationFrame(runtime.frameId);
|
|
316
603
|
if (runtime.refreshFrameId) cancelAnimationFrame(runtime.refreshFrameId);
|
|
604
|
+
if (runtime.layoutFrameId) cancelAnimationFrame(runtime.layoutFrameId);
|
|
317
605
|
runtime.frameId = 0;
|
|
318
606
|
runtime.refreshFrameId = 0;
|
|
607
|
+
runtime.layoutFrameId = 0;
|
|
608
|
+
clearPendingScroll();
|
|
319
609
|
runtime.ticking = false;
|
|
320
610
|
runtime.refreshing = false;
|
|
321
611
|
observer?.disconnect();
|
|
322
612
|
observer = null;
|
|
323
613
|
target = null;
|
|
324
614
|
headings = [];
|
|
615
|
+
listElement = null;
|
|
616
|
+
listHeight = 0;
|
|
617
|
+
linkRefs.clear();
|
|
325
618
|
},
|
|
326
619
|
});
|
|
327
620
|
|
package/src/css/avatar.css
CHANGED
package/src/css/components.css
CHANGED
package/src/css/editor.css
CHANGED
|
@@ -71,8 +71,6 @@
|
|
|
71
71
|
font-size: 1.5em;
|
|
72
72
|
letter-spacing: -0.02em;
|
|
73
73
|
margin-top: 2.25rem;
|
|
74
|
-
/* padding-top: 1.5rem; */
|
|
75
|
-
/* border-top: 1px solid var(--editor-border-subtle); */
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
.j-editor h3 {
|
|
@@ -348,8 +346,6 @@
|
|
|
348
346
|
background: var(--editor-code-bg);
|
|
349
347
|
font-size: 0.875em;
|
|
350
348
|
line-height: 1.6;
|
|
351
|
-
-moz-tab-size: 2;
|
|
352
|
-
-o-tab-size: 2;
|
|
353
349
|
tab-size: 2;
|
|
354
350
|
}
|
|
355
351
|
|