fix: insert parameters at cursor position (#2867)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Suchintan <suchintan@users.noreply.github.com> Co-authored-by: Jonathan Dobson <jon.m.dobson@gmail.com>
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
import { ChangeEventHandler, useEffect, useLayoutEffect, useRef } from "react";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import type { ChangeEventHandler, HTMLAttributes } from "react";
|
||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
} from "react";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -8,44 +15,73 @@ type Props = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
};
|
onClick?: React.MouseEventHandler<HTMLTextAreaElement>;
|
||||||
|
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>;
|
||||||
|
onSelect?: React.ReactEventHandler<HTMLTextAreaElement>;
|
||||||
|
} & Omit<HTMLAttributes<HTMLTextAreaElement>, "onChange" | "value">;
|
||||||
|
|
||||||
function AutoResizingTextarea({
|
const AutoResizingTextarea = forwardRef<HTMLTextAreaElement, Props>(
|
||||||
value,
|
(
|
||||||
onChange,
|
{
|
||||||
className,
|
value,
|
||||||
readOnly,
|
onChange,
|
||||||
placeholder,
|
className,
|
||||||
}: Props) {
|
readOnly,
|
||||||
const ref = useRef<HTMLTextAreaElement>(null);
|
placeholder,
|
||||||
|
onClick,
|
||||||
|
onKeyUp,
|
||||||
|
onSelect,
|
||||||
|
...restProps
|
||||||
|
},
|
||||||
|
forwardedRef,
|
||||||
|
) => {
|
||||||
|
const innerRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
const getTextarea = useCallback(() => innerRef.current, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
const setRefs = (element: HTMLTextAreaElement | null) => {
|
||||||
// size the textarea correctly on first render
|
innerRef.current = element;
|
||||||
if (!ref.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ref.current.style.height = `${ref.current.scrollHeight + 2}px`;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Forward to external ref
|
||||||
if (!ref.current) {
|
if (typeof forwardedRef === "function") {
|
||||||
return;
|
forwardedRef(element);
|
||||||
}
|
} else if (forwardedRef) {
|
||||||
ref.current.style.height = "auto";
|
forwardedRef.current = element;
|
||||||
ref.current.style.height = `${ref.current.scrollHeight + 2}px`;
|
}
|
||||||
}, [value]);
|
};
|
||||||
|
|
||||||
return (
|
useLayoutEffect(() => {
|
||||||
<Textarea
|
const textareaElement = getTextarea();
|
||||||
value={value}
|
if (!textareaElement) {
|
||||||
onChange={onChange}
|
return;
|
||||||
readOnly={readOnly}
|
}
|
||||||
placeholder={placeholder}
|
textareaElement.style.height = `${textareaElement.scrollHeight + 2}px`;
|
||||||
ref={ref}
|
}, [getTextarea]);
|
||||||
rows={1}
|
|
||||||
className={cn("min-h-0 resize-none overflow-y-hidden", className)}
|
useEffect(() => {
|
||||||
/>
|
const textareaElement = getTextarea();
|
||||||
);
|
if (!textareaElement) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
textareaElement.style.height = "auto";
|
||||||
|
textareaElement.style.height = `${textareaElement.scrollHeight + 2}px`;
|
||||||
|
}, [getTextarea, value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
readOnly={readOnly}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
onSelect={onSelect}
|
||||||
|
ref={setRefs}
|
||||||
|
rows={1}
|
||||||
|
className={cn("min-h-0 resize-none overflow-y-hidden", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export { AutoResizingTextarea };
|
export { AutoResizingTextarea };
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { cn } from "@/util/utils";
|
|||||||
import { AutoResizingTextarea } from "./AutoResizingTextarea/AutoResizingTextarea";
|
import { AutoResizingTextarea } from "./AutoResizingTextarea/AutoResizingTextarea";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||||
import { WorkflowBlockParameterSelect } from "@/routes/workflows/editor/nodes/WorkflowBlockParameterSelect";
|
import { WorkflowBlockParameterSelect } from "@/routes/workflows/editor/nodes/WorkflowBlockParameterSelect";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
type Props = Omit<
|
type Props = Omit<
|
||||||
React.ComponentProps<typeof AutoResizingTextarea>,
|
React.ComponentProps<typeof AutoResizingTextarea>,
|
||||||
@@ -14,14 +15,56 @@ type Props = Omit<
|
|||||||
|
|
||||||
function WorkflowBlockInputTextarea(props: Props) {
|
function WorkflowBlockInputTextarea(props: Props) {
|
||||||
const { nodeId, onChange, ...textAreaProps } = props;
|
const { nodeId, onChange, ...textAreaProps } = props;
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const [cursorPosition, setCursorPosition] = useState<{
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const handleTextareaSelect = () => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
setCursorPosition({
|
||||||
|
start: textareaRef.current.selectionStart,
|
||||||
|
end: textareaRef.current.selectionEnd,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertParameterAtCursor = (parameterKey: string) => {
|
||||||
|
const value = props.value ?? "";
|
||||||
|
const parameterText = `{{${parameterKey}}}`;
|
||||||
|
|
||||||
|
if (cursorPosition && textareaRef.current) {
|
||||||
|
const { start, end } = cursorPosition;
|
||||||
|
const newValue =
|
||||||
|
value.substring(0, start) + parameterText + value.substring(end);
|
||||||
|
|
||||||
|
onChange(newValue);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
const newPosition = start + parameterText.length;
|
||||||
|
textareaRef.current.focus();
|
||||||
|
textareaRef.current.setSelectionRange(newPosition, newPosition);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
onChange(`${value}${parameterText}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<AutoResizingTextarea
|
<AutoResizingTextarea
|
||||||
{...textAreaProps}
|
{...textAreaProps}
|
||||||
|
ref={textareaRef}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
onChange(event.target.value);
|
onChange(event.target.value);
|
||||||
|
handleTextareaSelect();
|
||||||
}}
|
}}
|
||||||
|
onClick={handleTextareaSelect}
|
||||||
|
onKeyUp={handleTextareaSelect}
|
||||||
|
onSelect={handleTextareaSelect}
|
||||||
className={cn("pr-9", props.className)}
|
className={cn("pr-9", props.className)}
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-0 top-0 flex size-9 cursor-pointer items-center justify-center">
|
<div className="absolute right-0 top-0 flex size-9 cursor-pointer items-center justify-center">
|
||||||
@@ -34,9 +77,7 @@ function WorkflowBlockInputTextarea(props: Props) {
|
|||||||
<PopoverContent className="w-[22rem]">
|
<PopoverContent className="w-[22rem]">
|
||||||
<WorkflowBlockParameterSelect
|
<WorkflowBlockParameterSelect
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
onAdd={(parameterKey) => {
|
onAdd={insertParameterAtCursor}
|
||||||
onChange(`${props.value ?? ""}{{${parameterKey}}}`);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
Reference in New Issue
Block a user