Skip to content

Commit 4747b25

Browse files
committed
#1987 introduce delta for editor
1 parent 7dc8e77 commit 4747b25

File tree

1 file changed

+56
-20
lines changed

1 file changed

+56
-20
lines changed

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

Lines changed: 56 additions & 20 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,30 @@ function RichTextEditor(props: IProps) {
207208
const [content, setContent] = useState("");
208209
const wrapperRef = useRef<HTMLDivElement>(null);
209210
const editorRef = useRef<ReactQuill>(null);
211+
212+
const getQuill = () => (editorRef.current as any)?.getEditor?.();
213+
214+
const tryParseDelta = (v: unknown) => {
215+
if (!v) return null;
216+
if (typeof v === "string") {
217+
try {
218+
const d = JSON.parse(v);
219+
return Array.isArray(d?.ops) ? d : null;
220+
} catch { return null; }
221+
}
222+
if (typeof v === "object" && Array.isArray((v as any).ops)) return v as any;
223+
return null;
224+
};
225+
210226
const isTypingRef = useRef(0);
211227

212228
const debounce = INPUT_DEFAULT_ONCHANGE_DEBOUNCE;
213229

214230
const originOnChangeRef = useRef(props.onChange);
215231
originOnChangeRef.current = props.onChange;
216232

217-
const onChangeRef = useRef(
218-
(v: string) => originOnChangeRef.current?.(v)
233+
const onChangeRef = useRef((html: string, deltaJSON: string, text: string) =>
234+
originOnChangeRef.current?.(html, deltaJSON, text)
219235
);
220236

221237
// react-quill will not take effect after the placeholder is updated
@@ -235,7 +251,7 @@ function RichTextEditor(props: IProps) {
235251
(editor.scroll.domNode as HTMLElement).tabIndex = props.tabIndex;
236252
}
237253
}
238-
}, [props.tabIndex, key]); // Also re-run when key changes due to placeholder update
254+
}, [props.tabIndex, key]);
239255

240256
const contains = (parent: HTMLElement, descendant: HTMLElement) => {
241257
try {
@@ -248,19 +264,31 @@ function RichTextEditor(props: IProps) {
248264
return parent.contains(descendant);
249265
};
250266

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

257268
useEffect(() => {
258-
let finalValue = props.value;
259-
if (!/^<\w+>.+<\/\w+>$/.test(props.value)) {
260-
finalValue = `<p class="">${props.value}</p>`;
269+
const q = getQuill();
270+
271+
if (!q) {
272+
const v = props.value ?? "";
273+
const looksHtml = /<\/?[a-z][\s\S]*>/i.test(v);
274+
setContent(looksHtml ? v : `<p class="">${v}</p>`);
275+
return;
261276
}
262-
setContent(finalValue);
277+
278+
const asDelta = tryParseDelta(props.value);
279+
if (asDelta) {
280+
q.setContents(asDelta, "api");
281+
const html = q.root?.innerHTML ?? "";
282+
setContent(html);
283+
return;
284+
}
285+
286+
const v = props.value ?? "";
287+
const looksHtml = /<\/?[a-z][\s\S]*>/i.test(v);
288+
const html = looksHtml ? v : `<p class="">${v}</p>`;
289+
setContent(html);
263290
}, [props.value]);
291+
264292

265293
const handleClickWrapper = (e: React.MouseEvent<HTMLDivElement>) => {
266294
// grid item prevents bubbling, quill can't listen to events on document.body, so it can't close the toolbar drop-down box
@@ -297,7 +325,13 @@ function RichTextEditor(props: IProps) {
297325
value={content}
298326
placeholder={props.placeholder}
299327
readOnly={props.readOnly}
300-
onChange={handleChange}
328+
onChange={(html, _delta, source, editor) => {
329+
setContent(html);
330+
const quill = editorRef.current?.getEditor?.();
331+
const fullDelta = quill?.getContents?.() ?? { ops: [] };
332+
const text = quill?.getText?.() ?? "";
333+
onChangeRef.current(html, JSON.stringify(fullDelta), text);
334+
}}
301335
/>
302336
</Suspense>
303337
</Wrapper>
@@ -306,15 +340,16 @@ function RichTextEditor(props: IProps) {
306340

307341
const RichTextEditorCompBase = new UICompBuilder(childrenMap, (props) => {
308342
const debouncedOnChangeRef = useRef(
309-
debounce((value: string) => {
310-
props.value.onChange(value);
343+
debounce((html: string, deltaJSON: string, text: string) => {
344+
props.value.onChange(html);
345+
props.delta.onChange(deltaJSON);
311346
props.onEvent("change");
312347
}, 1000)
313348
);
314349

315-
const handleChange = (value: string) => {
316-
debouncedOnChangeRef.current?.(value);
317-
};
350+
const handleChange = (html: string, deltaJSON: string, text: string) => {
351+
debouncedOnChangeRef.current?.(html, deltaJSON, text);
352+
};
318353

319354
return (
320355
<RichTextEditor
@@ -379,6 +414,7 @@ class RichTextEditorCompAutoHeight extends RichTextEditorCompBase {
379414

380415
export const RichTextEditorComp = withExposingConfigs(RichTextEditorCompAutoHeight, [
381416
new NameConfig("value", trans("export.richTextEditorValueDesc")),
417+
new NameConfig("delta", trans("export.richTextEditorDeltaDesc")),
382418
new NameConfig("readOnly", trans("export.richTextEditorReadOnlyDesc")),
383419
new NameConfig("hideToolbar", trans("export.richTextEditorHideToolBarDesc")),
384420
NameConfigHidden,

0 commit comments

Comments
 (0)