Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 70 additions & 23 deletions client/packages/lowcoder/src/comps/comps/richTextEditorComp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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;
Expand All @@ -207,15 +208,37 @@ function RichTextEditor(props: IProps) {
const [content, setContent] = useState("");
const wrapperRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<ReactQuill>(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;

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
Expand All @@ -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 {
Expand All @@ -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 = `<p class="">${props.value}</p>`;
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 : `<p class="">${v}</p>`;
setContent(html);
}, [props.value, editorReady]);


const handleClickWrapper = (e: React.MouseEvent<HTMLDivElement>) => {
// grid item prevents bubbling, quill can't listen to events on document.body, so it can't close the toolbar drop-down box
Expand Down Expand Up @@ -288,7 +318,7 @@ function RichTextEditor(props: IProps) {
<Suspense fallback={<Skeleton />}>
<ReactQuillEditor
key={key}
ref={editorRef}
ref={setEditorRef}
bounds={`#${id}`}
modules={{
toolbar: JSON.parse(props.toolbar),
Expand All @@ -297,23 +327,39 @@ function RichTextEditor(props: IProps) {
value={content}
placeholder={props.placeholder}
readOnly={props.readOnly}
onChange={handleChange}
onChange={(html, _delta, source, editor) => {
setContent(html);
const quill = editorRef.current?.getEditor?.();
const fullDelta = quill?.getContents?.() ?? { ops: [] };
const text = quill?.getText?.() ?? "";
onChangeRef.current(html, JSON.stringify(fullDelta), text);
}}
/>
</Suspense>
</Wrapper>
);
}

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 (
Expand Down Expand Up @@ -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,
Expand Down
Loading