From 7f74ac5bf0b20ea72ed23553ec33218b9a2b97e8 Mon Sep 17 00:00:00 2001 From: Shane Friedman Date: Fri, 4 Aug 2023 22:36:12 -0400 Subject: [PATCH] Fix domAtPos with MarkViews --- src/components/DocNodeView.tsx | 100 +++++++++++++++------ src/components/MarkView.tsx | 55 +++++++++++- src/components/NodeView.tsx | 138 ++++++++++++++--------------- src/components/TextNodeView.tsx | 33 +------ src/components/WidgetView.tsx | 35 ++++++++ src/decorations/ReactWidgetType.ts | 7 +- src/descriptors/ViewDesc.ts | 15 ++-- 7 files changed, 242 insertions(+), 141 deletions(-) create mode 100644 src/components/WidgetView.tsx diff --git a/src/components/DocNodeView.tsx b/src/components/DocNodeView.tsx index 4d09c4ef..b0349694 100644 --- a/src/components/DocNodeView.tsx +++ b/src/components/DocNodeView.tsx @@ -3,9 +3,11 @@ import { DecorationSet } from "prosemirror-view"; import React, { ForwardedRef, ReactNode, + cloneElement, createElement, forwardRef, useContext, + useImperativeHandle, useLayoutEffect, useRef, } from "react"; @@ -14,8 +16,12 @@ import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js" import { NodeViewContext } from "../contexts/NodeViewContext.js"; import { ReactWidgetType } from "../decorations/ReactWidgetType.js"; import { NodeViewDesc, ViewDesc, iterDeco } from "../descriptors/ViewDesc.js"; -import { DecorationSourceInternal } from "../prosemirror-internal/DecorationInternal.js"; +import { + DecorationSourceInternal, + NonWidgetType, +} from "../prosemirror-internal/DecorationInternal.js"; +import { MarkView } from "./MarkView.js"; import { NodeView } from "./NodeView.js"; import { TextNodeView } from "./TextNodeView.js"; @@ -27,12 +33,20 @@ type Props = { export const DocNodeView = forwardRef(function DocNodeView( { node, contentEditable, decorations, ...props }: Props, - ref: ForwardedRef + ref: ForwardedRef ) { const { posToDesc, domToDesc } = useContext(NodeViewContext); const siblingDescriptors = useContext(ChildDescriptorsContext); const childDescriptors: ViewDesc[] = []; - const innerRef = useRef(null); + const innerRef = useRef(null); + + useImperativeHandle( + ref, + () => { + return innerRef.current; + }, + [] + ); useLayoutEffect(() => { if (!innerRef.current) return; @@ -41,6 +55,7 @@ export const DocNodeView = forwardRef(function DocNodeView( const desc = new NodeViewDesc( undefined, + childDescriptors, node, [], DecorationSet.empty, @@ -50,7 +65,6 @@ export const DocNodeView = forwardRef(function DocNodeView( posToDesc, domToDesc ); - desc.children = childDescriptors; posToDesc.set(-1, desc); domToDesc.set(innerRef.current, desc); siblingDescriptors.push(desc); @@ -62,6 +76,7 @@ export const DocNodeView = forwardRef(function DocNodeView( const children: ReactNode[] = []; const innerPos = 0; + iterDeco( node, decorations, @@ -74,30 +89,59 @@ export const DocNodeView = forwardRef(function DocNodeView( }, (childNode, outerDeco, innerDeco, offset) => { const childPos = innerPos + offset; - if (childNode.isText) { - children.push( - - {(siblingDescriptors) => ( - - )} - - ); - } else { - children.push( - - ); - } + const nodeElement = childNode.isText ? ( + + {(siblingDescriptors) => ( + + )} + + ) : ( + + ); + + const childElement = outerDeco.reduce( + (element, deco) => { + const { + nodeName, + class: className, + style: _, + ...attrs + } = (deco.type as NonWidgetType).attrs; + + if (nodeName) { + return createElement( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + nodeName!, + { + className, + ...attrs, + }, + element + ); + } + return cloneElement(element, { + className, + ...attrs, + }); + }, + + childNode.marks.reduce( + (element, mark) => {element}, + nodeElement + ) + ); + + children.push(cloneElement(childElement, { key: childPos })); } ); diff --git a/src/components/MarkView.tsx b/src/components/MarkView.tsx index 1c1b865c..91e8e2e4 100644 --- a/src/components/MarkView.tsx +++ b/src/components/MarkView.tsx @@ -1,5 +1,16 @@ import { Mark } from "prosemirror-model"; -import React, { ReactNode, forwardRef } from "react"; +import React, { + ReactNode, + forwardRef, + useContext, + useImperativeHandle, + useLayoutEffect, + useRef, +} from "react"; + +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { NodeViewContext } from "../contexts/NodeViewContext.js"; +import { MarkViewDesc, ViewDesc } from "../descriptors/ViewDesc.js"; import { OutputSpec } from "./OutputSpec.js"; @@ -12,13 +23,51 @@ export const MarkView = forwardRef(function MarkView( { mark, children }: Props, ref ) { + const { posToDesc, domToDesc } = useContext(NodeViewContext); + const siblingDescriptors = useContext(ChildDescriptorsContext); + const childDescriptors: ViewDesc[] = []; + const domRef = useRef(null); + + useImperativeHandle( + ref, + () => { + return domRef.current; + }, + [] + ); + const outputSpec = mark.type.spec.toDOM?.(mark, true); if (!outputSpec) throw new Error(`Mark spec for ${mark.type.name} is missing toDOM`); + useLayoutEffect(() => { + if (!domRef.current) return; + + const firstChildDesc = childDescriptors[0]; + + const desc = new MarkViewDesc( + undefined, + childDescriptors, + mark, + domRef.current, + firstChildDesc?.dom.parentElement ?? domRef.current, + posToDesc, + domToDesc + ); + // posToDesc.set(pos, desc); + domToDesc.set(domRef.current, desc); + siblingDescriptors.push(desc); + + for (const childDesc of childDescriptors) { + childDesc.parent = desc; + } + }); + return ( - - {children} + + + {children} + ); }); diff --git a/src/components/NodeView.tsx b/src/components/NodeView.tsx index fe4f1b3c..6920d1ef 100644 --- a/src/components/NodeView.tsx +++ b/src/components/NodeView.tsx @@ -1,6 +1,5 @@ import { DOMOutputSpec, Node } from "prosemirror-model"; import { NodeSelection } from "prosemirror-state"; -import { DecorationSet } from "prosemirror-view"; import React, { ForwardRefExoticComponent, ReactNode, @@ -47,7 +46,6 @@ export function NodeView({ const siblingDescriptors = useContext(ChildDescriptorsContext); const childDescriptors: ViewDesc[] = []; const domRef = useRef(null); - const nodeDomRef = useRef(null); useLayoutEffect(() => { if (!domRef.current) return; @@ -56,16 +54,16 @@ export function NodeView({ const desc = new NodeViewDesc( undefined, + childDescriptors, node, [], - DecorationSet.empty, + innerDecorations, domRef.current, firstChildDesc?.dom.parentElement ?? null, - nodeDomRef.current ?? domRef.current, + domRef.current ?? domRef.current, posToDesc, domToDesc ); - desc.children = childDescriptors; posToDesc.set(pos, desc); domToDesc.set(domRef.current, desc); siblingDescriptors.push(desc); @@ -75,13 +73,14 @@ export function NodeView({ } }); - const content: ReactNode[] = []; + const children: ReactNode[] = []; const innerPos = pos + 1; + iterDeco( node, innerDecorations, (widget, offset, index) => { - content.push( + children.push( createElement((widget.type as ReactWidgetType).Component, { key: `${innerPos + offset}-${index}`, }) @@ -89,43 +88,66 @@ export function NodeView({ }, (childNode, outerDeco, innerDeco, offset) => { const childPos = innerPos + offset; - if (childNode.isText) { - content.push( - - {(siblingDescriptors) => ( - - )} - - ); - } else { - content.push( - - ); - } + const nodeElement = childNode.isText ? ( + + {(siblingDescriptors) => ( + + )} + + ) : ( + + ); + + const childElement = outerDeco.reduce( + (element, deco) => { + const { + nodeName, + class: className, + style: _, + ...attrs + } = (deco.type as NonWidgetType).attrs; + + if (nodeName) { + return createElement( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + nodeName!, + { + className, + ...attrs, + }, + element + ); + } + return cloneElement(element, { + className, + ...attrs, + }); + }, + + childNode.marks.reduce( + (element, mark) => {element}, + nodeElement + ) + ); + + children.push(cloneElement(childElement, { key: childPos })); } ); - if (!content.length) { - content.push(); + if (!children.length) { + children.push(); } - const children = ( - - {content} - - ); - let element: JSX.Element | null = null; const Component: @@ -167,39 +189,9 @@ export function NodeView({ throw new Error(`Node spec for ${node.type.name} is missing toDOM`); } - return decorations.reduce( - (element, deco) => { - const { - nodeName, - class: className, - style: _, - ...attrs - } = (deco.type as NonWidgetType).attrs; - - if (nodeName) { - return createElement( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - nodeName!, - { - className, - ...attrs, - }, - element - ); - } - return cloneElement(element, { - className, - ...attrs, - }); - }, - - node.marks.reduce( - (element, mark) => ( - - {element} - - ), - element - ) + return ( + + {element} + ); } diff --git a/src/components/TextNodeView.tsx b/src/components/TextNodeView.tsx index bab20a5d..e7a62ee7 100644 --- a/src/components/TextNodeView.tsx +++ b/src/components/TextNodeView.tsx @@ -1,6 +1,6 @@ import { Node } from "prosemirror-model"; import { DecorationSet } from "prosemirror-view"; -import React, { Component, createElement } from "react"; +import { Component } from "react"; import { findDOMNode } from "react-dom"; import { @@ -8,12 +8,7 @@ import { NodeViewContextValue, } from "../contexts/NodeViewContext.js"; import { TextViewDesc, ViewDesc } from "../descriptors/ViewDesc.js"; -import { - DecorationInternal, - NonWidgetType, -} from "../prosemirror-internal/DecorationInternal.js"; - -import { MarkView } from "./MarkView.js"; +import { DecorationInternal } from "../prosemirror-internal/DecorationInternal.js"; type Props = { node: Node; @@ -80,29 +75,7 @@ export class TextNodeView extends Component { } render() { - return this.props.decorations.reduce( - (element, deco) => { - const { - nodeName, - class: className, - style: _, - ...attrs - } = (deco.type as NonWidgetType).attrs; - - return createElement( - nodeName ?? "span", - { - className, - ...attrs, - }, - element - ); - }, - this.props.node.marks.reduce( - (children, mark) => {children}, - <>{this.props.node.text} - ) - ); + return this.props.node.text; } } diff --git a/src/components/WidgetView.tsx b/src/components/WidgetView.tsx new file mode 100644 index 00000000..d06879a4 --- /dev/null +++ b/src/components/WidgetView.tsx @@ -0,0 +1,35 @@ +import React, { useContext, useLayoutEffect, useRef } from "react"; + +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { NodeViewContext } from "../contexts/NodeViewContext.js"; +import { WidgetViewDesc } from "../descriptors/ViewDesc.js"; +import { ReactWidgetDecoration } from "../prosemirror-internal/DecorationInternal.js"; + +type Props = { + widget: ReactWidgetDecoration; +}; + +export function WidgetView({ widget }: Props) { + const { posToDesc, domToDesc } = useContext(NodeViewContext); + const siblingDescriptors = useContext(ChildDescriptorsContext); + const domRef = useRef(null); + + useLayoutEffect(() => { + if (!domRef.current) return; + + const desc = new WidgetViewDesc( + undefined, + widget, + domRef.current, + posToDesc, + domToDesc + ); + // posToDesc.set(pos, desc); + domToDesc.set(domRef.current, desc); + siblingDescriptors.push(desc); + }); + + const { Component } = widget.type; + + return ; +} diff --git a/src/decorations/ReactWidgetType.ts b/src/decorations/ReactWidgetType.ts index a4a34f4a..2d72613a 100644 --- a/src/decorations/ReactWidgetType.ts +++ b/src/decorations/ReactWidgetType.ts @@ -1,7 +1,7 @@ import { Mark } from "prosemirror-model"; import { Mappable } from "prosemirror-transform"; import { Decoration } from "prosemirror-view"; -import { ComponentType } from "react"; +import { ComponentType, ForwardRefExoticComponent, RefAttributes } from "react"; import { DecorationType } from "./DecorationType.js"; @@ -30,7 +30,10 @@ export class ReactWidgetType implements DecorationType { side: number; spec: ReactWidgetSpec; - constructor(public Component: ComponentType, spec: ReactWidgetSpec) { + constructor( + public Component: ForwardRefExoticComponent>, + spec: ReactWidgetSpec + ) { this.spec = spec ?? noSpec; this.side = this.spec.side ?? 0; } diff --git a/src/descriptors/ViewDesc.ts b/src/descriptors/ViewDesc.ts index 86a33dfb..54d83481 100644 --- a/src/descriptors/ViewDesc.ts +++ b/src/descriptors/ViewDesc.ts @@ -619,9 +619,9 @@ export class TrailingHackViewDesc extends ViewDesc { // A widget desc represents a widget decoration, which is a DOM node // drawn between the document nodes. -class WidgetViewDesc extends ViewDesc { +export class WidgetViewDesc extends ViewDesc { constructor( - parent: ViewDesc, + parent: ViewDesc | undefined, readonly widget: ReactWidgetDecoration, dom: DOMNode, posToDesc: Map, @@ -667,14 +667,15 @@ class WidgetViewDesc extends ViewDesc { // necessary. export class MarkViewDesc extends ViewDesc { constructor( - parent: ViewDesc, + parent: ViewDesc | undefined, + children: ViewDesc[], readonly mark: Mark, dom: DOMNode, contentDOM: HTMLElement, posToDesc: Map, domToDesc: Map ) { - super(parent, [], dom, contentDOM, posToDesc, domToDesc); + super(parent, children, dom, contentDOM, posToDesc, domToDesc); } parseRule(): ParseRule | null { @@ -722,6 +723,7 @@ export class NodeViewDesc extends ViewDesc { constructor( parent: ViewDesc | undefined, + children: ViewDesc[], public node: Node, public outerDeco: readonly DecorationInternal[], public innerDeco: DecorationSource, @@ -732,7 +734,7 @@ export class NodeViewDesc extends ViewDesc { posToDesc: Map, domToDesc: Map ) { - super(parent, [], dom, contentDOM, posToDesc, domToDesc); + super(parent, children, dom, contentDOM, posToDesc, domToDesc); this.size = this.node.nodeSize; this.border = this.node.isLeaf ? 0 : 1; } @@ -912,6 +914,7 @@ export class TextViewDesc extends NodeViewDesc { ) { super( parent, + [], node, outerDeco, innerDeco, @@ -921,6 +924,8 @@ export class TextViewDesc extends NodeViewDesc { posToDesc, domToDesc ); + + this.size = this.node.text!.length; } parseRule(): ParseRule {