UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

539 lines (533 loc) • 22.6 kB
import _extends from "@babel/runtime/helpers/extends"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; /* eslint-disable @repo/internal/react/no-class-components */ import React, { Component, useEffect, useLayoutEffect, useRef, useState } from 'react'; import memoizeOne from 'memoize-one'; import { NodeSelection } from '@atlaskit/editor-prosemirror/state'; import { fg } from '@atlaskit/platform-feature-flags'; import { getExtensionModuleNodePrivateProps, getNodeRenderer } from '../extensions'; import { getExtensionRenderer, nodeToJSON, toJSON } from '../utils'; import Extension from './Extension/Extension'; import { isEmptyBodiedMacro } from './Extension/Extension/extension-utils'; import InlineExtension from './Extension/InlineExtension'; import MultiBodiedExtension from './MultiBodiedExtension'; /* temporary type until FG cleaned up */ /* temporary type until FG cleaned up */ const getBodiedExtensionContent = node => { const bodiedExtensionContent = []; node.content.forEach(childNode => { bodiedExtensionContent.push(nodeToJSON(childNode)); }); return !!bodiedExtensionContent.length ? bodiedExtensionContent : node.attrs.text; }; export class ExtensionComponentOld extends Component { constructor(...args) { super(...args); _defineProperty(this, "privatePropsParsed", false); _defineProperty(this, "state", {}); _defineProperty(this, "mounted", false); // memoized to avoid rerender on extension state changes _defineProperty(this, "getNodeRenderer", memoizeOne(getNodeRenderer)); _defineProperty(this, "getExtensionModuleNodePrivateProps", memoizeOne(getExtensionModuleNodePrivateProps)); _defineProperty(this, "setIsNodeHovered", isHovered => { this.setState({ isNodeHovered: isHovered }); }); _defineProperty(this, "setShowBodiedExtensionRendererView", showRendererView => { this.setState({ showBodiedExtensionRendererView: showRendererView }); }); // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any _defineProperty(this, "setStateFromPromise", (stateKey, 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. */ _defineProperty(this, "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 */ } }); _defineProperty(this, "handleExtension", (pmNode, actions) => { var _pmNode$marks, _pmNode$marks$find, _pmNode$marks$find$at; const { extensionHandlers, editorView, rendererExtensionHandlers } = this.props; const { showBodiedExtensionRendererView } = this.state; // State will only be true if the gate is on and meets requirements const { extensionType, extensionKey, parameters, text } = pmNode.attrs; const isBodiedExtension = pmNode.type.name === 'bodiedExtension'; if (isBodiedExtension && !showBodiedExtensionRendererView) { return; } const fragmentLocalId = pmNode === null || pmNode === void 0 ? void 0 : (_pmNode$marks = pmNode.marks) === null || _pmNode$marks === void 0 ? void 0 : (_pmNode$marks$find = _pmNode$marks.find(m => m.type.name === 'fragment')) === null || _pmNode$marks$find === void 0 ? void 0 : (_pmNode$marks$find$at = _pmNode$marks$find.attrs) === null || _pmNode$marks$find$at === void 0 ? void 0 : _pmNode$marks$find$at.localId; const content = isBodiedExtension ? getBodiedExtensionContent(pmNode) : text; const node = { type: pmNode.type.name, extensionType, extensionKey, parameters, content, localId: pmNode.attrs.localId, fragmentLocalId }; if (isBodiedExtension) { const rendererExtensionHandler = rendererExtensionHandlers === null || rendererExtensionHandlers === void 0 ? void 0 : rendererExtensionHandlers[extensionType]; // Forge bodied extensions don't get rendererExtensionHandlers passed in and use extensionHandlerFromProvider from the below logic instead if (rendererExtensionHandler) { return getExtensionRenderer(rendererExtensionHandler)(node, toJSON(editorView.state.doc)); } } let result; if (extensionHandlers && extensionHandlers[extensionType]) { const render = getExtensionRenderer(extensionHandlers[extensionType]); result = render(node, editorView.state.doc, actions); } if (!result) { const extensionHandlerFromProvider = this.state.extensionProvider && this.getNodeRenderer(this.state.extensionProvider, extensionType, extensionKey); if (extensionHandlerFromProvider) { const NodeRenderer = extensionHandlerFromProvider; if (node.type === 'multiBodiedExtension') { return /*#__PURE__*/React.createElement(NodeRenderer, { node: node, references: this.props.references, actions: actions }); } else { return /*#__PURE__*/React.createElement(NodeRenderer, { node: node, references: this.props.references }); } } } return result; }); } // Ignored via go/ees005 // eslint-disable-next-line react/no-unsafe UNSAFE_componentWillMount() { this.mounted = true; const { node, showLivePagesBodiedMacrosRendererView } = this.props; // We only care about this empty state on first page load or insertion to determine the view if (!!(showLivePagesBodiedMacrosRendererView !== null && showLivePagesBodiedMacrosRendererView !== void 0 && showLivePagesBodiedMacrosRendererView(nodeToJSON(node))) && !isEmptyBodiedMacro(node)) { this.setState({ showBodiedExtensionRendererView: true }); } } componentDidMount() { const { extensionProvider } = this.props; if (extensionProvider) { this.setStateFromPromise('extensionProvider', extensionProvider); } } componentDidUpdate() { this.parsePrivateNodePropsIfNeeded(); } componentWillUnmount() { this.mounted = false; } // Ignored via go/ees005 // eslint-disable-next-line react/no-unsafe UNSAFE_componentWillReceiveProps(nextProps) { const { extensionProvider } = nextProps; if (extensionProvider && this.props.extensionProvider !== extensionProvider) { this.setStateFromPromise('extensionProvider', extensionProvider); } } render() { var _this$state$_privateP; const { node, handleContentDOMRef, editorView, references, editorAppearance, pluginInjectionApi, getPos, eventDispatcher, macroInteractionDesignFeatureFlags, showLivePagesBodiedMacrosRendererView, showUpdatedLivePages1PBodiedExtensionUI } = this.props; const { selection } = editorView.state; const selectedNode = selection instanceof NodeSelection && selection.node; const position = typeof getPos === 'function' && getPos(); const resolvedPosition = position && editorView.state.doc.resolve(position); const isNodeNested = !!(resolvedPosition && resolvedPosition.depth > 0); if (node.type.name === 'multiBodiedExtension') { return /*#__PURE__*/React.createElement(MultiBodiedExtension, { node: node, editorView: editorView, getPos: getPos, handleContentDOMRef: handleContentDOMRef, tryExtensionHandler: this.tryExtensionHandler.bind(this), eventDispatcher: eventDispatcher, pluginInjectionApi: pluginInjectionApi, editorAppearance: editorAppearance, macroInteractionDesignFeatureFlags: macroInteractionDesignFeatureFlags, isNodeSelected: selectedNode === node, isNodeNested: isNodeNested, isNodeHovered: this.state.isNodeHovered, setIsNodeHovered: this.setIsNodeHovered }); } const extensionHandlerResult = this.tryExtensionHandler(undefined); switch (node.type.name) { case 'extension': case 'bodiedExtension': return /*#__PURE__*/React.createElement(Extension, { node: node, getPos: this.props.getPos, references: references, extensionProvider: this.state.extensionProvider, handleContentDOMRef: handleContentDOMRef, view: editorView, editorAppearance: editorAppearance, hideFrame: (_this$state$_privateP = this.state._privateProps) === null || _this$state$_privateP === void 0 ? void 0 : _this$state$_privateP.__hideFrame, pluginInjectionApi: pluginInjectionApi, macroInteractionDesignFeatureFlags: macroInteractionDesignFeatureFlags, isNodeSelected: selectedNode === node, isNodeHovered: this.state.isNodeHovered, isNodeNested: isNodeNested, setIsNodeHovered: this.setIsNodeHovered, showLivePagesBodiedMacrosRendererView: !!(showLivePagesBodiedMacrosRendererView !== null && showLivePagesBodiedMacrosRendererView !== void 0 && showLivePagesBodiedMacrosRendererView(nodeToJSON(node))), showUpdatedLivePages1PBodiedExtensionUI: !!(showUpdatedLivePages1PBodiedExtensionUI !== null && showUpdatedLivePages1PBodiedExtensionUI !== void 0 && showUpdatedLivePages1PBodiedExtensionUI(nodeToJSON(node))), showBodiedExtensionRendererView: this.state.showBodiedExtensionRendererView, setShowBodiedExtensionRendererView: this.setShowBodiedExtensionRendererView }, extensionHandlerResult); case 'inlineExtension': return /*#__PURE__*/React.createElement(InlineExtension, { node: node, macroInteractionDesignFeatureFlags: macroInteractionDesignFeatureFlags, isNodeSelected: selectedNode === node, pluginInjectionApi: pluginInjectionApi, isNodeHovered: this.state.isNodeHovered, setIsNodeHovered: this.setIsNodeHovered }, extensionHandlerResult); default: return null; } } tryExtensionHandler(actions) { const { node } = this.props; try { const extensionContent = this.handleExtension(node, actions); if (extensionContent && /*#__PURE__*/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; } } export const ExtensionComponentNew = props => { const { extensionProvider: extensionProviderResolver, showLivePagesBodiedMacrosRendererView, node, ...restProps } = props; const [extensionProvider, setExtensionProvider] = useState(undefined); const [showBodiedExtensionRendererView, setShowBodiedExtensionRendererView] = useState(!!(showLivePagesBodiedMacrosRendererView !== null && showLivePagesBodiedMacrosRendererView !== void 0 && showLivePagesBodiedMacrosRendererView(nodeToJSON(node))) && !isEmptyBodiedMacro(node)); const mountedRef = useRef(true); useLayoutEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); useEffect(() => { extensionProviderResolver === null || extensionProviderResolver === void 0 ? void 0 : extensionProviderResolver.then(provider => { if (mountedRef.current) { setExtensionProvider(provider); } }); }, [extensionProviderResolver]); return /*#__PURE__*/React.createElement(ExtensionComponentInner // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading , _extends({}, restProps, { extensionProvider: extensionProvider, node: node, showLivePagesBodiedMacrosRendererView: showLivePagesBodiedMacrosRendererView, showBodiedExtensionRendererView: showBodiedExtensionRendererView, setShowBodiedExtensionRendererView: setShowBodiedExtensionRendererView })); }; class ExtensionComponentInner extends Component { constructor(...args) { super(...args); _defineProperty(this, "privatePropsParsed", false); _defineProperty(this, "state", {}); // memoized to avoid rerender on extension state changes _defineProperty(this, "getNodeRenderer", memoizeOne(getNodeRenderer)); _defineProperty(this, "getExtensionModuleNodePrivateProps", memoizeOne(getExtensionModuleNodePrivateProps)); _defineProperty(this, "setIsNodeHovered", isHovered => { this.setState({ isNodeHovered: isHovered }); }); /** * 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. */ _defineProperty(this, "parsePrivateNodePropsIfNeeded", async () => { if (this.privatePropsParsed || !this.props.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.props.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 */ } }); _defineProperty(this, "handleExtension", (pmNode, actions) => { var _pmNode$marks2, _pmNode$marks2$find, _pmNode$marks2$find$a; const { extensionHandlers, editorView, showBodiedExtensionRendererView, rendererExtensionHandlers } = this.props; const { extensionType, extensionKey, parameters, text } = pmNode.attrs; const isBodiedExtension = pmNode.type.name === 'bodiedExtension'; if (isBodiedExtension && !showBodiedExtensionRendererView) { return; } const fragmentLocalId = pmNode === null || pmNode === void 0 ? void 0 : (_pmNode$marks2 = pmNode.marks) === null || _pmNode$marks2 === void 0 ? void 0 : (_pmNode$marks2$find = _pmNode$marks2.find(m => m.type.name === 'fragment')) === null || _pmNode$marks2$find === void 0 ? void 0 : (_pmNode$marks2$find$a = _pmNode$marks2$find.attrs) === null || _pmNode$marks2$find$a === void 0 ? void 0 : _pmNode$marks2$find$a.localId; const content = isBodiedExtension ? getBodiedExtensionContent(pmNode) : text; const node = { type: pmNode.type.name, extensionType, extensionKey, parameters, content, localId: pmNode.attrs.localId, fragmentLocalId }; if (isBodiedExtension) { const rendererExtensionHandler = rendererExtensionHandlers === null || rendererExtensionHandlers === void 0 ? void 0 : rendererExtensionHandlers[extensionType]; // Forge bodied extensions don't get rendererExtensionHandlers passed in and use extensionHandlerFromProvider from the below logic instead if (rendererExtensionHandler) { return getExtensionRenderer(rendererExtensionHandler)(node, toJSON(editorView.state.doc)); } } let result; if (extensionHandlers && extensionHandlers[extensionType]) { const render = getExtensionRenderer(extensionHandlers[extensionType]); result = render(node, editorView.state.doc, actions); } if (!result) { const extensionHandlerFromProvider = this.props.extensionProvider && this.getNodeRenderer(this.props.extensionProvider, extensionType, extensionKey); if (extensionHandlerFromProvider) { const NodeRenderer = extensionHandlerFromProvider; if (node.type === 'multiBodiedExtension') { return /*#__PURE__*/React.createElement(NodeRenderer, { node: node, references: this.props.references, actions: actions }); } else { return /*#__PURE__*/React.createElement(NodeRenderer, { node: node, references: this.props.references }); } } } return result; }); } componentDidUpdate() { this.parsePrivateNodePropsIfNeeded(); } render() { var _this$state$_privateP2; const { node, handleContentDOMRef, editorView, references, editorAppearance, pluginInjectionApi, getPos, eventDispatcher, macroInteractionDesignFeatureFlags, extensionProvider, showLivePagesBodiedMacrosRendererView, showUpdatedLivePages1PBodiedExtensionUI, showBodiedExtensionRendererView, setShowBodiedExtensionRendererView } = this.props; const { selection } = editorView.state; const selectedNode = selection instanceof NodeSelection && selection.node; const position = typeof getPos === 'function' && getPos(); const resolvedPosition = position && editorView.state.doc.resolve(position); const isNodeNested = !!(resolvedPosition && resolvedPosition.depth > 0); if (node.type.name === 'multiBodiedExtension') { return /*#__PURE__*/React.createElement(MultiBodiedExtension, { node: node, editorView: editorView, getPos: getPos, handleContentDOMRef: handleContentDOMRef, tryExtensionHandler: this.tryExtensionHandler.bind(this), eventDispatcher: eventDispatcher, pluginInjectionApi: pluginInjectionApi, editorAppearance: editorAppearance, macroInteractionDesignFeatureFlags: macroInteractionDesignFeatureFlags, isNodeSelected: selectedNode === node, isNodeNested: isNodeNested, isNodeHovered: this.state.isNodeHovered, setIsNodeHovered: this.setIsNodeHovered }); } const extensionHandlerResult = this.tryExtensionHandler(undefined); switch (node.type.name) { case 'extension': case 'bodiedExtension': return /*#__PURE__*/React.createElement(Extension, { node: node, getPos: this.props.getPos, references: references, extensionProvider: extensionProvider, handleContentDOMRef: handleContentDOMRef, view: editorView, editorAppearance: editorAppearance, hideFrame: (_this$state$_privateP2 = this.state._privateProps) === null || _this$state$_privateP2 === void 0 ? void 0 : _this$state$_privateP2.__hideFrame, pluginInjectionApi: pluginInjectionApi, macroInteractionDesignFeatureFlags: macroInteractionDesignFeatureFlags, isNodeSelected: selectedNode === node, isNodeHovered: this.state.isNodeHovered, isNodeNested: isNodeNested, setIsNodeHovered: this.setIsNodeHovered, showLivePagesBodiedMacrosRendererView: !!(showLivePagesBodiedMacrosRendererView !== null && showLivePagesBodiedMacrosRendererView !== void 0 && showLivePagesBodiedMacrosRendererView(nodeToJSON(node))), showUpdatedLivePages1PBodiedExtensionUI: !!(showUpdatedLivePages1PBodiedExtensionUI !== null && showUpdatedLivePages1PBodiedExtensionUI !== void 0 && showUpdatedLivePages1PBodiedExtensionUI(nodeToJSON(node))), showBodiedExtensionRendererView: showBodiedExtensionRendererView, setShowBodiedExtensionRendererView: setShowBodiedExtensionRendererView }, extensionHandlerResult); case 'inlineExtension': return /*#__PURE__*/React.createElement(InlineExtension, { node: node, macroInteractionDesignFeatureFlags: macroInteractionDesignFeatureFlags, isNodeSelected: selectedNode === node, pluginInjectionApi: pluginInjectionApi, isNodeHovered: this.state.isNodeHovered, setIsNodeHovered: this.setIsNodeHovered }, extensionHandlerResult); default: return null; } } tryExtensionHandler(actions) { const { node } = this.props; try { const extensionContent = this.handleExtension(node, actions); if (extensionContent && /*#__PURE__*/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; } } export const ExtensionComponent = props => { if (fg('platform_editor_react18_extension_component_v2')) { // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading return /*#__PURE__*/React.createElement(ExtensionComponentNew, props); } // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading return /*#__PURE__*/React.createElement(ExtensionComponentOld, props); };