Skip to content

Commit

Permalink
Implement decorations
Browse files Browse the repository at this point in the history
  • Loading branch information
smoores-dev committed Aug 1, 2023
1 parent 0bbeb4c commit 0d035a4
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 71 deletions.
29 changes: 22 additions & 7 deletions demo/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import React, {
HTMLAttributes,
Ref,
forwardRef,
useState,
} from "react";
import { createRoot } from "react-dom/client";

Expand Down Expand Up @@ -68,7 +69,7 @@ const editorState = EditorState.create({
schema,
doc: schema.nodes.doc.create({}, [
schema.nodes.paragraph.create({}, [
schema.text("This", [schema.marks.em.create(), schema.marks.em.create()]),
schema.text("This", [schema.marks.em.create()]),
schema.text(" is the first paragraph"),
]),
schema.nodes.paragraph.create(
Expand Down Expand Up @@ -122,16 +123,30 @@ function TestWidget() {
}

function DemoEditor() {
const [state, setState] = useState(editorState);

const decorations = [Decoration.inline(5, 15, { class: "inline-deco" })];
state.doc.forEach((node, offset, index) => {
if (index === 1) {
decorations.push(
Decoration.node(offset, offset + node.nodeSize, {
nodeName: "div",
class: "node-deco",
})
);
}
if (index === 3) {
decorations.push(widget(offset + 10, TestWidget, { side: 0 }));
}
});

return (
<main>
<h1>React ProseMirror Demo</h1>
<EditorView
defaultState={editorState}
decorations={DecorationSet.create(editorState.doc, [
Decoration.inline(5, 15, { class: "inline-deco" }),
Decoration.node(29, 59, { class: "node-deco" }),
widget(40, TestWidget, { side: 0 }),
])}
state={state}
dispatchTransaction={(tr) => setState((prev) => prev.apply(tr))}
decorations={DecorationSet.create(state.doc, decorations)}
keymap={{
"Mod-i": toggleMark(schema.marks.em),
Backspace: chainCommands(
Expand Down
54 changes: 42 additions & 12 deletions src/components/DocNodeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, {
ForwardedRef,
HTMLAttributes,
ReactNode,
createElement,
forwardRef,
useContext,
useLayoutEffect,
Expand All @@ -13,10 +14,12 @@ import React, {

import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js";
import { NodeViewContext } from "../contexts/NodeViewContext.js";
import { NodeViewDesc, ViewDesc } from "../descriptors/ViewDesc.js";
import { ReactWidgetType } from "../decorations/ReactWidgetType.js";
import { NodeViewDesc, ViewDesc, iterDeco } from "../descriptors/ViewDesc.js";
import { DecorationSourceInternal } from "../prosemirror-internal/DecorationInternal.js";

import { NodeView } from "./NodeView.js";
import { TextNodeView } from "./TextNodeView.js";

type Props = {
node: Node;
Expand Down Expand Up @@ -61,17 +64,44 @@ export const DocNodeView = forwardRef(function DocNodeView(

const children: ReactNode[] = [];
const innerPos = 0;
node.content.forEach((childNode, offset) => {
const childPos = innerPos + offset;
children.push(
<NodeView
key={childPos}
node={childNode}
pos={childPos}
decorations={decorations.forChild(offset, childNode)}
/>
);
});
iterDeco(
node,
decorations,
(widget, offset, index) => {
children.push(
createElement((widget.type as ReactWidgetType).Component, {
key: `${innerPos + offset}-${index}`,
})
);
},
(childNode, outerDeco, innerDeco, offset) => {
const childPos = innerPos + offset;
if (childNode.isText) {
children.push(
<ChildDescriptorsContext.Consumer key={childPos}>
{(siblingDescriptors) => (
<TextNodeView
node={childNode}
pos={childPos}
siblingDescriptors={siblingDescriptors}
decorations={outerDeco}
/>
)}
</ChildDescriptorsContext.Consumer>
);
} else {
children.push(
<NodeView
key={childPos}
node={childNode}
pos={childPos}
decorations={outerDeco}
innerDecorations={innerDeco}
/>
);
}
}
);

return (
<div
Expand Down
152 changes: 111 additions & 41 deletions src/components/NodeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ import React, {
ForwardRefExoticComponent,
ReactNode,
RefAttributes,
cloneElement,
createElement,
useContext,
useLayoutEffect,
useRef,
} from "react";

import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js";
import { NodeViewContext } from "../contexts/NodeViewContext.js";
import { NodeViewDesc, ViewDesc } from "../descriptors/ViewDesc.js";
import { DecorationSourceInternal } from "../prosemirror-internal/DecorationInternal.js";
import { ReactWidgetType } from "../decorations/ReactWidgetType.js";
import { NodeViewDesc, ViewDesc, iterDeco } from "../descriptors/ViewDesc.js";
import {
DecorationInternal,
DecorationSourceInternal,
NonWidgetType,
} from "../prosemirror-internal/DecorationInternal.js";

import { MarkView } from "./MarkView.js";
import { NodeViewComponentProps } from "./NodeViewComponentProps.js";
Expand All @@ -24,10 +31,17 @@ import { TrailingHackView } from "./TrailingHackView.js";
type Props = {
node: Node;
pos: number;
decorations: DecorationSourceInternal;
decorations: readonly DecorationInternal[];
innerDecorations: DecorationSourceInternal;
};

export function NodeView({ node, pos, decorations }: Props) {
export function NodeView({
node,
pos,
decorations,
innerDecorations,
...props
}: Props) {
const { posToDesc, domToDesc, nodeViews, state } =
useContext(NodeViewContext);
const siblingDescriptors = useContext(ChildDescriptorsContext);
Expand Down Expand Up @@ -63,31 +77,44 @@ export function NodeView({ node, pos, decorations }: Props) {

const content: ReactNode[] = [];
const innerPos = pos + 1;
node.content.forEach((childNode, offset) => {
const childPos = innerPos + offset;
if (childNode.isText) {
iterDeco(
node,
innerDecorations,
(widget, offset, index) => {
content.push(
<ChildDescriptorsContext.Consumer key={childPos}>
{(siblingDescriptors) => (
<TextNodeView
node={childNode}
pos={childPos}
siblingDescriptors={siblingDescriptors}
/>
)}
</ChildDescriptorsContext.Consumer>
);
} else {
content.push(
<NodeView
key={childPos}
node={childNode}
pos={childPos}
decorations={decorations.forChild(offset, childNode)}
/>
createElement((widget.type as ReactWidgetType).Component, {
key: `${innerPos + offset}-${index}`,
})
);
},
(childNode, outerDeco, innerDeco, offset) => {
const childPos = innerPos + offset;
if (childNode.isText) {
content.push(
<ChildDescriptorsContext.Consumer key={childPos}>
{(siblingDescriptors) => (
<TextNodeView
node={childNode}
pos={childPos}
siblingDescriptors={siblingDescriptors}
decorations={outerDeco}
/>
)}
</ChildDescriptorsContext.Consumer>
);
} else {
content.push(
<NodeView
key={childPos}
node={childNode}
pos={childPos}
decorations={outerDeco}
innerDecorations={innerDeco}
/>
);
}
}
});
);

if (!content.length) {
content.push(<TrailingHackView key={innerPos} pos={innerPos} />);
Expand All @@ -99,28 +126,27 @@ export function NodeView({ node, pos, decorations }: Props) {
</ChildDescriptorsContext.Provider>
);

let element: JSX.Element | null = null;

const Component:
| ForwardRefExoticComponent<
NodeViewComponentProps & RefAttributes<HTMLElement>
>
| undefined = nodeViews[node.type.name];

if (Component) {
return node.marks.reduce(
(element, mark) => (
<MarkView mark={mark} ref={nodeDomRef}>
{element}
</MarkView>
),
element = (
<Component
ref={domRef}
node={node}
pos={pos}
decorations={[]}
decorations={decorations}
innerDecorations={innerDecorations}
isSelected={
state.selection instanceof NodeSelection &&
state.selection.node === node
}
{...props}
>
{children}
</Component>
Expand All @@ -130,17 +156,61 @@ export function NodeView({ node, pos, decorations }: Props) {
const outputSpec: DOMOutputSpec | undefined = node.type.spec.toDOM?.(node);

if (outputSpec) {
return node.marks.reduce(
(element, mark) => (
<MarkView ref={nodeDomRef} mark={mark}>
{element}
</MarkView>
),
<OutputSpec ref={domRef} outputSpec={outputSpec}>
element = (
<OutputSpec ref={domRef} outputSpec={outputSpec} {...props}>
{children}
</OutputSpec>
);
}

throw new Error(`Node spec for ${node.type.name} is missing toDOM`);
if (!element) {
throw new Error(`Node spec for ${node.type.name} is missing toDOM`);
}

const wrapDecorations: DecorationInternal[] = [];
for (const decoration of decorations) {
if ((decoration.type as NonWidgetType).attrs.nodeName) {
wrapDecorations.push(decoration);
} else {
const {
class: className,
style: _,
...attrs
} = (decoration.type as NonWidgetType).attrs;
element = cloneElement(element, {
className,
...attrs,
});
}
}

return wrapDecorations.reduce(
(element, deco) => {
const {
nodeName,
class: className,
style: _,
...attrs
} = (deco.type as NonWidgetType).attrs;

return createElement(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
nodeName!,
{
className,
...attrs,
},
element
);
},

node.marks.reduce(
(element, mark) => (
<MarkView ref={nodeDomRef} mark={mark}>
{element}
</MarkView>
),
element
)
);
}
3 changes: 2 additions & 1 deletion src/components/NodeViewComponentProps.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Node } from "prosemirror-model";
import { Decoration } from "prosemirror-view";
import { Decoration, DecorationSource } from "prosemirror-view";
import { ReactNode } from "react";

export type NodeViewComponentProps = {
decorations: readonly Decoration[];
innerDecorations: DecorationSource;
node: Node;
children?: ReactNode | ReactNode[];
isSelected: boolean;
Expand Down
4 changes: 2 additions & 2 deletions src/components/OutputSpec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type Props = {
};

export const OutputSpec = forwardRef(function OutputSpec(
{ outputSpec, children }: Props,
{ outputSpec, children, ...initialProps }: Props,
ref
) {
if (typeof outputSpec === "string") {
Expand All @@ -23,7 +23,7 @@ export const OutputSpec = forwardRef(function OutputSpec(
const tagName = tagSpec.replace(" ", ":");
const attrs = outputSpec[1];

const props: Record<string, unknown> = { ref };
const props: Record<string, unknown> = { ...initialProps, ref };
let start = 1;
if (
attrs &&
Expand Down
Loading

0 comments on commit 0d035a4

Please sign in to comment.