UNPKG

@atlaskit/editor-common

Version:

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

432 lines (409 loc) • 19.9 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.getInlineNodeViewProducer = getInlineNodeViewProducer; exports.inlineNodeViewClassname = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireDefault(require("react")); var _react2 = require("@emotion/react"); var _model = require("@atlaskit/editor-prosemirror/model"); var _platformFeatureFlags = require("@atlaskit/platform-feature-flags"); var _analytics = require("../analytics"); var _isSsr = require("../core-utils/is-ssr"); var _ErrorBoundary = require("../ui/ErrorBoundary"); var _utils = require("../utils"); var _browser = require("../utils/browser"); var _whitespace = require("../whitespace"); var _generateUniqueNodeKey = require("./generateUniqueNodeKey"); var _onVisibleObserverFactory = require("./onVisibleObserverFactory"); function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } /** * @jsxRuntime classic * @jsx jsx */ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766 var inlineNodeViewClassname = exports.inlineNodeViewClassname = 'inlineNodeView'; var canRenderFallback = function canRenderFallback(node) { return node.type.isInline && node.type.isAtom && node.type.isLeaf; }; // list of inline nodes with toDOM fallback implementations that can be virtualized. As // additional nodes are converted they should be added here var virtualizedNodeAllowlist = ['inlineCard']; function createNodeView(_ref) { var nodeViewParams = _ref.nodeViewParams, pmPluginFactoryParams = _ref.pmPluginFactoryParams, Component = _ref.Component, extraComponentProps = _ref.extraComponentProps, extraNodeViewProps = _ref.extraNodeViewProps; // We set a variable for the current node which is // used for comparisions when doing updates, before being // overwritten to the updated node. var currentNode = nodeViewParams.node; var key = (0, _generateUniqueNodeKey.generateUniqueNodeKey)(); // 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. var domRef = document.createElement('span'); domRef.contentEditable = 'false'; setDomAttrs(nodeViewParams.node, domRef); var fallbackRef = { current: null }; // @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("".concat(nodeViewParams.node.type.name, "View-content-wrap"), "".concat(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(_utils.analyticsEventKey, { payload: 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: dispatchAnalyticsEvent, currentNode: currentNode, nodeViewParams: nodeViewParams, Component: Component, extraComponentProps: extraComponentProps }), domRef, key); } var _getPerformanceOption = (0, _utils.getPerformanceOptions)(nodeViewParams.view), samplingRate = _getPerformanceOption.samplingRate, slowThreshold = _getPerformanceOption.slowThreshold, trackingEnabled = _getPerformanceOption.trackingEnabled; trackingEnabled && (0, _utils.startMeasureReactNodeViewRendered)({ nodeTypeName: currentNode.type.name }); // We render the component while creating the node view renderComponent(); trackingEnabled && (0, _utils.stopMeasureReactNodeViewRendered)({ nodeTypeName: currentNode.type.name, dispatchAnalyticsEvent: dispatchAnalyticsEvent, samplingRate: samplingRate, slowThreshold: slowThreshold }); var extraNodeViewPropsWithStopEvent = _objectSpread({}, extraNodeViewProps); // https://prosemirror.net/docs/ref/#view.NodeView var nodeView = _objectSpread({ get dom() { return domRef; }, update: function 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: function 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(key); // @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; fallbackRef.current = null; } }, extraNodeViewPropsWithStopEvent); return nodeView; } function createNodeViewVirtualized(_ref2) { var nodeViewParams = _ref2.nodeViewParams, pmPluginFactoryParams = _ref2.pmPluginFactoryParams, Component = _ref2.Component, extraComponentProps = _ref2.extraComponentProps, extraNodeViewProps = _ref2.extraNodeViewProps; // We set a variable for the current node which is // used for comparisions when doing updates, before being // overwritten to the updated node. var currentNode = nodeViewParams.node; var key = (0, _generateUniqueNodeKey.generateUniqueNodeKey)(); // 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. var domRef = document.createElement('span'); domRef.contentEditable = 'false'; setDomAttrs(nodeViewParams.node, domRef); var fallbackRef = { current: null }; // @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("".concat(nodeViewParams.node.type.name, "View-content-wrap"), "".concat(inlineNodeViewClassname)); function onBeforeReactDomRender() { if (!fallbackRef.current) { return; } domRef.removeChild(fallbackRef.current); fallbackRef.current = null; } // 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(_utils.analyticsEventKey, { payload: 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: dispatchAnalyticsEvent, currentNode: currentNode, nodeViewParams: nodeViewParams, Component: Component, extraComponentProps: extraComponentProps }), domRef, key, onBeforeReactDomRender); } var didRenderComponentWithIntersectionObserver = false; var destroyed = false; var removeIntersectionObserver = function removeIntersectionObserver() {}; function renderFallback() { var _currentNode$type; if (!canRenderFallback(currentNode) || typeof ((_currentNode$type = currentNode.type) === null || _currentNode$type === void 0 || (_currentNode$type = _currentNode$type.spec) === null || _currentNode$type === void 0 ? void 0 : _currentNode$type.toDOM) !== 'function') { return; } var fallback = _model.DOMSerializer.renderSpec(document, currentNode.type.spec.toDOM(currentNode)); var dom = fallback.dom; fallbackRef.current = dom; domRef.replaceChildren(dom); } function attachNodeViewObserver() { var observer = (0, _onVisibleObserverFactory.getOrCreateOnVisibleObserver)(nodeViewParams.view); if (domRef) { removeIntersectionObserver = observer.observe(domRef, function () { if (!didRenderComponentWithIntersectionObserver && !destroyed) { renderComponent(); didRenderComponentWithIntersectionObserver = true; } }); } } renderFallback(); // allow the fallback to render first before attaching the observer. // Will tweak this in a follow up PR to optimise rendering of visible // nodes without fallback rendering. setTimeout(function () { attachNodeViewObserver(); }, 0); var extraNodeViewPropsWithStopEvent = _objectSpread(_objectSpread({}, extraNodeViewProps), {}, { stopEvent: function stopEvent(event) { var maybeStopEvent = extraNodeViewProps === null || extraNodeViewProps === void 0 ? void 0 : extraNodeViewProps.stopEvent; if (typeof maybeStopEvent === 'function') { return maybeStopEvent(event); } return false; } }); // https://prosemirror.net/docs/ref/#view.NodeView var nodeView = _objectSpread({ get dom() { return domRef; }, update: function 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; if (didRenderComponentWithIntersectionObserver) { renderComponent(); } return true; }, destroy: function destroy() { removeIntersectionObserver(); destroyed = true; // 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(key); // @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; fallbackRef.current = null; } }, extraNodeViewPropsWithStopEvent); 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(function (attr) { element.setAttribute(attr, node.attrs[attr]); }); } function getPortalChildren(_ref3) { var dispatchAnalyticsEvent = _ref3.dispatchAnalyticsEvent, currentNode = _ref3.currentNode, nodeViewParams = _ref3.nodeViewParams, Component = _ref3.Component, extraComponentProps = _ref3.extraComponentProps; return function portalChildren() { var _currentNode$type$nam, _currentNode$type2; // 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 (0, _react2.jsx)(_ErrorBoundary.ErrorBoundary, { component: _analytics.ACTION_SUBJECT.REACT_NODE_VIEW, componentId: (_currentNode$type$nam = currentNode === null || currentNode === void 0 || (_currentNode$type2 = currentNode.type) === null || _currentNode$type2 === void 0 ? void 0 : _currentNode$type2.name) !== null && _currentNode$type$nam !== void 0 ? _currentNode$type$nam : _analytics.ACTION_SUBJECT_ID.UNKNOWN_NODE, dispatchAnalyticsEvent: dispatchAnalyticsEvent }, (0, _react2.jsx)("span", { className: "zeroWidthSpaceContainer" }, (0, _react2.jsx)("span", { className: "".concat(inlineNodeViewClassname, "AddZeroWidthSpace") }), _whitespace.ZERO_WIDTH_SPACE), (0, _react2.jsx)(Component, (0, _extends2.default)({ view: nodeViewParams.view // TODO: ED-13910 - Remove the boolean to fix the prosemirror view type // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any , getPos: nodeViewParams.getPos, node: currentNode // eslint-disable-next-line react/jsx-props-no-spreading -- Spreading props to pass through dynamic component props }, extraComponentProps)), (0, _browser.getBrowserInfo)().android ? // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 (0, _react2.jsx)("span", { className: "zeroWidthSpaceContainer", contentEditable: "false" }, (0, _react2.jsx)("span", { className: "".concat(inlineNodeViewClassname, "AddZeroWidthSpace") }), _whitespace.ZERO_WIDTH_SPACE) : // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 (0, _react2.jsx)("span", { className: "".concat(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 // } var counterPerEditorViewMap = new WeakMap(); // This return of this function is intended to be the value of a key // in a ProseMirror nodeViews object. function getInlineNodeViewProducer(_ref4) { var pmPluginFactoryParams = _ref4.pmPluginFactoryParams, Component = _ref4.Component, extraComponentProps = _ref4.extraComponentProps, extraNodeViewProps = _ref4.extraNodeViewProps; function nodeViewProducer() { var _node$type; var view = arguments.length <= 1 ? undefined : arguments[1]; var node = arguments.length <= 0 ? undefined : arguments[0]; var parameters = { nodeViewParams: { node: node, view: view, getPos: arguments.length <= 2 ? undefined : arguments[2], decorations: arguments.length <= 3 ? undefined : arguments[3] }, pmPluginFactoryParams: pmPluginFactoryParams, Component: Component, extraComponentProps: extraComponentProps, extraNodeViewProps: extraNodeViewProps }; var isNodeTypeAllowedToBeVirtualized = virtualizedNodeAllowlist.includes((node === null || node === void 0 || (_node$type = node.type) === null || _node$type === void 0 ? void 0 : _node$type.name) || ''); if (!isNodeTypeAllowedToBeVirtualized || (0, _isSsr.isSSR)()) { return createNodeView(parameters); } if ((0, _platformFeatureFlags.fg)('platform_editor_inline_node_virt_threshold_override')) { return createNodeViewVirtualized(parameters); } var inlineNodeViewsVirtualizationCounter = counterPerEditorViewMap.get(view) || 0; inlineNodeViewsVirtualizationCounter += 1; counterPerEditorViewMap.set(view, inlineNodeViewsVirtualizationCounter); // We never virtualize the 100th first elements if (inlineNodeViewsVirtualizationCounter <= 100) { return createNodeView(parameters); } return createNodeViewVirtualized(parameters); } return nodeViewProducer; }