@abreen/tada 1.15.4 → 1.15.6
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/build/utils/trace-core.ts +1 -1
- package/package.json +1 -1
- package/src/hash-target.ts +29 -0
- package/src/index.ts +2 -5
- package/src/navigate/index.ts +8 -7
- package/src/slides/index.ts +1 -1
- package/src/trace/index.ts +198 -0
- package/src/trace/style.scss +30 -0
|
@@ -380,5 +380,5 @@ export function renderTraceWidgetHtml({
|
|
|
380
380
|
return `<div class="trace-source" data-trace-source-file="${escapeAttr(source.file)}"${hidden}>${source.highlightedSource}</div>`;
|
|
381
381
|
})
|
|
382
382
|
.join('');
|
|
383
|
-
return `<div class="${wrapperClass}"${wrapperAttrs}><noscript><p>This interactive trace requires JavaScript.</p></noscript><div class="trace-body"><div class="trace-toolbar"><div class="trace-controls" role="toolbar" aria-label="Trace navigation">${controls}</div></div><div class="trace-content"><div class="trace-diagram"></div><div class="trace-source-wrapper">${sourcePanels}</div></div></div></div>`;
|
|
383
|
+
return `<div class="${wrapperClass}"${wrapperAttrs}><noscript><p>This interactive trace requires JavaScript.</p></noscript><div class="trace-body"><div class="trace-toolbar"><div class="trace-controls" role="toolbar" aria-label="Trace navigation">${controls}</div></div><div class="trace-content"><div class="trace-diagram"></div><div class="trace-resizer" role="separator" aria-label="Resize trace panes" aria-orientation="horizontal" tabindex="0"></div><div class="trace-source-wrapper">${sourcePanels}</div></div></div></div>`;
|
|
384
384
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* markdown-it-anchor's slugifier uses encodeURIComponent() so generated
|
|
3
|
+
* heading IDs can safely be used as URL hashes.
|
|
4
|
+
*
|
|
5
|
+
* Tada also allows raw HTML, so an author can create a decoded ID like
|
|
6
|
+
* <h2 id="hello world">Hello</h2>. A link to that target becomes
|
|
7
|
+
* #hello%20world, and getElementById('hello%20world') would not find it.
|
|
8
|
+
*/
|
|
9
|
+
export function getHashTarget(
|
|
10
|
+
document: Document,
|
|
11
|
+
hash: string,
|
|
12
|
+
): HTMLElement | null {
|
|
13
|
+
const id = hash.slice(1);
|
|
14
|
+
if (!id) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const rawTarget = document.getElementById(id);
|
|
19
|
+
if (rawTarget) {
|
|
20
|
+
return rawTarget;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const decodedId = decodeURIComponent(id);
|
|
25
|
+
return decodedId === id ? null : document.getElementById(decodedId);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import mountTop from './top';
|
|
|
21
21
|
import mountNavigate from './navigate';
|
|
22
22
|
import mountPageUpdate from './page-update';
|
|
23
23
|
import { mountPerPageComponents } from './navigate/lifecycle';
|
|
24
|
+
import { getHashTarget } from './hash-target';
|
|
24
25
|
|
|
25
26
|
import { scheduleTask, formatDuration } from './util';
|
|
26
27
|
|
|
@@ -68,11 +69,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
68
69
|
// finished mutating the DOM. On a fresh cold load the browser already
|
|
69
70
|
// scrolled, so this is a harmless re-align.
|
|
70
71
|
if (window.location.hash) {
|
|
71
|
-
|
|
72
|
-
const el = window.document.getElementById(id);
|
|
73
|
-
if (el) {
|
|
74
|
-
el.scrollIntoView();
|
|
75
|
-
}
|
|
72
|
+
getHashTarget(window.document, window.location.hash)?.scrollIntoView();
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
for (const [name, reason] of Object.entries(failed)) {
|
package/src/navigate/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
setHistoryIndex,
|
|
14
14
|
} from './runtime';
|
|
15
15
|
import { globals } from '../globals';
|
|
16
|
+
import { getHashTarget } from '../hash-target';
|
|
16
17
|
|
|
17
18
|
function findAnchor(event: MouseEvent): HTMLAnchorElement | null {
|
|
18
19
|
const target = event.target as HTMLElement | null;
|
|
@@ -78,7 +79,11 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
78
79
|
clearSearch(window.document);
|
|
79
80
|
closeHeaderDetails(window.document);
|
|
80
81
|
if (url.hash) {
|
|
81
|
-
|
|
82
|
+
if (url.hash === window.location.hash) {
|
|
83
|
+
getHashTarget(window.document, url.hash)?.scrollIntoView();
|
|
84
|
+
} else {
|
|
85
|
+
globals.setLocationHash(window, url.hash);
|
|
86
|
+
}
|
|
82
87
|
}
|
|
83
88
|
return;
|
|
84
89
|
}
|
|
@@ -112,12 +117,8 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
// Same page, different hash: fall back to the fragment target
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const el = window.document.getElementById(hash);
|
|
118
|
-
if (el) {
|
|
119
|
-
el.scrollIntoView();
|
|
120
|
-
}
|
|
120
|
+
if (window.location.hash) {
|
|
121
|
+
getHashTarget(window.document, window.location.hash)?.scrollIntoView();
|
|
121
122
|
} else {
|
|
122
123
|
window.scrollTo({ top: 0 });
|
|
123
124
|
}
|
package/src/slides/index.ts
CHANGED
|
@@ -33,7 +33,7 @@ function isInteractive(
|
|
|
33
33
|
target instanceof view.Element ? target : target.parentElement;
|
|
34
34
|
return Boolean(
|
|
35
35
|
element?.closest(
|
|
36
|
-
'a, button, input, select, textarea, summary, label, [role="button"], [contenteditable=""], [contenteditable="true"]',
|
|
36
|
+
'a, button, input, select, textarea, summary, label, [role="button"], [role="separator"], [contenteditable=""], [contenteditable="true"]',
|
|
37
37
|
),
|
|
38
38
|
);
|
|
39
39
|
}
|
package/src/trace/index.ts
CHANGED
|
@@ -7,12 +7,20 @@ interface WidgetState {
|
|
|
7
7
|
currentStep: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
interface TraceResizeState {
|
|
11
|
+
sourceHeight: number | null;
|
|
12
|
+
dragStartY: number | null;
|
|
13
|
+
dragStartSourceHeight: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
interface WidgetElements {
|
|
11
17
|
root: HTMLElement;
|
|
12
18
|
sourceWrapper: HTMLElement;
|
|
13
19
|
controls: HTMLElement;
|
|
14
20
|
content: HTMLElement;
|
|
15
21
|
diagram: HTMLElement;
|
|
22
|
+
resizer: HTMLElement | null;
|
|
23
|
+
resizeState: TraceResizeState;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
function getStep(state: WidgetState): TraceChunkEntry {
|
|
@@ -216,6 +224,187 @@ function scaleDiagramSvg(diagram: HTMLElement, doc: Document): void {
|
|
|
216
224
|
svg.setAttribute('height', String(baseHeight * scale));
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
function parseCssPixelValue(rawValue: string | null | undefined): number {
|
|
228
|
+
if (!rawValue || rawValue === 'none') {
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const parsed = Number.parseFloat(rawValue);
|
|
233
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function elementHeight(element: HTMLElement | null): number {
|
|
237
|
+
if (!element) {
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return element.getBoundingClientRect().height || element.clientHeight;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getTraceContentGap(content: HTMLElement, doc: Document): number {
|
|
245
|
+
const view = doc.defaultView;
|
|
246
|
+
if (!view) {
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const styles = view.getComputedStyle(content);
|
|
251
|
+
return parseCssPixelValue(styles.rowGap || styles.gap);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getVisibleSourceContentHeight(
|
|
255
|
+
sourceWrapper: HTMLElement,
|
|
256
|
+
): number | null {
|
|
257
|
+
const visibleSource = Array.from(
|
|
258
|
+
sourceWrapper.querySelectorAll('.trace-source'),
|
|
259
|
+
).find(source => !(source as HTMLElement).hidden) as HTMLElement | undefined;
|
|
260
|
+
|
|
261
|
+
if (!visibleSource) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return visibleSource.scrollHeight > 0 ? visibleSource.scrollHeight : null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getResizableSourceBounds(
|
|
269
|
+
elements: WidgetElements,
|
|
270
|
+
doc: Document,
|
|
271
|
+
): { min: number; max: number } {
|
|
272
|
+
const view = doc.defaultView;
|
|
273
|
+
const sourceStyles = view?.getComputedStyle(elements.sourceWrapper);
|
|
274
|
+
const diagramStyles = view?.getComputedStyle(elements.diagram);
|
|
275
|
+
const minSourceHeight = Math.max(
|
|
276
|
+
0,
|
|
277
|
+
parseCssPixelValue(sourceStyles?.minHeight),
|
|
278
|
+
);
|
|
279
|
+
const minDiagramHeight = Math.max(
|
|
280
|
+
0,
|
|
281
|
+
parseCssPixelValue(diagramStyles?.minHeight),
|
|
282
|
+
);
|
|
283
|
+
const contentHeight = elementHeight(elements.content);
|
|
284
|
+
if (contentHeight === 0) {
|
|
285
|
+
return { min: minSourceHeight, max: Number.POSITIVE_INFINITY };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const directChildren = Array.from(elements.content.children).filter(
|
|
289
|
+
child => !child.hasAttribute('hidden'),
|
|
290
|
+
).length;
|
|
291
|
+
const gapTotal =
|
|
292
|
+
Math.max(0, directChildren - 1) * getTraceContentGap(elements.content, doc);
|
|
293
|
+
const output = elements.content.querySelector(
|
|
294
|
+
':scope > .trace-output',
|
|
295
|
+
) as HTMLElement | null;
|
|
296
|
+
const fixedHeight = elementHeight(elements.resizer) + elementHeight(output);
|
|
297
|
+
const layoutMaxSourceHeight =
|
|
298
|
+
contentHeight - minDiagramHeight - fixedHeight - gapTotal;
|
|
299
|
+
const contentMaxSourceHeight = getVisibleSourceContentHeight(
|
|
300
|
+
elements.sourceWrapper,
|
|
301
|
+
);
|
|
302
|
+
const maxSourceHeight =
|
|
303
|
+
contentMaxSourceHeight === null
|
|
304
|
+
? layoutMaxSourceHeight
|
|
305
|
+
: Math.min(layoutMaxSourceHeight, contentMaxSourceHeight);
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
min: minSourceHeight,
|
|
309
|
+
max: Math.max(minSourceHeight, maxSourceHeight),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function applySourceHeight(
|
|
314
|
+
elements: WidgetElements,
|
|
315
|
+
doc: Document,
|
|
316
|
+
nextHeight: number,
|
|
317
|
+
): void {
|
|
318
|
+
const bounds = getResizableSourceBounds(elements, doc);
|
|
319
|
+
const sourceHeight = Math.min(Math.max(nextHeight, bounds.min), bounds.max);
|
|
320
|
+
elements.resizeState.sourceHeight = sourceHeight;
|
|
321
|
+
elements.sourceWrapper.style.flex = `0 0 ${sourceHeight}px`;
|
|
322
|
+
elements.sourceWrapper.style.maxHeight = 'none';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function reclampSourceHeight(elements: WidgetElements, doc: Document): void {
|
|
326
|
+
if (elements.resizeState.sourceHeight === null) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
applySourceHeight(elements, doc, elements.resizeState.sourceHeight);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function setupTraceResizer(elements: WidgetElements, doc: Document): void {
|
|
334
|
+
const resizer = elements.resizer;
|
|
335
|
+
if (!resizer) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
resizer.addEventListener('pointerdown', (event: PointerEvent) => {
|
|
340
|
+
if (event.button !== 0) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
event.preventDefault();
|
|
345
|
+
event.stopPropagation();
|
|
346
|
+
elements.resizeState.dragStartY = event.clientY;
|
|
347
|
+
elements.resizeState.dragStartSourceHeight = elementHeight(
|
|
348
|
+
elements.sourceWrapper,
|
|
349
|
+
);
|
|
350
|
+
resizer.setPointerCapture?.(event.pointerId);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
resizer.addEventListener('pointermove', (event: PointerEvent) => {
|
|
354
|
+
const dragStartY = elements.resizeState.dragStartY;
|
|
355
|
+
if (dragStartY === null) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if ((event.buttons & 1) !== 1) {
|
|
360
|
+
elements.resizeState.dragStartY = null;
|
|
361
|
+
resizer.releasePointerCapture?.(event.pointerId);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
event.preventDefault();
|
|
366
|
+
event.stopPropagation();
|
|
367
|
+
const deltaY = event.clientY - dragStartY;
|
|
368
|
+
applySourceHeight(
|
|
369
|
+
elements,
|
|
370
|
+
doc,
|
|
371
|
+
elements.resizeState.dragStartSourceHeight - deltaY,
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const endDrag = (event: PointerEvent): void => {
|
|
376
|
+
if (elements.resizeState.dragStartY === null) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
event.stopPropagation();
|
|
382
|
+
elements.resizeState.dragStartY = null;
|
|
383
|
+
resizer.releasePointerCapture?.(event.pointerId);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
resizer.addEventListener('pointerup', endDrag);
|
|
387
|
+
resizer.addEventListener('pointercancel', endDrag);
|
|
388
|
+
resizer.addEventListener('click', (event: MouseEvent) => {
|
|
389
|
+
event.preventDefault();
|
|
390
|
+
event.stopPropagation();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
resizer.addEventListener('keydown', (event: KeyboardEvent) => {
|
|
394
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
event.preventDefault();
|
|
399
|
+
const currentHeight =
|
|
400
|
+
elements.resizeState.sourceHeight ??
|
|
401
|
+
elementHeight(elements.sourceWrapper);
|
|
402
|
+
const step = event.shiftKey ? 40 : 10;
|
|
403
|
+
const direction = event.key === 'ArrowUp' ? 1 : -1;
|
|
404
|
+
applySourceHeight(elements, doc, currentHeight + direction * step);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
219
408
|
function updateOutput(
|
|
220
409
|
content: HTMLElement,
|
|
221
410
|
output: TraceOutputEvent[],
|
|
@@ -273,6 +462,7 @@ function renderWidgetState(
|
|
|
273
462
|
output.push(...(s.output ?? []));
|
|
274
463
|
}
|
|
275
464
|
updateOutput(elements.content, output, doc);
|
|
465
|
+
reclampSourceHeight(elements, doc);
|
|
276
466
|
|
|
277
467
|
const prevHeight = elements.diagram.scrollHeight;
|
|
278
468
|
elements.diagram.innerHTML = entry.svg;
|
|
@@ -311,6 +501,7 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
|
|
|
311
501
|
) as HTMLElement;
|
|
312
502
|
const content = root.querySelector('.trace-content') as HTMLElement;
|
|
313
503
|
const diagram = root.querySelector('.trace-diagram') as HTMLElement;
|
|
504
|
+
const resizer = root.querySelector('.trace-resizer') as HTMLElement | null;
|
|
314
505
|
|
|
315
506
|
const controls = root.querySelector('.trace-controls') as HTMLElement;
|
|
316
507
|
const firstBtn = controls.querySelector('.trace-first') as HTMLButtonElement;
|
|
@@ -358,8 +549,15 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
|
|
|
358
549
|
controls,
|
|
359
550
|
content,
|
|
360
551
|
diagram,
|
|
552
|
+
resizer,
|
|
553
|
+
resizeState: {
|
|
554
|
+
sourceHeight: null,
|
|
555
|
+
dragStartY: null,
|
|
556
|
+
dragStartSourceHeight: 0,
|
|
557
|
+
},
|
|
361
558
|
};
|
|
362
559
|
|
|
560
|
+
setupTraceResizer(elements, doc);
|
|
363
561
|
renderWidgetState(state, elements, doc);
|
|
364
562
|
}
|
|
365
563
|
|
package/src/trace/style.scss
CHANGED
|
@@ -14,6 +14,36 @@
|
|
|
14
14
|
min-height: 0;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
.trace-content > .trace-resizer {
|
|
18
|
+
position: relative;
|
|
19
|
+
flex: 0 0 16px;
|
|
20
|
+
width: 100%;
|
|
21
|
+
margin-block: calc(-0.5 * var(--gap));
|
|
22
|
+
touch-action: none;
|
|
23
|
+
cursor: ns-resize;
|
|
24
|
+
|
|
25
|
+
&::before {
|
|
26
|
+
position: absolute;
|
|
27
|
+
top: 50%;
|
|
28
|
+
left: 50%;
|
|
29
|
+
width: 25%;
|
|
30
|
+
height: 6px;
|
|
31
|
+
content: '';
|
|
32
|
+
background: var(--bg2-color);
|
|
33
|
+
border-radius: 999px;
|
|
34
|
+
transform: translate(-50%, -50%);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&:focus-visible {
|
|
38
|
+
outline: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&:focus-visible::before {
|
|
42
|
+
outline: 2px solid var(--theme-color);
|
|
43
|
+
outline-offset: 2px;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
17
47
|
.trace-content > .trace-diagram {
|
|
18
48
|
flex: 1 1 100%;
|
|
19
49
|
min-height: 8rem;
|