Files
coco-app/src/components/Search/AutoResizeTextarea.tsx
BiggerRain f7c0600480 feat: add open button to launch installed extension (#1013)
* chore: up

* support query string main_extension_id

* chore: up

* fix tests

* open non-group/extension extensions

* dbg

* chore: upadate

* extension SearchSource now accepts empty querystring

* update

* chore: open

* chore: input

* remove DBG statements

* chore: icon

* style: adjust styles

* docs: update release notes

---------

Co-authored-by: Steve Lau <stevelauc@outlook.com>
2025-12-18 15:50:26 +08:00

150 lines
4.0 KiB
TypeScript

import { cn } from "@/lib/utils";
import { useAppStore } from "@/stores/appStore";
import { useWebConfigStore } from "@/stores/webConfigStore";
import { useBoolean } from "ahooks";
import {
useImperativeHandle,
forwardRef,
KeyboardEvent,
useCallback,
ChangeEvent,
useRef,
useEffect,
} from "react";
import { useTranslation } from "react-i18next";
const MAX_HEIGHT = 240;
interface AutoResizeTextareaProps {
isChatMode: boolean;
input: string;
setInput: (value: string) => void;
handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
chatPlaceholder?: string;
lineCount?: number;
onLineCountChange?: (lineCount: number) => void;
firstLineMaxWidth: number;
}
// Forward ref to allow parent to interact with this component
const AutoResizeTextarea = forwardRef<
{ reset: () => void; focus: () => void },
AutoResizeTextareaProps
>(
(
{
isChatMode,
input,
setInput,
handleKeyDown,
chatPlaceholder,
lineCount,
onLineCountChange,
firstLineMaxWidth,
},
ref
) => {
const { t } = useTranslation();
const [isComposition, { setTrue, setFalse }] = useBoolean();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const calcRef = useRef<HTMLDivElement>(null);
// Expose methods to the parent via ref
useImperativeHandle(ref, () => ({
reset: () => {
setInput("");
},
focus: () => {
textareaRef.current?.focus();
},
}));
const handleKeyPress = (event: KeyboardEvent<HTMLTextAreaElement>) => {
if (isComposition) {
return event.stopPropagation();
}
handleKeyDown?.(event);
};
useEffect(() => {
const textarea = textareaRef.current;
if (!textarea || !calcRef.current) return;
if (!calcRef.current) return;
textarea.style.height = "auto";
const computedStyle = getComputedStyle(textarea);
const lineHeight = parseInt(computedStyle.lineHeight);
let height = lineHeight;
let minHeight = lineHeight;
const hasNewline = /[\r\n]/.test(input);
const hasContent = input.length > 0;
const firstLineExceeds =
hasContent &&
(calcRef.current?.offsetWidth ?? 0) >= Math.max(firstLineMaxWidth - 32, 0);
if (hasNewline || firstLineExceeds) {
minHeight = lineHeight * 2;
height = Math.min(
Math.max(minHeight, textarea.scrollHeight),
MAX_HEIGHT
);
}
textarea.style.height = `${height}px`;
textarea.style.minHeight = `${minHeight}px`;
onLineCountChange?.(height / lineHeight);
}, [input, firstLineMaxWidth]);
const handleChange = useCallback(
(event: ChangeEvent<HTMLTextAreaElement>) => {
setInput(event.currentTarget.value);
},
[setInput]
);
const { isTauri } = useAppStore();
const { disabled } = useWebConfigStore();
return (
<>
<textarea
ref={textareaRef}
id={isChatMode ? "chat-textarea" : "search-textarea"}
autoFocus
autoComplete="off"
autoCapitalize="none"
spellCheck="false"
className={cn(
"auto-resize-textarea text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar resize-none overflow-y-auto",
{
"overflow-y-hidden": lineCount === 1,
}
)}
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
aria-label={t("search.textarea.ariaLabel")}
value={input}
onChange={handleChange}
onKeyDown={handleKeyPress}
onCompositionStart={setTrue}
onCompositionEnd={() => {
setTimeout(setFalse, 0);
}}
rows={1}
disabled={!isTauri && disabled}
/>
<div ref={calcRef} className="absolute whitespace-nowrap -z-10">
{input}
</div>
</>
);
}
);
export default AutoResizeTextarea;