diff --git a/client/packages/lowcoder/src/comps/comps/richTextEditorComp.tsx b/client/packages/lowcoder/src/comps/comps/richTextEditorComp.tsx index 64ec62a95..a75120511 100644 --- a/client/packages/lowcoder/src/comps/comps/richTextEditorComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/richTextEditorComp.tsx @@ -171,7 +171,8 @@ const toolbarOptions = [ ]; const childrenMap = { - value: stringExposingStateControl("value"), + value: stringExposingStateControl("value"), + delta: stringExposingStateControl("delta"), hideToolbar: BoolControl, readOnly: BoolControl, autoHeight: withDefault(AutoHeightControl, "fixed"), @@ -194,7 +195,7 @@ interface IProps { hideToolbar: boolean; readOnly: boolean; autoHeight: boolean; - onChange: (value: string) => void; + onChange: (html: string, deltaJSON: string, text: string) => void; $style: RichTextEditorStyleType; contentScrollBar: boolean; tabIndex?: number; @@ -207,6 +208,28 @@ function RichTextEditor(props: IProps) { const [content, setContent] = useState(""); const wrapperRef = useRef(null); const editorRef = useRef(null); + + // know exactly when the editor mounts + const [editorReady, setEditorReady] = useState(false); + const setEditorRef = (node: ReactQuill | null) => { + (editorRef as any).current = node as any; + setEditorReady(!!node); + }; + + const getQuill = () => (editorRef.current as any)?.getEditor?.(); + + const tryParseDelta = (v: unknown) => { + if (!v) return null; + if (typeof v === "string") { + try { + const d = JSON.parse(v); + return Array.isArray(d?.ops) ? d : null; + } catch { return null; } + } + if (typeof v === "object" && Array.isArray((v as any).ops)) return v as any; + return null; + }; + const isTypingRef = useRef(0); const debounce = INPUT_DEFAULT_ONCHANGE_DEBOUNCE; @@ -214,8 +237,8 @@ function RichTextEditor(props: IProps) { const originOnChangeRef = useRef(props.onChange); originOnChangeRef.current = props.onChange; - const onChangeRef = useRef( - (v: string) => originOnChangeRef.current?.(v) + const onChangeRef = useRef((html: string, deltaJSON: string, text: string) => + originOnChangeRef.current?.(html, deltaJSON, text) ); // react-quill will not take effect after the placeholder is updated @@ -235,7 +258,7 @@ function RichTextEditor(props: IProps) { (editor.scroll.domNode as HTMLElement).tabIndex = props.tabIndex; } } - }, [props.tabIndex, key]); // Also re-run when key changes due to placeholder update + }, [props.tabIndex, key]); const contains = (parent: HTMLElement, descendant: HTMLElement) => { try { @@ -248,19 +271,26 @@ function RichTextEditor(props: IProps) { return parent.contains(descendant); }; - const handleChange = (value: string) => { - setContent(value); - // props.onChange(value); - onChangeRef.current(value); - }; useEffect(() => { - let finalValue = props.value; - if (!/^<\w+>.+<\/\w+>$/.test(props.value)) { - finalValue = `

${props.value}

`; + const q = getQuill(); + if (!q) { + return; } - setContent(finalValue); - }, [props.value]); + + const asDelta = tryParseDelta(props.value); + if (asDelta) { + q.setContents(asDelta, "api"); + const html = q.root?.innerHTML ?? ""; + setContent(html); + return; + } + const v = props.value ?? ""; + const looksHtml = /<\/?[a-z][\s\S]*>/i.test(v); + const html = looksHtml ? v : `

${v}

`; + setContent(html); + }, [props.value, editorReady]); + const handleClickWrapper = (e: React.MouseEvent) => { // grid item prevents bubbling, quill can't listen to events on document.body, so it can't close the toolbar drop-down box @@ -288,7 +318,7 @@ function RichTextEditor(props: IProps) { }> { + setContent(html); + const quill = editorRef.current?.getEditor?.(); + const fullDelta = quill?.getContents?.() ?? { ops: [] }; + const text = quill?.getText?.() ?? ""; + onChangeRef.current(html, JSON.stringify(fullDelta), text); + }} /> @@ -305,15 +341,25 @@ function RichTextEditor(props: IProps) { } const RichTextEditorCompBase = new UICompBuilder(childrenMap, (props) => { + const propsRef = useRef(props); + propsRef.current = props; + const debouncedOnChangeRef = useRef( - debounce((value: string) => { - props.value.onChange(value); - props.onEvent("change"); - }, 1000) + debounce((html: string, deltaJSON: string, text: string) => { + propsRef.current.value.onChange(html); + propsRef.current.delta.onChange(deltaJSON); + propsRef.current.onEvent("change"); + }, 500) ); - const handleChange = (value: string) => { - debouncedOnChangeRef.current?.(value); + useEffect(() => { + return () => { + debouncedOnChangeRef.current?.cancel(); + }; + }, []); + + const handleChange = (html: string, deltaJSON: string, text: string) => { + debouncedOnChangeRef.current?.(html, deltaJSON, text); }; return ( @@ -379,6 +425,7 @@ class RichTextEditorCompAutoHeight extends RichTextEditorCompBase { export const RichTextEditorComp = withExposingConfigs(RichTextEditorCompAutoHeight, [ new NameConfig("value", trans("export.richTextEditorValueDesc")), + new NameConfig("delta", trans("export.richTextEditorDeltaDesc")), new NameConfig("readOnly", trans("export.richTextEditorReadOnlyDesc")), new NameConfig("hideToolbar", trans("export.richTextEditorHideToolBarDesc")), NameConfigHidden,