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