UNPKG

@atlaskit/editor-common

Version:

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

170 lines (167 loc) 6.2 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; var _class4; import React from 'react'; import PropTypes from 'prop-types'; import { createPortal, unmountComponentAtNode, unstable_renderSubtreeIntoContainer } from 'react-dom'; import { injectIntl, RawIntlProvider, useIntl } from 'react-intl-next'; import { default as AnalyticsReactContext } from '@atlaskit/analytics-next-stable-react-context'; import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '../../analytics'; import { EventDispatcher } from '../../event-dispatcher'; import IntlProviderIfMissingWrapper from '../IntlProviderIfMissingWrapper'; export class PortalProviderAPI extends EventDispatcher { constructor(intl, onAnalyticsEvent, analyticsContext) { super(); _defineProperty(this, "portals", new Map()); _defineProperty(this, "setContext", context => { this.context = context; }); this.intl = intl; this.onAnalyticsEvent = onAnalyticsEvent; this.useAnalyticsContext = analyticsContext; } render(children, container, hasAnalyticsContext = false, hasIntlContext = false) { this.portals.set(container, { children: children, hasAnalyticsContext, hasIntlContext }); let wrappedChildren = this.useAnalyticsContext ? /*#__PURE__*/React.createElement(AnalyticsContextWrapper, null, children()) : children(); if (hasIntlContext) { wrappedChildren = /*#__PURE__*/React.createElement(RawIntlProvider, { value: this.intl }, wrappedChildren); } unstable_renderSubtreeIntoContainer(this.context, wrappedChildren, container); } // TODO: until https://product-fabric.atlassian.net/browse/ED-5013 // we (unfortunately) need to re-render to pass down any updated context. // selectively do this for nodeviews that opt-in via `hasAnalyticsContext` forceUpdate({ intl }) { this.intl = intl; this.portals.forEach((portal, container) => { if (!portal.hasAnalyticsContext && !this.useAnalyticsContext && !portal.hasIntlContext) { return; } let wrappedChildren = portal.children(); if (portal.hasAnalyticsContext && this.useAnalyticsContext) { wrappedChildren = /*#__PURE__*/React.createElement(AnalyticsContextWrapper, null, wrappedChildren); } if (portal.hasIntlContext) { wrappedChildren = /*#__PURE__*/React.createElement(RawIntlProvider, { value: this.intl }, wrappedChildren); } unstable_renderSubtreeIntoContainer(this.context, wrappedChildren, container); }); } remove(container) { this.portals.delete(container); // There is a race condition that can happen caused by Prosemirror vs React, // where Prosemirror removes the container from the DOM before React gets // around to removing the child from the container // This will throw a NotFoundError: The node to be removed is not a child of this node // Both Prosemirror and React remove the elements asynchronously, and in edge // cases Prosemirror beats React try { unmountComponentAtNode(container); } catch (error) { if (this.onAnalyticsEvent) { this.onAnalyticsEvent({ payload: { action: ACTION.FAILED_TO_UNMOUNT, actionSubject: ACTION_SUBJECT.EDITOR, actionSubjectId: ACTION_SUBJECT_ID.REACT_NODE_VIEW, attributes: { error: error, domNodes: { container: container ? container.className : undefined, child: container.firstElementChild ? container.firstElementChild.className : undefined } }, eventType: EVENT_TYPE.OPERATIONAL } }); } } } } class BasePortalProvider extends React.Component { constructor(props) { super(props); this.portalProviderAPI = new PortalProviderAPI(props.intl, props.onAnalyticsEvent, props.useAnalyticsContext); } render() { return this.props.render(this.portalProviderAPI); } componentDidUpdate() { this.portalProviderAPI.forceUpdate({ intl: this.props.intl }); } } _defineProperty(BasePortalProvider, "displayName", 'PortalProvider'); export const PortalProvider = injectIntl(BasePortalProvider); export const PortalProviderWithThemeProviders = ({ onAnalyticsEvent, useAnalyticsContext, render }) => /*#__PURE__*/React.createElement(IntlProviderIfMissingWrapper, null, /*#__PURE__*/React.createElement(PortalProviderWithThemeAndIntlProviders, { onAnalyticsEvent: onAnalyticsEvent, useAnalyticsContext: useAnalyticsContext, render: render })); const PortalProviderWithThemeAndIntlProviders = ({ onAnalyticsEvent, useAnalyticsContext, render }) => { const intl = useIntl(); return /*#__PURE__*/React.createElement(BasePortalProvider, { intl: intl, onAnalyticsEvent: onAnalyticsEvent, useAnalyticsContext: useAnalyticsContext, render: render }); }; export class PortalRenderer extends React.Component { constructor(props) { super(props); _defineProperty(this, "handleUpdate", portals => this.setState({ portals })); props.portalProviderAPI.setContext(this); props.portalProviderAPI.on('update', this.handleUpdate); this.state = { portals: new Map() }; } render() { const { portals } = this.state; return /*#__PURE__*/React.createElement(React.Fragment, null, Array.from(portals.entries()).map(([container, children]) => /*#__PURE__*/createPortal(children, container))); } } /** * Wrapper to re-provide modern analytics context to ReactNodeViews. */ const dummyAnalyticsContext = { getAtlaskitAnalyticsContext() {}, getAtlaskitAnalyticsEventHandlers() {} }; const AnalyticsContextWrapper = (_class4 = class AnalyticsContextWrapper extends React.Component { render() { const { value } = this.context.contextAdapter.analytics || { value: dummyAnalyticsContext }; return /*#__PURE__*/React.createElement(AnalyticsReactContext.Provider, { value: value }, this.props.children); } }, _defineProperty(_class4, "contextTypes", { contextAdapter: PropTypes.object }), _class4);