Skip to content

Commit fdb14fe

Browse files
committed
ENG-1438: Port Keyboard shortcut keys/triggers settings
1 parent 0359747 commit fdb14fe

6 files changed

Lines changed: 103 additions & 129 deletions

File tree

apps/roam/src/components/DiscourseNodeMenu.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { getNewDiscourseNodeText } from "~/utils/formatUtils";
2727
import { OnloadArgs } from "roamjs-components/types";
2828
import { formatHexColor } from "./settings/DiscourseNodeCanvasSettings";
2929
import posthog from "posthog-js";
30+
import { setPersonalSetting } from "~/components/settings/utils/accessors";
3031

3132
type Props = {
3233
textarea?: HTMLTextAreaElement;
@@ -406,6 +407,13 @@ export const getModifiersFromCombo = (comboKey: IKeyCombo) => {
406407
].filter(Boolean);
407408
};
408409

410+
export const comboToString = (combo: IKeyCombo): string => {
411+
if (!combo.key) return "";
412+
const modifiers = getModifiersFromCombo(combo);
413+
const comboString = [...modifiers, combo.key].join("+");
414+
return normalizeKeyCombo(comboString).join("+");
415+
};
416+
409417
export const NodeMenuTriggerComponent = ({
410418
extensionAPI,
411419
}: {
@@ -427,19 +435,15 @@ export const NodeMenuTriggerComponent = ({
427435
const comboObj = getKeyCombo(e.nativeEvent);
428436
if (!comboObj.key) return;
429437

430-
setComboKey({ key: comboObj.key, modifiers: comboObj.modifiers });
431-
extensionAPI.settings.set("personal-node-menu-trigger", comboObj);
438+
const combo = { key: comboObj.key, modifiers: comboObj.modifiers };
439+
setComboKey(combo);
440+
void extensionAPI.settings.set("personal-node-menu-trigger", combo);
441+
setPersonalSetting(["Personal node menu trigger"], combo);
432442
},
433443
[extensionAPI],
434444
);
435445

436-
const shortcut = useMemo(() => {
437-
if (!comboKey.key) return "";
438-
439-
const modifiers = getModifiersFromCombo(comboKey);
440-
const comboString = [...modifiers, comboKey.key].join("+");
441-
return normalizeKeyCombo(comboString).join("+");
442-
}, [comboKey]);
446+
const shortcut = useMemo(() => comboToString(comboKey), [comboKey]);
443447

444448
return (
445449
<InputGroup
@@ -455,7 +459,8 @@ export const NodeMenuTriggerComponent = ({
455459
icon={"remove"}
456460
onClick={() => {
457461
setComboKey({ modifiers: 0, key: "" });
458-
extensionAPI.settings.set("personal-node-menu-trigger", "");
462+
void extensionAPI.settings.set("personal-node-menu-trigger", "");
463+
setPersonalSetting(["Personal node menu trigger"], "");
459464
}}
460465
minimal
461466
/>

apps/roam/src/components/DiscourseNodeSearchMenu.tsx

Lines changed: 58 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpr
2626
import { Result } from "~/utils/types";
2727
import { getSetting } from "~/utils/extensionSettings";
2828
import fuzzy from "fuzzy";
29+
import { setPersonalSetting } from "~/components/settings/utils/accessors";
2930

3031
type Props = {
3132
textarea: HTMLTextAreaElement;
@@ -232,61 +233,61 @@ const NodeSearchMenu = ({
232233

233234
const onSelect = useCallback(
234235
(item: Result) => {
235-
if (!blockUid) {
236-
onClose();
237-
return;
238-
}
239-
void waitForBlock({ uid: blockUid, text: textarea.value })
240-
.then(() => {
241-
onClose();
242-
243-
setTimeout(() => {
244-
const originalText = getTextByBlockUid(blockUid);
245-
246-
const prefix = originalText.substring(0, triggerPosition);
247-
const suffix = originalText.substring(textarea.selectionStart);
248-
const pageRef = `[[${item.text}]]`;
249-
250-
const newText = `${prefix}${pageRef}${suffix}`;
251-
void updateBlock({ uid: blockUid, text: newText }).then(() => {
252-
const newCursorPosition = triggerPosition + pageRef.length;
253-
254-
if (window.roamAlphaAPI.ui.setBlockFocusAndSelection) {
255-
void window.roamAlphaAPI.ui.setBlockFocusAndSelection({
256-
location: {
257-
// eslint-disable-next-line @typescript-eslint/naming-convention
258-
"block-uid": blockUid,
259-
// eslint-disable-next-line @typescript-eslint/naming-convention
260-
"window-id": windowId,
261-
},
262-
selection: { start: newCursorPosition },
263-
});
264-
} else {
265-
setTimeout(() => {
266-
const textareaElements =
267-
document.querySelectorAll("textarea");
268-
for (const el of textareaElements) {
269-
if (getUids(el).blockUid === blockUid) {
270-
el.focus();
271-
el.setSelectionRange(
272-
newCursorPosition,
273-
newCursorPosition,
274-
);
275-
break;
276-
}
277-
}
278-
}, 50);
279-
}
280-
});
281-
posthog.capture("Discourse Node: Selected from Search Menu", {
282-
id: item.id,
283-
text: item.text,
284-
});
285-
}, 10);
286-
})
287-
.catch((error) => {
288-
console.error("Error waiting for block:", error);
289-
});
236+
if (!blockUid) {
237+
onClose();
238+
return;
239+
}
240+
void waitForBlock({ uid: blockUid, text: textarea.value })
241+
.then(() => {
242+
onClose();
243+
244+
setTimeout(() => {
245+
const originalText = getTextByBlockUid(blockUid);
246+
247+
const prefix = originalText.substring(0, triggerPosition);
248+
const suffix = originalText.substring(textarea.selectionStart);
249+
const pageRef = `[[${item.text}]]`;
250+
251+
const newText = `${prefix}${pageRef}${suffix}`;
252+
void updateBlock({ uid: blockUid, text: newText }).then(() => {
253+
const newCursorPosition = triggerPosition + pageRef.length;
254+
255+
if (window.roamAlphaAPI.ui.setBlockFocusAndSelection) {
256+
void window.roamAlphaAPI.ui.setBlockFocusAndSelection({
257+
location: {
258+
// eslint-disable-next-line @typescript-eslint/naming-convention
259+
"block-uid": blockUid,
260+
// eslint-disable-next-line @typescript-eslint/naming-convention
261+
"window-id": windowId,
262+
},
263+
selection: { start: newCursorPosition },
264+
});
265+
} else {
266+
setTimeout(() => {
267+
const textareaElements =
268+
document.querySelectorAll("textarea");
269+
for (const el of textareaElements) {
270+
if (getUids(el).blockUid === blockUid) {
271+
el.focus();
272+
el.setSelectionRange(
273+
newCursorPosition,
274+
newCursorPosition,
275+
);
276+
break;
277+
}
278+
}
279+
}, 50);
280+
}
281+
});
282+
posthog.capture("Discourse Node: Selected from Search Menu", {
283+
id: item.id,
284+
text: item.text,
285+
});
286+
}, 10);
287+
})
288+
.catch((error) => {
289+
console.error("Error waiting for block:", error);
290+
});
290291
},
291292
[blockUid, onClose, textarea, triggerPosition, windowId],
292293
);
@@ -627,7 +628,8 @@ export const NodeSearchMenuTriggerSetting = ({
627628
.trim();
628629

629630
setNodeSearchTrigger(trigger);
630-
extensionAPI.settings.set("node-search-trigger", trigger);
631+
void extensionAPI.settings.set("node-search-trigger", trigger);
632+
setPersonalSetting(["Node search menu trigger"], trigger);
631633
};
632634
return (
633635
<InputGroup

apps/roam/src/components/settings/HomePersonalSettings.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
5757
<KeyboardShortcutInput
5858
onloadArgs={onloadArgs}
5959
settingKey={DISCOURSE_TOOL_SHORTCUT_KEY}
60+
blockPropKey="Discourse tool shortcut"
6061
label="Discourse tool keyboard shortcut"
6162
description="Set a single key to activate the discourse tool in tldraw. Only single keys (no modifiers) are supported. Leave empty for no shortcut."
6263
placeholder="Click to set single key"

apps/roam/src/components/settings/KeyboardShortcutInput.tsx

Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,62 +9,22 @@ import {
99
} from "@blueprintjs/core";
1010
import Description from "roamjs-components/components/Description";
1111
import { DISCOURSE_TOOL_SHORTCUT_KEY } from "~/data/userSettings";
12+
import { setPersonalSetting } from "~/components/settings/utils/accessors";
13+
import { comboToString } from "~/components/DiscourseNodeMenu";
1214

1315
type KeyboardShortcutInputProps = {
1416
onloadArgs: OnloadArgs;
1517
settingKey: string;
18+
blockPropKey: string;
1619
label: string;
1720
description: string;
1821
placeholder?: string;
1922
};
2023

21-
// Reuse the keyboard combo utilities from NodeMenuTriggerComponent
22-
const isMac = () => {
23-
const platform =
24-
typeof navigator !== "undefined" ? navigator.platform : undefined;
25-
return platform == null ? false : /Mac|iPod|iPhone|iPad/.test(platform);
26-
};
27-
28-
const MODIFIER_BIT_MASKS = {
29-
alt: 1,
30-
ctrl: 2,
31-
meta: 4,
32-
shift: 8,
33-
};
34-
35-
const ALIASES: { [key: string]: string } = {
36-
cmd: "meta",
37-
command: "meta",
38-
escape: "esc",
39-
minus: "-",
40-
mod: isMac() ? "meta" : "ctrl",
41-
option: "alt",
42-
plus: "+",
43-
return: "enter",
44-
win: "meta",
45-
};
46-
47-
const normalizeKeyCombo = (combo: string) => {
48-
const keys = combo.replace(/\s/g, "").split("+");
49-
return keys.map((key) => {
50-
const keyName = ALIASES[key] != null ? ALIASES[key] : key;
51-
return keyName === "meta" ? (isMac() ? "cmd" : "win") : keyName;
52-
});
53-
};
54-
55-
const getModifiersFromCombo = (comboKey: IKeyCombo) => {
56-
if (!comboKey) return [];
57-
return [
58-
comboKey.modifiers & MODIFIER_BIT_MASKS.alt && "alt",
59-
comboKey.modifiers & MODIFIER_BIT_MASKS.ctrl && "ctrl",
60-
comboKey.modifiers & MODIFIER_BIT_MASKS.shift && "shift",
61-
comboKey.modifiers & MODIFIER_BIT_MASKS.meta && "meta",
62-
].filter(Boolean);
63-
};
64-
6524
const KeyboardShortcutInput = ({
6625
onloadArgs,
6726
settingKey,
27+
blockPropKey,
6828
label,
6929
description,
7030
placeholder = "Click to set shortcut",
@@ -104,6 +64,7 @@ const KeyboardShortcutInput = ({
10464
extensionAPI.settings
10565
.set(settingKey, comboObj)
10666
.catch(() => console.error("Failed to set setting"));
67+
setPersonalSetting([blockPropKey], comboObj);
10768
}
10869
return;
10970
}
@@ -112,28 +73,26 @@ const KeyboardShortcutInput = ({
11273
const comboObj = getKeyCombo(e.nativeEvent);
11374
if (!comboObj.key) return;
11475

115-
setComboKey({ key: comboObj.key, modifiers: comboObj.modifiers });
76+
const combo = { key: comboObj.key, modifiers: comboObj.modifiers };
77+
setComboKey(combo);
11678
extensionAPI.settings
117-
.set(settingKey, comboObj)
79+
.set(settingKey, combo)
11880
.catch(() => console.error("Failed to set setting"));
81+
setPersonalSetting([blockPropKey], combo);
11982
},
120-
[extensionAPI, settingKey],
83+
[extensionAPI, settingKey, blockPropKey],
12184
);
12285

123-
const shortcut = useMemo(() => {
124-
if (!comboKey.key) return "";
125-
126-
const modifiers = getModifiersFromCombo(comboKey);
127-
const comboString = [...modifiers, comboKey.key].join("+");
128-
return normalizeKeyCombo(comboString).join("+");
129-
}, [comboKey]);
86+
const shortcut = useMemo(() => comboToString(comboKey), [comboKey]);
13087

13188
const handleClear = useCallback(() => {
132-
setComboKey({ modifiers: 0, key: "" });
89+
const clearedCombo = { modifiers: 0, key: "" };
90+
setComboKey(clearedCombo);
13391
extensionAPI.settings
134-
.set(settingKey, { modifiers: 0, key: "" })
92+
.set(settingKey, clearedCombo)
13593
.catch(() => console.error("Failed to set setting"));
136-
}, [extensionAPI, settingKey]);
94+
setPersonalSetting([blockPropKey], clearedCombo);
95+
}, [extensionAPI, settingKey, blockPropKey]);
13796

13897
return (
13998
<Label>

apps/roam/src/components/settings/utils/zodSchema.example.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -355,9 +355,9 @@ const personalSettings: PersonalSettings = {
355355
},
356356
},
357357
},
358-
"Personal node menu trigger": ";;",
358+
"Personal node menu trigger": { modifiers: 0, key: ";;" },
359359
"Node search menu trigger": "//",
360-
"Discourse tool shortcut": "d",
360+
"Discourse tool shortcut": { modifiers: 0, key: "d" },
361361
"Discourse context overlay": true,
362362
"Suggestive mode overlay": true,
363363
"Overlay in canvas": false,
@@ -383,9 +383,9 @@ const personalSettings: PersonalSettings = {
383383

384384
const defaultPersonalSettings: PersonalSettings = {
385385
"Left sidebar": {},
386-
"Personal node menu trigger": "",
386+
"Personal node menu trigger": { modifiers: 0, key: "" },
387387
"Node search menu trigger": "",
388-
"Discourse tool shortcut": "",
388+
"Discourse tool shortcut": { modifiers: 0, key: "" },
389389
"Discourse context overlay": false,
390390
"Suggestive mode overlay": false,
391391
"Overlay in canvas": false,

apps/roam/src/components/settings/utils/zodSchema.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,16 @@ export const QuerySettingsSchema = z.object({
230230

231231
export const PersonalSettingsSchema = z.object({
232232
"Left sidebar": LeftSidebarPersonalSettingsSchema,
233-
"Personal node menu trigger": z.string().default(""),
233+
"Personal node menu trigger": z
234+
.union([
235+
z.object({ modifiers: z.number(), key: z.string() }),
236+
z.literal(""),
237+
])
238+
.default({ modifiers: 0, key: "" }),
234239
"Node search menu trigger": z.string().default("@"),
235-
"Discourse tool shortcut": z.string().default(""),
240+
"Discourse tool shortcut": z
241+
.object({ modifiers: z.number(), key: z.string() })
242+
.default({ modifiers: 0, key: "" }),
236243
"Discourse context overlay": z.boolean().default(false),
237244
"Suggestive mode overlay": z.boolean().default(false),
238245
"Overlay in canvas": z.boolean().default(false),

0 commit comments

Comments
 (0)