Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions apps/roam/src/components/DiscourseNodeSearchMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import {
Position,
Checkbox,
Button,
InputGroup,
} from "@blueprintjs/core";
import ReactDOM from "react-dom";
import getUids from "roamjs-components/dom/getUids";
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";
Expand All @@ -28,6 +30,7 @@ type Props = {
textarea: HTMLTextAreaElement;
triggerPosition: number;
onClose: () => void;
triggerText: string;
};

const waitForBlock = (
Expand All @@ -51,6 +54,7 @@ const NodeSearchMenu = ({
onClose,
textarea,
triggerPosition,
triggerText,
}: { onClose: () => void } & Props) => {
const [activeIndex, setActiveIndex] = useState(0);
const [searchTerm, setSearchTerm] = useState("");
Expand Down Expand Up @@ -236,19 +240,21 @@ 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]);

const keydownListener = useCallback(
(e: KeyboardEvent) => {
Expand All @@ -274,7 +280,7 @@ const NodeSearchMenu = ({
e.stopPropagation();
}
},
[allItems, setActiveIndex, onSelect, onClose],
[allItems, activeIndex, setActiveIndex, onSelect, onClose],
);

useEffect(() => {
Expand Down Expand Up @@ -520,4 +526,38 @@ export const renderDiscourseNodeSearchMenu = (props: Props) => {
);
};

export const NodeSearchMenuTriggerSetting = ({
onloadArgs,
}: {
onloadArgs: OnloadArgs;
}) => {
const extensionAPI = onloadArgs.extensionAPI;
const [nodeSearchTrigger, setNodeSearchTrigger] = useState<string>(
extensionAPI.settings.get("node-search-trigger") as string,
);

const handleNodeSearchTriggerChange = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
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 (
<InputGroup
value={nodeSearchTrigger}
onChange={handleNodeSearchTriggerChange}
placeholder="Click to set trigger"
maxLength={5}
/>
);
};


export default NodeSearchMenu;
10 changes: 10 additions & 0 deletions apps/roam/src/components/settings/HomePersonalSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
hideFeedbackButton,
showFeedbackButton,
} from "~/components/BirdEatsBugs";
import { NodeSearchMenuTriggerSetting } from "../DiscourseNodeSearchMenu";

const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
const extensionAPI = onloadArgs.extensionAPI;
Expand All @@ -28,6 +29,15 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
/>
<NodeMenuTriggerComponent extensionAPI={extensionAPI} />
</Label>
<Label>
Node Search Menu Trigger
<Description
description={
"Set the trigger character for the Node Search Menu. Must refresh after editing."
}
/>
<NodeSearchMenuTriggerSetting onloadArgs={onloadArgs} />
</Label>
<Checkbox
defaultChecked={
extensionAPI.settings.get("discourse-context-overlay") as boolean
Expand Down
7 changes: 2 additions & 5 deletions apps/roam/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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",
});
Expand Down
56 changes: 40 additions & 16 deletions apps/roam/src/utils/initializeObserversAndListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,30 +186,54 @@ 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;

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) {
if (
target.tagName === "TEXTAREA" &&
target.classList.contains("rm-block-input")
) {
const textarea = target as HTMLTextAreaElement;

if (!customTrigger) return;

const cursorPos = textarea.selectionStart;
const textBeforeCursor = textarea.value.substring(0, cursorPos);

const lastTriggerPos = textBeforeCursor.lastIndexOf(customTrigger);

if (lastTriggerPos >= 0) {
const charBeforeTrigger =
lastTriggerPos > 0
? textBeforeCursor.charAt(lastTriggerPos - 1)
: null;

const isValidTriggerPosition =
lastTriggerPos === 0 ||
charBeforeTrigger === " " ||
charBeforeTrigger === "\n";

const isCursorAfterTrigger =
cursorPos === lastTriggerPos + customTrigger.length;

if (isValidTriggerPosition && isCursorAfterTrigger) {
// 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: () => {},
textarea: textarea,
triggerPosition: cursorPos,
triggerPosition: lastTriggerPos,
triggerText: customTrigger,
});
}
}
Expand Down