From 3bfdae7990b4dc17668941ccffd7ec16e5d6205a Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 24 Apr 2026 02:47:12 +0100 Subject: [PATCH] feat(base): insert palette items at cursor and refocus editor --- .../components/formula/formula-editor.tsx | 41 +++++++++++-- .../base/components/formula/formula-input.tsx | 60 ++++++++++--------- 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/apps/client/src/features/base/components/formula/formula-editor.tsx b/apps/client/src/features/base/components/formula/formula-editor.tsx index 002b40397..097fc33c1 100644 --- a/apps/client/src/features/base/components/formula/formula-editor.tsx +++ b/apps/client/src/features/base/components/formula/formula-editor.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Button, Divider, @@ -45,6 +45,8 @@ export function FormulaEditor({ onCancel, }: Props) { const [source, setSource] = useState(initialSource); + const textareaRef = useRef(null); + const pendingCursorRef = useRef(null); const parseState = useFormulaParser( source, properties, @@ -52,8 +54,36 @@ export function FormulaEditor({ registry, ); const canSave = parseState.state === "ok" && !disabled; - const insertAtEnd = (snippet: string) => - setSource((s) => `${s}${s ? " " : ""}${snippet}`); + + // After a palette insert mutates `source`, wait for React to flush the + // new value into the textarea, then focus + restore the cursor. Using + // useEffect (not RAF) guarantees the DOM update ran first. + useEffect(() => { + if (pendingCursorRef.current === null) return; + const pos = pendingCursorRef.current; + pendingCursorRef.current = null; + const ta = textareaRef.current; + if (!ta) return; + ta.focus(); + ta.setSelectionRange(pos, pos); + }, [source]); + + const insertAtCursor = (snippet: string, cursorOffsetFromEnd = 0) => { + const ta = textareaRef.current; + const start = ta?.selectionStart ?? source.length; + const end = ta?.selectionEnd ?? source.length; + const before = source.slice(0, start); + const after = source.slice(end); + // Add a space separator when inserting after content that would + // otherwise mash against the snippet (e.g. `2` + `prop("A")`). + const prev = before.slice(-1); + const needsSpace = prev !== "" && !/[\s(,]/.test(prev); + const prefix = needsSpace ? " " : ""; + const next = before + prefix + snippet + after; + pendingCursorRef.current = + before.length + prefix.length + snippet.length - cursorOffsetFromEnd; + setSource(next); + }; return ( p.id !== editingPropertyId)} - onInsert={(name) => insertAtEnd(`prop("${name}")`)} + onInsert={(name) => insertAtCursor(`prop("${name}")`)} /> @@ -142,7 +173,7 @@ export function FormulaEditor({ insertAtEnd(`${name}()`)} + onInsert={(name) => insertAtCursor(`${name}()`, 1)} /> diff --git a/apps/client/src/features/base/components/formula/formula-input.tsx b/apps/client/src/features/base/components/formula/formula-input.tsx index 11c3c12c0..7846139be 100644 --- a/apps/client/src/features/base/components/formula/formula-input.tsx +++ b/apps/client/src/features/base/components/formula/formula-input.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from "react"; import { Textarea } from "@mantine/core"; type Props = { @@ -6,31 +7,34 @@ type Props = { hasError?: boolean; }; -export function FormulaInput({ value, onChange, hasError }: Props) { - return ( -