@abreen/tada 1.11.3 → 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.
Files changed (79) hide show
  1. package/README.md +65 -64
  2. package/bin/tada.ts +61 -66
  3. package/build/build-manifest.ts +14 -3
  4. package/build/bundle.ts +0 -1
  5. package/build/config-files.ts +183 -0
  6. package/build/config-loader.ts +159 -0
  7. package/build/copy.ts +0 -83
  8. package/build/generate-content-assets.ts +75 -160
  9. package/build/globals.ts +48 -89
  10. package/build/log.ts +27 -18
  11. package/build/pagefind.ts +33 -26
  12. package/build/pipeline.ts +98 -79
  13. package/build/reachability.ts +51 -248
  14. package/build/serve.ts +35 -6
  15. package/build/site-variables.ts +20 -11
  16. package/build/source-model.ts +400 -0
  17. package/build/{watch/records.ts → source-records.ts} +111 -49
  18. package/build/template-globals.ts +91 -0
  19. package/build/templates.ts +50 -40
  20. package/build/test-helpers.ts +15 -0
  21. package/build/toc-plugin.ts +5 -1
  22. package/build/types.d.ts +9 -1
  23. package/build/util.ts +8 -0
  24. package/build/utils/code.ts +6 -16
  25. package/build/utils/content-files.ts +57 -126
  26. package/build/utils/final-html.ts +239 -0
  27. package/build/utils/markdown.ts +237 -17
  28. package/build/utils/paths.ts +2 -1
  29. package/build/utils/render.ts +108 -99
  30. package/build/validate-config-links.ts +91 -14
  31. package/build/watch/assets.ts +0 -2
  32. package/build/watch/build-full.ts +14 -15
  33. package/build/watch/build-incremental.ts +15 -8
  34. package/build/watch/compiler-types.d.ts +6 -1
  35. package/build/watch/compiler.ts +27 -15
  36. package/build/watch/index.ts +2 -7
  37. package/build/watch/planner.ts +32 -16
  38. package/build/watch/reload.ts +4 -0
  39. package/build/watch/runtime.ts +29 -45
  40. package/build/watch/snapshot.ts +50 -402
  41. package/build/watch/validation.ts +6 -3
  42. package/build/watch-reachability-state.ts +16 -23
  43. package/build/watch-reload-client.ts +11 -4
  44. package/init/content/index.md +1 -1
  45. package/init/content/labs/02/index.md +61 -0
  46. package/init/content/labs/02/selection_sort.py +23 -0
  47. package/init/content/labs/index.md +4 -2
  48. package/init/content/lectures/01/index.md +1 -1
  49. package/init/content/markdown.md +1 -1
  50. package/package.json +3 -4
  51. package/src/code/index.ts +46 -18
  52. package/src/code.scss +2 -1
  53. package/src/globals.ts +47 -0
  54. package/src/index.ts +1 -0
  55. package/src/navigate/index.ts +2 -5
  56. package/src/navigate/lifecycle.ts +3 -1
  57. package/src/navigate/runtime.ts +6 -5
  58. package/src/page-update/index.ts +20 -16
  59. package/src/search/index.ts +53 -25
  60. package/src/slides/index.ts +467 -0
  61. package/src/slides/style.scss +104 -0
  62. package/src/style.scss +8 -3
  63. package/src/timezone/index.ts +16 -20
  64. package/src/top/index.ts +2 -4
  65. package/src/trace/index.ts +91 -7
  66. package/src/trace/style.scss +1 -1
  67. package/src/util.ts +6 -2
  68. package/templates/_author.html +2 -2
  69. package/templates/_heading.html +1 -1
  70. package/templates/_nav.html +2 -2
  71. package/templates/_theme.scss +3 -3
  72. package/templates/_top.html +14 -14
  73. package/templates/default.html +8 -0
  74. package/types/dev.d.ts +1 -1
  75. package/types/site-variables.d.ts +4 -5
  76. package/watch/fs-commit.ts +63 -10
  77. package/build/apply-base-path-plugin.ts +0 -186
  78. package/build/content-watch.ts +0 -104
  79. package/build/validate-internal-links-plugin.ts +0 -207
@@ -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
- async function doSearch(state: State) {
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 (!state.value) {
57
- state.results = [];
58
- state.totalResults = 0;
59
- return;
64
+ if (!query) {
65
+ return { results: [], totalResults: 0 };
60
66
  }
61
67
 
62
- const search = await pagefind.search(state.value);
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
- state.totalResults = grouped.length;
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 import(
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(applyBasePath('/pagefind/pagefind-entry.json'), {
305
- cache: 'no-cache',
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 || Date.now() - lastIndexCheck < 3000) {
320
+ if (indexCheckInFlight || globals.now() - lastIndexCheck < 3000) {
315
321
  return;
316
322
  }
317
- lastIndexCheck = Date.now();
323
+ lastIndexCheck = globals.now();
318
324
  indexCheckInFlight = true;
319
325
  try {
320
- const res = await fetch(applyBasePath('/pagefind/pagefind-entry.json'), {
321
- method: 'HEAD',
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
- await update();
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
- async function update() {
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
- await doSearch(state);
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
- update().catch(() => {});
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
- update().catch(() => {});
428
+ queueUpdate();
401
429
  }
402
430
  }
403
431
 
@@ -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
+ }