@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
170 lines (167 loc) • 6.2 kB
JavaScript
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);