UNPKG

@atlaskit/editor-common

Version:

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

225 lines (213 loc) • 8.96 kB
import _extends from "@babel/runtime/helpers/extends"; /** @jsx jsx */ import React from 'react'; import { jsx } from '@emotion/react'; import { ACTION_SUBJECT, ACTION_SUBJECT_ID } from '../analytics'; import { ErrorBoundary } from '../ui/ErrorBoundary'; import { browser } from '../utils'; import { analyticsEventKey, getPerformanceOptions, startMeasureReactNodeViewRendered, stopMeasureReactNodeViewRendered, ZERO_WIDTH_SPACE } from '../utils'; export const inlineNodeViewClassname = 'inlineNodeView'; function createNodeView({ nodeViewParams, pmPluginFactoryParams, Component, extraComponentProps }) { // We set a variable for the current node which is // used for comparisions when doing updates, before being // overwritten to the updated node. let currentNode = nodeViewParams.node; // First we setup the dom element which will be rendered and "tracked" by prosemirror // and also used as a "editor portal" (not react portal) target by the editor // portal provider api, for rendering the Component passed. let domRef = document.createElement('span'); domRef.contentEditable = 'false'; setDomAttrs(nodeViewParams.node, domRef); // @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 domRef.classList.add(`${nodeViewParams.node.type.name}View-content-wrap`, `${inlineNodeViewClassname}`); // This util is shared for tracking rendering, and the ErrorBoundary that // is setup to wrap the Component when rendering // NOTE: This is not a prosemirror dispatch function dispatchAnalyticsEvent(payload) { pmPluginFactoryParams.eventDispatcher.emit(analyticsEventKey, { payload }); } // This is called to render the Component into domRef which is inside the // prosemirror View. // Internally it uses the unstable_renderSubtreeIntoContainer api to render, // to the passed dom element (domRef) which means it is automatically // "cleaned up" when you do a "re render". function renderComponent() { pmPluginFactoryParams.portalProviderAPI.render(getPortalChildren({ dispatchAnalyticsEvent, currentNode, nodeViewParams, Component, extraComponentProps }), domRef, false, // node views should be rendered with intl context true); } const { samplingRate, slowThreshold, trackingEnabled } = getPerformanceOptions(nodeViewParams.view); trackingEnabled && startMeasureReactNodeViewRendered({ nodeTypeName: currentNode.type.name }); // We render the component while creating the node view renderComponent(); trackingEnabled && stopMeasureReactNodeViewRendered({ nodeTypeName: currentNode.type.name, dispatchAnalyticsEvent, samplingRate, slowThreshold }); // https://prosemirror.net/docs/ref/#view.NodeView const nodeView = { get dom() { return domRef; }, update(nextNode, _decorations) { // Let ProseMirror handle the update if node types are different. // This prevents an issue where it was not possible to select the // inline node view then replace it by entering text - the node // view contents would be deleted but the node view itself would // stay in the view and become uneditable. if (currentNode.type !== nextNode.type) { return false; } // On updates, we only set the new attributes if the type, attributes, and marks // have changed on the node. // NOTE: this could mean attrs changes aren't reflected in the dom, // when an attribute key which was previously present is no longer // present. // ie. // -> Original attributes { text: "hello world", color: "red" } // -> Updated attributes { color: "blue" } // in this case, the dom text attribute will not be cleared. // // This may not be an issue with any of our current node schemas. if (!currentNode.sameMarkup(nextNode)) { setDomAttrs(nextNode, domRef); } currentNode = nextNode; renderComponent(); return true; }, destroy() { // When prosemirror destroys the node view, we need to clean up // what we have previously rendered using the editor portal // provider api. pmPluginFactoryParams.portalProviderAPI.remove(domRef); // @ts-expect-error Expect an error as domRef is expected to be // of HTMLSpanElement type however once the node view has // been destroyed no other consumers should still be using it. domRef = undefined; } }; return nodeView; } /** * Copies the attributes from a ProseMirror Node to a DOM node. * @param node The Prosemirror Node from which to source the attributes */ function setDomAttrs(node, element) { Object.keys(node.attrs || {}).forEach(attr => { element.setAttribute(attr, node.attrs[attr]); }); } function getPortalChildren({ dispatchAnalyticsEvent, currentNode, nodeViewParams, Component, extraComponentProps }) { return function portalChildren() { var _currentNode$type$nam, _currentNode$type; // All inline nodes use `display: inline` to allow for multi-line // wrapping. This does produce an issue in Chrome where it is not // possible to click select the position after the node, // see: https://product-fabric.atlassian.net/browse/ED-12003 // however this is only a problem for node views that use // `display: inline-block` somewhere within the Component. // Looking at the below structure, spans with className // `inlineNodeViewAddZeroWidthSpace` have pseudo elements that // add a zero width space which fixes the problem. // Without the additional zero width space before the Component, // it is not possible to use the keyboard to range select in Safari. // // Zero width spaces on either side of the Component also prevent // the cursor from appearing inside the node view on all browsers. // // Note: // In future it is worth considering prohibiting the use of `display: inline-block` // within inline node view Components however would require a sizable // refactor. A test suite to catch any instances of this is ideal however // the refactor required is currently out of scope for https://product-fabric.atlassian.net/browse/ED-14176 return jsx(ErrorBoundary, { component: ACTION_SUBJECT.REACT_NODE_VIEW, componentId: (_currentNode$type$nam = currentNode === null || currentNode === void 0 ? void 0 : (_currentNode$type = currentNode.type) === null || _currentNode$type === void 0 ? void 0 : _currentNode$type.name) !== null && _currentNode$type$nam !== void 0 ? _currentNode$type$nam : ACTION_SUBJECT_ID.UNKNOWN_NODE, dispatchAnalyticsEvent: dispatchAnalyticsEvent }, jsx("span", { className: `zeroWidthSpaceContainer` }, jsx("span", { className: `${inlineNodeViewClassname}AddZeroWidthSpace` }), ZERO_WIDTH_SPACE), jsx(Component, _extends({ view: nodeViewParams.view // TODO: ED-13910 - Remove the boolean to fix the prosemirror view type , getPos: nodeViewParams.getPos, node: currentNode }, extraComponentProps)), browser.android ? jsx("span", { className: `zeroWidthSpaceContainer`, contentEditable: "false" }, jsx("span", { className: `${inlineNodeViewClassname}AddZeroWidthSpace` }), ZERO_WIDTH_SPACE) : jsx("span", { className: `${inlineNodeViewClassname}AddZeroWidthSpace` })); }; } // https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews // The prosemirror EditorProps has a nodeViews key which has the rough shape: // type nodeViews: { // [nodeViewName: string]: (node, editorView, getPos, decorations, innerDecorations) => NodeView // } // So the value of the keys on the nodeViews object, are a function which should return a NodeView. // The following type NodeViewProducer, refers to these functions which return a NodeView. // // So the above type could also be described as // type NodeViewProducer = (node, editorView, getPos, decorations, innerDecorations) => NodeView // nodeViews: { // [nodeViewName: string]: NodeViewProducer // } // This return of this function is intended to be the value of a key // in a ProseMirror nodeViews object. export function getInlineNodeViewProducer({ pmPluginFactoryParams, Component, extraComponentProps }) { function nodeViewProducer(...nodeViewProducerParameters) { const nodeView = createNodeView({ nodeViewParams: { node: nodeViewProducerParameters[0], view: nodeViewProducerParameters[1], getPos: nodeViewProducerParameters[2], decorations: nodeViewProducerParameters[3] }, pmPluginFactoryParams, Component, extraComponentProps }); return nodeView; } return nodeViewProducer; }