Merge pull request #234 from webstudio-is/lazy-load-text-editor

Lazy load text editor
This commit is contained in:
Oleg Isonen authored and GitHub committed 2022-08-22 13:58:07 +02:00
commit 901255dbbd
13 files changed
+108 -93

No files matched your search

+3 -10
View File
@@ -45,10 +45,7 @@ import { registerContainers } from "./shared/immerhin";
import { useTrackHoveredElement } from "./shared/use-track-hovered-element";
import { usePublishScrollState } from "./shared/use-publish-scroll-state";
import { useDragAndDrop } from "./shared/use-drag-drop";
import {
LexicalComposer,
config,
} from "~/canvas/features/wrapper-component/text-editor";
import { setInstanceChildrenMutable } from "~/shared/tree-utils";
registerContainers();
@@ -133,18 +130,14 @@ export const Canvas = ({ data }: CanvasProps): JSX.Element | null => {
if (elements === undefined) return null;
const children = (
<LexicalComposer initialConfig={config}>{elements}</LexicalComposer>
);
if (isPreviewMode) {
return children;
return elements;
}
return (
<>
<DesignMode treeId={data.tree.id} project={data.project} />
{children}
{elements}
</>
);
};
@@ -1,7 +1,7 @@
import { InstanceNode } from "./nodes/node-instance";
export const config = {
namespace: "ComponentTextEditor",
namespace: "WsTextEditor",
onError(error: Error) {
throw error;
},
@@ -0,0 +1,22 @@
import { memo, lazy, Suspense } from "react";
import type { EditorProps } from "./editor";
const EditorLazy = lazy(() => import("./editor"));
type EditorWithSuspenseProps = EditorProps & {
fallback: JSX.Element;
};
const EditorWithSuspense = ({
fallback,
...props
}: EditorWithSuspenseProps) => {
return (
<Suspense fallback={fallback}>
<EditorLazy {...props} />
</Suspense>
);
};
// Prevent rerender because in editing mode Editor controls the node.
export const Editor = memo(EditorWithSuspense, () => true);
@@ -1,23 +1,40 @@
import { type ChildrenUpdates, type Instance } from "@webstudio-is/react-sdk";
import { RichTextPlugin, HistoryPlugin } from "./lexical";
import type { ChildrenUpdates, Instance } from "@webstudio-is/react-sdk";
import {
RichTextPlugin,
HistoryPlugin,
LexicalComposer,
useLexicalComposerContext,
} from "./lexical";
import { InstancePlugin } from "./plugins/plugin-instance";
import { ToolbarConnectorPlugin } from "./plugins/plugin-toolbar-connector";
import { OnChangePlugin } from "./plugins/plugin-on-change";
import { memo } from "react";
import { config } from "./config";
import { useCallback } from "react";
type EditorProps = {
children?: JSX.Element;
editable: JSX.Element;
type InstanceRenderProps = {
ref: (element: HTMLElement | null) => void;
contentEditable: boolean;
};
export type EditorProps = {
renderInstance: (renderProps: InstanceRenderProps) => JSX.Element;
instance: Instance;
onChange: (updates: ChildrenUpdates) => void;
};
export const Editor = ({
instance,
editable,
children,
onChange,
}: EditorProps) => {
const useSetElement = () => {
const [editor] = useLexicalComposerContext();
return useCallback(
(element) => {
editor.setRootElement(element);
},
[editor]
);
};
const Editor = ({ instance, renderInstance, onChange }: EditorProps) => {
const ref = useSetElement();
const editable = renderInstance({ ref, contentEditable: true });
return (
<>
<RichTextPlugin contentEditable={editable} placeholder="" />
@@ -25,10 +42,16 @@ export const Editor = ({
<HistoryPlugin />
<InstancePlugin>{instance.children}</InstancePlugin>
<ToolbarConnectorPlugin />
{children}
</>
);
};
// Prevent rerender because in editing mode Editor controls the node.
export const EditorMemoized = memo(Editor, () => true);
const EditorConfigured = (props: EditorProps) => {
return (
<LexicalComposer initialConfig={config}>
<Editor {...props} />
</LexicalComposer>
);
};
export default EditorConfigured;
@@ -9,7 +9,7 @@ import { Box, Button } from "@webstudio-is/design-system";
import { LexicalComposer, ContentEditable } from "./lexical";
import { TreeViewPlugin } from "./plugins/tree-view-plugin";
import { config } from "./config";
import { Editor } from "./editor";
import Editor from "./editor";
type ExampleTextEditorProps = {
onChange: (state: ChildrenUpdates) => void;
@@ -100,7 +100,7 @@ export const ExampleTextEditor = ({ onChange }: ExampleTextEditorProps) => {
}}
>
<Editor
editable={
renderInstance={() => (
<ContentEditable
style={{
minHeight: 150,
@@ -114,7 +114,7 @@ export const ExampleTextEditor = ({ onChange }: ExampleTextEditorProps) => {
background: "#eee",
}}
/>
}
)}
instance={
{
component: "Paragraph",
@@ -1,31 +0,0 @@
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useCallback, useEffect } from "react";
type RefCallback = (rootElement: null | HTMLElement) => void;
const props = {
//autoCapitalize: false,
//autoComplete: false,
contentEditable: true,
};
export const useContentEditable = (
isEditing: boolean
): [RefCallback, typeof props | undefined] => {
const [editor] = useLexicalComposerContext();
const ref: RefCallback = useCallback(
(rootElement) => {
editor.setRootElement(rootElement);
},
[editor]
);
useEffect(() => {
if (isEditing === true) return;
// Lets unset the root element when we are not editing
editor.setRootElement(null);
}, [editor, isEditing]);
return [ref, isEditing ? props : undefined];
};
@@ -1,4 +1 @@
export { LexicalComposer } from "./lexical";
export { config } from "./config";
export { useContentEditable } from "./hooks/use-content-editable";
export { EditorMemoized } from "./editor";
export { Editor } from "./editor-lazy";
@@ -11,7 +11,7 @@ import {
import { useBreakpoints, useTextEditingInstanceId } from "~/shared/nano-states";
import { useCss } from "./use-css";
import { useEnsureFocus } from "./use-ensure-focus";
import { EditorMemoized, useContentEditable } from "./text-editor";
import { Editor } from "./text-editor";
import noop from "lodash.noop";
import { useSelectedElement } from "~/canvas/shared/nano-states";
@@ -32,18 +32,15 @@ export const WrapperComponentDev = ({
const className = useCss({ instance, css });
const [editingInstanceId] = useTextEditingInstanceId();
const [, setSelectedElement] = useSelectedElement();
const isEditing = editingInstanceId === instance.id;
const [editableRefCallback, editableProps] = useContentEditable(isEditing);
const focusRefCallback = useEnsureFocus();
const refCallback = useCallback(
(element) => {
if (isEditing) editableRefCallback(element);
focusRefCallback(element);
// When entering text editing we unmount the instance element, so we need to update the reference, otherwise we have a detached element referenced and bounding box will be wrong.
if (element !== null) setSelectedElement(element);
},
[focusRefCallback, editableRefCallback, setSelectedElement, isEditing]
[focusRefCallback, setSelectedElement]
);
const userProps = useUserProps(instance.id);
@@ -56,12 +53,13 @@ export const WrapperComponentDev = ({
...userProps,
...rest,
...readonlyProps,
...editableProps,
// @todo merge className with props
className: className,
// @todo stop using id to free it up to the user
id: instance.id,
className,
tabIndex: 0,
// @todo stop using id to free it up to the user
// we should replace id, data-component and data-id with "data-ws"=instance.id and grab the rest always over the id
// for this we need to also make search by id fast
id: instance.id,
"data-component": instance.component,
"data-id": instance.id,
ref: refCallback,
@@ -76,20 +74,32 @@ export const WrapperComponentDev = ({
},
};
if (isEditing) {
return (
<EditorMemoized
instance={instance}
editable={<Component {...props} />}
onChange={(updates) => {
onChangeChildren({ instanceId: instance.id, updates });
}}
/>
);
const instanceElement = (
<Component {...props}>{renderWrapperComponentChildren(children)}</Component>
);
if (editingInstanceId !== instance.id) {
return instanceElement;
}
return (
<Component {...props}>{renderWrapperComponentChildren(children)}</Component>
<Editor
instance={instance}
fallback={instanceElement}
renderInstance={({ ref, ...renderProps }) => (
<Component
{...props}
{...renderProps}
ref={(element: HTMLElement | null) => {
props.ref(element);
ref(element);
}}
/>
)}
onChange={(updates) => {
onChangeChildren({ instanceId: instance.id, updates });
}}
/>
);
};
+1 -1
View File
@@ -4,7 +4,7 @@
"include": ["./src/**/*.ts"],
"compilerOptions": {
"module": "commonjs",
"target": "ES2019",
"target": "ES2020",
"strict": true,
"baseUrl": ".",
"outDir": "./lib"
+2 -2
View File
@@ -3,10 +3,10 @@
"extends": "../../node_modules/@webstudio-is/tsconfig/base.json",
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"lib": ["DOM", "DOM.Iterable", "ES2020"],
"module": "commonjs",
"jsx": "react-jsx",
"target": "ES2019",
"target": "ES2020",
"strict": true,
"baseUrl": ".",
"paths": {
+2 -2
View File
@@ -3,10 +3,10 @@
"extends": "../../node_modules/@webstudio-is/tsconfig/base.json",
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"lib": ["DOM", "DOM.Iterable", "ES2020"],
"module": "commonjs",
"jsx": "react-jsx",
"target": "ES2019",
"target": "ES2020",
"strict": true,
"baseUrl": ".",
"paths": {
+2 -2
View File
@@ -3,10 +3,10 @@
"extends": "../../node_modules/@webstudio-is/tsconfig/base.json",
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"lib": ["DOM", "DOM.Iterable", "ES2020"],
"module": "commonjs",
"jsx": "react-jsx",
"target": "ES2019",
"target": "ES2020",
"strict": true,
"baseUrl": ".",
"paths": {
+3 -2
View File
@@ -5,8 +5,9 @@
"compilerOptions": {
"noEmit": true,
"jsx": "react-jsx",
"target": "ES2019",
"lib": ["DOM", "DOM.Iterable", "ES2019"]
"target": "ES2020",
"module": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ES2020"]
},
"include": ["app", "remix.env.d.ts"],
"exclude": ["node_modules"]