From 2233ecd61f3e3f6ad75d92f840c651f4526d5278 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Fri, 16 May 2025 12:42:09 -0400 Subject: [PATCH 1/6] setting menu done --- .../components/DiscourseNodeSearchMenu.tsx | 36 +++++++++++++++++++ .../settings/HomePersonalSettings.tsx | 10 ++++++ 2 files changed, 46 insertions(+) diff --git a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx index b5acc8ab0..196ea92cd 100644 --- a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx @@ -12,6 +12,7 @@ import { Position, Checkbox, Button, + InputGroup, } from "@blueprintjs/core"; import ReactDOM from "react-dom"; import getUids from "roamjs-components/dom/getUids"; @@ -19,6 +20,7 @@ import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; import updateBlock from "roamjs-components/writes/updateBlock"; import posthog from "posthog-js"; import { getCoordsFromTextarea } from "roamjs-components/components/CursorMenu"; +import { OnloadArgs } from "roamjs-components/types"; import getDiscourseNodes, { DiscourseNode } from "~/utils/getDiscourseNodes"; import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression"; import { escapeCljString } from "~/utils/formatUtils"; @@ -520,4 +522,38 @@ export const renderDiscourseNodeSearchMenu = (props: Props) => { ); }; +export const NodeSearchMenuTriggerSetting = ({ + onloadArgs, +}: { + onloadArgs: OnloadArgs; +}) => { + const extensionAPI = onloadArgs.extensionAPI; + const [nodeSearchTrigger, setNodeSearchTrigger] = useState( + extensionAPI.settings.get("node-search-trigger") as string, + ); + + const handleNodeSearchTriggerChange = ( + e: React.ChangeEvent, + ) => { + const value = e.target.value.trim(); + const trigger = value + .replace(/"/g, "") + .replace(/\\/g, "\\\\") + .replace(/\+/g, "\\+") + .trim(); + + setNodeSearchTrigger(trigger); + extensionAPI.settings.set("node-search-trigger", trigger); + }; + return ( + + ); +}; + + export default NodeSearchMenu; diff --git a/apps/roam/src/components/settings/HomePersonalSettings.tsx b/apps/roam/src/components/settings/HomePersonalSettings.tsx index de30859cb..f277bb263 100644 --- a/apps/roam/src/components/settings/HomePersonalSettings.tsx +++ b/apps/roam/src/components/settings/HomePersonalSettings.tsx @@ -12,6 +12,7 @@ import { hideFeedbackButton, showFeedbackButton, } from "~/components/BirdEatsBugs"; +import { NodeSearchMenuTriggerSetting } from "../DiscourseNodeSearchMenu"; const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { const extensionAPI = onloadArgs.extensionAPI; @@ -28,6 +29,15 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { /> + Date: Fri, 16 May 2025 13:02:25 -0400 Subject: [PATCH 2/6] trigger works correctly --- .../components/DiscourseNodeSearchMenu.tsx | 12 ++- apps/roam/src/index.ts | 7 +- .../utils/initializeObserversAndListeners.ts | 94 +++++++++++++++---- 3 files changed, 85 insertions(+), 28 deletions(-) diff --git a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx index 196ea92cd..91078fb39 100644 --- a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx @@ -30,6 +30,7 @@ type Props = { textarea: HTMLTextAreaElement; triggerPosition: number; onClose: () => void; + triggerText: string; }; const waitForBlock = ( @@ -53,6 +54,7 @@ const NodeSearchMenu = ({ onClose, textarea, triggerPosition, + triggerText, }: { onClose: () => void } & Props) => { const [activeIndex, setActiveIndex] = useState(0); const [searchTerm, setSearchTerm] = useState(""); @@ -238,19 +240,23 @@ const NodeSearchMenu = ({ ); const handleTextAreaInput = useCallback(() => { - const atTriggerRegex = /@(.*)$/; + const triggerRegex = new RegExp( + `${triggerText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(.*)$`, + ); const textBeforeCursor = textarea.value.substring( triggerPosition, textarea.selectionStart, ); - const match = atTriggerRegex.exec(textBeforeCursor); + const match = triggerRegex.exec(textBeforeCursor); if (match) { debouncedSearchTerm(match[1]); } else { onClose(); return; } - }, [textarea, onClose, debouncedSearchTerm, triggerPosition]); + }, [textarea, onClose, debouncedSearchTerm, triggerPosition, triggerText]); + + console.log(activeIndex); const keydownListener = useCallback( (e: KeyboardEvent) => { diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index d25c39bf6..f0a3080d8 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -107,7 +107,7 @@ export default runExtension(async (onloadArgs) => { document.addEventListener("roamjs:query-builder:action", pageActionListener); window.addEventListener("hashchange", hashChangeListener); document.addEventListener("keydown", nodeMenuTriggerListener); - document.addEventListener("keydown", discourseNodeSearchTriggerListener); + document.addEventListener("input", discourseNodeSearchTriggerListener); const { extensionAPI } = onloadArgs; window.roamjs.extension.queryBuilder = { @@ -138,10 +138,7 @@ export default runExtension(async (onloadArgs) => { ); window.removeEventListener("hashchange", hashChangeListener); document.removeEventListener("keydown", nodeMenuTriggerListener); - document.removeEventListener( - "keydown", - discourseNodeSearchTriggerListener, - ); + document.removeEventListener("input", discourseNodeSearchTriggerListener); window.roamAlphaAPI.ui.graphView.wholeGraph.removeCallback({ label: "discourse-node-styling", }); diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 9c2479116..3ebf32fa0 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -189,28 +189,82 @@ export const initObservers = async ({ const discourseNodeSearchTriggerListener = (e: Event) => { const evt = e as KeyboardEvent; const target = evt.target as HTMLElement; + + // Don't process if the menu is already open if (document.querySelector(".discourse-node-search-menu")) return; - if (evt.key === "@" || (evt.key === "2" && evt.shiftKey)) { - if ( - target.tagName === "TEXTAREA" && - target.classList.contains("rm-block-input") - ) { - const textarea = target as HTMLTextAreaElement; - const location = window.roamAlphaAPI.ui.getFocusedBlock(); - if (!location) return; - - const cursorPos = textarea.selectionStart; - const isBeginningOrAfterSpace = - cursorPos === 0 || - textarea.value.charAt(cursorPos - 1) === " " || - textarea.value.charAt(cursorPos - 1) === "\n"; - if (isBeginningOrAfterSpace) { - renderDiscourseNodeSearchMenu({ - onClose: () => {}, - textarea: textarea, - triggerPosition: cursorPos, - }); + // Only handle for textarea inputs in Roam blocks + if ( + target.tagName === "TEXTAREA" && + target.classList.contains("rm-block-input") + ) { + const textarea = target as HTMLTextAreaElement; + const location = window.roamAlphaAPI.ui.getFocusedBlock(); + if (!location) return; + + // Get custom trigger from settings + const customTrigger = onloadArgs.extensionAPI.settings.get( + "node-search-trigger", + ) as string; + + // If no trigger is set, don't activate the menu + if (!customTrigger) return; + + const cursorPos = textarea.selectionStart; + const textBeforeCursor = textarea.value.substring(0, cursorPos); + + // Find the last instance of the trigger before cursor + const lastTriggerPos = textBeforeCursor.lastIndexOf(customTrigger); + + // Only proceed if we found the trigger in the text + if (lastTriggerPos >= 0) { + // Check if the trigger is at the beginning of text or after whitespace + const charBeforeTrigger = + lastTriggerPos > 0 + ? textBeforeCursor.charAt(lastTriggerPos - 1) + : null; + + const isValidTriggerPosition = + lastTriggerPos === 0 || + charBeforeTrigger === " " || + charBeforeTrigger === "\n"; + + // Check if cursor is right after the trigger + const isCursorAfterTrigger = + cursorPos === lastTriggerPos + customTrigger.length; + + // Only open the menu if the trigger is valid and the cursor is right after it + if (isValidTriggerPosition && isCursorAfterTrigger) { + // If user pressed a key that isn't part of navigation/editing + const isActionKey = + evt.ctrlKey || + evt.altKey || + evt.metaKey || + [ + "Shift", + "Control", + "Alt", + "Meta", + "CapsLock", + "Escape", + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "ArrowDown", + "Home", + "End", + "PageUp", + "PageDown", + ].includes(evt.key); + + if (!isActionKey) { + renderDiscourseNodeSearchMenu({ + onClose: () => {}, + textarea: textarea, + triggerPosition: lastTriggerPos, + triggerText: customTrigger, + }); + } } } } From 9e3dbc14678e63e2b5bdf3fba75fe5c10c718292 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Fri, 16 May 2025 13:07:15 -0400 Subject: [PATCH 3/6] clean up --- .../components/DiscourseNodeSearchMenu.tsx | 4 +- .../utils/initializeObserversAndListeners.ts | 45 +++---------------- 2 files changed, 7 insertions(+), 42 deletions(-) diff --git a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx index 91078fb39..3f8953710 100644 --- a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx @@ -256,8 +256,6 @@ const NodeSearchMenu = ({ } }, [textarea, onClose, debouncedSearchTerm, triggerPosition, triggerText]); - console.log(activeIndex); - const keydownListener = useCallback( (e: KeyboardEvent) => { if (e.key === "ArrowDown" && allItems.length) { @@ -282,7 +280,7 @@ const NodeSearchMenu = ({ e.stopPropagation(); } }, - [allItems, setActiveIndex, onSelect, onClose], + [allItems, activeIndex, setActiveIndex, onSelect, onClose], ); useEffect(() => { diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 3ebf32fa0..8ee08ce19 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -190,10 +190,8 @@ export const initObservers = async ({ const evt = e as KeyboardEvent; const target = evt.target as HTMLElement; - // Don't process if the menu is already open if (document.querySelector(".discourse-node-search-menu")) return; - // Only handle for textarea inputs in Roam blocks if ( target.tagName === "TEXTAREA" && target.classList.contains("rm-block-input") @@ -202,23 +200,18 @@ export const initObservers = async ({ const location = window.roamAlphaAPI.ui.getFocusedBlock(); if (!location) return; - // Get custom trigger from settings const customTrigger = onloadArgs.extensionAPI.settings.get( "node-search-trigger", ) as string; - // If no trigger is set, don't activate the menu if (!customTrigger) return; const cursorPos = textarea.selectionStart; const textBeforeCursor = textarea.value.substring(0, cursorPos); - // Find the last instance of the trigger before cursor const lastTriggerPos = textBeforeCursor.lastIndexOf(customTrigger); - // Only proceed if we found the trigger in the text if (lastTriggerPos >= 0) { - // Check if the trigger is at the beginning of text or after whitespace const charBeforeTrigger = lastTriggerPos > 0 ? textBeforeCursor.charAt(lastTriggerPos - 1) @@ -229,42 +222,16 @@ export const initObservers = async ({ charBeforeTrigger === " " || charBeforeTrigger === "\n"; - // Check if cursor is right after the trigger const isCursorAfterTrigger = cursorPos === lastTriggerPos + customTrigger.length; - // Only open the menu if the trigger is valid and the cursor is right after it if (isValidTriggerPosition && isCursorAfterTrigger) { - // If user pressed a key that isn't part of navigation/editing - const isActionKey = - evt.ctrlKey || - evt.altKey || - evt.metaKey || - [ - "Shift", - "Control", - "Alt", - "Meta", - "CapsLock", - "Escape", - "ArrowLeft", - "ArrowRight", - "ArrowUp", - "ArrowDown", - "Home", - "End", - "PageUp", - "PageDown", - ].includes(evt.key); - - if (!isActionKey) { - renderDiscourseNodeSearchMenu({ - onClose: () => {}, - textarea: textarea, - triggerPosition: lastTriggerPos, - triggerText: customTrigger, - }); - } + renderDiscourseNodeSearchMenu({ + onClose: () => {}, + textarea: textarea, + triggerPosition: lastTriggerPos, + triggerText: customTrigger, + }); } } } From 70a8fcb472c419a7f1cb1a628e04a31c12d6470c Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 19 May 2025 14:08:01 -0400 Subject: [PATCH 4/6] address PR comment --- apps/roam/src/utils/initializeObserversAndListeners.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 8ee08ce19..4e4a894d2 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -186,6 +186,10 @@ export const initObservers = async ({ } }; + const customTrigger = onloadArgs.extensionAPI.settings.get( + "node-search-trigger", + ) as string; + const discourseNodeSearchTriggerListener = (e: Event) => { const evt = e as KeyboardEvent; const target = evt.target as HTMLElement; @@ -200,10 +204,6 @@ export const initObservers = async ({ const location = window.roamAlphaAPI.ui.getFocusedBlock(); if (!location) return; - const customTrigger = onloadArgs.extensionAPI.settings.get( - "node-search-trigger", - ) as string; - if (!customTrigger) return; const cursorPos = textarea.selectionStart; From 36524f2c48aaac5beb5287a6607ad4243711459e Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 19 May 2025 14:57:59 -0400 Subject: [PATCH 5/6] optimize getFocusedBlock() --- apps/roam/src/utils/initializeObserversAndListeners.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 4e4a894d2..0a66ad784 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -201,8 +201,6 @@ export const initObservers = async ({ target.classList.contains("rm-block-input") ) { const textarea = target as HTMLTextAreaElement; - const location = window.roamAlphaAPI.ui.getFocusedBlock(); - if (!location) return; if (!customTrigger) return; @@ -226,6 +224,9 @@ export const initObservers = async ({ cursorPos === lastTriggerPos + customTrigger.length; if (isValidTriggerPosition && isCursorAfterTrigger) { + const location = window.roamAlphaAPI.ui.getFocusedBlock(); + if (!location) return; + renderDiscourseNodeSearchMenu({ onClose: () => {}, textarea: textarea, From bc2195864eb9237e000b9c0496601ea79eee7224 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 19 May 2025 22:21:56 -0400 Subject: [PATCH 6/6] address nit comments --- apps/roam/src/components/DiscourseNodeSearchMenu.tsx | 2 +- apps/roam/src/utils/initializeObserversAndListeners.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx index 3f8953710..89905dabc 100644 --- a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx @@ -553,7 +553,7 @@ export const NodeSearchMenuTriggerSetting = ({ ); diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 0a66ad784..ec0423936 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -224,8 +224,10 @@ export const initObservers = async ({ cursorPos === lastTriggerPos + customTrigger.length; if (isValidTriggerPosition && isCursorAfterTrigger) { - const location = window.roamAlphaAPI.ui.getFocusedBlock(); - if (!location) return; + // Double-check we have an active block context via Roam's API + // This guards against edge cases where the DOM shows an input but Roam's internal state disagrees + const isEditingBlock = !!window.roamAlphaAPI.ui.getFocusedBlock(); + if (!isEditingBlock) return; renderDiscourseNodeSearchMenu({ onClose: () => {},