Skip to content

Commit

Permalink
Add a useSelectNode hook
Browse files Browse the repository at this point in the history
  • Loading branch information
smoores-dev committed Oct 11, 2024
1 parent 0f227c7 commit 84da502
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 75 deletions.
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ yarn add @nytimes/react-prosemirror
- [`useEditorEventListener`](#useeditoreventlistener-1)
- [`useEditorEffect`](#useeditoreffect-1)
- [`NodeViewComponentProps`](#nodeviewcomponentprops)
- [`useStopEvent`](#usestopevent)
- [`useSelectNode`](#useselectnode)
- [`widget`](#widget)

<!-- tocstop -->
Expand Down Expand Up @@ -573,12 +575,13 @@ export function SelectionWidget() {

```tsx
type NodeViewComponentProps = {
decorations: readonly Decoration[];
innerDecorations: DecorationSource;
node: Node;
children?: ReactNode | ReactNode[];
isSelected: boolean;
pos: number;
nodeProps: {
decorations: readonly Decoration[];
innerDecorations: DecorationSource;
node: Node;
children?: ReactNode | ReactNode[];
getPos: () => number;
};
} & HTMLAttributes<HTMLElement>;
```

Expand All @@ -594,6 +597,27 @@ and should pass them through to their top-level DOM element.
In addition to accepting these props, all node view components _must_ forward
their ref to their top-level DOM element.

### `useStopEvent`

```tsx
type useStopEvent = (stopEvent: (view: EditorView, event: Event) => boolean): void
```
This hook can be used within a node view component to register a
[stopEvent handler](https://prosemirror.net/docs/ref/#view.NodeView.stopEvent).
Events for which this returns true are not handled by the editor.
### `useSelectNode`
```tsx
type useSelectNode = (selectNode: () => void, deselectNode?: () => void): void
```
This hook can be used within a node view component to register
[selectNode and deselectNode handlers](https://prosemirror.net/docs/ref/#view.NodeView.selectNode).
The selectNode handler will only be called when a NodeSelection is created whose
node is this one.
### `widget`
```tsx
Expand Down
28 changes: 14 additions & 14 deletions docs/assets/index-BGqMXhJy.js → docs/assets/index-Bc7OF9RB.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React-ProseMirror Demo</title>
<script type="module" crossorigin src="/react-prosemirror/assets/index-BGqMXhJy.js"></script>
<script type="module" crossorigin src="/react-prosemirror/assets/index-Bc7OF9RB.js"></script>
<link rel="stylesheet" crossorigin href="/react-prosemirror/assets/index-DAGU9WLy.css">
</head>
<body>
Expand Down
63 changes: 34 additions & 29 deletions src/components/NodeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { createPortal } from "react-dom";
import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js";
import { EditorContext } from "../contexts/EditorContext.js";
import { NodeViewContext } from "../contexts/NodeViewContext.js";
import { SelectNodeContext } from "../contexts/SelectNodeContext.js";
import { StopEventContext } from "../contexts/StopEventContext.js";
import { useNodeViewDescriptor } from "../hooks/useNodeViewDescriptor.js";

Expand Down Expand Up @@ -122,17 +123,22 @@ export const NodeView = memo(function NodeView({
customNodeViewRootRef.current.appendChild(dom);
}, [customNodeView, view, innerDeco, node, outerDeco, getPos]);

const { hasContentDOM, childDescriptors, setStopEvent, nodeViewDescRef } =
useNodeViewDescriptor(
node,
() => getPos.current(),
domRef,
nodeDomRef,
innerDeco,
outerDeco,
undefined,
contentDomRef
);
const {
hasContentDOM,
childDescriptors,
setStopEvent,
setSelectNode,
nodeViewDescRef,
} = useNodeViewDescriptor(
node,
() => getPos.current(),
domRef,
nodeDomRef,
innerDeco,
outerDeco,
undefined,
contentDomRef
);

const finalProps = {
...props,
Expand All @@ -147,9 +153,6 @@ export const NodeView = memo(function NodeView({
getPos: getPosFunc,
decorations: outerDeco,
innerDecorations: innerDeco,
isSelected: false,
// state.selection instanceof NodeSelection &&
// state.selection.node === node,
}),
[getPosFunc, innerDeco, node, outerDeco]
);
Expand Down Expand Up @@ -245,20 +248,22 @@ export const NodeView = memo(function NodeView({
);

return (
<StopEventContext.Provider value={setStopEvent}>
<ChildDescriptorsContext.Provider value={childContextValue}>
{cloneElement(
markedElement,
node.marks.length ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outerDeco.some((d) => (d as any).type.attrs.nodeName)
? { ref: domRef }
: // If all of the node decorations were attr-only, then
// we've already passed the domRef to the NodeView component
// as a prop
undefined
)}
</ChildDescriptorsContext.Provider>
</StopEventContext.Provider>
<SelectNodeContext.Provider value={setSelectNode}>
<StopEventContext.Provider value={setStopEvent}>
<ChildDescriptorsContext.Provider value={childContextValue}>
{cloneElement(
markedElement,
node.marks.length ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outerDeco.some((d) => (d as any).type.attrs.nodeName)
? { ref: domRef }
: // If all of the node decorations were attr-only, then
// we've already passed the domRef to the NodeView component
// as a prop
undefined
)}
</ChildDescriptorsContext.Provider>
</StopEventContext.Provider>
</SelectNodeContext.Provider>
);
});
1 change: 0 additions & 1 deletion src/components/NodeViewComponentProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export type NodeViewComponentProps = {
innerDecorations: DecorationSource;
node: Node;
children?: ReactNode | ReactNode[];
isSelected: boolean;
getPos: () => number;
};
} & HTMLAttributes<HTMLElement>;
10 changes: 10 additions & 0 deletions src/contexts/SelectNodeContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createContext } from "react";

type SelectNodeContextValue = (
selectNode: () => void,
deselectNode: () => void
) => void;

export const SelectNodeContext = createContext<SelectNodeContextValue>(
null as unknown as SelectNodeContextValue
);
8 changes: 7 additions & 1 deletion src/hooks/useEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,13 @@ export function useEditor<T extends HTMLElement = HTMLElement>(
tempDom,
null,
tempDom,
() => false
() => false,
() => {
/* The doc node can't have a node selection*/
},
() => {
/* The doc node can't have a node selection*/
}
)
);

Expand Down
36 changes: 34 additions & 2 deletions src/hooks/useNodeViewDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,30 @@ export function useNodeViewDescriptor(
},
[]
);
const selectNode = useRef<() => void>(() => {
if (!nodeDomRef.current || !node) return;
if (nodeDomRef.current.nodeType == 1)
nodeDomRef.current.classList.add("ProseMirror-selectednode");
if (contentDOMRef?.current || !node.type.spec.draggable)
(domRef?.current ?? nodeDomRef.current).draggable = true;
});
const deselectNode = useRef<() => void>(() => {
if (!nodeDomRef.current || !node) return;
if (nodeDomRef.current.nodeType == 1) {
(nodeDomRef.current as HTMLElement).classList.remove(
"ProseMirror-selectednode"
);
if (contentDOMRef?.current || !node.type.spec.draggable)
(domRef?.current ?? nodeDomRef.current).removeAttribute("draggable");
}
});
const setSelectNode = useCallback(
(newSelectNode: () => void, newDeselectNode: () => void) => {
selectNode.current = newSelectNode;
deselectNode.current = newDeselectNode;
},
[]
);
const { siblingsRef, parentRef } = useContext(ChildDescriptorsContext);
const childDescriptors = useRef<ViewDesc[]>([]);

Expand Down Expand Up @@ -64,7 +88,9 @@ export function useNodeViewDescriptor(
domRef?.current ?? nodeDomRef.current,
firstChildDesc?.dom.parentElement ?? null,
nodeDomRef.current,
(event) => !!stopEvent.current(event)
(event) => !!stopEvent.current(event),
() => selectNode.current(),
() => deselectNode.current()
);
} else {
nodeViewDescRef.current.parent = parentRef.current;
Expand Down Expand Up @@ -141,5 +167,11 @@ export function useNodeViewDescriptor(
};
});

return { hasContentDOM, childDescriptors, nodeViewDescRef, setStopEvent };
return {
hasContentDOM,
childDescriptors,
nodeViewDescRef,
setStopEvent,
setSelectNode,
};
}
23 changes: 23 additions & 0 deletions src/hooks/useSelectNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useContext } from "react";

import { SelectNodeContext } from "../contexts/SelectNodeContext.js";

import { useEditorEffect } from "./useEditorEffect.js";
import { useEditorEventCallback } from "./useEditorEventCallback.js";

export function useSelectNode(
selectNode: () => void,
deselectNode?: () => void
) {
const register = useContext(SelectNodeContext);
const selectNodeMemo = useEditorEventCallback(selectNode);
const deselectNodeMemo = useEditorEventCallback(
deselectNode ??
(() => {
// empty
})
);
return useEditorEffect(() => {
register(selectNodeMemo, deselectNodeMemo);
}, [deselectNodeMemo, register, selectNodeMemo]);
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { useEditorEventCallback } from "./hooks/useEditorEventCallback.js";
export { useEditorEventListener } from "./hooks/useEditorEventListener.js";
export { useEditorState } from "./hooks/useEditorState.js";
export { useStopEvent } from "./hooks/useStopEvent.js";
export { useSelectNode } from "./hooks/useSelectNode.js";
export { reactKeys } from "./plugins/reactKeys.js";
export { widget } from "./decorations/ReactWidgetType.js";

Expand Down
31 changes: 10 additions & 21 deletions src/viewdesc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,9 @@ export class NodeViewDesc extends ViewDesc {
dom: DOMNode,
contentDOM: HTMLElement | null,
public nodeDOM: DOMNode,
public stopEvent: (event: Event) => boolean
public stopEvent: (event: Event) => boolean,
public selectNode: () => void,
public deselectNode: () => void
) {
super(parent, children, getPos, dom, contentDOM);
}
Expand Down Expand Up @@ -829,25 +831,6 @@ export class NodeViewDesc extends ViewDesc {
return true;
}

// Mark this node as being the selected node.
selectNode() {
if (this.nodeDOM.nodeType == 1)
(this.nodeDOM as HTMLElement).classList.add("ProseMirror-selectednode");
if (this.contentDOM || !this.node.type.spec.draggable)
(this.dom as HTMLElement).draggable = true;
}

// Remove selected node marking from this node.
deselectNode() {
if (this.nodeDOM.nodeType == 1) {
(this.nodeDOM as HTMLElement).classList.remove(
"ProseMirror-selectednode"
);
if (this.contentDOM || !this.node.type.spec.draggable)
(this.dom as HTMLElement).removeAttribute("draggable");
}
}

get domAtom() {
return this.node.isAtom;
}
Expand All @@ -874,7 +857,13 @@ export class TextViewDesc extends NodeViewDesc {
dom,
null,
nodeDOM,
() => false
() => false,
() => {
/* Text nodes can't have node selections */
},
() => {
/* Text nodes can't have node selections */
}
);
}

Expand Down

0 comments on commit 84da502

Please sign in to comment.