Skip to content

Commit 67a325a

Browse files
authored
feat: added a node view for rendering react block (#163)
1 parent ab2ad24 commit 67a325a

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './core';
33
export * from './toolbar';
44
export * from './react';
55
export * from './react-utils/hooks';
6+
export * from './react-utils/react-node-view';
67
export * from './classname';
78
export * from './logger';
89
export * from './extensions';
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import React from 'react';
2+
import {createPortal} from 'react-dom';
3+
import type {NodeView, EditorView} from 'prosemirror-view';
4+
import type {Node} from 'prosemirror-model';
5+
6+
import {ExtensionDeps, NodeViewConstructor, Serializer} from '../core';
7+
import {getReactRendererFromState} from '../extensions';
8+
9+
type ReactNodeViewOptions<T> = {
10+
isInline?: boolean;
11+
reactNodeWrapperCn?: string;
12+
extensionOptions?: T;
13+
};
14+
15+
export const ReactNodeStopEventCn = 'prosemirror-stop-event';
16+
export type ReactNodeViewProps<T extends object = {}> = {
17+
dom: HTMLElement;
18+
view: EditorView;
19+
updateAttributes: (attrs: object) => void;
20+
node: Node;
21+
getPos: () => number;
22+
serializer: Serializer;
23+
extensionOptions?: T;
24+
};
25+
26+
export class ReactNodeView<T extends object = {}> implements NodeView {
27+
readonly dom: HTMLElement;
28+
node: Node;
29+
readonly view;
30+
readonly serializer;
31+
readonly renderItem;
32+
readonly getPos;
33+
34+
constructor(
35+
Component: React.FC<ReactNodeViewProps<T>>,
36+
opts: {
37+
node: Node;
38+
view: EditorView;
39+
getPos: () => number;
40+
serializer: Serializer;
41+
options?: ReactNodeViewOptions<T>;
42+
},
43+
) {
44+
const {getPos, node, serializer, view, options} = opts;
45+
this.node = node;
46+
47+
this.dom = options?.isInline
48+
? document.createElement('span')
49+
: document.createElement('div');
50+
this.dom.classList.add('react-node-wrapper');
51+
if (options?.reactNodeWrapperCn) {
52+
this.dom.classList.add(options?.reactNodeWrapperCn);
53+
}
54+
this.dom.contentEditable = 'false';
55+
56+
this.serializer = serializer;
57+
this.view = view;
58+
this.getPos = getPos;
59+
60+
this.renderItem = getReactRendererFromState(view.state).createItem(
61+
`${Component.displayName || this.node.type.name}-view`,
62+
() =>
63+
createPortal(
64+
<Component
65+
dom={this.dom}
66+
view={this.view}
67+
updateAttributes={this.updateAttributes.bind(this)}
68+
node={this.node}
69+
getPos={this.getPos.bind(this)}
70+
serializer={this.serializer}
71+
extensionOptions={options?.extensionOptions}
72+
/>,
73+
this.dom,
74+
),
75+
);
76+
}
77+
78+
update(node: Node) {
79+
if (node.type !== this.node.type) return false;
80+
if (this.node === node) return true;
81+
82+
this.node = node;
83+
this.renderItem.rerender();
84+
85+
return true;
86+
}
87+
88+
destroy() {
89+
this.renderItem.remove();
90+
}
91+
92+
ignoreMutation() {
93+
return true;
94+
}
95+
96+
updateAttributes(attributes: {}) {
97+
const pos = this.getPos();
98+
const {tr} = this.view.state;
99+
100+
tr.setNodeMarkup(pos, undefined, {
101+
...this.node.attrs,
102+
...attributes,
103+
});
104+
105+
this.view.dispatch(tr);
106+
}
107+
108+
stopEvent(e: Event) {
109+
const target = e.target as Element;
110+
const isInput = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA'].includes(target.tagName);
111+
112+
if (
113+
isInput ||
114+
(typeof target.className === 'string' &&
115+
target.className.includes(ReactNodeStopEventCn))
116+
) {
117+
return true;
118+
}
119+
120+
return false;
121+
}
122+
}
123+
124+
export const reactNodeViewFactory: <T extends object = {}>(
125+
Component: React.FC<ReactNodeViewProps<T>>,
126+
options?: ReactNodeViewOptions<T>,
127+
) => (deps: ExtensionDeps) => NodeViewConstructor =
128+
(Component, options) =>
129+
({serializer}) =>
130+
(node, view, getPos) =>
131+
new ReactNodeView(Component, {node, view, getPos, serializer, options});

0 commit comments

Comments
 (0)