mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
chore: Moves ProseMirror NodeView to render within main React context (#7736)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user