From 75bb8c104e89091ef2d4950ba06e1d23b5815ea0 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 17 Feb 2026 20:05:53 +0530 Subject: [PATCH] ENG-1438: Port Keyboard shortcut keys/triggers settings --- .../roam/src/components/DiscourseNodeMenu.tsx | 25 ++-- .../components/DiscourseNodeSearchMenu.tsx | 114 +++++++++--------- .../settings/HomePersonalSettings.tsx | 1 + .../settings/KeyboardShortcutInput.tsx | 73 +++-------- .../settings/utils/zodSchema.example.ts | 8 +- .../components/settings/utils/zodSchema.ts | 11 +- 6 files changed, 103 insertions(+), 129 deletions(-) diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index d73975c2f..f458e80a7 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -27,6 +27,7 @@ import { getNewDiscourseNodeText } from "~/utils/formatUtils"; import { OnloadArgs } from "roamjs-components/types"; import { formatHexColor } from "./settings/DiscourseNodeCanvasSettings"; import posthog from "posthog-js"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; type Props = { textarea?: HTMLTextAreaElement; @@ -406,6 +407,13 @@ export const getModifiersFromCombo = (comboKey: IKeyCombo) => { ].filter(Boolean); }; +export const comboToString = (combo: IKeyCombo): string => { + if (!combo.key) return ""; + const modifiers = getModifiersFromCombo(combo); + const comboString = [...modifiers, combo.key].join("+"); + return normalizeKeyCombo(comboString).join("+"); +}; + export const NodeMenuTriggerComponent = ({ extensionAPI, }: { @@ -427,19 +435,15 @@ export const NodeMenuTriggerComponent = ({ const comboObj = getKeyCombo(e.nativeEvent); if (!comboObj.key) return; - setComboKey({ key: comboObj.key, modifiers: comboObj.modifiers }); - extensionAPI.settings.set("personal-node-menu-trigger", comboObj); + const combo = { key: comboObj.key, modifiers: comboObj.modifiers }; + setComboKey(combo); + void extensionAPI.settings.set("personal-node-menu-trigger", combo); + setPersonalSetting(["Personal node menu trigger"], combo); }, [extensionAPI], ); - const shortcut = useMemo(() => { - if (!comboKey.key) return ""; - - const modifiers = getModifiersFromCombo(comboKey); - const comboString = [...modifiers, comboKey.key].join("+"); - return normalizeKeyCombo(comboString).join("+"); - }, [comboKey]); + const shortcut = useMemo(() => comboToString(comboKey), [comboKey]); return ( { setComboKey({ modifiers: 0, key: "" }); - extensionAPI.settings.set("personal-node-menu-trigger", ""); + void extensionAPI.settings.set("personal-node-menu-trigger", ""); + setPersonalSetting(["Personal node menu trigger"], ""); }} minimal /> diff --git a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx index b64d7b36d..bdb01241a 100644 --- a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx @@ -26,6 +26,7 @@ import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpr import { Result } from "~/utils/types"; import { getSetting } from "~/utils/extensionSettings"; import fuzzy from "fuzzy"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; type Props = { textarea: HTMLTextAreaElement; @@ -232,61 +233,61 @@ const NodeSearchMenu = ({ const onSelect = useCallback( (item: Result) => { - if (!blockUid) { - onClose(); - return; - } - void waitForBlock({ uid: blockUid, text: textarea.value }) - .then(() => { - onClose(); - - setTimeout(() => { - const originalText = getTextByBlockUid(blockUid); - - const prefix = originalText.substring(0, triggerPosition); - const suffix = originalText.substring(textarea.selectionStart); - const pageRef = `[[${item.text}]]`; - - const newText = `${prefix}${pageRef}${suffix}`; - void updateBlock({ uid: blockUid, text: newText }).then(() => { - const newCursorPosition = triggerPosition + pageRef.length; - - if (window.roamAlphaAPI.ui.setBlockFocusAndSelection) { - void window.roamAlphaAPI.ui.setBlockFocusAndSelection({ - location: { - // eslint-disable-next-line @typescript-eslint/naming-convention - "block-uid": blockUid, - // eslint-disable-next-line @typescript-eslint/naming-convention - "window-id": windowId, - }, - selection: { start: newCursorPosition }, - }); - } else { - setTimeout(() => { - const textareaElements = - document.querySelectorAll("textarea"); - for (const el of textareaElements) { - if (getUids(el).blockUid === blockUid) { - el.focus(); - el.setSelectionRange( - newCursorPosition, - newCursorPosition, - ); - break; - } - } - }, 50); - } - }); - posthog.capture("Discourse Node: Selected from Search Menu", { - id: item.id, - text: item.text, - }); - }, 10); - }) - .catch((error) => { - console.error("Error waiting for block:", error); - }); + if (!blockUid) { + onClose(); + return; + } + void waitForBlock({ uid: blockUid, text: textarea.value }) + .then(() => { + onClose(); + + setTimeout(() => { + const originalText = getTextByBlockUid(blockUid); + + const prefix = originalText.substring(0, triggerPosition); + const suffix = originalText.substring(textarea.selectionStart); + const pageRef = `[[${item.text}]]`; + + const newText = `${prefix}${pageRef}${suffix}`; + void updateBlock({ uid: blockUid, text: newText }).then(() => { + const newCursorPosition = triggerPosition + pageRef.length; + + if (window.roamAlphaAPI.ui.setBlockFocusAndSelection) { + void window.roamAlphaAPI.ui.setBlockFocusAndSelection({ + location: { + // eslint-disable-next-line @typescript-eslint/naming-convention + "block-uid": blockUid, + // eslint-disable-next-line @typescript-eslint/naming-convention + "window-id": windowId, + }, + selection: { start: newCursorPosition }, + }); + } else { + setTimeout(() => { + const textareaElements = + document.querySelectorAll("textarea"); + for (const el of textareaElements) { + if (getUids(el).blockUid === blockUid) { + el.focus(); + el.setSelectionRange( + newCursorPosition, + newCursorPosition, + ); + break; + } + } + }, 50); + } + }); + posthog.capture("Discourse Node: Selected from Search Menu", { + id: item.id, + text: item.text, + }); + }, 10); + }) + .catch((error) => { + console.error("Error waiting for block:", error); + }); }, [blockUid, onClose, textarea, triggerPosition, windowId], ); @@ -627,7 +628,8 @@ export const NodeSearchMenuTriggerSetting = ({ .trim(); setNodeSearchTrigger(trigger); - extensionAPI.settings.set("node-search-trigger", trigger); + void extensionAPI.settings.set("node-search-trigger", trigger); + setPersonalSetting(["Node search menu trigger"], trigger); }; return ( { { - const platform = - typeof navigator !== "undefined" ? navigator.platform : undefined; - return platform == null ? false : /Mac|iPod|iPhone|iPad/.test(platform); -}; - -const MODIFIER_BIT_MASKS = { - alt: 1, - ctrl: 2, - meta: 4, - shift: 8, -}; - -const ALIASES: { [key: string]: string } = { - cmd: "meta", - command: "meta", - escape: "esc", - minus: "-", - mod: isMac() ? "meta" : "ctrl", - option: "alt", - plus: "+", - return: "enter", - win: "meta", -}; - -const normalizeKeyCombo = (combo: string) => { - const keys = combo.replace(/\s/g, "").split("+"); - return keys.map((key) => { - const keyName = ALIASES[key] != null ? ALIASES[key] : key; - return keyName === "meta" ? (isMac() ? "cmd" : "win") : keyName; - }); -}; - -const getModifiersFromCombo = (comboKey: IKeyCombo) => { - if (!comboKey) return []; - return [ - comboKey.modifiers & MODIFIER_BIT_MASKS.alt && "alt", - comboKey.modifiers & MODIFIER_BIT_MASKS.ctrl && "ctrl", - comboKey.modifiers & MODIFIER_BIT_MASKS.shift && "shift", - comboKey.modifiers & MODIFIER_BIT_MASKS.meta && "meta", - ].filter(Boolean); -}; - const KeyboardShortcutInput = ({ onloadArgs, settingKey, + blockPropKey, label, description, placeholder = "Click to set shortcut", @@ -104,6 +64,7 @@ const KeyboardShortcutInput = ({ extensionAPI.settings .set(settingKey, comboObj) .catch(() => console.error("Failed to set setting")); + setPersonalSetting([blockPropKey], comboObj); } return; } @@ -112,28 +73,26 @@ const KeyboardShortcutInput = ({ const comboObj = getKeyCombo(e.nativeEvent); if (!comboObj.key) return; - setComboKey({ key: comboObj.key, modifiers: comboObj.modifiers }); + const combo = { key: comboObj.key, modifiers: comboObj.modifiers }; + setComboKey(combo); extensionAPI.settings - .set(settingKey, comboObj) + .set(settingKey, combo) .catch(() => console.error("Failed to set setting")); + setPersonalSetting([blockPropKey], combo); }, - [extensionAPI, settingKey], + [extensionAPI, settingKey, blockPropKey], ); - const shortcut = useMemo(() => { - if (!comboKey.key) return ""; - - const modifiers = getModifiersFromCombo(comboKey); - const comboString = [...modifiers, comboKey.key].join("+"); - return normalizeKeyCombo(comboString).join("+"); - }, [comboKey]); + const shortcut = useMemo(() => comboToString(comboKey), [comboKey]); const handleClear = useCallback(() => { - setComboKey({ modifiers: 0, key: "" }); + const clearedCombo = { modifiers: 0, key: "" }; + setComboKey(clearedCombo); extensionAPI.settings - .set(settingKey, { modifiers: 0, key: "" }) + .set(settingKey, clearedCombo) .catch(() => console.error("Failed to set setting")); - }, [extensionAPI, settingKey]); + setPersonalSetting([blockPropKey], clearedCombo); + }, [extensionAPI, settingKey, blockPropKey]); return (