serialize changes inside lexical

This commit is contained in:
Oleg Isonen committed 2022-06-29 21:06:50 +02:00
1 parent f9e4b2b8c2
commit 86ab0dc873
11 files changed
+184 -51

No files matched your search

@@ -1,20 +1,23 @@
import { type Instance } from "@webstudio-is/sdk";
import { type ChildrenUpdates, type Instance } from "@webstudio-is/sdk";
import {
RichTextPlugin,
HistoryPlugin,
InstancePlugin,
ToolbarPlugin,
OnChangePlugin,
} from "~/shared/text-editor";
type EditorProps = {
editable: JSX.Element;
children: Array<Instance | string>;
onChange: (udpates: ChildrenUpdates) => void;
};
export const Editor = ({ children, editable }: EditorProps) => {
export const Editor = ({ children, editable, onChange }: EditorProps) => {
return (
<>
<RichTextPlugin contentEditable={editable} placeholder="" />
<OnChangePlugin onChange={onChange} />
<HistoryPlugin />
<InstancePlugin>{children}</InstancePlugin>
<ToolbarPlugin />
@@ -13,6 +13,7 @@ import { useEnsureFocus } from "./use-ensure-focus";
import { Editor } from "./editor/editor";
import { useIsEditing } from "./editor/use-is-editing";
import { useContentEditable } from "~/shared/text-editor";
import noop from "lodash.noop";
type WrapperComponentDevProps = {
instance: Instance;
@@ -25,13 +26,12 @@ export const WrapperComponentDev = ({
instance,
css,
children,
onChangeChildren: _onChangeChildren,
onChangeChildren = noop,
...rest
}: WrapperComponentDevProps) => {
const className = useCss({ instance, css });
const [isEditing, isEditingProps] = useIsEditing(instance);
const [editableRefCallback, editableProps] = useContentEditable(isEditing);
const { dragRefCallback, ...draggableProps } = useDraggable({
instance,
// We can't drag if we are editing text.
@@ -87,7 +87,16 @@ export const WrapperComponentDev = ({
);
if (isEditing) {
return <Editor editable={element}>{instance.children}</Editor>;
return (
<Editor
editable={element}
onChange={(updates) => {
onChangeChildren({ instanceId: instance.id, updates });
}}
>
{instance.children}
</Editor>
);
}
return element;
@@ -1,31 +1,17 @@
import React from "react";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { action } from "@storybook/addon-actions";
import { ExampleTextEditor } from "./example-text-editor";
import "./styles.css";
import { $getRoot, $getSelection, type EditorState } from "./lexical";
// When the editor changes, you can get notified via the
// LexicalOnChangePlugin!
const onChange = (editorState: EditorState) => {
editorState.read(() => {
// Read the contents of the EditorState here.
const root = $getRoot();
const selection = $getSelection();
console.log(root, selection);
});
};
export default {
title: "Text Editor",
component: ExampleTextEditor,
} as ComponentMeta<typeof ExampleTextEditor>;
const Template: ComponentStory<typeof ExampleTextEditor> = (args) => {
export const TextEditor: ComponentStory<typeof ExampleTextEditor> = (args) => {
return <ExampleTextEditor {...args} />;
};
export const TextEditor = Template.bind({});
TextEditor.args = {
onChange,
onChange: action("onChange"),
};
@@ -1,19 +1,22 @@
import { publish, useAllUserProps } from "@webstudio-is/sdk";
import {
type EditorState,
type ChildrenUpdates,
publish,
useAllUserProps,
} from "@webstudio-is/sdk";
import {
LexicalComposer,
RichTextPlugin,
ContentEditable,
HistoryPlugin,
OnChangePlugin,
} from "./lexical";
import { OnChangePlugin } from "./plugins/plugin-on-change";
import { InstancePlugin } from "./plugins/plugin-instance";
import { TreeViewPlugin } from "./plugins/tree-view-plugin";
import { config } from "./config";
import { createInstance } from "../tree-utils";
type ExampleTextEditorProps = {
onChange: (state: EditorState) => void;
onChange: (state: ChildrenUpdates) => void;
};
const Menu = () => {
+2
View File
@@ -3,8 +3,10 @@ export {
RichTextPlugin,
ContentEditable,
HistoryPlugin,
type EditorState,
} from "./lexical";
export { config } from "./config";
export { InstancePlugin } from "./plugins/plugin-instance";
export { ToolbarPlugin } from "./plugins/plugin-toolbar";
export { OnChangePlugin } from "./plugins/plugin-on-change";
export { useContentEditable } from "./content-editable";
+6
View File
@@ -3,7 +3,13 @@ export type {
EditorState,
LexicalCommand,
LexicalEditor,
LexicalNode,
RangeSelection,
SerializedEditorState,
SerializedLexicalNode,
SerializedElementNode,
SerializedTextNode,
SerializedRootNode,
} from "lexical";
export {
ElementNode,
@@ -5,44 +5,23 @@ import {
type Instance,
toCss,
} from "@webstudio-is/sdk";
import { useCss } from "~/canvas/features/wrapper-component/use-css";
import { useBreakpoints } from "~/shared/nano-states";
import { primitives } from "../../canvas-components";
import { $createInstanceNode, InstanceNode } from "../nodes/node-instance";
import {
createCommand,
$isRangeSelection,
$getSelection,
$createTextNode,
$createParagraphNode,
$getRoot,
COMMAND_PRIORITY_EDITOR,
useLexicalComposerContext,
type LexicalCommand,
} from "../lexical";
import { useCss } from "~/canvas/features/wrapper-component/use-css";
import { useBreakpoints } from "~/shared/nano-states";
import { toLexicalNodes } from "../utils/to-lexical-nodes";
const populateRoot = (children: Instance["children"]) => {
const nodes = children.map((child) => {
if (typeof child === "string") {
const paragraph = $createParagraphNode();
paragraph.append($createTextNode(child));
return paragraph;
}
// @todo
const props = {};
// Inline components should always have a single child string
const text = typeof child.children[0] === "string" ? child.children[0] : "";
return $createInstanceNode({
component: (
<InlineWrapperComponent {...props} instance={child}>
{text}
</InlineWrapperComponent>
),
text,
});
});
const nodes = toLexicalNodes(children, InlineWrapperComponent);
const root = $getRoot();
root.clear();
for (const node of nodes) {
@@ -0,0 +1,23 @@
import { type ChildrenUpdates } from "@webstudio-is/sdk";
import {
OnChangePlugin as LexicalOnChangePlugin,
type EditorState,
} from "../lexical";
import { toUpdates } from "../utils/to-updates";
export const OnChangePlugin = ({
onChange,
}: {
onChange: (updates: ChildrenUpdates) => void;
}) => {
const handleChange = (state: EditorState) => {
state.read(() => {
const updates = toUpdates(state.toJSON().root);
onChange(updates);
console.log(state.toJSON());
console.log(updates);
});
};
return <LexicalOnChangePlugin onChange={handleChange} />;
};
@@ -0,0 +1,29 @@
import { type Instance } from "@webstudio-is/sdk";
import { $createInstanceNode } from "../nodes/node-instance";
import { $createTextNode, $createParagraphNode } from "../lexical";
export const toLexicalNodes = (
children: Instance["children"],
InlineWrapperComponent: (props: {
instance: Instance;
children: string;
}) => JSX.Element
) => {
return children.map((child) => {
if (typeof child === "string") {
const paragraph = $createParagraphNode();
paragraph.append($createTextNode(child));
return paragraph;
}
// Inline components should always have a single child string
const text = typeof child.children[0] === "string" ? child.children[0] : "";
return $createInstanceNode({
component: (
<InlineWrapperComponent instance={child}>{text}</InlineWrapperComponent>
),
text,
});
});
};
@@ -0,0 +1,55 @@
import { toUpdates } from "./to-updates";
describe("toUpdates", () => {
test("two lines of text", () => {
const state = {
root: {
children: [
{
children: [
{
detail: 0,
format: 0,
mode: "normal",
style: "",
text: "a",
type: "text",
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
type: "paragraph",
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: "normal",
style: "",
text: "b",
type: "text",
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
type: "paragraph",
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
type: "root",
version: 1,
},
} as const;
const updates = toUpdates(state.root);
expect(updates).toStrictEqual(["a\nb"]);
});
});
@@ -0,0 +1,38 @@
import { type ChildrenUpdates } from "@webstudio-is/sdk";
import {
OnChangePlugin as LexicalOnChangePlugin,
type EditorState,
type SerializedEditorState,
type SerializedTextNode,
type SerializedElementNode,
type SerializedRootNode,
type SerializedLexicalNode,
} from "../lexical";
type Node =
| SerializedRootNode
| SerializedElementNode
| SerializedTextNode
| SerializedLexicalNode;
export const toUpdates = (
node: Node,
updates: ChildrenUpdates = []
): ChildrenUpdates => {
if ("text" in node) {
const last = updates[updates.length - 1];
if (typeof last === "string") {
// Every new Paragraph node is basically just a new line.
updates[updates.length - 1] = `${last}\n${node.text}`;
} else {
updates.push(node.text);
}
}
if ("children" in node) {
for (const child of node.children) {
toUpdates(child, updates);
}
}
return updates;
};