import React, { ForwardedRef, forwardRef, HTMLProps, LegacyRef, MutableRefObject, } from 'react' import ReactDOM, { flushSync } from 'react-dom' import { Editor } from './Editor.js' import { ReactRenderer } from './ReactRenderer.js' const mergeRefs = ( ...refs: Array | LegacyRef | undefined> ) => { return (node: T) => { refs.forEach(ref => { if (typeof ref === 'function') { ref(node) } else if (ref) { (ref as MutableRefObject).current = node } }) } } const Portals: React.FC<{ renderers: Record }> = ({ renderers }) => { return ( <> {Object.entries(renderers).map(([key, renderer]) => { return ReactDOM.createPortal(renderer.reactElement, renderer.element, key) })} ) } export interface EditorContentProps extends HTMLProps { editor: Editor | null; innerRef?: ForwardedRef; } export interface EditorContentState { renderers: Record; } export class PureEditorContent extends React.Component { editorContentRef: React.RefObject initialized: boolean constructor(props: EditorContentProps) { super(props) this.editorContentRef = React.createRef() this.initialized = false this.state = { renderers: {}, } } componentDidMount() { this.init() } componentDidUpdate() { this.init() } init() { const { editor } = this.props if (editor && editor.options.element) { if (editor.contentComponent) { return } const element = this.editorContentRef.current element.append(...editor.options.element.childNodes) editor.setOptions({ element, }) editor.contentComponent = this editor.createNodeViews() this.initialized = true } } maybeFlushSync(fn: () => void) { // Avoid calling flushSync until the editor is initialized. // Initialization happens during the componentDidMount or componentDidUpdate // lifecycle methods, and React doesn't allow calling flushSync from inside // a lifecycle method. if (this.initialized) { flushSync(fn) } else { fn() } } setRenderer(id: string, renderer: ReactRenderer) { this.maybeFlushSync(() => { this.setState(({ renderers }) => ({ renderers: { ...renderers, [id]: renderer, }, })) }) } removeRenderer(id: string) { this.maybeFlushSync(() => { this.setState(({ renderers }) => { const nextRenderers = { ...renderers } delete nextRenderers[id] return { renderers: nextRenderers } }) }) } componentWillUnmount() { const { editor } = this.props if (!editor) { return } this.initialized = false if (!editor.isDestroyed) { editor.view.setProps({ nodeViews: {}, }) } editor.contentComponent = null if (!editor.options.element.firstChild) { return } const newElement = document.createElement('div') newElement.append(...editor.options.element.childNodes) editor.setOptions({ element: newElement, }) } render() { const { editor, innerRef, ...rest } = this.props return ( <>
{/* @ts-ignore */} ) } } // EditorContent should be re-created whenever the Editor instance changes const EditorContentWithKey = forwardRef( (props: Omit, ref) => { const key = React.useMemo(() => { return Math.floor(Math.random() * 0xFFFFFFFF).toString() }, [props.editor]) // Can't use JSX here because it conflicts with the type definition of Vue's JSX, so use createElement return React.createElement(PureEditorContent, { key, innerRef: ref, ...props, }) }, ) export const EditorContent = React.memo(EditorContentWithKey)