diff --git a/frontend/src/pages/system/sys-params/index.less b/frontend/src/pages/system/sys-params/index.less index 32cb1b2..b08db60 100644 --- a/frontend/src/pages/system/sys-params/index.less +++ b/frontend/src/pages/system/sys-params/index.less @@ -74,22 +74,64 @@ line-height: 1.6; } -.param-value-rich-editor { +.param-value-rich-editor-shell { + position: relative; + min-height: var(--param-value-editor-min-height); + max-height: 260px; +} + +.param-value-rich-editor, +.param-value-rich-editor-highlight { + box-sizing: border-box; min-height: var(--param-value-editor-min-height); max-height: 260px; padding: 6px 11px; - overflow: auto; - border: 1px solid #d9d9d9; - border-radius: 6px; - background: #fff; - color: rgba(0, 0, 0, 0.88); font-family: 'SFMono-Regular', Consolas, Menlo, monospace; font-size: 14px; line-height: 1.5715; - outline: none; white-space: pre-wrap; word-break: break-word; +} + +.param-value-rich-editor-highlight { + position: absolute; + inset: 0; + overflow: hidden; + border: 1px solid transparent; + border-radius: 6px; + background: #fff; + color: rgba(0, 0, 0, 0.88); + pointer-events: none; +} + +.param-value-rich-editor-highlight .param-value-template-token { + margin: 0 2px; + vertical-align: 1px; +} + +.param-value-rich-editor { + position: relative; + z-index: 1; + overflow: auto; + border: 1px solid #d9d9d9; + border-radius: 6px; + background: transparent !important; + color: transparent !important; + caret-color: rgba(0, 0, 0, 0.88); + outline: none; transition: border-color 0.2s, box-shadow 0.2s; + -webkit-text-fill-color: transparent; +} + +.param-value-rich-editor::placeholder { + color: rgba(0, 0, 0, 0.25); + -webkit-text-fill-color: rgba(0, 0, 0, 0.25); +} + +.param-value-rich-editor::selection { + background: #bae0ff; + color: rgba(0, 0, 0, 0.88); + -webkit-text-fill-color: rgba(0, 0, 0, 0.88); } .param-value-rich-editor:hover { @@ -101,16 +143,6 @@ box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1); } -.param-value-rich-editor:empty::before { - color: rgba(0, 0, 0, 0.25); - content: attr(data-placeholder); -} - -.param-value-rich-editor .param-value-template-token { - margin: 0 2px; - vertical-align: 1px; -} - .param-boolean-segmented { width: 176px; padding: 2px; diff --git a/frontend/src/pages/system/sys-params/index.tsx b/frontend/src/pages/system/sys-params/index.tsx index cd2334c..0e82652 100644 --- a/frontend/src/pages/system/sys-params/index.tsx +++ b/frontend/src/pages/system/sys-params/index.tsx @@ -1,5 +1,5 @@ import { Button, Col, Drawer, Form, Input, Popconfirm, Popover, Row, Segmented, Select, Space, Switch, Tag, Tooltip, Typography, message } from "antd"; -import { type CSSProperties, type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; +import { type ChangeEvent, type CSSProperties, type ReactNode, type UIEvent, useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { DeleteOutlined, EditOutlined, InfoCircleOutlined, PlusOutlined, SearchOutlined, SettingOutlined } from "@ant-design/icons"; import { createParam, deleteParam, pageParams, updateParam } from "@/api"; @@ -54,49 +54,6 @@ const renderTemplateParamSegments = (text: string) => { return segments; }; -const readEditorText = (element: HTMLElement) => element.innerText.replace(/\r\n/g, "\n"); - -const getCaretTextOffset = (element: HTMLElement) => { - const selection = window.getSelection(); - if (!selection || selection.rangeCount === 0 || !selection.anchorNode || !element.contains(selection.anchorNode)) { - return null; - } - - const range = selection.getRangeAt(0); - const prefixRange = range.cloneRange(); - prefixRange.selectNodeContents(element); - prefixRange.setEnd(range.endContainer, range.endOffset); - return prefixRange.toString().length; -}; - -const restoreCaretTextOffset = (element: HTMLElement, offset: number) => { - const selection = window.getSelection(); - if (!selection) return; - - const range = document.createRange(); - const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); - let remaining = offset; - let node = walker.nextNode(); - - while (node) { - const textLength = node.textContent?.length ?? 0; - if (remaining <= textLength) { - range.setStart(node, remaining); - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - return; - } - remaining -= textLength; - node = walker.nextNode(); - } - - range.selectNodeContents(element); - range.collapse(false); - selection.removeAllRanges(); - selection.addRange(range); -}; - function TemplateParamValueEditor({ value, onChange, @@ -112,52 +69,43 @@ function TemplateParamValueEditor({ placeholder?: string; rows?: number; }) { - const editorRef = useRef(null); - const pendingCaretOffsetRef = useRef(null); + const highlightRef = useRef(null); const text = value === undefined || value === null ? "" : String(value); const editorStyle = { "--param-value-editor-min-height": `${rows * 22 + 20}px` } as CSSProperties; - useLayoutEffect(() => { - const editor = editorRef.current; - const caretOffset = pendingCaretOffsetRef.current; - if (!editor || caretOffset === null || document.activeElement !== editor) { - return; - } - - restoreCaretTextOffset(editor, caretOffset); - pendingCaretOffsetRef.current = null; - }, [text]); - - const handleInput = () => { - const editor = editorRef.current; - if (!editor) return; - - pendingCaretOffsetRef.current = getCaretTextOffset(editor); - const nextValue = readEditorText(editor); + const handleChange = (event: ChangeEvent) => { + const nextValue = event.target.value; onChange?.(nextValue); onTextChange?.(nextValue); }; + const handleScroll = (event: UIEvent) => { + if (!highlightRef.current) return; + + highlightRef.current.scrollTop = event.currentTarget.scrollTop; + highlightRef.current.scrollLeft = event.currentTarget.scrollLeft; + }; + return ( -
{ - event.preventDefault(); - document.execCommand("insertText", false, event.clipboardData.getData("text/plain")); - }} - > - {text ? renderTemplateParamSegments(text) : null} +
+
+ {text ? ( + <> + {renderTemplateParamSegments(text)} + + + ) : null} +
+
); }