UNPKG

@atlaskit/editor-common

Version:

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

248 lines (234 loc) • 10.1 kB
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 { ACTION_SUBJECT, ACTION_SUBJECT_ID } from '../analytics'; 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 default class ReactNodeView { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/max-params constructor(_node, view, getPos, portalProviderAPI, eventDispatcher, reactComponentProps, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any reactComponent, viewShouldUpdate) { // 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 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() { 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 }); this.renderReactComponent(() => this.render(this.reactComponentProps, this.handleRef)); trackingEnabled && stopMeasureReactNodeViewRendered({ nodeTypeName: this.node.type.name, dispatchAnalyticsEvent: this.dispatchAnalyticsEvent, samplingRate, slowThreshold }); return this; } // 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()); }; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.portalProviderAPI.render(componentWithErrorBoundary, this.domRef, this.key); } 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 // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any let oldIgnoreMutation; let selectionBookmark; let parentOffset = 0; let mutationsIgnored = false; // move the contentDOM node inside the inner reference after rendering if (node && contentDOM && !node.contains(contentDOM)) { var _this$view$state$sele; // @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(); } // ... and capture parent offset before DOM change if (((_this$view$state$sele = this.view.state.selection) === null || _this$view$state$sele === void 0 ? void 0 : _this$view$state$sele.ranges.length) > 0) { var _this$view$state$sele2, _this$view$state$sele3, _this$view$state$sele4; parentOffset = (_this$view$state$sele2 = (_this$view$state$sele3 = this.view.state.selection) === null || _this$view$state$sele3 === void 0 ? void 0 : (_this$view$state$sele4 = _this$view$state$sele3.ranges[0].$from) === null || _this$view$state$sele4 === void 0 ? void 0 : _this$view$state$sele4.parentOffset) !== null && _this$view$state$sele2 !== void 0 ? _this$view$state$sele2 : 0; } node.appendChild(contentDOM); requestAnimationFrame(() => { // Reset ignoreMutation after frame .... // @ts-ignore - this may not have been declared by implementing class this.ignoreMutation = oldIgnoreMutation; // only trigger selection re-sync if the selection is not at the start of the // node, and DOM mutations were ignored if (selectionBookmark && mutationsIgnored && parentOffset > 0) { // ... and dispatch expected selection state this.view.dispatch(this.view.state.tr.setSelection(selectionBookmark.resolve(this.view.state.tr.doc))); } }); } } // 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 // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading }, props)) : null; } // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/max-params 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() { // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting return this.domRef; } destroy() { if (!this.domRef) { return; } this.portalProviderAPI.remove(this.key); this.domRef = undefined; this.contentDOM = undefined; } // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/max-params static fromComponent( // 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(); } }