-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Nest node views according to node hierarchy
- Loading branch information
1 parent
5f319c5
commit bb3df55
Showing
9 changed files
with
414 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
releases: | ||
"@nytimes/react-prosemirror": patch |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { ReactPortal, createContext } from "react"; | ||
|
||
/** | ||
* A map of node view keys to portals. | ||
* | ||
* Each node view registers a portal under its parent's | ||
* key. Each can then retrieve the list of portals under their | ||
* key, allowing portals to be rendered with the appropriate | ||
* hierarchy. | ||
*/ | ||
export const PortalRegistryContext = createContext< | ||
Record<PortalRegistryKey, ReactPortal[]> | ||
>(null as unknown as Record<PortalRegistryKey, ReactPortal[]>); | ||
|
||
/** | ||
* Node views that don't have any React node view ancestors | ||
* can specify their parent node view as the "root", and | ||
* will be have their portals rendered as direct children of | ||
* the ProseMirror component. | ||
*/ | ||
export const PORTAL_REGISTRY_ROOT_KEY = Symbol("portal registry root key"); | ||
|
||
export type PortalRegistryKey = string | typeof PORTAL_REGISTRY_ROOT_KEY; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { render, screen } from "@testing-library/react"; | ||
import { Schema } from "prosemirror-model"; | ||
import { EditorState } from "prosemirror-state"; | ||
import React, { createContext, useContext, useState } from "react"; | ||
|
||
import { ProseMirror } from "../../components/ProseMirror.js"; | ||
import { NodeViewComponentProps } from "../../nodeViews/createReactNodeViewConstructor.js"; | ||
import { useNodeViews } from "../useNodeViews.js"; | ||
|
||
const schema = new Schema({ | ||
nodes: { | ||
doc: { content: "block+" }, | ||
list: { group: "block", content: "list_item+" }, | ||
list_item: { content: "inline*" }, | ||
text: { group: "inline" }, | ||
}, | ||
}); | ||
|
||
const editorState = EditorState.create({ | ||
doc: schema.topNodeType.create(null, schema.nodes.list.createAndFill()), | ||
schema, | ||
}); | ||
|
||
describe("useNodeViews", () => { | ||
it("should render node views", () => { | ||
function List({ children }: NodeViewComponentProps) { | ||
return ( | ||
<> | ||
<span contentEditable={false}>list</span> | ||
<ul>{children}</ul> | ||
</> | ||
); | ||
} | ||
|
||
function ListItem({ children }: NodeViewComponentProps) { | ||
return ( | ||
<> | ||
<span contentEditable={false}>list item</span> | ||
<li>{children}</li> | ||
</> | ||
); | ||
} | ||
|
||
const reactNodeViews = { | ||
list: () => ({ | ||
component: List, | ||
dom: document.createElement("div"), | ||
contentDOM: document.createElement("div"), | ||
}), | ||
list_item: () => ({ | ||
component: ListItem, | ||
dom: document.createElement("div"), | ||
contentDOM: document.createElement("div"), | ||
}), | ||
}; | ||
|
||
function TestEditor() { | ||
const { nodeViews, renderNodeViews } = useNodeViews(reactNodeViews); | ||
const [mount, setMount] = useState<HTMLDivElement | null>(null); | ||
|
||
return ( | ||
<ProseMirror mount={mount} state={editorState} nodeViews={nodeViews}> | ||
<div ref={setMount} /> | ||
{renderNodeViews()} | ||
</ProseMirror> | ||
); | ||
} | ||
|
||
render(<TestEditor />); | ||
|
||
expect(screen.getByText("list")).toBeTruthy(); | ||
expect(screen.getByText("list item")).toBeTruthy(); | ||
}); | ||
|
||
it("should render child node views as children of their parents", () => { | ||
const TestContext = createContext("default"); | ||
|
||
function List({ children }: NodeViewComponentProps) { | ||
return ( | ||
<TestContext.Provider value="overriden"> | ||
<ul>{children}</ul> | ||
</TestContext.Provider> | ||
); | ||
} | ||
|
||
function ListItem({ children }: NodeViewComponentProps) { | ||
const testContextValue = useContext(TestContext); | ||
|
||
return ( | ||
<> | ||
<span contentEditable={false}>{testContextValue}</span> | ||
<li>{children}</li> | ||
</> | ||
); | ||
} | ||
|
||
const reactNodeViews = { | ||
list: () => ({ | ||
component: List, | ||
dom: document.createElement("div"), | ||
contentDOM: document.createElement("div"), | ||
}), | ||
list_item: () => ({ | ||
component: ListItem, | ||
dom: document.createElement("div"), | ||
contentDOM: document.createElement("div"), | ||
}), | ||
}; | ||
|
||
function TestEditor() { | ||
const { nodeViews, renderNodeViews } = useNodeViews(reactNodeViews); | ||
const [mount, setMount] = useState<HTMLDivElement | null>(null); | ||
|
||
return ( | ||
<ProseMirror mount={mount} state={editorState} nodeViews={nodeViews}> | ||
<div ref={setMount} /> | ||
{renderNodeViews()} | ||
</ProseMirror> | ||
); | ||
} | ||
|
||
render(<TestEditor />); | ||
|
||
expect(screen.getByText("overriden")).toBeTruthy(); | ||
}); | ||
}); |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import React, { ReactPortal, useCallback, useMemo, useState } from "react"; | ||
import { createPortal } from "react-dom"; | ||
|
||
import { | ||
PORTAL_REGISTRY_ROOT_KEY, | ||
PortalRegistryContext, | ||
PortalRegistryKey, | ||
} from "../contexts/PortalRegistryContext.js"; | ||
import { | ||
ReactNodeViewConstructor, | ||
RegisterElement, | ||
createReactNodeViewConstructor, | ||
} from "../nodeViews/createReactNodeViewConstructor.js"; | ||
|
||
type Props = { | ||
portals: Record<PortalRegistryKey, ReactPortal[]>; | ||
}; | ||
|
||
function NodeViews({ portals }: Props) { | ||
const rootPortals = portals[PORTAL_REGISTRY_ROOT_KEY]; | ||
|
||
return ( | ||
<PortalRegistryContext.Provider value={portals}> | ||
{rootPortals} | ||
</PortalRegistryContext.Provider> | ||
); | ||
} | ||
|
||
export function useNodeViews( | ||
nodeViews: Record<string, ReactNodeViewConstructor> | ||
) { | ||
const [portals, setPortals] = useState( | ||
{} as Record<PortalRegistryKey, ReactPortal[]> | ||
); | ||
|
||
const registerPortal: RegisterElement = useCallback( | ||
(registrationKey, child, container, key) => { | ||
const portal = createPortal(child, container, key); | ||
setPortals((oldPortals) => { | ||
const oldChildPortals = oldPortals[registrationKey] ?? []; | ||
const newChildPortals = oldChildPortals.concat(portal); | ||
return { | ||
...oldPortals, | ||
[registrationKey]: newChildPortals, | ||
}; | ||
}); | ||
|
||
return () => { | ||
setPortals((oldPortals) => { | ||
const oldChildPortals = oldPortals[registrationKey] ?? []; | ||
const newChildPortals = oldChildPortals.filter((p) => p !== portal); | ||
return { | ||
...oldPortals, | ||
[registrationKey]: newChildPortals, | ||
}; | ||
}); | ||
}; | ||
}, | ||
[] | ||
); | ||
|
||
const reactNodeViews = useMemo(() => { | ||
const nodeViewEntries = Object.entries(nodeViews); | ||
const reactNodeViewEntries = nodeViewEntries.map(([name, constructor]) => [ | ||
name, | ||
createReactNodeViewConstructor(constructor, registerPortal), | ||
]); | ||
return Object.fromEntries(reactNodeViewEntries); | ||
}, [nodeViews, registerPortal]); | ||
|
||
return { | ||
nodeViews: reactNodeViews, | ||
renderNodeViews: () => <NodeViews portals={portals} />, | ||
}; | ||
} |
Oops, something went wrong.