Skip to content

Commit dab6d7d

Browse files
feat: added basic browser functionality
You can now press: `right arrow`, `l`, `d`, `enter`, `space` to go to the next slide, and: `left arrow`, a`, `h` to go to the previous slide. There is also mouse, touch screen, and scroll support to navigate to a different slide.
1 parent 3c2e83b commit dab6d7d

File tree

2 files changed

+233
-5
lines changed

2 files changed

+233
-5
lines changed

lib/core/browser/html.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
baseTailwind,
77
tailwindConfig,
88
} from '#lib/core/browser/css';
9+
import { setupScriptCode } from '#lib/core/browser/page-logic.browser';
910

1011
/** The properties a */
1112
export interface SlydeHtmlDocumentHtmlProperties extends SlydeHtmlDocumentCssProperties {
@@ -36,13 +37,13 @@ export const htmlDocument = function htmlDocument(args: SlydeHtmlDocumentHtmlPro
3637
<!DOCTYPE html>
3738
<html lang="en">
3839
<head>
39-
<!-- <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> -->
40-
<!-- TODO: wget this script and then inject it as a string instead. -->
41-
<script src="https://cdn.tailwindcss.com"></script>
42-
<script> tailwind.config = ${JSON.stringify(tailwindConfig({ ...args }))}; </script>
4340
<style> ${baseCSS({ ...args })} </style>
4441
${baseTailwind()}
4542
<title>${args.title}</title>
43+
<!-- TODO: wget this script and then inject it as a string instead. -->
44+
<script src="https://cdn.tailwindcss.com"></script>
45+
<script id="tailwind-config-setup"> tailwind.config = ${JSON.stringify(tailwindConfig({ ...args }))}; </script>
46+
<script id="event-listening-setup" type="module">${setupScriptCode}</script>
4647
<meta charset="UTF-8">
4748
<meta name="darkreader-lock">
4849
<meta property="og:title" content="${args.title}">
@@ -57,7 +58,7 @@ export const htmlDocument = function htmlDocument(args: SlydeHtmlDocumentHtmlPro
5758
<meta name="theme-color" content="${args.primary}" />
5859
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
5960
<link rel="icon" href="${args.icon}">
60-
<link rel="apple-touch-icon" href="${args.icon}">
61+
<link rel="apple-touch-icon" href="${args.icon}">
6162
</head>
6263
<body>
6364
${args.content}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/* eslint-disable no-console */
2+
3+
import type { DeepReadonly } from '#lib/types';
4+
5+
let hideTimeout: NodeJS.Timeout | undefined; // eslint-disable-line @typescript-eslint/init-declarations
6+
7+
/** Hides mouse cursor when its not moving, shows the mouse cursor when it is. */
8+
export const handleMouseMove = function handleMouseMove(): void {
9+
document.documentElement.style.cursor = "default";
10+
11+
clearTimeout(hideTimeout);
12+
13+
const second = 1000;
14+
hideTimeout = setTimeout(() => {
15+
document.documentElement.style.cursor = 'none';
16+
}, second);
17+
}
18+
19+
/**
20+
* The Regex the Hash will comply with.
21+
*/
22+
export const regex = /^#slide-(?<slide>\d+)/iu;
23+
24+
/**
25+
* Sets the browsers location hash (the `#id` part) to the given index.
26+
*/
27+
export const setCurrentSlideUrlHash = function setCurrentSlideUrlHash(index: number): void {
28+
window.location.hash = `slide-${index}`;
29+
};
30+
31+
/**
32+
* Gets the browser's location hash and returns the slide index. If `allowMissing` is true,
33+
* returns `number | undefined` when no slide is found; otherwise, throws an error.
34+
*
35+
* **Example**
36+
* ```TypeScript
37+
* // throws an error
38+
* window.location.hash = "$slide-a";
39+
* getCurrentSlideUrlHash();
40+
*
41+
* // returns null
42+
* window.location.hash = "$slide-a";
43+
* getCurrentSlideUrlHash({ allowMissing: true });
44+
*
45+
* // returns the number 1
46+
* window.location.hash = "$slide-1";
47+
* getCurrentSlideUrlHash({ ... });
48+
* ```
49+
*
50+
* Used to control the page's slide index.
51+
*/
52+
export function getCurrentSlideUrlHash(): number;
53+
export function getCurrentSlideUrlHash({
54+
allowMissing,
55+
}: {
56+
readonly allowMissing: true;
57+
}): number | null;
58+
export function getCurrentSlideUrlHash({
59+
allowMissing = false,
60+
}: {
61+
readonly allowMissing?: boolean;
62+
} = {}): number | null {
63+
const match = regex.exec(window.location.hash);
64+
65+
if (!match?.groups) {
66+
if (allowMissing) return null;
67+
throw new Error(
68+
`Expected window.location.hash to be of the form ${regex}, but found ${window.location.hash}.`
69+
);
70+
}
71+
72+
const { slide } = match.groups;
73+
return Number.parseInt(slide, 10);
74+
}
75+
76+
/** Goes to the next slide. */
77+
export const goToNextSlide = function goToNextSlide(): void {
78+
const currentSlide = getCurrentSlideUrlHash();
79+
const nextSlide = currentSlide + 1;
80+
console.log(`Going to the next slide: ${nextSlide}`);
81+
setCurrentSlideUrlHash(nextSlide);
82+
};
83+
84+
/** Goes to the previous slide. */
85+
export const goToPreviousSlide = function goToPreviousSlide(): void {
86+
const currentSlide = getCurrentSlideUrlHash();
87+
const previousSlide = currentSlide - 1;
88+
89+
if (previousSlide < 1) {
90+
console.log(`At the start of the presentation, cannot go back further.`);
91+
return;
92+
}
93+
94+
console.log(`Going to the previous slide: ${previousSlide}`);
95+
setCurrentSlideUrlHash(previousSlide);
96+
};
97+
98+
/**
99+
* The callback to exec when the window has loaded to ensure that a hash slide is set.
100+
*/
101+
const setupUrlHashCallBack = function setupUrlHashCallBack(): void {
102+
const currentSlide = getCurrentSlideUrlHash({ allowMissing: true });
103+
if (currentSlide === null) setCurrentSlideUrlHash(1);
104+
};
105+
106+
/**
107+
* Watches for changes in the hash of the URL (the # part), and resets the hash if it is incorrect.
108+
*/
109+
export const handleUrlHashChange = function handleUrlHashChange(
110+
event: DeepReadonly<HashChangeEvent>
111+
): void {
112+
const oldUrl = new URL(event.oldURL);
113+
const newURL = new URL(event.newURL);
114+
const match = regex.exec(newURL.hash);
115+
116+
if (!match?.groups) {
117+
let message = `The new URL hash: ${newURL.hash} does not have a hash of the right shape: ${regex}.`;
118+
message += `Resetting it to the old value of ${oldUrl.hash}`;
119+
console.warn(message);
120+
history.replaceState(null, '', event.oldURL);
121+
return;
122+
}
123+
124+
console.log(`Moved to: ${event.newURL}`);
125+
};
126+
127+
/** Handles mouse presses on the document, and moves slides accordingly. */
128+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
129+
export const handleMousePresses = function handleMousePresses(event: MouseEvent): void {
130+
// Letting the "handle touch" function handle clicks if they are on mobile.
131+
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) return;
132+
133+
const leftClick = 0;
134+
if (event.button === leftClick) goToNextSlide();
135+
136+
const rightClick = 2;
137+
if (event.button === rightClick) goToPreviousSlide();
138+
};
139+
140+
/** Handles key presses on the document, and moves slides accordingly. */
141+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
142+
export const handleKeyPresses = function handleKeyPresses(event: KeyboardEvent): void {
143+
switch (event.key) {
144+
case 'Enter':
145+
case ' ':
146+
case 'd':
147+
case 'l':
148+
case 'ArrowRight':
149+
goToNextSlide();
150+
break;
151+
case 'a':
152+
case 'h':
153+
case 'ArrowLeft':
154+
goToPreviousSlide();
155+
break;
156+
default:
157+
}
158+
};
159+
160+
/** Handles scrolling through on the document, and moves slides accordingly. */
161+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
162+
export const handleScrollEvents = function handleScrollEvents(event: WheelEvent): void {
163+
if (event.deltaY < 0) goToNextSlide();
164+
if (event.deltaY > 0) goToPreviousSlide();
165+
};
166+
167+
/**
168+
* Handles touch events the screen. Goes to the next slide, when tapping the right half,
169+
* and the previous slide when tapping the left half of the screen.
170+
*/
171+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
172+
export const handleTouchEvents = function handleTouchEvents(event: TouchEvent): void {
173+
const half = 2;
174+
const screenWidth = window.innerWidth;
175+
for (const touch of event.touches) {
176+
177+
// Ignoring touches outside of the presentation, or are not on document nodes.
178+
const { target } = touch;
179+
if (!(target instanceof Node)) continue;
180+
if (!document.body.contains(target)) continue;
181+
if (target === document.documentElement) continue;
182+
183+
if (touch.clientX > screenWidth / half) goToNextSlide();
184+
else goToPreviousSlide();
185+
}
186+
}
187+
188+
/** Prevents context menus from showing up when right clicking. */
189+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
190+
export const preventContextMenus = function preventContextMenus(event: MouseEvent): void {
191+
event.preventDefault();
192+
};
193+
194+
/** Adds a bunch of events listeners as setup. */
195+
export const addEventListeners = function addEventListeners(): void {
196+
window.addEventListener('prev:slide', goToPreviousSlide);
197+
window.addEventListener('next:slide', goToNextSlide);
198+
window.addEventListener('contextmenu', preventContextMenus);
199+
window.addEventListener('load', setupUrlHashCallBack);
200+
window.addEventListener('hashchange', handleUrlHashChange);
201+
window.addEventListener('keydown', handleKeyPresses);
202+
window.addEventListener('mousedown', handleMousePresses);
203+
window.addEventListener('wheel', handleScrollEvents);
204+
window.addEventListener('mousemove', handleMouseMove);
205+
window.addEventListener("touchstart", handleTouchEvents);
206+
};
207+
208+
/** The full setup code to run when first opening the document */
209+
// eslint-disable-next-line no-inline-comments
210+
export const setupScriptCode = /*JavaScript*/ `
211+
"use strict";
212+
let hideTimeout;
213+
${handleMouseMove.toString()}
214+
const regex = ${regex.toString()};
215+
${setCurrentSlideUrlHash.toString()}
216+
${getCurrentSlideUrlHash.toString()}
217+
${goToNextSlide.toString()}
218+
${goToPreviousSlide.toString()}
219+
${setupUrlHashCallBack.toString()}
220+
${handleUrlHashChange.toString()}
221+
${handleMousePresses.toString()}
222+
${handleKeyPresses.toString()}
223+
${handleScrollEvents.toString()}
224+
${handleTouchEvents.toString()}
225+
${preventContextMenus.toString()}
226+
(${addEventListeners.toString()})(); // Executes immediately.
227+
`;

0 commit comments

Comments
 (0)