summaryrefslogtreecommitdiff
path: root/apps/web
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/src/constants/keybindings.ts204
-rw-r--r--apps/web/src/hooks/use-keybindings.ts76
2 files changed, 76 insertions, 204 deletions
diff --git a/apps/web/src/constants/keybindings.ts b/apps/web/src/constants/keybindings.ts
deleted file mode 100644
index a5cdea1c..00000000
--- a/apps/web/src/constants/keybindings.ts
+++ /dev/null
@@ -1,204 +0,0 @@
-import { isAppleDevice, isDOMElement, isTypableElement } from "@/lib/utils";
-import { useEffect } from "react";
-import { ActionWithOptionalArgs, invokeAction } from "./actions";
-
-/**
- * This variable keeps track whether keybindings are being accepted
- * true -> Keybindings are checked
- * false -> Key presses are ignored (Keybindings are not checked)
- */
-let keybindingsEnabled = true;
-
-/**
- * Alt is also regarded as macOS OPTION (⌥) key
- * Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
- */
-type ModifierKeys =
- | "ctrl"
- | "alt"
- | "shift"
- | "ctrl-shift"
- | "alt-shift"
- | "ctrl-alt"
- | "ctrl-alt-shift";
-
-/* eslint-disable prettier/prettier */
-// prettier-ignore
-type Key =
- | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j"
- | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t"
- | "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3"
- | "4" | "5" | "6" | "7" | "8" | "9" | "up" | "down" | "left"
- | "right" | "/" | "?" | "." | "enter" | "tab" | "space" | "home"
- | "end" | "delete" | "backspace";
-/* eslint-enable */
-
-type ModifierBasedShortcutKey = `${ModifierKeys}-${Key}`;
-// Singular keybindings (these will be disabled when an input-ish area has been focused)
-type SingleCharacterShortcutKey = `${Key}`;
-
-type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey;
-
-// Base bindings available on all platforms
-const baseBindings: {
- [_ in ShortcutKey]?: ActionWithOptionalArgs;
-} = {
- space: "toggle-play",
- j: "seek-backward",
- k: "toggle-play",
- l: "seek-forward",
- left: "frame-step-backward",
- right: "frame-step-forward",
- "shift-left": "jump-backward",
- "shift-right": "jump-forward",
- home: "goto-start",
- end: "goto-end",
- s: "split-element",
- n: "toggle-snapping",
- "ctrl-a": "select-all",
- "ctrl-d": "duplicate-selected",
- "ctrl-z": "undo",
- "ctrl-shift-z": "redo",
- "ctrl-y": "redo",
- delete: "delete-selected",
- backspace: "delete-selected",
-};
-
-/**
- * Get bindings based on the current kernel mode
- */
-function getActiveBindings(): typeof baseBindings {
- return baseBindings;
-}
-
-export const bindings = getActiveBindings();
-
-/**
- * A composable that hooks to the caller component's
- * lifecycle and hooks to the keyboard events to fire
- * the appropriate actions based on keybindings
- */
-export function useKeybindingsListener() {
- useEffect(() => {
- document.addEventListener("keydown", handleKeyDown);
-
- return () => {
- document.removeEventListener("keydown", handleKeyDown);
- };
- }, []);
-}
-
-function handleKeyDown(ev: KeyboardEvent) {
- // Do not check keybinds if the mode is disabled
- if (!keybindingsEnabled) return;
-
- const binding = generateKeybindingString(ev);
- if (!binding) return;
-
- const activeBindings = getActiveBindings();
- const boundAction = activeBindings[binding];
- if (!boundAction) return;
-
- ev.preventDefault();
-
- invokeAction(boundAction, undefined, "keypress");
-}
-
-function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
- const target = ev.target;
-
- // We may or may not have a modifier key
- const modifierKey = getActiveModifier(ev);
-
- // We will always have a non-modifier key
- const key = getPressedKey(ev);
- if (!key) return null;
-
- // All key combos backed by modifiers are valid shortcuts (whether currently typing or not)
- if (modifierKey) {
- // If the modifier is shift and the target is an input, we ignore
- if (
- modifierKey === "shift" &&
- isDOMElement(target) &&
- isTypableElement(target)
- ) {
- return null;
- }
-
- return `${modifierKey}-${key}`;
- }
-
- // no modifier key here then we do not do anything while on input
- if (isDOMElement(target) && isTypableElement(target)) return null;
-
- // single key while not input
- return `${key}`;
-}
-
-function getPressedKey(ev: KeyboardEvent): Key | null {
- // Sometimes the property code is not available on the KeyboardEvent object
- const key = (ev.key ?? "").toLowerCase();
-
- // Check arrow keys
- if (key.startsWith("arrow")) {
- return key.slice(5) as Key;
- }
-
- // Check for Tab key
- if (key === "tab") return "tab";
-
- // Check for special keys
- if (key === "home") return "home";
- if (key === "end") return "end";
- if (key === "delete") return "delete";
- if (key === "backspace") return "backspace";
-
- // Check letter keys
- const isLetter = key.length === 1 && key >= "a" && key <= "z";
- if (isLetter) return key as Key;
-
- // Check if number keys
- const isDigit = key.length === 1 && key >= "0" && key <= "9";
- if (isDigit) return key as Key;
-
- // Check if slash, period or enter
- if (key === "/" || key === "." || key === "enter") return key;
-
- // If no other cases match, this is not a valid key
- return null;
-}
-
-function getActiveModifier(ev: KeyboardEvent): ModifierKeys | null {
- const modifierKeys = {
- ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
- alt: ev.altKey,
- shift: ev.shiftKey,
- };
-
- // active modifier: ctrl | alt | ctrl-alt | ctrl-shift | ctrl-alt-shift | alt-shift
- // modiferKeys object's keys are sorted to match the above order
- const activeModifier = Object.keys(modifierKeys)
- .filter((key) => modifierKeys[key as keyof typeof modifierKeys])
- .join("-");
-
- return activeModifier === "" ? null : (activeModifier as ModifierKeys);
-}
-
-/**
- * This composable allows for the UI component to be disabled if the component in question is mounted
- */
-export function useKeybindingDisabler() {
- // TODO: Move to a lock based system that keeps the bindings disabled until all locks are lifted
- const disableKeybindings = () => {
- keybindingsEnabled = false;
- };
-
- const enableKeybindings = () => {
- keybindingsEnabled = true;
- };
-
- return {
- disableKeybindings,
- enableKeybindings,
- };
-}
diff --git a/apps/web/src/hooks/use-keybindings.ts b/apps/web/src/hooks/use-keybindings.ts
new file mode 100644
index 00000000..6c7c0d06
--- /dev/null
+++ b/apps/web/src/hooks/use-keybindings.ts
@@ -0,0 +1,76 @@
+import { useEffect } from "react";
+import { ActionWithOptionalArgs, invokeAction } from "../constants/actions";
+import { useKeybindingsStore } from "@/stores/keybindings-store";
+
+/**
+ * This variable keeps track whether keybindings are being accepted
+ * true -> Keybindings are checked
+ * false -> Key presses are ignored (Keybindings are not checked)
+ */
+let keybindingsEnabled = true;
+
+/**
+ * A composable that hooks to the caller component's
+ * lifecycle and hooks to the keyboard events to fire
+ * the appropriate actions based on keybindings
+ */
+export function useKeybindingsListener() {
+ const { keybindings, getKeybindingString } = useKeybindingsStore();
+
+ useEffect(() => {
+ const handleKeyDown = (ev: KeyboardEvent) => {
+ // Do not check keybinds if the mode is disabled
+ if (!keybindingsEnabled) return;
+
+ const binding = getKeybindingString(ev);
+ if (!binding) return;
+
+ const boundAction = keybindings[binding];
+ if (!boundAction) return;
+
+ ev.preventDefault();
+
+ // Handle actions with default arguments
+ let actionArgs: any = undefined;
+
+ if (boundAction === "seek-forward") {
+ actionArgs = { seconds: 1 };
+ } else if (boundAction === "seek-backward") {
+ actionArgs = { seconds: 1 };
+ } else if (boundAction === "jump-forward") {
+ actionArgs = { seconds: 5 };
+ } else if (boundAction === "jump-backward") {
+ actionArgs = { seconds: 5 };
+ }
+
+ invokeAction(boundAction, actionArgs, "keypress");
+ };
+
+ document.addEventListener("keydown", handleKeyDown);
+
+ return () => {
+ document.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [keybindings, getKeybindingString]);
+}
+
+/**
+ * This composable allows for the UI component to be disabled if the component in question is mounted
+ */
+export function useKeybindingDisabler() {
+ const disableKeybindings = () => {
+ keybindingsEnabled = false;
+ };
+
+ const enableKeybindings = () => {
+ keybindingsEnabled = true;
+ };
+
+ return {
+ disableKeybindings,
+ enableKeybindings,
+ };
+}
+
+// Export the bindings for backward compatibility
+export const bindings = {};