@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
270 lines (256 loc) • 11.1 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
// Disable no-re-export rule for entry point files
/* eslint-disable @atlaskit/editor/no-re-export */
import React from 'react';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { ACTION_SUBJECT, ACTION_SUBJECT_ID } from '../analytics';
import { isSSR } from '../core-utils';
import { createDispatch } from '../event-dispatcher';
import { ErrorBoundary } from '../ui/ErrorBoundary';
import { getPerformanceOptions, startMeasureReactNodeViewRendered, stopMeasureReactNodeViewRendered } from '../utils';
import { analyticsEventKey } from '../utils/analytics';
import { generateUniqueNodeKey } from './generateUniqueNodeKey';
export { getInlineNodeViewProducer, inlineNodeViewClassname } from './getInlineNodeViewProducer';
export { NodeViewContentHole } from './NodeViewContentHole';
export default class ReactNodeView {
constructor(_node, view, getPos, portalProviderAPI, eventDispatcher, reactComponentProps,
// Spreading props to pass through dynamic component props
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reactComponent, viewShouldUpdate, shouldRenderImmediatelyInPortal) {
// Spreading props to pass through dynamic component props
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_defineProperty(this, "decorations", []);
_defineProperty(this, "handleRef", node => this._handleRef(node));
_defineProperty(this, "dispatchAnalyticsEvent", payload => {
if (this.eventDispatcher) {
const dispatch = createDispatch(this.eventDispatcher);
dispatch(analyticsEventKey, {
payload
});
}
});
this.node = _node;
this.view = view;
this.getPos = getPos;
this.portalProviderAPI = portalProviderAPI;
this.reactComponentProps = reactComponentProps || {};
this.reactComponent = reactComponent;
this._viewShouldUpdate = viewShouldUpdate;
this.eventDispatcher = eventDispatcher;
this.key = generateUniqueNodeKey();
this.shouldRenderImmediatelyInPortal = shouldRenderImmediatelyInPortal || false;
}
/**
* This method exists to move initialization logic out of the constructor,
* so object can be initialized properly before calling render first time.
*
* Example:
* Instance properties get added to an object only after super call in
* constructor, which leads to some methods being undefined during the
* first render.
*/
init(shouldSkipInitRender = false) {
this.domRef = this.createDomRef();
this.setDomAttrs(this.node, this.domRef);
const {
dom: contentDOMWrapper,
contentDOM
} = this.getContentDOM() || {
dom: undefined,
contentDOM: undefined
};
if (this.domRef && contentDOMWrapper) {
this.domRef.appendChild(contentDOMWrapper);
this.contentDOM = contentDOM ? contentDOM : contentDOMWrapper;
this.contentDOMWrapper = contentDOMWrapper || contentDOM;
}
// @see ED-3790
// something gets messed up during mutation processing inside of a
// nodeView if DOM structure has nested plain "div"s, it doesn't see the
// difference between them and it kills the nodeView
this.domRef.classList.add(`${this.node.type.name}View-content-wrap`);
const {
samplingRate,
slowThreshold,
trackingEnabled
} = getPerformanceOptions(this.view);
trackingEnabled && startMeasureReactNodeViewRendered({
nodeTypeName: this.node.type.name
});
if (!shouldSkipInitRender) {
this.renderReactComponent(() => this.render(this.reactComponentProps, this.handleRef));
// During SSR, renderToStaticMarkup + container.innerHTML (in portal) clobbers the
// contentDOMWrapper that was appended above. The React ref callback
// (forwardRef) never fires in renderToStaticMarkup, so contentDOM is
// left detached. Re-attach it by finding the marked SSR ref target.
if (isSSR() && this.domRef && expValEquals('platform_editor_editor_ssr_streaming', 'isEnabled', true)) {
const refTarget = this.domRef.querySelector('[data-ssr-content-dom-ref]');
if (refTarget) {
this.handleRef(refTarget);
}
}
}
trackingEnabled && stopMeasureReactNodeViewRendered({
nodeTypeName: this.node.type.name,
dispatchAnalyticsEvent: this.dispatchAnalyticsEvent,
samplingRate,
slowThreshold
});
return this;
}
// Spreading props to pass through dynamic component props
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
renderReactComponent(component) {
if (!this.domRef || !component) {
return;
}
const componentWithErrorBoundary = () => {
var _this$node$type$name, _this$node, _this$node$type;
return /*#__PURE__*/React.createElement(ErrorBoundary, {
component: ACTION_SUBJECT.REACT_NODE_VIEW,
componentId: (_this$node$type$name = this === null || this === void 0 ? void 0 : (_this$node = this.node) === null || _this$node === void 0 ? void 0 : (_this$node$type = _this$node.type) === null || _this$node$type === void 0 ? void 0 : _this$node$type.name) !== null && _this$node$type$name !== void 0 ? _this$node$type$name : ACTION_SUBJECT_ID.UNKNOWN_NODE,
dispatchAnalyticsEvent: this.dispatchAnalyticsEvent
}, component());
};
this.portalProviderAPI.render(componentWithErrorBoundary,
// Spreading props to pass through dynamic component props
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.domRef, this.key, undefined, this.shouldRenderImmediatelyInPortal);
}
createDomRef() {
if (!this.node.isInline) {
return document.createElement('div');
}
const htmlElement = document.createElement('span');
return htmlElement;
}
getContentDOM() {
return undefined;
}
_handleRef(node) {
const contentDOM = this.contentDOMWrapper || this.contentDOM;
// @ts-ignore
// Spreading props to pass through dynamic component props
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let oldIgnoreMutation;
let selectionBookmark;
let mutationsIgnored = false;
// move the contentDOM node inside the inner reference after rendering
if (node && contentDOM && !node.contains(contentDOM)) {
// @ts-ignore - ignoreMutation may not be declared
oldIgnoreMutation = this.ignoreMutation; // store ref to previous ignoreMutation
// ignore all mutations caused by ProseMirror's MutationObserver triggering
// after DOM change, except selection changes
// @ts-ignore ProseMirror adds selection type to MutationRecord
this.ignoreMutation = m => {
const isSelectionMutation = m.type === 'selection';
if (!isSelectionMutation) {
mutationsIgnored = true;
}
return !isSelectionMutation;
};
// capture document selection state before React DOM changes triggers ProseMirror selection change transaction
if (this.view.state.selection.visible) {
selectionBookmark = this.view.state.selection.getBookmark();
}
node.appendChild(contentDOM);
// After the next frame:
requestAnimationFrame(() => {
// Restore the original mutation handler
// @ts-ignore - this may not have been declared by implementing class
this.ignoreMutation = oldIgnoreMutation;
// Restore the selection only if:
// - We have a selection bookmark
// - Mutations were ignored during the table move
// - The bookmarked selection is different from the current selection.
if (selectionBookmark && mutationsIgnored) {
const resolvedSelection = selectionBookmark.resolve(this.view.state.tr.doc);
// Don't set the selection if it's the same as the current selection.
if (!resolvedSelection.eq(this.view.state.selection)) {
const tr = this.view.state.tr.setSelection(resolvedSelection);
tr.setMeta('source', 'ReactNodeView:_handleRef:selection-resync');
this.view.dispatch(tr);
}
}
});
}
}
// Spreading props to pass through dynamic component props
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
render(props, forwardRef) {
return this.reactComponent ? /*#__PURE__*/React.createElement(this.reactComponent, _extends({
view: this.view,
getPos: this.getPos,
node: this.node,
forwardRef: forwardRef
// eslint-disable-next-line react/jsx-props-no-spreading -- Spreading props to pass through dynamic component props
}, props)) : null;
}
update(node, decorations, _innerDecorations, validUpdate = () => true) {
// @see https://github.com/ProseMirror/prosemirror/issues/648
const isValidUpdate = this.node.type === node.type && validUpdate(this.node, node);
this.decorations = decorations;
if (!isValidUpdate) {
return false;
}
if (this.domRef && !this.node.sameMarkup(node)) {
this.setDomAttrs(node, this.domRef);
}
// View should not process a re-render if this is false.
// We dont want to destroy the view, so we return true.
// TODO: ED-13910 - Fix viewShouldUpdate readonly decoration array
if (!this.viewShouldUpdate(node, decorations)) {
this.node = node;
return true;
}
this.node = node;
this.renderReactComponent(() => this.render(this.reactComponentProps, this.handleRef));
return true;
}
viewShouldUpdate(nextNode, _decorations) {
if (this._viewShouldUpdate) {
return this._viewShouldUpdate(nextNode);
}
return true;
}
/**
* Copies the attributes from a ProseMirror Node to a DOM node.
* @param node The Prosemirror Node from which to source the attributes
*/
setDomAttrs(node, element) {
Object.keys(node.attrs || {}).forEach(attr => {
element.setAttribute(attr, node.attrs[attr]);
});
}
get dom() {
// Only return reference if domRef is defined
if (this.domRef === undefined) {
//raise an error
throw new Error('domRef is not defined or may have been destroyed');
}
// Spreading props to pass through dynamic component props
return this.domRef;
}
destroy() {
if (!this.domRef) {
return;
}
this.portalProviderAPI.remove(this.key);
this.domRef = undefined;
this.contentDOM = undefined;
}
static fromComponent(
// Spreading props to pass through dynamic component props
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component, portalProviderAPI, eventDispatcher, props, viewShouldUpdate) {
return (node, view, getPos) => new ReactNodeView(node, view, getPos, portalProviderAPI, eventDispatcher, props, component, viewShouldUpdate).init();
}
}