chore: Moves ProseMirror NodeView to render within main React context (#7736)

This commit is contained in:
Tom Moor
2024-10-07 20:58:00 -04:00
committed by GitHub
parent 98d8435b15
commit e857d00e3d
4 changed files with 79 additions and 52 deletions
+47 -45
View File
@@ -1,29 +1,51 @@
import { Node as ProsemirrorNode } from "prosemirror-model";
import { EditorView, Decoration } from "prosemirror-view";
import * as React from "react";
import ReactDOM from "react-dom";
import { ThemeProvider } from "styled-components";
import { FunctionComponent } from "react";
import Extension from "@shared/editor/lib/Extension";
import { ComponentProps } from "@shared/editor/types";
import { Editor } from "~/editor";
import { NodeViewRenderer } from "./NodeViewRenderer";
type Component = (props: ComponentProps) => React.ReactElement;
type ComponentViewConstructor = {
/** The editor instance. */
editor: Editor;
/** The extension the view belongs to. */
extension: Extension;
/** The node that the view is responsible for. */
node: ProsemirrorNode;
/** The editor view instance. */
view: EditorView;
/** A function that returns the current position of the node. */
getPos: () => number;
/** The decorations applied to the node. */
decorations: Decoration[];
};
export default class ComponentView {
component: Component;
/** The React component to render. */
component: FunctionComponent<ComponentProps>;
/** The editor instance. */
editor: Editor;
/** The extension the view belongs to. */
extension: Extension;
/** The node that the view is responsible for. */
node: ProsemirrorNode;
/** The editor view instance. */
view: EditorView;
/** A function that returns the current position of the node. */
getPos: () => number;
/** The decorations applied to the node. */
decorations: Decoration[];
/** The renderer instance. */
renderer: NodeViewRenderer<ComponentProps>;
/** Whether the node is selected. */
isSelected = false;
/** The DOM element that the node is rendered into. */
dom: HTMLElement | null;
// See https://prosemirror.net/docs/ref/#view.NodeView
constructor(
component: Component,
component: FunctionComponent<ComponentProps>,
{
editor,
extension,
@@ -31,14 +53,7 @@ export default class ComponentView {
view,
getPos,
decorations,
}: {
editor: Editor;
extension: Extension;
node: ProsemirrorNode;
view: EditorView;
getPos: () => number;
decorations: Decoration[];
}
}: ComponentViewConstructor
) {
this.component = component;
this.editor = editor;
@@ -52,51 +67,33 @@ export default class ComponentView {
: document.createElement("div");
this.dom.classList.add(`component-${node.type.name}`);
this.renderer = new NodeViewRenderer(this.dom, this.component, this.props);
this.renderElement();
window.addEventListener("theme-changed", this.renderElement);
window.addEventListener("location-changed", this.renderElement);
// Add the renderer to the editor's set of renderers so that it is included in the React tree.
this.editor.renderers.add(this.renderer);
}
renderElement = () => {
const { theme } = this.editor.props;
const children = this.component({
theme,
node: this.node,
view: this.view,
isSelected: this.isSelected,
isEditable: this.view.editable,
getPos: this.getPos,
});
ReactDOM.render(
<ThemeProvider theme={theme}>{children}</ThemeProvider>,
this.dom
);
};
update(node: ProsemirrorNode) {
if (node.type !== this.node.type) {
return false;
}
this.node = node;
this.renderElement();
this.renderer.updateProps(this.props);
return true;
}
selectNode() {
if (this.view.editable) {
this.isSelected = true;
this.renderElement();
this.renderer.updateProps(this.props);
}
}
deselectNode() {
if (this.view.editable) {
this.isSelected = false;
this.renderElement();
this.renderer.updateProps(this.props);
}
}
@@ -105,16 +102,21 @@ export default class ComponentView {
}
destroy() {
window.removeEventListener("theme-changed", this.renderElement);
window.removeEventListener("location-changed", this.renderElement);
if (this.dom) {
ReactDOM.unmountComponentAtNode(this.dom);
}
this.editor.renderers.delete(this.renderer);
this.dom = null;
}
ignoreMutation() {
return true;
}
get props() {
return {
node: this.node,
view: this.view,
isSelected: this.isSelected,
isEditable: this.view.editable,
getPos: this.getPos,
} as ComponentProps;
}
}
@@ -0,0 +1,28 @@
import isEqual from "lodash/isEqual";
import { action, computed, observable } from "mobx";
import React, { FunctionComponent } from "react";
import { createPortal } from "react-dom";
export class NodeViewRenderer<T extends object> {
@observable public props: T;
public constructor(
public element: HTMLElement,
private Component: FunctionComponent,
props: T
) {
this.props = props;
}
@computed
public get content() {
return createPortal(<this.Component {...this.props} />, this.element);
}
@action
public updateProps(props: T) {
if (!isEqual(props, this.props)) {
this.props = props;
}
}
}
+4 -1
View File
@@ -38,7 +38,7 @@ import Mark from "@shared/editor/marks/Mark";
import { basicExtensions as extensions } from "@shared/editor/nodes";
import Node from "@shared/editor/nodes/Node";
import ReactNode from "@shared/editor/nodes/ReactNode";
import { EventType } from "@shared/editor/types";
import { ComponentProps, EventType } from "@shared/editor/types";
import { ProsemirrorData, UserPreferences } from "@shared/types";
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import EventEmitter from "@shared/utils/events";
@@ -50,6 +50,7 @@ import ComponentView from "./components/ComponentView";
import EditorContext from "./components/EditorContext";
import { SearchResult } from "./components/LinkEditor";
import LinkToolbar from "./components/LinkToolbar";
import { NodeViewRenderer } from "./components/NodeViewRenderer";
import SelectionToolbar from "./components/SelectionToolbar";
import WithTheme from "./components/WithTheme";
@@ -192,6 +193,7 @@ export class Editor extends React.PureComponent<
};
widgets: { [name: string]: (props: WidgetProps) => React.ReactElement };
renderers: Set<NodeViewRenderer<ComponentProps>> = new Set();
nodes: { [name: string]: NodeSpec };
marks: { [name: string]: MarkSpec };
commands: Record<string, CommandFactory>;
@@ -838,6 +840,7 @@ export class Editor extends React.PureComponent<
Object.values(this.widgets).map((Widget, index) => (
<Widget key={String(index)} rtl={isRTL} readOnly={readOnly} />
))}
{Array.from(this.renderers).map((view) => view.content)}
</Flex>
</EditorContext.Provider>
</PortalContext.Provider>
-6
View File
@@ -32,12 +32,6 @@ void PluginManager.loadPlugins();
initI18n(env.DEFAULT_LANGUAGE);
const element = window.document.getElementById("root");
history.listen(() => {
requestAnimationFrame(() =>
window.dispatchEvent(new Event("location-changed"))
);
});
if (env.SENTRY_DSN) {
initSentry(history);
}