diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index 7fc24683..cb486295 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -36,6 +36,7 @@ Information about release notes of Coco Server is provided here. - fix: fix selection issue after renaming #800 - fix: fix shortcut issue in windows context menu #804 - fix: panic caused by "state() called before manage()" #806 +- fix: fix multiline input issue #808 ### ✈️ Improvements @@ -59,7 +60,6 @@ Information about release notes of Coco Server is provided here. - refactor: clean up unsupported characters from query string in Win Search #802 - chore: display backtrace in panic log #805 - ## 0.6.0 (2025-06-29) ### ❌ Breaking changes diff --git a/src/components/Search/AutoResizeTextarea.tsx b/src/components/Search/AutoResizeTextarea.tsx index d6ce4c55..c455032a 100644 --- a/src/components/Search/AutoResizeTextarea.tsx +++ b/src/components/Search/AutoResizeTextarea.tsx @@ -1,17 +1,16 @@ -import { useBoolean, useDebounceFn } from "ahooks"; +import { useBoolean } from "ahooks"; import { - useRef, useImperativeHandle, forwardRef, KeyboardEvent, - useEffect, useCallback, + ChangeEvent, + useRef, + useEffect, } from "react"; import { useTranslation } from "react-i18next"; -const LINE_HEIGHT = 24; // 1.5rem -const MAX_FIRST_LINE_WIDTH = 470; // Width in pixels for first line -const MAX_HEIGHT = 240; // 15rem +const MAX_HEIGHT = 240; interface AutoResizeTextareaProps { isChatMode: boolean; @@ -21,6 +20,7 @@ interface AutoResizeTextareaProps { chatPlaceholder?: string; lineCount?: number; onLineCountChange?: (lineCount: number) => void; + firstLineMaxWidth: number; } // Forward ref to allow parent to interact with this component @@ -35,87 +35,15 @@ const AutoResizeTextarea = forwardRef< setInput, handleKeyDown, chatPlaceholder, - lineCount = 1, onLineCountChange, + firstLineMaxWidth, }, ref ) => { const { t } = useTranslation(); - const textareaRef = useRef(null); const [isComposition, { setTrue, setFalse }] = useBoolean(); - - // Memoize resize logic - const { run: debouncedResize } = useDebounceFn( - () => { - const textarea = textareaRef.current; - if (!textarea) return; - if (typeof window === "undefined" || typeof document === "undefined") - return; - - // Reset height to auto to get the correct scrollHeight - textarea.style.height = "auto"; - - // Create a hidden span to measure first line width - const span = document.createElement("span"); - span.style.visibility = "hidden"; - span.style.position = "absolute"; - span.style.whiteSpace = "pre"; - span.style.font = window.getComputedStyle(textarea).font; - - // Get first line content - const content = textarea.value; - const firstLineEnd = - content.indexOf("\n") === -1 ? content.length : content.indexOf("\n"); - span.textContent = content.slice(0, firstLineEnd); - document.body.appendChild(span); - - // Calculate lines based on first line width - const firstLineWidth = span.offsetWidth; - document.body.removeChild(span); - - // Start with 1 line - let lines = 1; - - // Add a line if first line exceeds max width - if (firstLineWidth > MAX_FIRST_LINE_WIDTH) { - lines += 1; - } - - // Add lines based on scrollHeight for remaining content - const scrollHeight = textarea.scrollHeight; - const remainingLines = Math.floor( - (scrollHeight - LINE_HEIGHT) / LINE_HEIGHT - ); - lines += Math.max(0, remainingLines); - - // Calculate final height - const newHeight = Math.min(lines * LINE_HEIGHT, MAX_HEIGHT); - - // Only update if height actually changed - if (textarea.style.height !== `${newHeight}px`) { - textarea.style.height = `${newHeight}px`; - onLineCountChange?.(lines); - } - }, - { wait: 100 } - ); - - // Handle input changes and initial setup - useEffect(() => { - if (textareaRef.current) { - debouncedResize(); - } - }, [input, debouncedResize]); - - useEffect(() => { - if (textareaRef.current) { - requestAnimationFrame(() => { - // Set cursor position to end - const length = textareaRef.current?.value.length || 0; - textareaRef.current?.setSelectionRange(length, length); - }); - } - }, [lineCount]); + const textareaRef = useRef(null); + const calcRef = useRef(null); // Expose methods to the parent via ref useImperativeHandle(ref, () => ({ @@ -135,40 +63,67 @@ const AutoResizeTextarea = forwardRef< 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; + + if (calcRef.current?.offsetWidth >= firstLineMaxWidth - 32) { + 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( - (e: React.ChangeEvent) => { - setInput(e.target.value); + (event: ChangeEvent) => { + setInput(event.currentTarget.value); }, [setInput] ); return ( -