197 lines
5.6 KiB
TypeScript
197 lines
5.6 KiB
TypeScript
|
import { useProcesses } from "contexts/process";
|
||
|
import { useSession } from "contexts/session";
|
||
|
import { useProcessesRef } from "hooks/useProcessesRef";
|
||
|
import { useEffect, useRef } from "react";
|
||
|
import {
|
||
|
haltEvent,
|
||
|
toggleFullScreen,
|
||
|
toggleShowDesktop,
|
||
|
} from "utils/functions";
|
||
|
|
||
|
type NavigatorWithKeyboard = Navigator & {
|
||
|
keyboard?: {
|
||
|
lock?: (keys?: string[]) => void;
|
||
|
unlock?: () => void;
|
||
|
};
|
||
|
};
|
||
|
|
||
|
const openStartMenu = (): void =>
|
||
|
(
|
||
|
document.querySelector(
|
||
|
"main>nav>button[title='Start']"
|
||
|
) as HTMLButtonElement
|
||
|
)?.click();
|
||
|
|
||
|
let metaDown = false;
|
||
|
let metaComboUsed = false;
|
||
|
let triggeringBinding = false;
|
||
|
|
||
|
const haltAndDebounceBinding = (event: KeyboardEvent): boolean => {
|
||
|
haltEvent(event);
|
||
|
|
||
|
if (triggeringBinding) return true;
|
||
|
|
||
|
triggeringBinding = true;
|
||
|
setTimeout(() => {
|
||
|
triggeringBinding = false;
|
||
|
}, 150);
|
||
|
|
||
|
return false;
|
||
|
};
|
||
|
|
||
|
const metaCombos = new Set(["ARROWDOWN", "ARROWUP", "D", "E", "R"]);
|
||
|
|
||
|
const useGlobalKeyboardShortcuts = (): void => {
|
||
|
const { closeWithTransition, maximize, minimize, open } = useProcesses();
|
||
|
const processesRef = useProcessesRef();
|
||
|
const { foregroundId } = useSession();
|
||
|
const altBindingsRef = useRef<Record<string, () => void>>({});
|
||
|
const shiftBindingsRef = useRef<Record<string, () => void>>({
|
||
|
E: () => open("FileExplorer"),
|
||
|
ESCAPE: openStartMenu,
|
||
|
F10: () => open("Terminal"),
|
||
|
F12: () => open("DevTools"),
|
||
|
F5: () => window.location.reload(),
|
||
|
R: () => open("Run"),
|
||
|
});
|
||
|
|
||
|
useEffect(() => {
|
||
|
const onKeyDown = (event: KeyboardEvent): void => {
|
||
|
const { altKey, ctrlKey, key, shiftKey } = event;
|
||
|
const keyName = key?.toUpperCase();
|
||
|
|
||
|
if (!keyName) return;
|
||
|
|
||
|
if (shiftKey) {
|
||
|
if (
|
||
|
(ctrlKey || !metaCombos.has(keyName)) &&
|
||
|
shiftBindingsRef.current?.[keyName] &&
|
||
|
!haltAndDebounceBinding(event)
|
||
|
) {
|
||
|
shiftBindingsRef.current[keyName]();
|
||
|
}
|
||
|
} else if (keyName === "F11") {
|
||
|
haltEvent(event);
|
||
|
toggleFullScreen();
|
||
|
} else if (
|
||
|
document.activeElement === document.body &&
|
||
|
keyName.startsWith("ARROW")
|
||
|
) {
|
||
|
document.body.querySelector("main ol li button")?.dispatchEvent(
|
||
|
new MouseEvent("mousedown", {
|
||
|
bubbles: true,
|
||
|
})
|
||
|
);
|
||
|
} else if (document.fullscreenElement) {
|
||
|
if (keyName === "META") metaDown = true;
|
||
|
else if (altKey && altBindingsRef.current?.[keyName]) {
|
||
|
haltEvent(event);
|
||
|
altBindingsRef.current?.[keyName]?.();
|
||
|
} else if (keyName === "ESCAPE") {
|
||
|
setTimeout(
|
||
|
// eslint-disable-next-line unicorn/consistent-destructuring
|
||
|
() => !event.defaultPrevented && document.exitFullscreen(),
|
||
|
0
|
||
|
);
|
||
|
} else if (
|
||
|
metaDown &&
|
||
|
metaCombos.has(keyName) &&
|
||
|
shiftBindingsRef.current?.[keyName] &&
|
||
|
!haltAndDebounceBinding(event)
|
||
|
) {
|
||
|
metaComboUsed = true;
|
||
|
shiftBindingsRef.current[keyName]();
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
const onKeyUp = (event: KeyboardEvent): void => {
|
||
|
if (
|
||
|
metaDown &&
|
||
|
document.fullscreenElement &&
|
||
|
event.key?.toUpperCase() === "META"
|
||
|
) {
|
||
|
metaDown = false;
|
||
|
if (metaComboUsed) metaComboUsed = false;
|
||
|
else openStartMenu();
|
||
|
}
|
||
|
};
|
||
|
const onFullScreen = ({ target }: Event): void => {
|
||
|
if (target === document.documentElement) {
|
||
|
try {
|
||
|
if (document.fullscreenElement) {
|
||
|
(navigator as NavigatorWithKeyboard)?.keyboard?.lock?.([
|
||
|
"MetaLeft",
|
||
|
"MetaRight",
|
||
|
"Escape",
|
||
|
]);
|
||
|
} else {
|
||
|
(navigator as NavigatorWithKeyboard)?.keyboard?.unlock?.();
|
||
|
}
|
||
|
} catch {
|
||
|
// Ignore failure to lock keys
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
document.addEventListener("keydown", onKeyDown, {
|
||
|
capture: true,
|
||
|
});
|
||
|
document.addEventListener("keyup", onKeyUp, {
|
||
|
capture: true,
|
||
|
passive: true,
|
||
|
});
|
||
|
document.addEventListener("fullscreenchange", onFullScreen, {
|
||
|
passive: true,
|
||
|
});
|
||
|
|
||
|
return () => {
|
||
|
document.removeEventListener("keydown", onKeyDown);
|
||
|
document.removeEventListener("keyup", onKeyUp);
|
||
|
document.removeEventListener("fullscreenchange", onFullScreen);
|
||
|
};
|
||
|
}, []);
|
||
|
|
||
|
useEffect(() => {
|
||
|
altBindingsRef.current = {
|
||
|
...altBindingsRef.current,
|
||
|
F4: () => closeWithTransition(foregroundId),
|
||
|
};
|
||
|
}, [closeWithTransition, foregroundId]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
shiftBindingsRef.current = {
|
||
|
...shiftBindingsRef.current,
|
||
|
ARROWDOWN: () => {
|
||
|
const {
|
||
|
hideMinimizeButton = false,
|
||
|
maximized,
|
||
|
minimized,
|
||
|
} = processesRef.current[foregroundId];
|
||
|
|
||
|
if (maximized) {
|
||
|
maximize(foregroundId);
|
||
|
} else if (!minimized && !hideMinimizeButton) {
|
||
|
minimize(foregroundId);
|
||
|
}
|
||
|
},
|
||
|
ARROWUP: () => {
|
||
|
const {
|
||
|
allowResizing = true,
|
||
|
hideMaximizeButton = false,
|
||
|
maximized,
|
||
|
minimized,
|
||
|
} = processesRef.current[foregroundId];
|
||
|
|
||
|
if (minimized) {
|
||
|
minimize(foregroundId);
|
||
|
} else if (!maximized && allowResizing && !hideMaximizeButton) {
|
||
|
maximize(foregroundId);
|
||
|
}
|
||
|
},
|
||
|
D: () => toggleShowDesktop(processesRef.current, minimize),
|
||
|
};
|
||
|
}, [foregroundId, maximize, minimize, processesRef]);
|
||
|
};
|
||
|
|
||
|
export default useGlobalKeyboardShortcuts;
|