UNPKG

@atlaskit/editor-common

Version:

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

330 lines (321 loc) 16.5 kB
import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; import _createClass from "@babel/runtime/helpers/createClass"; import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn"; import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf"; import _get from "@babel/runtime/helpers/get"; import _inherits from "@babel/runtime/helpers/inherits"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); } function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); } function _superPropGet(t, o, e, r) { var p = _get(_getPrototypeOf(1 & r ? t.prototype : t), o, e); return 2 & r && "function" == typeof p ? function (t) { return p.apply(e, t); } : p; } import React from 'react'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { isSSR } from '../core-utils'; import ReactNodeView from '../react-node-view'; import { Extension } from './Extension'; import { ExtensionNodeWrapper } from './ExtensionNodeWrapper'; /** * Allowlists of extension keys + layout * Currently, only toc with default layout allows to skip React render. * Extensions NOT in this list always follow the normal React render path. */ var ssrHydrationExtensionAllowlist = ['toc']; var ssrHydrationLayoutAllowlist = ['default']; var isSSRHydrationEligible = function isSSRHydrationEligible(node) { var _node$attrs; if (node.type.name !== 'extension') { return false; } var _ref = (_node$attrs = node.attrs) !== null && _node$attrs !== void 0 ? _node$attrs : {}, extensionKey = _ref.extensionKey, layout = _ref.layout; if (!ssrHydrationExtensionAllowlist.includes(extensionKey)) { return false; } // Treat a missing layout attr as `default` (the schema default). var effectiveLayout = layout !== null && layout !== void 0 ? layout : 'default'; return ssrHydrationLayoutAllowlist.includes(effectiveLayout); }; /** * Per-(EditorView, extensionKey + localId) record of which extension identities * have already had their initial-hydration init pass. SSR DOM reuse is only valid * the very first time an ExtensionNode init runs for a given identity in a given * editor — that is the only moment a real SSR-rendered element for that identity * can exist in the editor DOM. * * After the first init, any element matching the SSR selector is the previous * node view's React-rendered domRef that ProseMirror has not yet detached (e.g. * during DnD / layout resize, where ProseMirror constructs the new node view * BEFORE destroying the old one). Reusing it as if it were SSR DOM causes the * new node view to skip React rendering and leaves the extension invisible * (EDITOR-6613). */ var consumedHydrationIdentitiesByEditor = new WeakMap(); var _getHydrationIdentityKey = function getHydrationIdentityKey(extensionKey, localId) { if (typeof extensionKey !== 'string' || typeof localId !== 'string') { return null; } if (extensionKey === '' || localId === '') { return null; } return "".concat(extensionKey, "::").concat(localId); }; var hasHydrationIdentityBeenConsumed = function hasHydrationIdentityBeenConsumed(view, identityKey) { var consumed = consumedHydrationIdentitiesByEditor.get(view); return consumed ? consumed.has(identityKey) : false; }; var markHydrationIdentityAsConsumed = function markHydrationIdentityAsConsumed(view, identityKey) { var consumed = consumedHydrationIdentitiesByEditor.get(view); if (!consumed) { consumed = new Set(); consumedHydrationIdentitiesByEditor.set(view, consumed); } consumed.add(identityKey); }; // getInlineNodeViewProducer is a new api to use instead of ReactNodeView // when creating inline node views, however, it is difficult to test the impact // on selections when migrating inlineExtension to use the new api. // The ReactNodeView api will be visited in the second phase of the selections // project whilst investigating block nodes. We will revisit the Extension node view there too. export var ExtensionNode = /*#__PURE__*/function (_ReactNodeView) { function ExtensionNode() { var _this; _classCallCheck(this, ExtensionNode); for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } _this = _callSuper(this, ExtensionNode, [].concat(args)); /** True between SSR DOM adoption in `createDomRef` and the SSR→React handoff in `update`. */ _defineProperty(_this, "didReuseSsrDom", false); return _this; } _inherits(ExtensionNode, _ReactNodeView); return _createClass(ExtensionNode, [{ key: "ignoreMutation", value: function ignoreMutation(mutation) { // Extensions can perform async operations that will change the DOM. // To avoid having their tree rebuilt, we need to ignore the mutation // for atom based extensions if its not a layout, we need to give // children a chance to recalc return this.node.type.isAtom || mutation.type !== 'selection' && mutation.attributeName !== 'data-layout'; } /** See {@link consumedHydrationIdentitiesByEditor}. Null when attrs are missing → SSR reuse skipped. */ }, { key: "getHydrationIdentityKey", value: function getHydrationIdentityKey() { var _this$node$attrs, _this$node$attrs2; return _getHydrationIdentityKey((_this$node$attrs = this.node.attrs) === null || _this$node$attrs === void 0 ? void 0 : _this$node$attrs.extensionKey, (_this$node$attrs2 = this.node.attrs) === null || _this$node$attrs2 === void 0 ? void 0 : _this$node$attrs2.localId); } /** True only for the first ExtensionNode of this identity in this editor. See {@link consumedHydrationIdentitiesByEditor}. */ }, { key: "isInInitialHydrationWindow", value: function isInInitialHydrationWindow() { var identityKey = this.getHydrationIdentityKey(); if (identityKey === null) { return false; } return !hasHydrationIdentityBeenConsumed(this.view, identityKey); } // Reserve height by setting a minimum height for the extension node view element }, { key: "createDomRef", value: function createDomRef() { if (!fg('confluence_connect_macro_preset_height')) { // SSR DOM reuse — see {@link consumedHydrationIdentitiesByEditor}. if (!isSSR() && isSSRHydrationEligible(this.node) && this.isInInitialHydrationWindow() && expValEquals('platform_editor_hydration_skip_react_portal', 'isEnabled', true)) { var ssrElement = this.findSSRElement(); if (ssrElement) { this.didReuseSsrDom = true; return ssrElement; } } return _superPropGet(ExtensionNode, "createDomRef", this, 3)([]); } if (!this.node.isInline) { var _this$reactComponentP, _this$reactComponentP2; var _htmlElement = document.createElement('div'); var extensionHeight = (_this$reactComponentP = this.reactComponentProps) === null || _this$reactComponentP === void 0 || (_this$reactComponentP = _this$reactComponentP.extensionNodeViewOptions) === null || _this$reactComponentP === void 0 || (_this$reactComponentP2 = _this$reactComponentP.getExtensionHeight) === null || _this$reactComponentP2 === void 0 ? void 0 : _this$reactComponentP2.call(_this$reactComponentP, this.node); if (extensionHeight) { _htmlElement.style.setProperty('min-height', "".concat(extensionHeight, "px")); } return _htmlElement; } var htmlElement = document.createElement('span'); return htmlElement; } /** * Cache for SSR element lookup to avoid repeated DOM queries. * undefined = not yet searched, null = searched but not found, HTMLElement = found */ }, { key: "findSSRElement", value: /** * Attempts to find an existing SSR'd DOM element for this extension node by extensionKey and localId * which should uniquely identify the * extension node within the editor content. * * @returns The SSR'd element if found, otherwise null */ function findSSRElement() { if (this.cachedSsrElement !== undefined) { return this.cachedSsrElement || null; } var extensionKey = this.node.attrs.extensionKey; var localId = this.node.attrs.localId; var editorDom = this.view.dom; if (extensionKey && localId) { var selector = "[extensionkey=\"".concat(extensionKey, "\"][localid=\"").concat(localId, "\"]"); var element = editorDom.querySelector(selector); if (element && element instanceof HTMLElement) { this.cachedSsrElement = element; return element; } } this.cachedSsrElement = null; return null; } /** Skip React Portal render on first init when reusing SSR DOM. See {@link consumedHydrationIdentitiesByEditor}. */ }, { key: "init", value: function init() { if (!expValEquals('platform_editor_hydration_skip_react_portal', 'isEnabled', true)) { _superPropGet(ExtensionNode, "init", this, 3)([]); } else { var isEligibleForSsrReuse = !isSSR() && isSSRHydrationEligible(this.node); if (isEligibleForSsrReuse && this.isInInitialHydrationWindow()) { var ssrElement = this.findSSRElement(); var shouldSkipInitRender = ssrElement !== null; _superPropGet(ExtensionNode, "init", this, 3)([shouldSkipInitRender]); var identityKey = this.getHydrationIdentityKey(); if (identityKey !== null) { // Close the hydration window — see {@link consumedHydrationIdentitiesByEditor}. markHydrationIdentityAsConsumed(this.view, identityKey); } } else { _superPropGet(ExtensionNode, "init", this, 3)([]); } } return this; } }, { key: "update", value: function update(node, decorations, _innerDecorations) { var validUpdate = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : function () { return true; }; // Remove extensionNodeWrapper aka span.relative if we previously reused SSR DOM // control is back to React afterwards if (this.didReuseSsrDom && expValEquals('platform_editor_hydration_skip_react_portal', 'isEnabled', true)) { var ssrElement = this.findSSRElement(); if (ssrElement) { var extensionNodeWrapper = ssrElement.querySelector('[data-testid="extension-node-wrapper"]'); if (extensionNodeWrapper) { extensionNodeWrapper.remove(); } this.didReuseSsrDom = false; } } return _superPropGet(ExtensionNode, "update", this, 3)([node, decorations, _innerDecorations, validUpdate]); } /** * When interacting with input elements inside an extension's body, the events * bubble up to the editor and get handled by it. This almost always gets in the way * of being able to actually interact with said input in the extension, i.e. * typing inside a text field (in an extension body) will print the text in the editor * content area instead. This change prevents the editor view from trying to handle these events, * when the target of the event is an input element, so the extension can. */ }, { key: "stopEvent", value: function stopEvent(event) { if (fg('forge-ui-extensionnodeview-stop-event-for-textarea')) { return event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement; } return event.target instanceof HTMLInputElement; } }, { key: "getContentDOM", value: function getContentDOM() { var _this$reactComponentP3; if (this.node.isInline) { return; } if (this.didReuseSsrDom) { return; } var contentDomWrapper = document.createElement('div'); contentDomWrapper.className = "".concat(this.node.type.name, "-content-dom-wrapper"); var isBodiedExtension = this.node.type.name === 'bodiedExtension'; var showMacroInteractionDesignUpdates = (_this$reactComponentP3 = this.reactComponentProps) === null || _this$reactComponentP3 === void 0 || (_this$reactComponentP3 = _this$reactComponentP3.macroInteractionDesignFeatureFlags) === null || _this$reactComponentP3 === void 0 ? void 0 : _this$reactComponentP3.showMacroInteractionDesignUpdates; if (isBodiedExtension && showMacroInteractionDesignUpdates && expValEquals('platform_editor_bodiedextension_layoutshift_fix', 'isEnabled', true)) { // Create outer wrapper to hold space for the lozenge var outerWrapper = document.createElement('div'); outerWrapper.className = "".concat(this.node.type.name, "-content-outer-wrapper"); // Create inner wrapper to position inner content var innerWrapper = document.createElement('div'); innerWrapper.className = "".concat(this.node.type.name, "-content-inner-wrapper"); innerWrapper.appendChild(contentDomWrapper); outerWrapper.appendChild(innerWrapper); return { dom: outerWrapper, contentDOM: contentDomWrapper }; } return { dom: contentDomWrapper }; } }, { key: "render", value: function render(props, forwardRef) { var _props$extensionNodeV; // While sitting on SSR DOM, skip the React portal — see {@link didReuseSsrDom}. if (this.didReuseSsrDom) { return null; } return /*#__PURE__*/React.createElement(ExtensionNodeWrapper, { intl: props.intl, nodeType: this.node.type.name, macroInteractionDesignFeatureFlags: props.macroInteractionDesignFeatureFlags }, /*#__PURE__*/React.createElement(Extension, { editorView: this.view, node: this.node, eventDispatcher: this.eventDispatcher // The getPos arg is always a function when used with nodes // the version of the types we use has a union with the type // for marks. // This has been fixed in later versions of the definitly typed // types (and also in prosmirror-views inbuilt types). // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/57384 , getPos: this.getPos, providerFactory: props.providerFactory, handleContentDOMRef: forwardRef, extensionHandlers: props.extensionHandlers, editorAppearance: (_props$extensionNodeV = props.extensionNodeViewOptions) === null || _props$extensionNodeV === void 0 ? void 0 : _props$extensionNodeV.appearance, pluginInjectionApi: props.pluginInjectionApi, macroInteractionDesignFeatureFlags: props.macroInteractionDesignFeatureFlags, showLivePagesBodiedMacrosRendererView: props.showLivePagesBodiedMacrosRendererView, showUpdatedLivePages1PBodiedExtensionUI: props.showUpdatedLivePages1PBodiedExtensionUI, rendererExtensionHandlers: props.rendererExtensionHandlers })); } }]); }(ReactNodeView); export default function ExtensionNodeView(portalProviderAPI, eventDispatcher, providerFactory, extensionHandlers, extensionNodeViewOptions, pluginInjectionApi, macroInteractionDesignFeatureFlags, showLivePagesBodiedMacrosRendererView, showUpdatedLivePages1PBodiedExtensionUI, rendererExtensionHandlers, intl) { return function (node, view, getPos) { return new ExtensionNode(node, view, getPos, portalProviderAPI, eventDispatcher, { providerFactory: providerFactory, extensionHandlers: extensionHandlers, extensionNodeViewOptions: extensionNodeViewOptions, pluginInjectionApi: pluginInjectionApi, macroInteractionDesignFeatureFlags: macroInteractionDesignFeatureFlags, showLivePagesBodiedMacrosRendererView: showLivePagesBodiedMacrosRendererView, showUpdatedLivePages1PBodiedExtensionUI: showUpdatedLivePages1PBodiedExtensionUI, rendererExtensionHandlers: rendererExtensionHandlers, intl: intl }).init(); }; }