@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
330 lines (321 loc) • 16.5 kB
JavaScript
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();
};
}