Files
outline/app/editor/components/ComponentView.tsx
T
2025-12-28 08:56:32 -05:00

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;
}
}