diff --git a/app/calcTextareaHeight.ts b/app/calcTextareaHeight.ts deleted file mode 100644 index 555c8ceb..00000000 --- a/app/calcTextareaHeight.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * fork from element-plus - * https://github.com/element-plus/element-plus/blob/dev/packages/components/input/src/utils.ts - */ - -import { isFirefox } from "./utils"; - -let hiddenTextarea: HTMLTextAreaElement | undefined = undefined; - -const HIDDEN_STYLE = ` - height:0 !important; - visibility:hidden !important; - ${isFirefox() ? "" : "overflow:hidden !important;"} - position:absolute !important; - z-index:-1000 !important; - top:0 !important; - right:0 !important; -`; - -const CONTEXT_STYLE = [ - "letter-spacing", - "line-height", - "padding-top", - "padding-bottom", - "font-family", - "font-weight", - "font-size", - "text-rendering", - "text-transform", - "width", - "text-indent", - "padding-left", - "padding-right", - "border-width", - "box-sizing", -]; - -type NodeStyle = { - contextStyle: string; - boxSizing: string; - paddingSize: number; - borderSize: number; -}; - -type TextAreaHeight = { - height: string; - minHeight?: string; -}; - -function calculateNodeStyling(targetElement: Element): NodeStyle { - const style = window.getComputedStyle(targetElement); - - const boxSizing = style.getPropertyValue("box-sizing"); - - const paddingSize = - Number.parseFloat(style.getPropertyValue("padding-bottom")) + - Number.parseFloat(style.getPropertyValue("padding-top")); - - const borderSize = - Number.parseFloat(style.getPropertyValue("border-bottom-width")) + - Number.parseFloat(style.getPropertyValue("border-top-width")); - - const contextStyle = CONTEXT_STYLE.map( - (name) => `${name}:${style.getPropertyValue(name)}`, - ).join(";"); - - return { contextStyle, paddingSize, borderSize, boxSizing }; -} - -export default function calcTextareaHeight( - targetElement: HTMLTextAreaElement, - minRows: number = 2, - maxRows?: number, -): TextAreaHeight { - if (!hiddenTextarea) { - hiddenTextarea = document.createElement("textarea"); - document.body.appendChild(hiddenTextarea); - } - - const { paddingSize, borderSize, boxSizing, contextStyle } = - calculateNodeStyling(targetElement); - - hiddenTextarea.setAttribute("style", `${contextStyle};${HIDDEN_STYLE}`); - hiddenTextarea.value = targetElement.value || targetElement.placeholder || ""; - - let height = hiddenTextarea.scrollHeight; - const result = {} as TextAreaHeight; - - if (boxSizing === "border-box") { - height = height + borderSize; - } else if (boxSizing === "content-box") { - height = height - paddingSize; - } - - hiddenTextarea.value = ""; - const singleRowHeight = hiddenTextarea.scrollHeight - paddingSize; - - if (minRows) { - let minHeight = singleRowHeight * minRows; - if (boxSizing === "border-box") { - minHeight = minHeight + paddingSize + borderSize; - } - height = Math.max(minHeight, height); - result.minHeight = `${minHeight}px`; - } - if (maxRows) { - let maxHeight = singleRowHeight * maxRows; - if (boxSizing === "border-box") { - maxHeight = maxHeight + paddingSize + borderSize; - } - height = Math.min(maxHeight, height); - } - result.height = `${height}px`; - hiddenTextarea.parentNode?.removeChild(hiddenTextarea); - hiddenTextarea = undefined; - - return result; -} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 0e719d84..c5c257e5 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1,4 +1,4 @@ -import { useDebouncedCallback } from "use-debounce"; +import { useDebounce, useDebouncedCallback } from "use-debounce"; import { memo, useState, useRef, useEffect, useLayoutEffect } from "react"; import SendWhiteIcon from "../icons/send-white.svg"; @@ -27,6 +27,7 @@ import { getEmojiUrl, isMobileScreen, selectOrCopy, + autoGrowTextArea, } from "../utils"; import dynamic from "next/dynamic"; @@ -39,9 +40,7 @@ import { IconButton } from "./button"; import styles from "./home.module.scss"; import chatStyle from "./chat.module.scss"; -import { Input, Modal, showModal, showToast } from "./ui-lib"; - -import calcTextareaHeight from "../calcTextareaHeight"; +import { Input, Modal, showModal } from "./ui-lib"; const Markdown = dynamic( async () => memo((await import("./markdown")).Markdown), @@ -333,10 +332,6 @@ function useScrollToBottom() { export function Chat(props: { showSideBar?: () => void; sideBarShowing?: boolean; - autoSize: { - minRows: number; - maxRows?: number; - }; }) { type RenderMessage = Message & { preview?: boolean }; @@ -354,7 +349,6 @@ export function Chat(props: { const { submitKey, shouldSubmit } = useSubmitHandler(); const { scrollRef, setAutoScroll } = useScrollToBottom(); const [hitBottom, setHitBottom] = useState(false); - const [textareaStyle, setTextareaStyle] = useState({}); const onChatBodyScroll = (e: HTMLElement) => { const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20; @@ -388,13 +382,26 @@ export function Chat(props: { dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum; }; - // textarea has an adaptive height - const resizeTextarea = () => { - const dom = inputRef.current; - if (!dom) return; - const { minRows, maxRows } = props.autoSize; - setTextareaStyle(calcTextareaHeight(dom, minRows, maxRows)); - }; + // auto grow input + const [inputRows, setInputRows] = useState(2); + const measure = useDebouncedCallback( + () => { + const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1; + const inputRows = Math.min( + 5, + Math.max(2 + Number(!isMobileScreen()), rows), + ); + setInputRows(inputRows); + }, + 100, + { + leading: true, + trailing: true, + }, + ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(measure, [userInput]); // only search prompts when user input is short const SEARCH_TEXT_LIMIT = 30; @@ -410,9 +417,6 @@ export function Chat(props: { // check if need to trigger auto completion if (text.startsWith("/")) { let searchText = text.slice(1); - if (searchText.length === 0) { - searchText = " "; - } onSearch(searchText); } } @@ -527,12 +531,6 @@ export function Chat(props: { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Textarea Adaptive height - useEffect(() => { - resizeTextarea(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userInput]); - return (