mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-07 20:44:05 +09:00
Merge pull request #234 from webstudio-is/lazy-load-text-editor
Lazy load text editor
This commit is contained in:
13 files changed
+108
-93
No files matched your search
@@ -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;
|
||||
+3
-3
@@ -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",
|
||||
|
||||
-31
@@ -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 });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"include": ["./src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2019",
|
||||
"target": "ES2020",
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"outDir": "./lib"
|
||||
|
||||
@@ -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,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,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": {
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in new issue
Block a user