mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 13:05:08 +09:00
serialize changes inside lexical
This commit is contained in:
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 = () => {
|
||||
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user