import React from 'react'; import { Component } from 'react'; import { EditorView } from 'prosemirror-view'; import { Node as PMNode } from 'prosemirror-model'; import memoizeOne from 'memoize-one'; import { ExtensionHandlers, getExtensionRenderer, getNodeRenderer, ExtensionProvider, getExtensionModuleNodePrivateProps, } from '@atlaskit/editor-common'; import { ADFEntity } from '@atlaskit/adf-utils'; import Extension from './Extension'; import InlineExtension from './InlineExtension'; import { EditorAppearance } from '../../../../types/editor-appearance'; export interface Props { editorView: EditorView; node: PMNode; handleContentDOMRef: (node: HTMLElement | null) => void; extensionHandlers: ExtensionHandlers; extensionProvider?: Promise; refNode?: ADFEntity; editorAppearance?: EditorAppearance; } export interface State { extensionProvider?: ExtensionProvider; extensionHandlersFromProvider?: ExtensionHandlers; _privateProps?: { __hideFrame?: boolean; }; } export default class ExtensionComponent extends Component { private privatePropsParsed = false; state: State = {}; mounted = false; UNSAFE_componentWillMount() { this.mounted = true; } componentDidMount() { const { extensionProvider } = this.props; if (extensionProvider) { this.setStateFromPromise('extensionProvider', extensionProvider); } } componentDidUpdate() { this.parsePrivateNodePropsIfNeeded(); } componentWillUnmount() { this.mounted = false; } UNSAFE_componentWillReceiveProps(nextProps: Props) { const { extensionProvider } = nextProps; if ( extensionProvider && this.props.extensionProvider !== extensionProvider ) { this.setStateFromPromise('extensionProvider', extensionProvider); } } // memoized to avoid rerender on extension state changes getNodeRenderer = memoizeOne(getNodeRenderer); getExtensionModuleNodePrivateProps = memoizeOne( getExtensionModuleNodePrivateProps, ); render() { const { node, handleContentDOMRef, editorView, refNode, editorAppearance, } = this.props; const extensionHandlerResult = this.tryExtensionHandler(); switch (node.type.name) { case 'extension': case 'bodiedExtension': return ( {extensionHandlerResult} ); case 'inlineExtension': return ( {extensionHandlerResult} ); default: return null; } } private setStateFromPromise = ( stateKey: keyof State, promise?: Promise, ) => { promise && promise.then((p) => { if (!this.mounted) { return; } this.setState({ [stateKey]: p, }); }); }; /** * Parses any private nodes once an extension provider is available. * * We do this separately from resolving a node renderer component since the * private props come from extension provider, rather than an extension * handler which only handles `render`/component concerns. */ private parsePrivateNodePropsIfNeeded = async () => { if (this.privatePropsParsed || !this.state.extensionProvider) { return; } this.privatePropsParsed = true; const { extensionType, extensionKey } = this.props.node.attrs; /** * getExtensionModuleNodePrivateProps can throw if there are issues in the * manifest */ try { const privateProps = await this.getExtensionModuleNodePrivateProps( this.state.extensionProvider, extensionType, extensionKey, ); this.setState({ _privateProps: privateProps, }); } catch (e) { // eslint-disable-next-line no-console console.error('Provided extension handler has thrown an error\n', e); /** We don't want this error to block renderer */ /** We keep rendering the default content */ } }; private tryExtensionHandler() { const { node } = this.props; try { const extensionContent = this.handleExtension(node); if (extensionContent && React.isValidElement(extensionContent)) { return extensionContent; } } catch (e) { // eslint-disable-next-line no-console console.error('Provided extension handler has thrown an error\n', e); /** We don't want this error to block renderer */ /** We keep rendering the default content */ } return null; } private handleExtension = (pmNode: PMNode) => { const { extensionHandlers, editorView } = this.props; const { extensionType, extensionKey, parameters, text } = pmNode.attrs; const isBodiedExtension = pmNode.type.name === 'bodiedExtension'; if (isBodiedExtension) { return; } const node = { type: pmNode.type.name as | 'extension' | 'inlineExtension' | 'bodiedExtension', extensionType, extensionKey, parameters, content: text, }; let result; if (extensionHandlers && extensionHandlers[extensionType]) { const render = getExtensionRenderer(extensionHandlers[extensionType]); result = render(node, editorView.state.doc); } if (!result) { const extensionHandlerFromProvider = this.state.extensionProvider && this.getNodeRenderer( this.state.extensionProvider, extensionType, extensionKey, ); if (extensionHandlerFromProvider) { const NodeRenderer = extensionHandlerFromProvider; return ; } } return result; }; }