Skip to content

Commit

Permalink
feat: added a node view for rendering react block
Browse files Browse the repository at this point in the history
  • Loading branch information
smsochneg committed Dec 7, 2023
1 parent ab2ad24 commit 3c38214
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './core';
export * from './toolbar';
export * from './react';
export * from './react-utils/hooks';
export * from './react-utils/react-node-view';
export * from './classname';
export * from './logger';
export * from './extensions';
Expand Down
131 changes: 131 additions & 0 deletions src/react-utils/react-node-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React from 'react';
import {createPortal} from 'react-dom';
import type {NodeView, EditorView} from 'prosemirror-view';
import type {Node} from 'prosemirror-model';

import {ExtensionDeps, NodeViewConstructor, Serializer} from '../core';
import {getReactRendererFromState} from '../extensions';

type ReactNodeViewOptions<T> = {
isInline?: boolean;
reactNodeWrapperCn?: string;
extensionOptions?: T;
};

export const ReactNodeStopEventCn = 'prosemirror-stop-event';
export type ReactNodeViewProps<T extends object = {}> = {
dom: HTMLElement;
view: EditorView;
updateAttributes: (attrs: object) => void;
node: Node;
getPos: () => number;
serializer: Serializer;
extensionOptions?: T;
};

export class ReactNodeView<T extends object = {}> implements NodeView {
readonly dom: HTMLElement;
node: Node;
readonly view;
readonly serializer;
readonly renderItem;
readonly getPos;

constructor(
Component: React.FC<ReactNodeViewProps<T>>,
opts: {
node: Node;
view: EditorView;
getPos: () => number;
serializer: Serializer;
options?: ReactNodeViewOptions<T>;
},
) {
const {getPos, node, serializer, view, options} = opts;
this.node = node;

this.dom = options?.isInline
? document.createElement('span')
: document.createElement('div');
this.dom.classList.add('react-node-wrapper');
if (options?.reactNodeWrapperCn) {
this.dom.classList.add(options?.reactNodeWrapperCn);
}
this.dom.contentEditable = 'false';

this.serializer = serializer;
this.view = view;
this.getPos = getPos;

this.renderItem = getReactRendererFromState(view.state).createItem(
`${Component.displayName || this.node.type.name}-view`,
() =>
createPortal(
<Component
dom={this.dom}
view={this.view}
updateAttributes={this.updateAttributes.bind(this)}
node={this.node}
getPos={this.getPos.bind(this)}
serializer={this.serializer}
extensionOptions={options?.extensionOptions}
/>,
this.dom,
),
);
}

update(node: Node) {
if (node.type !== this.node.type) return false;
if (this.node === node) return true;

this.node = node;
this.renderItem.rerender();

return true;
}

destroy() {
this.renderItem.remove();
}

ignoreMutation() {
return true;
}

updateAttributes(attributes: {}) {
const pos = this.getPos();
const {tr} = this.view.state;

tr.setNodeMarkup(pos, undefined, {
...this.node.attrs,
...attributes,
});

this.view.dispatch(tr);
}

stopEvent(e: Event) {
const target = e.target as Element;
const isInput = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA'].includes(target.tagName);

if (
isInput ||
(typeof target.className === 'string' &&
target.className.includes(ReactNodeStopEventCn))
) {
return true;
}

return false;
}
}

export const reactNodeViewFactory: <T extends object = {}>(
Component: React.FC<ReactNodeViewProps<T>>,
options?: ReactNodeViewOptions<T>,
) => (deps: ExtensionDeps) => NodeViewConstructor =
(Component, options) =>
({serializer}) =>
(node, view, getPos) =>
new ReactNodeView(Component, {node, view, getPos, serializer, options});

0 comments on commit 3c38214

Please sign in to comment.