mirror of
https://github.com/docmost/docmost.git
synced 2026-06-10 10:13:01 +08:00
feat(base): insert palette items at cursor and refocus editor
This commit is contained in:
@@ -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<HTMLTextAreaElement>(null);
|
||||
const pendingCursorRef = useRef<number | null>(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 (
|
||||
<Paper
|
||||
@@ -97,6 +127,7 @@ export function FormulaEditor({
|
||||
|
||||
<Stack gap={6} px={14} pt={10} pb={8}>
|
||||
<FormulaInput
|
||||
ref={textareaRef}
|
||||
value={source}
|
||||
onChange={setSource}
|
||||
hasError={parseState.state === "error"}
|
||||
@@ -130,7 +161,7 @@ export function FormulaEditor({
|
||||
<Stack gap={8} px={14} pt={10} pb={10}>
|
||||
<PropertyChipRow
|
||||
properties={properties.filter((p) => p.id !== editingPropertyId)}
|
||||
onInsert={(name) => insertAtEnd(`prop("${name}")`)}
|
||||
onInsert={(name) => insertAtCursor(`prop("${name}")`)}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -142,7 +173,7 @@ export function FormulaEditor({
|
||||
</Text>
|
||||
<FunctionPalette
|
||||
registry={registry}
|
||||
onInsert={(name) => insertAtEnd(`${name}()`)}
|
||||
onInsert={(name) => insertAtCursor(`${name}()`, 1)}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Textarea
|
||||
autosize
|
||||
minRows={3}
|
||||
maxRows={8}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.currentTarget.value)}
|
||||
placeholder='prop("Price") * prop("Qty")'
|
||||
styles={{
|
||||
input: {
|
||||
fontFamily:
|
||||
"ui-monospace, SFMono-Regular, Menlo, 'JetBrains Mono', monospace",
|
||||
fontSize: 13,
|
||||
lineHeight: 1.65,
|
||||
backgroundColor: "var(--mantine-color-gray-0)",
|
||||
borderColor: hasError
|
||||
? "var(--mantine-color-red-6)"
|
||||
: "var(--mantine-color-blue-6)",
|
||||
borderWidth: 1.5,
|
||||
boxShadow: hasError
|
||||
? "0 0 0 3px var(--mantine-color-red-1)"
|
||||
: "0 0 0 3px var(--mantine-color-blue-1)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const FormulaInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
function FormulaInput({ value, onChange, hasError }, ref) {
|
||||
return (
|
||||
<Textarea
|
||||
ref={ref}
|
||||
autosize
|
||||
minRows={3}
|
||||
maxRows={8}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.currentTarget.value)}
|
||||
placeholder='prop("Price") * prop("Qty")'
|
||||
styles={{
|
||||
input: {
|
||||
fontFamily:
|
||||
"ui-monospace, SFMono-Regular, Menlo, 'JetBrains Mono', monospace",
|
||||
fontSize: 13,
|
||||
lineHeight: 1.65,
|
||||
backgroundColor: "var(--mantine-color-gray-0)",
|
||||
borderColor: hasError
|
||||
? "var(--mantine-color-red-6)"
|
||||
: "var(--mantine-color-blue-6)",
|
||||
borderWidth: 1.5,
|
||||
boxShadow: hasError
|
||||
? "0 0 0 3px var(--mantine-color-red-1)"
|
||||
: "0 0 0 3px var(--mantine-color-blue-1)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user