Skip to content

Commit 79acea5

Browse files
Merge pull request #2003 from iamfaran/fix/1987-rich-editor
[Fix]: #1987 introduce delta for editor
2 parents 4c8b4ea + c7c02ac commit 79acea5

File tree

1 file changed

+70
-23
lines changed

1 file changed

+70
-23
lines changed

client/packages/lowcoder/src/comps/comps/richTextEditorComp.tsx

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ const toolbarOptions = [
171171
];
172172

173173
const childrenMap = {
174-
value: stringExposingStateControl("value"),
174+
value: stringExposingStateControl("value"),
175+
delta: stringExposingStateControl("delta"),
175176
hideToolbar: BoolControl,
176177
readOnly: BoolControl,
177178
autoHeight: withDefault(AutoHeightControl, "fixed"),
@@ -194,7 +195,7 @@ interface IProps {
194195
hideToolbar: boolean;
195196
readOnly: boolean;
196197
autoHeight: boolean;
197-
onChange: (value: string) => void;
198+
onChange: (html: string, deltaJSON: string, text: string) => void;
198199
$style: RichTextEditorStyleType;
199200
contentScrollBar: boolean;
200201
tabIndex?: number;
@@ -207,15 +208,37 @@ function RichTextEditor(props: IProps) {
207208
const [content, setContent] = useState("");
208209
const wrapperRef = useRef<HTMLDivElement>(null);
209210
const editorRef = useRef<ReactQuill>(null);
211+
212+
// know exactly when the editor mounts
213+
const [editorReady, setEditorReady] = useState(false);
214+
const setEditorRef = (node: ReactQuill | null) => {
215+
(editorRef as any).current = node as any;
216+
setEditorReady(!!node);
217+
};
218+
219+
const getQuill = () => (editorRef.current as any)?.getEditor?.();
220+
221+
const tryParseDelta = (v: unknown) => {
222+
if (!v) return null;
223+
if (typeof v === "string") {
224+
try {
225+
const d = JSON.parse(v);
226+
return Array.isArray(d?.ops) ? d : null;
227+
} catch { return null; }
228+
}
229+
if (typeof v === "object" && Array.isArray((v as any).ops)) return v as any;
230+
return null;
231+
};
232+
210233
const isTypingRef = useRef(0);
211234

212235
const debounce = INPUT_DEFAULT_ONCHANGE_DEBOUNCE;
213236

214237
const originOnChangeRef = useRef(props.onChange);
215238
originOnChangeRef.current = props.onChange;
216239

217-
const onChangeRef = useRef(
218-
(v: string) => originOnChangeRef.current?.(v)
240+
const onChangeRef = useRef((html: string, deltaJSON: string, text: string) =>
241+
originOnChangeRef.current?.(html, deltaJSON, text)
219242
);
220243

221244
// react-quill will not take effect after the placeholder is updated
@@ -235,7 +258,7 @@ function RichTextEditor(props: IProps) {
235258
(editor.scroll.domNode as HTMLElement).tabIndex = props.tabIndex;
236259
}
237260
}
238-
}, [props.tabIndex, key]); // Also re-run when key changes due to placeholder update
261+
}, [props.tabIndex, key]);
239262

240263
const contains = (parent: HTMLElement, descendant: HTMLElement) => {
241264
try {
@@ -248,19 +271,26 @@ function RichTextEditor(props: IProps) {
248271
return parent.contains(descendant);
249272
};
250273

251-
const handleChange = (value: string) => {
252-
setContent(value);
253-
// props.onChange(value);
254-
onChangeRef.current(value);
255-
};
256274

257275
useEffect(() => {
258-
let finalValue = props.value;
259-
if (!/^<\w+>.+<\/\w+>$/.test(props.value)) {
260-
finalValue = `<p class="">${props.value}</p>`;
276+
const q = getQuill();
277+
if (!q) {
278+
return;
261279
}
262-
setContent(finalValue);
263-
}, [props.value]);
280+
281+
const asDelta = tryParseDelta(props.value);
282+
if (asDelta) {
283+
q.setContents(asDelta, "api");
284+
const html = q.root?.innerHTML ?? "";
285+
setContent(html);
286+
return;
287+
}
288+
const v = props.value ?? "";
289+
const looksHtml = /<\/?[a-z][\s\S]*>/i.test(v);
290+
const html = looksHtml ? v : `<p class="">${v}</p>`;
291+
setContent(html);
292+
}, [props.value, editorReady]);
293+
264294

265295
const handleClickWrapper = (e: React.MouseEvent<HTMLDivElement>) => {
266296
// 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) {
288318
<Suspense fallback={<Skeleton />}>
289319
<ReactQuillEditor
290320
key={key}
291-
ref={editorRef}
321+
ref={setEditorRef}
292322
bounds={`#${id}`}
293323
modules={{
294324
toolbar: JSON.parse(props.toolbar),
@@ -297,23 +327,39 @@ function RichTextEditor(props: IProps) {
297327
value={content}
298328
placeholder={props.placeholder}
299329
readOnly={props.readOnly}
300-
onChange={handleChange}
330+
onChange={(html, _delta, source, editor) => {
331+
setContent(html);
332+
const quill = editorRef.current?.getEditor?.();
333+
const fullDelta = quill?.getContents?.() ?? { ops: [] };
334+
const text = quill?.getText?.() ?? "";
335+
onChangeRef.current(html, JSON.stringify(fullDelta), text);
336+
}}
301337
/>
302338
</Suspense>
303339
</Wrapper>
304340
);
305341
}
306342

307343
const RichTextEditorCompBase = new UICompBuilder(childrenMap, (props) => {
344+
const propsRef = useRef(props);
345+
propsRef.current = props;
346+
308347
const debouncedOnChangeRef = useRef(
309-
debounce((value: string) => {
310-
props.value.onChange(value);
311-
props.onEvent("change");
312-
}, 1000)
348+
debounce((html: string, deltaJSON: string, text: string) => {
349+
propsRef.current.value.onChange(html);
350+
propsRef.current.delta.onChange(deltaJSON);
351+
propsRef.current.onEvent("change");
352+
}, 500)
313353
);
314354

315-
const handleChange = (value: string) => {
316-
debouncedOnChangeRef.current?.(value);
355+
useEffect(() => {
356+
return () => {
357+
debouncedOnChangeRef.current?.cancel();
358+
};
359+
}, []);
360+
361+
const handleChange = (html: string, deltaJSON: string, text: string) => {
362+
debouncedOnChangeRef.current?.(html, deltaJSON, text);
317363
};
318364

319365
return (
@@ -379,6 +425,7 @@ class RichTextEditorCompAutoHeight extends RichTextEditorCompBase {
379425

380426
export const RichTextEditorComp = withExposingConfigs(RichTextEditorCompAutoHeight, [
381427
new NameConfig("value", trans("export.richTextEditorValueDesc")),
428+
new NameConfig("delta", trans("export.richTextEditorDeltaDesc")),
382429
new NameConfig("readOnly", trans("export.richTextEditorReadOnlyDesc")),
383430
new NameConfig("hideToolbar", trans("export.richTextEditorHideToolBarDesc")),
384431
NameConfigHidden,

0 commit comments

Comments
 (0)