@abreen/tada 1.11.2 → 1.13.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 +65 -64
- package/bin/tada.ts +61 -66
- package/build/build-manifest.ts +14 -3
- package/build/bundle.ts +0 -1
- package/build/config-files.ts +183 -0
- package/build/config-loader.ts +159 -0
- package/build/copy.ts +0 -83
- package/build/generate-content-assets.ts +75 -160
- package/build/globals.ts +48 -89
- package/build/log.ts +27 -18
- package/build/pagefind.ts +33 -26
- package/build/pipeline.ts +98 -79
- package/build/reachability.ts +51 -248
- package/build/serve.ts +35 -6
- package/build/site-variables.ts +20 -11
- package/build/source-model.ts +400 -0
- package/build/{watch/records.ts → source-records.ts} +111 -49
- package/build/template-globals.ts +91 -0
- package/build/templates.ts +50 -40
- package/build/test-helpers.ts +15 -0
- package/build/toc-plugin.ts +5 -1
- package/build/types.d.ts +9 -1
- package/build/util.ts +8 -0
- package/build/utils/code.ts +6 -16
- package/build/utils/content-files.ts +57 -126
- package/build/utils/final-html.ts +239 -0
- package/build/utils/markdown.ts +237 -17
- package/build/utils/paths.ts +2 -1
- package/build/utils/render.ts +108 -99
- package/build/validate-config-links.ts +91 -14
- package/build/watch/assets.ts +0 -2
- package/build/watch/build-full.ts +14 -15
- package/build/watch/build-incremental.ts +15 -8
- package/build/watch/compiler-types.d.ts +6 -1
- package/build/watch/compiler.ts +27 -15
- package/build/watch/index.ts +2 -7
- package/build/watch/planner.ts +32 -16
- package/build/watch/reload.ts +4 -0
- package/build/watch/runtime.ts +29 -45
- package/build/watch/snapshot.ts +50 -402
- package/build/watch/validation.ts +6 -3
- package/build/watch-reachability-state.ts +16 -23
- package/build/watch-reload-client.ts +11 -4
- package/init/content/index.md +1 -1
- package/init/content/labs/02/index.md +61 -0
- package/init/content/labs/02/selection_sort.py +23 -0
- package/init/content/labs/index.md +4 -2
- package/init/content/lectures/01/index.md +1 -1
- package/init/content/markdown.md +1 -1
- package/package.json +3 -4
- package/src/code/index.ts +46 -18
- package/src/code.scss +2 -1
- package/src/globals.ts +47 -0
- package/src/index.ts +1 -0
- package/src/navigate/index.ts +2 -5
- package/src/navigate/lifecycle.ts +3 -1
- package/src/navigate/runtime.ts +6 -5
- package/src/page-update/index.ts +20 -16
- package/src/search/index.ts +53 -25
- package/src/search/style.scss +1 -1
- package/src/slides/index.ts +467 -0
- package/src/slides/style.scss +104 -0
- package/src/style.scss +8 -3
- package/src/timezone/index.ts +16 -20
- package/src/top/index.ts +2 -4
- package/src/trace/index.ts +91 -7
- package/src/trace/style.scss +1 -1
- package/src/util.ts +6 -2
- package/templates/_author.html +2 -2
- package/templates/_heading.html +1 -1
- package/templates/_nav.html +2 -2
- package/templates/_theme.scss +3 -3
- package/templates/_top.html +14 -14
- package/templates/default.html +8 -0
- package/types/dev.d.ts +1 -1
- package/types/site-variables.d.ts +4 -5
- package/watch/fs-commit.ts +63 -10
- package/build/apply-base-path-plugin.ts +0 -186
- package/build/content-watch.ts +0 -104
- package/build/validate-internal-links-plugin.ts +0 -207
package/src/search/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
hasResponseValidatorsChanged,
|
|
7
7
|
type ResponseValidators,
|
|
8
8
|
} from '../validators';
|
|
9
|
+
import { globals } from '../globals';
|
|
9
10
|
|
|
10
11
|
const MAX_RESULTS = 24;
|
|
11
12
|
|
|
@@ -39,6 +40,11 @@ interface Pagefind {
|
|
|
39
40
|
|
|
40
41
|
let pagefind: Pagefind | null = null;
|
|
41
42
|
|
|
43
|
+
// Unit tests need to clear the module-scoped Pagefind instance between cases.
|
|
44
|
+
export function resetPagefindForTest() {
|
|
45
|
+
pagefind = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
type SubResult = { title: string; url: string; excerpt: string };
|
|
43
49
|
|
|
44
50
|
type State = {
|
|
@@ -48,18 +54,18 @@ type State = {
|
|
|
48
54
|
totalResults: number;
|
|
49
55
|
};
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
type SearchState = Pick<State, 'results' | 'totalResults'> | null;
|
|
58
|
+
|
|
59
|
+
async function doSearch(query: string, window: Window): Promise<SearchState> {
|
|
52
60
|
if (pagefind == null) {
|
|
53
|
-
return;
|
|
61
|
+
return null;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
if (!
|
|
57
|
-
|
|
58
|
-
state.totalResults = 0;
|
|
59
|
-
return;
|
|
64
|
+
if (!query) {
|
|
65
|
+
return { results: [], totalResults: 0 };
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
const search = await pagefind.search(
|
|
68
|
+
const search = await pagefind.search(query);
|
|
63
69
|
const slice = search.results.slice(0, MAX_RESULTS);
|
|
64
70
|
const data = await Promise.all(slice.map(r => r.data()));
|
|
65
71
|
|
|
@@ -95,8 +101,7 @@ async function doSearch(state: State) {
|
|
|
95
101
|
});
|
|
96
102
|
|
|
97
103
|
const grouped = groupPdfResults(results);
|
|
98
|
-
|
|
99
|
-
state.results = grouped;
|
|
104
|
+
return { totalResults: grouped.length, results: grouped };
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
function applyHighlight(
|
|
@@ -295,15 +300,16 @@ export default (window: Window) => {
|
|
|
295
300
|
return;
|
|
296
301
|
}
|
|
297
302
|
|
|
298
|
-
pagefind = (await
|
|
299
|
-
applyBasePath('/pagefind/pagefind.js')
|
|
303
|
+
pagefind = (await globals.importModule(
|
|
304
|
+
applyBasePath('/pagefind/pagefind.js'),
|
|
300
305
|
)) as Pagefind;
|
|
301
306
|
|
|
302
307
|
await pagefind.init();
|
|
303
308
|
|
|
304
|
-
const res = await fetch(
|
|
305
|
-
|
|
306
|
-
|
|
309
|
+
const res = await globals.fetch(
|
|
310
|
+
applyBasePath('/pagefind/pagefind-entry.json'),
|
|
311
|
+
{ cache: 'no-cache' },
|
|
312
|
+
);
|
|
307
313
|
|
|
308
314
|
if (res.ok) {
|
|
309
315
|
entryValidators = getResponseValidators(res);
|
|
@@ -311,24 +317,26 @@ export default (window: Window) => {
|
|
|
311
317
|
}
|
|
312
318
|
|
|
313
319
|
async function checkForIndexUpdate() {
|
|
314
|
-
if (indexCheckInFlight ||
|
|
320
|
+
if (indexCheckInFlight || globals.now() - lastIndexCheck < 3000) {
|
|
315
321
|
return;
|
|
316
322
|
}
|
|
317
|
-
lastIndexCheck =
|
|
323
|
+
lastIndexCheck = globals.now();
|
|
318
324
|
indexCheckInFlight = true;
|
|
319
325
|
try {
|
|
320
|
-
const res = await fetch(
|
|
321
|
-
|
|
322
|
-
cache: 'no-cache',
|
|
323
|
-
|
|
326
|
+
const res = await globals.fetch(
|
|
327
|
+
applyBasePath('/pagefind/pagefind-entry.json'),
|
|
328
|
+
{ method: 'HEAD', cache: 'no-cache' },
|
|
329
|
+
);
|
|
324
330
|
if (!res.ok) {
|
|
325
331
|
return;
|
|
326
332
|
}
|
|
327
333
|
const newValidators = getResponseValidators(res);
|
|
328
334
|
if (hasResponseValidatorsChanged(entryValidators, newValidators)) {
|
|
335
|
+
invalidateUpdates();
|
|
329
336
|
pagefind = null;
|
|
330
337
|
await loadPagefind();
|
|
331
|
-
|
|
338
|
+
const updateId = ++latestUpdateId;
|
|
339
|
+
await update(updateId);
|
|
332
340
|
}
|
|
333
341
|
} catch {
|
|
334
342
|
// best-effort
|
|
@@ -348,17 +356,36 @@ export default (window: Window) => {
|
|
|
348
356
|
totalResults: 0,
|
|
349
357
|
};
|
|
350
358
|
|
|
351
|
-
|
|
359
|
+
let latestUpdateId = 0;
|
|
360
|
+
|
|
361
|
+
async function update(updateId: number) {
|
|
352
362
|
if (state.showResults) {
|
|
353
363
|
render(input!, resultsContainer, state, true);
|
|
354
364
|
}
|
|
355
|
-
|
|
365
|
+
const query = state.value;
|
|
366
|
+
const nextState = await doSearch(query, window);
|
|
367
|
+
if (updateId !== latestUpdateId) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (nextState) {
|
|
371
|
+
state.results = nextState.results;
|
|
372
|
+
state.totalResults = nextState.totalResults;
|
|
373
|
+
}
|
|
356
374
|
if (!state.showResults) {
|
|
357
375
|
return;
|
|
358
376
|
}
|
|
359
377
|
render(input!, resultsContainer, state, false);
|
|
360
378
|
}
|
|
361
379
|
|
|
380
|
+
function queueUpdate() {
|
|
381
|
+
const updateId = ++latestUpdateId;
|
|
382
|
+
update(updateId).catch(() => {});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function invalidateUpdates() {
|
|
386
|
+
latestUpdateId += 1;
|
|
387
|
+
}
|
|
388
|
+
|
|
362
389
|
function hide() {
|
|
363
390
|
if (!state.showResults) {
|
|
364
391
|
return;
|
|
@@ -374,13 +401,14 @@ export default (window: Window) => {
|
|
|
374
401
|
}
|
|
375
402
|
state.value = value;
|
|
376
403
|
if (value.length === 0) {
|
|
404
|
+
invalidateUpdates();
|
|
377
405
|
hide();
|
|
378
406
|
state.results = [];
|
|
379
407
|
state.totalResults = 0;
|
|
380
408
|
return;
|
|
381
409
|
}
|
|
382
410
|
state.showResults = true;
|
|
383
|
-
|
|
411
|
+
queueUpdate();
|
|
384
412
|
}
|
|
385
413
|
|
|
386
414
|
function handleWindowPointerMove() {
|
|
@@ -397,7 +425,7 @@ export default (window: Window) => {
|
|
|
397
425
|
checkForIndexUpdate().catch(() => {});
|
|
398
426
|
if (!state.showResults) {
|
|
399
427
|
state.showResults = true;
|
|
400
|
-
|
|
428
|
+
queueUpdate();
|
|
401
429
|
}
|
|
402
430
|
}
|
|
403
431
|
|
package/src/search/style.scss
CHANGED
|
@@ -129,6 +129,7 @@ header {
|
|
|
129
129
|
|
|
130
130
|
ol a .title {
|
|
131
131
|
font-weight: 500;
|
|
132
|
+
line-height: var(--heading-line-height);
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
ol a .title.code-page code {
|
|
@@ -178,7 +179,6 @@ header {
|
|
|
178
179
|
.title {
|
|
179
180
|
font-size: var(--font-size-small);
|
|
180
181
|
font-weight: 500;
|
|
181
|
-
line-height: var(--heading-line-height);
|
|
182
182
|
color: var(--fg-color);
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
interface TraceToolbarState {
|
|
2
|
+
hidden: boolean;
|
|
3
|
+
ariaHidden: string | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type PresentationMode = 'normal' | 'fullscreen';
|
|
7
|
+
|
|
8
|
+
function isInteractive(
|
|
9
|
+
view: Window & typeof globalThis,
|
|
10
|
+
target: EventTarget | null,
|
|
11
|
+
): boolean {
|
|
12
|
+
if (!(target instanceof view.Node)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const element =
|
|
17
|
+
target instanceof view.Element ? target : target.parentElement;
|
|
18
|
+
return Boolean(
|
|
19
|
+
element?.closest(
|
|
20
|
+
'a, button, input, select, textarea, summary, label, [role="button"], [contenteditable=""], [contenteditable="true"], .question-a-body',
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function updateActiveSlide(
|
|
26
|
+
slides: HTMLElement[],
|
|
27
|
+
activeIndex: number,
|
|
28
|
+
isPresenting: boolean,
|
|
29
|
+
): void {
|
|
30
|
+
for (const [index, slide] of slides.entries()) {
|
|
31
|
+
slide.classList.toggle('is-active', isPresenting && index === activeIndex);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isTraceReady(widget: HTMLElement): boolean {
|
|
36
|
+
return (
|
|
37
|
+
widget.querySelector('.trace-line-active') !== null ||
|
|
38
|
+
widget.querySelector('.trace-diagram > *') !== null
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function mountSlides(window: Window): void | (() => void) {
|
|
43
|
+
const { document } = window;
|
|
44
|
+
const view = document.defaultView;
|
|
45
|
+
const presentButton = document.querySelector(
|
|
46
|
+
'[data-slides-present]',
|
|
47
|
+
) as HTMLButtonElement | null;
|
|
48
|
+
const presentFullscreenButton = document.querySelector(
|
|
49
|
+
'[data-slides-present-fullscreen]',
|
|
50
|
+
) as HTMLButtonElement | null;
|
|
51
|
+
const rootScopeButton = presentButton ?? presentFullscreenButton;
|
|
52
|
+
const scopedRoot = rootScopeButton
|
|
53
|
+
?.closest('main')
|
|
54
|
+
?.querySelector('[data-slides-root]');
|
|
55
|
+
const root = (scopedRoot ??
|
|
56
|
+
document.querySelector('[data-slides-root]')) as HTMLElement | null;
|
|
57
|
+
|
|
58
|
+
if (!view || !root || (!presentButton && !presentFullscreenButton)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const domView = view;
|
|
63
|
+
const domDocument = document as Document & {
|
|
64
|
+
fullscreenElement?: Element | null;
|
|
65
|
+
exitFullscreen?: () => Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
const fullscreenTarget = document.documentElement as HTMLElement & {
|
|
68
|
+
requestFullscreen?: () => Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
const slidesRoot = root;
|
|
71
|
+
const slides = Array.from(
|
|
72
|
+
slidesRoot.querySelectorAll('[data-slide-index]'),
|
|
73
|
+
) as HTMLElement[];
|
|
74
|
+
if (slides.length === 0) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (presentButton) {
|
|
79
|
+
presentButton.disabled = false;
|
|
80
|
+
}
|
|
81
|
+
if (presentFullscreenButton) {
|
|
82
|
+
presentFullscreenButton.disabled = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const overlay = document.createElement('div');
|
|
86
|
+
overlay.className = 'slides-toolbar';
|
|
87
|
+
overlay.hidden = true;
|
|
88
|
+
overlay.dataset.slidesOverlay = '';
|
|
89
|
+
|
|
90
|
+
const closeButton = document.createElement('button');
|
|
91
|
+
closeButton.type = 'button';
|
|
92
|
+
closeButton.textContent = 'Close';
|
|
93
|
+
closeButton.dataset.slidesClose = '';
|
|
94
|
+
|
|
95
|
+
overlay.append(closeButton);
|
|
96
|
+
document.body.appendChild(overlay);
|
|
97
|
+
|
|
98
|
+
let isPresenting = false;
|
|
99
|
+
let activeIndex = -1;
|
|
100
|
+
let presentationMode: PresentationMode | null = null;
|
|
101
|
+
let isToolbarPinnedVisible = false;
|
|
102
|
+
|
|
103
|
+
const toolbarStates = new WeakMap<HTMLElement, TraceToolbarState>();
|
|
104
|
+
const slideTabIndexes = new WeakMap<HTMLElement, string | null>();
|
|
105
|
+
|
|
106
|
+
function hidePresentationCursor(): void {
|
|
107
|
+
document.body.classList.add('is-presentation-cursor-hidden');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function showPresentationCursor(): void {
|
|
111
|
+
document.body.classList.remove('is-presentation-cursor-hidden');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hidePresentationControls(resetPinned = true): void {
|
|
115
|
+
if (resetPinned) {
|
|
116
|
+
isToolbarPinnedVisible = false;
|
|
117
|
+
}
|
|
118
|
+
overlay.hidden = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function showPresentationControls(): void {
|
|
122
|
+
if (!isPresenting || presentationMode === 'fullscreen') {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
overlay.hidden = false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function pinPresentationControlsVisible(): void {
|
|
130
|
+
if (!isPresenting || presentationMode === 'fullscreen') {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
isToolbarPinnedVisible = true;
|
|
135
|
+
overlay.hidden = false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getToolbarRevealBottom(): number {
|
|
139
|
+
const wasHidden = overlay.hidden;
|
|
140
|
+
if (wasHidden) {
|
|
141
|
+
overlay.hidden = false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const bottom = closeButton.getBoundingClientRect().bottom;
|
|
145
|
+
|
|
146
|
+
if (wasHidden && !isToolbarPinnedVisible) {
|
|
147
|
+
overlay.hidden = true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return bottom;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function setTraceToolbarVisibility(hidden: boolean): void {
|
|
154
|
+
const toolbars = slidesRoot.querySelectorAll('.trace-toolbar');
|
|
155
|
+
for (const toolbar of toolbars) {
|
|
156
|
+
const element = toolbar as HTMLElement;
|
|
157
|
+
|
|
158
|
+
if (hidden) {
|
|
159
|
+
if (!toolbarStates.has(element)) {
|
|
160
|
+
toolbarStates.set(element, {
|
|
161
|
+
hidden: element.hidden,
|
|
162
|
+
ariaHidden: element.getAttribute('aria-hidden'),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
element.hidden = true;
|
|
166
|
+
element.setAttribute('aria-hidden', 'true');
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const previous = toolbarStates.get(element);
|
|
171
|
+
element.hidden = previous?.hidden ?? false;
|
|
172
|
+
if (previous?.ariaHidden == null) {
|
|
173
|
+
element.removeAttribute('aria-hidden');
|
|
174
|
+
} else {
|
|
175
|
+
element.setAttribute('aria-hidden', previous.ariaHidden);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function setActiveSlide(index: number): void {
|
|
181
|
+
activeIndex = Math.max(0, Math.min(index, slides.length - 1));
|
|
182
|
+
updateActiveSlide(slides, activeIndex, isPresenting);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function focusSlide(slide: HTMLElement | undefined): void {
|
|
186
|
+
if (!slide) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!slideTabIndexes.has(slide)) {
|
|
191
|
+
slideTabIndexes.set(slide, slide.getAttribute('tabindex'));
|
|
192
|
+
}
|
|
193
|
+
slide.tabIndex = -1;
|
|
194
|
+
slide.focus();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function restoreSlideFocusability(): void {
|
|
198
|
+
for (const slide of slides) {
|
|
199
|
+
const prior = slideTabIndexes.get(slide);
|
|
200
|
+
if (prior === undefined) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (prior == null) {
|
|
204
|
+
slide.removeAttribute('tabindex');
|
|
205
|
+
} else {
|
|
206
|
+
slide.setAttribute('tabindex', prior);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function exitPresentation(): void {
|
|
212
|
+
if (!isPresenting) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const shouldExitFullscreen =
|
|
217
|
+
presentationMode === 'fullscreen' &&
|
|
218
|
+
domDocument.fullscreenElement != null &&
|
|
219
|
+
typeof domDocument.exitFullscreen === 'function';
|
|
220
|
+
|
|
221
|
+
isPresenting = false;
|
|
222
|
+
activeIndex = -1;
|
|
223
|
+
presentationMode = null;
|
|
224
|
+
hidePresentationControls();
|
|
225
|
+
showPresentationCursor();
|
|
226
|
+
document.body.classList.remove('is-presenting');
|
|
227
|
+
updateActiveSlide(slides, activeIndex, isPresenting);
|
|
228
|
+
restoreSlideFocusability();
|
|
229
|
+
setTraceToolbarVisibility(false);
|
|
230
|
+
|
|
231
|
+
if (shouldExitFullscreen) {
|
|
232
|
+
void domDocument.exitFullscreen?.();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function resetReadyTraces(): void {
|
|
237
|
+
const widgets = slidesRoot.querySelectorAll(
|
|
238
|
+
'.trace-widget:not(.trace-disabled)',
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
for (const widget of widgets) {
|
|
242
|
+
const widgetEl = widget as HTMLElement;
|
|
243
|
+
if (!isTraceReady(widgetEl)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const firstButton = widgetEl.querySelector(
|
|
248
|
+
'.trace-first:not([disabled])',
|
|
249
|
+
) as HTMLButtonElement | null;
|
|
250
|
+
|
|
251
|
+
firstButton?.click();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function enterPresentation(mode: PresentationMode): void {
|
|
256
|
+
isPresenting = true;
|
|
257
|
+
presentationMode = mode;
|
|
258
|
+
hidePresentationControls();
|
|
259
|
+
showPresentationCursor();
|
|
260
|
+
document.body.classList.add('is-presenting');
|
|
261
|
+
setTraceToolbarVisibility(true);
|
|
262
|
+
resetReadyTraces();
|
|
263
|
+
setActiveSlide(0);
|
|
264
|
+
focusSlide(slides[0]);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function driveTrace(delta: 1 | -1): boolean {
|
|
268
|
+
if (activeIndex < 0) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const slide = slides[activeIndex];
|
|
273
|
+
const selector =
|
|
274
|
+
delta === 1
|
|
275
|
+
? '.trace-next:not([disabled])'
|
|
276
|
+
: '.trace-prev:not([disabled])';
|
|
277
|
+
const widgets = slide.querySelectorAll(
|
|
278
|
+
'.trace-widget:not(.trace-disabled)',
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
for (const widget of widgets) {
|
|
282
|
+
const widgetEl = widget as HTMLElement;
|
|
283
|
+
if (!isTraceReady(widgetEl)) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const button = widgetEl.querySelector(
|
|
288
|
+
selector,
|
|
289
|
+
) as HTMLButtonElement | null;
|
|
290
|
+
if (!button) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
button.click();
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function move(delta: 1 | -1): void {
|
|
302
|
+
if (!isPresenting) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
hidePresentationCursor();
|
|
307
|
+
|
|
308
|
+
if (delta === -1) {
|
|
309
|
+
hidePresentationControls();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (driveTrace(delta)) {
|
|
313
|
+
if (delta === 1) {
|
|
314
|
+
hidePresentationControls();
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (delta === 1 && activeIndex >= slides.length - 1) {
|
|
320
|
+
pinPresentationControlsVisible();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (delta === 1) {
|
|
325
|
+
hidePresentationControls();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
setActiveSlide(activeIndex + delta);
|
|
329
|
+
focusSlide(slides[activeIndex]);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function handlePresentClick(): void {
|
|
333
|
+
enterPresentation('normal');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function handlePresentFullscreenClick(): Promise<void> {
|
|
337
|
+
enterPresentation('fullscreen');
|
|
338
|
+
|
|
339
|
+
if (typeof fullscreenTarget.requestFullscreen !== 'function') {
|
|
340
|
+
presentationMode = 'normal';
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
await fullscreenTarget.requestFullscreen();
|
|
346
|
+
} catch {
|
|
347
|
+
presentationMode = 'normal';
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function handlePresentFullscreenButtonClick(): void {
|
|
352
|
+
void handlePresentFullscreenClick();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function handleCloseClick(): void {
|
|
356
|
+
exitPresentation();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function handleFullscreenChange(): void {
|
|
360
|
+
if (
|
|
361
|
+
isPresenting &&
|
|
362
|
+
presentationMode === 'fullscreen' &&
|
|
363
|
+
domDocument.fullscreenElement == null
|
|
364
|
+
) {
|
|
365
|
+
exitPresentation();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
370
|
+
if (!isPresenting) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (
|
|
375
|
+
(event.key === 'ArrowRight' ||
|
|
376
|
+
event.key === 'ArrowLeft' ||
|
|
377
|
+
event.key === ' ' ||
|
|
378
|
+
event.key === 'Spacebar') &&
|
|
379
|
+
isInteractive(domView, document.activeElement)
|
|
380
|
+
) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (event.key === 'ArrowRight') {
|
|
385
|
+
event.preventDefault();
|
|
386
|
+
move(1);
|
|
387
|
+
} else if (event.key === 'ArrowLeft') {
|
|
388
|
+
event.preventDefault();
|
|
389
|
+
move(-1);
|
|
390
|
+
} else if (event.key === ' ' || event.key === 'Spacebar') {
|
|
391
|
+
event.preventDefault();
|
|
392
|
+
move(1);
|
|
393
|
+
} else if (event.key === 'Escape') {
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
exitPresentation();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function handleSlideClick(event: MouseEvent): void {
|
|
400
|
+
if (!isPresenting || isInteractive(domView, event.target)) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const activeSlide = slides[activeIndex];
|
|
405
|
+
if (!activeSlide) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (
|
|
410
|
+
!(event.target instanceof domView.Node) ||
|
|
411
|
+
!activeSlide.contains(event.target)
|
|
412
|
+
) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
move(1);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function handleMouseMove(event: MouseEvent): void {
|
|
420
|
+
if (!isPresenting) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
showPresentationCursor();
|
|
425
|
+
|
|
426
|
+
if (presentationMode === 'fullscreen') {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (isToolbarPinnedVisible) {
|
|
431
|
+
showPresentationControls();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (event.clientY <= getToolbarRevealBottom()) {
|
|
436
|
+
showPresentationControls();
|
|
437
|
+
} else {
|
|
438
|
+
hidePresentationControls(false);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
presentButton?.addEventListener('click', handlePresentClick);
|
|
443
|
+
presentFullscreenButton?.addEventListener(
|
|
444
|
+
'click',
|
|
445
|
+
handlePresentFullscreenButtonClick,
|
|
446
|
+
);
|
|
447
|
+
closeButton.addEventListener('click', handleCloseClick);
|
|
448
|
+
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
449
|
+
window.addEventListener('keydown', handleKeydown);
|
|
450
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
451
|
+
slidesRoot.addEventListener('click', handleSlideClick);
|
|
452
|
+
|
|
453
|
+
return () => {
|
|
454
|
+
exitPresentation();
|
|
455
|
+
presentButton?.removeEventListener('click', handlePresentClick);
|
|
456
|
+
presentFullscreenButton?.removeEventListener(
|
|
457
|
+
'click',
|
|
458
|
+
handlePresentFullscreenButtonClick,
|
|
459
|
+
);
|
|
460
|
+
closeButton.removeEventListener('click', handleCloseClick);
|
|
461
|
+
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
462
|
+
window.removeEventListener('keydown', handleKeydown);
|
|
463
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
464
|
+
slidesRoot.removeEventListener('click', handleSlideClick);
|
|
465
|
+
overlay.remove();
|
|
466
|
+
};
|
|
467
|
+
}
|