mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
163 lines
4.5 KiB
TypeScript
163 lines
4.5 KiB
TypeScript
import type { Node as ProsemirrorNode } from "prosemirror-model";
|
|
import type { EditorView, Decoration } from "prosemirror-view";
|
|
import type { FunctionComponent } from "react";
|
|
import type Extension from "@shared/editor/lib/Extension";
|
|
import type { ComponentProps } from "@shared/editor/types";
|
|
import type { Editor } from "~/editor";
|
|
import { NodeViewRenderer } from "./NodeViewRenderer";
|
|
|
|
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 {
|
|
/** 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;
|
|
/** The base class name for the node's DOM element. */
|
|
className?: string;
|
|
|
|
// See https://prosemirror.net/docs/ref/#view.NodeView
|
|
constructor(
|
|
component: FunctionComponent<ComponentProps>,
|
|
{
|
|
editor,
|
|
extension,
|
|
node,
|
|
view,
|
|
getPos,
|
|
decorations,
|
|
}: ComponentViewConstructor
|
|
) {
|
|
this.component = component;
|
|
this.editor = editor;
|
|
this.extension = extension;
|
|
this.getPos = getPos;
|
|
this.decorations = decorations;
|
|
this.node = node;
|
|
this.view = view;
|
|
this.dom = node.type.spec.inline
|
|
? document.createElement("span")
|
|
: document.createElement("div");
|
|
|
|
this.className = `component-${node.type.name}`;
|
|
this.dom.classList.add(this.className);
|
|
this.renderer = new NodeViewRenderer(this.dom, this.component, this.props);
|
|
|
|
// 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);
|
|
|
|
// Apply decoration classes to the DOM element.
|
|
this.applyDecorationClasses();
|
|
}
|
|
|
|
update(node: ProsemirrorNode, decorations: Decoration[]) {
|
|
if (node.type !== this.node.type) {
|
|
return false;
|
|
}
|
|
|
|
this.node = node;
|
|
this.decorations = decorations;
|
|
this.applyDecorationClasses();
|
|
this.renderer.updateProps(this.props);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Apply decoration classes to the DOM element.
|
|
* Extracts classes from inline decorations that overlap with this node's position.
|
|
*/
|
|
private applyDecorationClasses() {
|
|
if (!this.dom) {
|
|
return;
|
|
}
|
|
|
|
// Remove all existing decoration classes.
|
|
this.dom.classList.forEach((className) => {
|
|
if (className !== this.className) {
|
|
this.dom?.classList.remove(className);
|
|
}
|
|
});
|
|
|
|
// Apply classes from inline decorations.
|
|
this.decorations.forEach((decoration) => {
|
|
// For inline decorations, attrs contain the class property.
|
|
const attrs = (decoration as any).type?.attrs;
|
|
if (attrs?.class) {
|
|
const classes = attrs.class.split(" ");
|
|
classes.forEach((className: string) => {
|
|
if (className && this.dom) {
|
|
this.dom.classList.add(className);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
selectNode() {
|
|
if (this.view.editable) {
|
|
this.isSelected = true;
|
|
this.renderer.updateProps(this.props);
|
|
}
|
|
}
|
|
|
|
deselectNode() {
|
|
if (this.view.editable) {
|
|
this.isSelected = false;
|
|
this.renderer.updateProps(this.props);
|
|
}
|
|
}
|
|
|
|
stopEvent(event: Event) {
|
|
return event.type !== "mousedown" && !event.type.startsWith("drag");
|
|
}
|
|
|
|
destroy() {
|
|
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,
|
|
decorations: this.decorations,
|
|
} as ComponentProps;
|
|
}
|
|
}
|