Skip to content

Commit 85fb223

Browse files
committed
chore: wire up lexical editor to db
1 parent 974bccd commit 85fb223

File tree

9 files changed

+78
-42
lines changed

9 files changed

+78
-42
lines changed

packages/root-cms/shared/richtext.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ export interface RichTextCustomBlock<TypeName = string, DataType = any> {
5656
}
5757

5858
export interface RichTextData {
59-
[key: string]: any;
6059
blocks: RichTextBlock[];
60+
time: number;
61+
version: string;
6162
}
6263

6364
export function testValidRichTextData(data: RichTextData) {

packages/root-cms/ui/components/DocEditor/fields/RichTextField.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ export function RichTextField(props: FieldProps) {
1111
const [value, setValue] = useState<RichTextData | null>(null);
1212

1313
function onChange(newValue: RichTextData | null) {
14-
props.draft.updateKey(props.deepKey, newValue);
15-
setValue(newValue);
14+
setValue((oldValue) => {
15+
if (oldValue?.time !== newValue?.time) {
16+
props.draft.updateKey(props.deepKey, newValue);
17+
}
18+
return newValue;
19+
});
1620
}
1721

1822
useEffect(() => {

packages/root-cms/ui/components/RichTextEditor/LexicalEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function LexicalEditor(props: LexicalEditorProps) {
7171

7272
interface EditorProps {
7373
placeholder?: string;
74-
value?: RichTextData;
74+
value?: RichTextData | null;
7575
onChange?: (value: RichTextData | null) => void;
7676
}
7777

packages/root-cms/ui/components/RichTextEditor/LexicalTheme.css

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,20 @@
2525
padding-left: 16px;
2626
}
2727
.LexicalTheme__h1 {
28-
font-size: 20px;
29-
font-weight: 600;
30-
margin: 0;
28+
font-size: 18px;
3129
}
3230
.LexicalTheme__h2 {
3331
font-size: 16px;
34-
font-weight: 600;
35-
margin: 0;
3632
}
3733
.LexicalTheme__h3 {
3834
font-size: 14px;
39-
font-weight: 600;
40-
margin: 0;
4135
}
4236
.LexicalTheme__h1,
4337
.LexicalTheme__h2,
4438
.LexicalTheme__h3 {
39+
margin: 0;
4540
margin-top: 8px;
41+
font-weight: 500;
4642
}
4743
.LexicalTheme__h1:first-child,
4844
.LexicalTheme__h2:first-child,
@@ -462,7 +458,7 @@
462458
margin-top: 0;
463459
}
464460
.LexicalTheme__listItem {
465-
margin: 4px 20px;
461+
margin: 2px 20px;
466462
}
467463
.LexicalTheme__listItem::marker {
468464
color: var(--listitem-marker-color);

packages/root-cms/ui/components/RichTextEditor/RichTextEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function RichTextEditor(props: RichTextEditorProps) {
2121
className={props.className}
2222
placeholder={props.placeholder}
2323
value={props.value}
24-
// onChange={props.onChange}
24+
onChange={props.onChange}
2525
/>
2626
);
2727
}

packages/root-cms/ui/components/RichTextEditor/plugins/FloatingToolbarPlugin.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
will-change: transform;
1717
}
1818

19+
.LexicalEditor__floatingToolbar .LexicalEditor__toolbar__actionIcon button {
20+
color: black;
21+
}
22+
1923
.LexicalEditor__floatingToolbar .LexicalEditor__toolbar__actionIcon--active button {
2024
border: 1px solid rgb(206, 212, 218)
2125
}

packages/root-cms/ui/components/RichTextEditor/plugins/OnChangePlugin.tsx

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import {useEffect} from 'preact/hooks';
1+
import {useEffect, useState} from 'preact/hooks';
22
import {RichTextData} from '../../../../shared/richtext.js';
33
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
44
import {convertToRichTextData} from '../utils/convert-from-lexical.js';
55
import {convertToLexical} from '../utils/convert-to-lexical.js';
6+
import {OnChangePlugin as LexicalOnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
7+
import {EditorState} from 'lexical';
68

79
export interface OnChangePluginProps {
810
value?: RichTextData | null;
@@ -12,25 +14,35 @@ export interface OnChangePluginProps {
1214
export function OnChangePlugin(props: OnChangePluginProps) {
1315
const [editor] = useLexicalComposerContext();
1416

15-
useEffect(() => {
16-
console.log('rte value change upstream:', props.value);
17-
editor.update(() => {
18-
convertToLexical(props.value);
19-
});
20-
}, [editor, props.value]);
17+
const [timeSaved, setTimeSaved] = useState(0);
18+
const [isUpdating, setIsUpdating] = useState(false);
2119

2220
useEffect(() => {
23-
return editor.registerUpdateListener(({editorState}) => {
24-
editorState.read(() => {
25-
const data = toRichTextData();
26-
if (props.onChange) {
27-
props.onChange(data);
28-
}
21+
const time = props.value?.time || 0;
22+
if (timeSaved !== time) {
23+
editor.update(() => {
24+
console.log('lexical update, props.value changed:', props.value);
25+
setIsUpdating(true);
26+
convertToLexical(props.value);
2927
});
28+
}
29+
}, [editor, props.value]);
30+
31+
function onChange(editorState: EditorState) {
32+
if (isUpdating) {
33+
setIsUpdating(false);
34+
return;
35+
}
36+
editorState.read(() => {
37+
const richTextData = toRichTextData();
38+
setTimeSaved(richTextData?.time || 0);
39+
if (props.onChange) {
40+
props.onChange(richTextData);
41+
}
3042
});
31-
}, [editor, props.onChange]);
43+
}
3244

33-
return null;
45+
return <LexicalOnChangePlugin onChange={onChange} ignoreSelectionChange />;
3446
}
3547

3648
function toRichTextData(): RichTextData | null {

packages/root-cms/ui/components/RichTextEditor/utils/convert-from-lexical.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,19 +106,30 @@ export function convertToRichTextData(): RichTextData | null {
106106
}
107107
});
108108

109-
// If the last block is an empty paragraph, remove it.
110-
const lastBlock = blocks.length > 0 && blocks.at(-1);
111-
if (lastBlock && lastBlock.type === 'paragraph' && !lastBlock.data?.text) {
109+
// If the last block is empty, remove it.
110+
while (testLastBlockIsEmpty(blocks)) {
112111
blocks.pop();
113112
}
114113

114+
// Use `null` when the RTE is empty, which allows components to use boolean
115+
// expressions to determine whether to render the RTE field.
115116
if (blocks.length === 0) {
116117
return null;
117118
}
118119

120+
// NOTE(stevenle): The RTE was originally implemented with EditorJS, the data
121+
// format is preserved for backward compatibility.
119122
return {
120123
time: Date.now(),
121124
blocks,
122125
version: 'lexical-0.31.2',
123126
};
124127
}
128+
129+
function testLastBlockIsEmpty(blocks: RichTextBlock[]) {
130+
const lastBlock = blocks.length > 0 && blocks.at(-1);
131+
if (lastBlock && lastBlock.type === 'paragraph' && !lastBlock.data?.text) {
132+
return true;
133+
}
134+
return false;
135+
}

packages/root-cms/ui/components/RichTextEditor/utils/convert-to-lexical.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {$applyNodeReplacement, $createParagraphNode, $createTextNode, $getRoot} from 'lexical';
22
import {RichTextData, RichTextListItem, RichTextListItemNestedList, RichTextListItemText} from '../../../../shared/richtext.js';
3-
import {$createHeadingNode} from '@lexical/rich-text';
3+
import {$createHeadingNode, HeadingTagType} from '@lexical/rich-text';
44
import {$createLinkNode} from '@lexical/link';
55
import {$createListItemNode, $createListNode, ListItemNode} from '@lexical/list';
66

@@ -23,7 +23,8 @@ export function convertToLexical(data?: RichTextData | null) {
2323
}
2424
root.append(paragraphNode);
2525
} else if (block.type === 'heading') {
26-
const headingNode = $createHeadingNode();
26+
const tagName = `h${block.data?.level || 2}` as HeadingTagType;
27+
const headingNode = $createHeadingNode(tagName);
2728
if (block.data.text) {
2829
const children = createNodesFromHTML(block.data.text);
2930
headingNode.append(...children);
@@ -54,29 +55,34 @@ function createNodesFromHTML(htmlString: string) {
5455

5556
if (domNode.nodeType === Node.ELEMENT_NODE) {
5657
const el = domNode as HTMLElement;
57-
const children = Array.from(el.childNodes).map(parseNode).filter(Boolean);
58+
const children = Array.from(el.childNodes).map(parseNode).filter(Boolean).flat();
5859

5960
switch (el.tagName.toLowerCase()) {
6061
case 'b':
6162
case 'strong':
62-
return children.map(child => {
63-
child.setFormat('bold');
63+
return children.map((child) => {
64+
child.toggleFormat('bold');
6465
return child;
6566
});
6667
case 'i':
6768
case 'em':
68-
return children.map(child => {
69-
child.setFormat('italic');
69+
return children.map((child) => {
70+
child.toggleFormat('italic');
7071
return child;
7172
});
7273
case 'u':
73-
return children.map(child => {
74-
child.setFormat('underline');
74+
return children.map((child) => {
75+
child.toggleFormat('underline');
76+
return child;
77+
});
78+
case 's':
79+
return children.map((child) => {
80+
child.toggleFormat('strikethrough');
7581
return child;
7682
});
7783
case 'sup':
78-
return children.map(child => {
79-
child.setFormat('superscript');
84+
return children.map((child) => {
85+
child.toggleFormat('superscript');
8086
return child;
8187
});
8288
case 'a':
@@ -86,6 +92,8 @@ function createNodesFromHTML(htmlString: string) {
8692
),
8793
];
8894
default:
95+
console.log('unhandled tag: ' + el.tagName);
96+
console.log(children);
8997
return children;
9098
}
9199
}

0 commit comments

Comments
 (0)