@atlaskit/editor-plugin-breakout
Version:
Breakout plugin for @atlaskit/editor-core
371 lines (358 loc) • 18.9 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.resizeHandleMessage = exports.createPragmaticResizer = void 0;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _react = _interopRequireWildcard(require("react"));
var _bindEventListener = require("bind-event-listener");
var _messages = require("@atlaskit/editor-common/messages");
var _vanillaTooltip = require("@atlaskit/editor-common/vanilla-tooltip");
var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
var _elementAdapter = require("@atlaskit/pragmatic-drag-and-drop/adapter/element-adapter");
var _disableNativeDragPreview = require("@atlaskit/pragmatic-drag-and-drop/utils/disable-native-drag-preview");
var _preventUnhandled = require("@atlaskit/pragmatic-drag-and-drop/utils/prevent-unhandled");
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
var _Tooltip = _interopRequireDefault(require("@atlaskit/tooltip/Tooltip"));
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
var getNodeName = function getNodeName(nodeName) {
if (nodeName === 'layoutSection') {
return 'layout';
} else if (nodeName === 'codeBlock' || nodeName === 'expand') {
return nodeName;
} else if ((nodeName === 'rule' || nodeName === 'panel' || nodeName === 'panel_c1') && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_lovability_resize_dividers_panels', 'isEnabled', true)) {
return nodeName;
} else if ((nodeName === 'extension' || nodeName === 'bodiedExtension' || nodeName === 'multiBodiedExtension') && (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_lovability_resize_extensions')) {
return nodeName;
} else {
return 'node';
}
};
/**
* Read by `.pm-breakout-resize-tooltip-anchor`, declared in `editor-core`'s `resizerStyles.ts` and
* its Compiled mirror. Rename in all three or the tooltip pins to the top of the rail.
*/
var tooltipAnchorTopVar = '--pm-breakout-resize-tooltip-anchor-top';
var resizeHandleMessage = exports.resizeHandleMessage = {
expand: _messages.breakoutMessages.resizeExpand,
codeBlock: _messages.breakoutMessages.resizeCodeBlock,
layout: _messages.breakoutMessages.resizeLayout,
rule: _messages.breakoutMessages.resizeRule,
panel: _messages.breakoutMessages.resizePanel,
panel_c1: _messages.breakoutMessages.resizePanel,
node: _messages.breakoutMessages.resizeElement,
extension: _messages.breakoutMessages.resizeExtension,
bodiedExtension: _messages.breakoutMessages.resizeExtension,
multiBodiedExtension: _messages.breakoutMessages.resizeExtension
};
/**
* Remove during `platform_editor_use_vanilla_components` cleanup — replaced by
* `createHandleTooltip`, which does the same job without a React tree per resize handle.
*/
var RailWithTooltip = function RailWithTooltip(_ref) {
var rail = _ref.rail,
target = _ref.target,
intl = _ref.intl;
var _useState = (0, _react.useState)('node'),
_useState2 = (0, _slicedToArray2.default)(_useState, 2),
nodeName = _useState2[0],
setNodeName = _useState2[1];
(0, _react.useLayoutEffect)(function () {
var node = target.querySelector('[data-prosemirror-node-name]');
var name = getNodeName(node === null || node === void 0 ? void 0 : node.dataset.prosemirrorNodeName);
setNodeName(name);
}, [target]);
return /*#__PURE__*/_react.default.createElement(_Tooltip.default, {
content: intl.formatMessage(resizeHandleMessage[nodeName]),
position: "mouse"
}, /*#__PURE__*/_react.default.createElement("div", {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
className: "pm-breakout-resize-handle-rail-inside-tooltip",
ref: function ref(el) {
if (el && rail.parentNode !== el) {
el.appendChild(rail);
}
}
}));
};
/** Identifies the vanilla tooltip for tests and debugging. */
var TOOLTIP_CLASS_NAME = 'pm-breakout-resize-tooltip';
/**
* Classes applied to the tooltip element: the shared default look, plus our own hook. The default
* still reaches a `[popover]` because the tooltip is painted in the top layer but stays in the DOM
* under `.ProseMirror`.
*/
var TOOLTIP_CLASS_NAMES = "".concat(_vanillaTooltip.VANILLA_TOOLTIP_DEFAULT_CLASS, " ").concat(TOOLTIP_CLASS_NAME);
/**
* Vanilla DOM equivalent of `RailWithTooltip`, behind `platform_editor_use_vanilla_components`.
*
* Anchored to a point rather than to the rail, which spans the node's full height and would strand
* the tooltip at the top of a tall node. This reproduces the control arm's `position="mouse"`,
* which latches the cursor as the tooltip opens and does not move it afterwards — so no cursor
* tracking is needed, just one style write per hover.
*/
var createHandleTooltip = function createHandleTooltip(_ref2) {
var anchor = _ref2.anchor,
intl = _ref2.intl,
target = _ref2.target;
// Only correct once ProseMirror has populated `contentDOM`, so callers must build on first
// hover — earlier reads fall back to the generic 'node' label.
var node = target.querySelector('[data-prosemirror-node-name]');
var nodeName = getNodeName(node === null || node === void 0 ? void 0 : node.dataset.prosemirrorNodeName);
return new _vanillaTooltip.VanillaTooltip(anchor, intl.formatMessage(resizeHandleMessage[nodeName]),
// Let the tooltip generate its own id.
undefined, TOOLTIP_CLASS_NAMES,
// Default delay, matching the ADS tooltip.
300,
// No inline styles
undefined,
// No `onShow`.
undefined,
// Below the cursor, matching the control arm's `position="mouse"`.
'bottom');
};
var createPragmaticResizer = exports.createPragmaticResizer = function createPragmaticResizer(_ref3) {
var target = _ref3.target,
_onDragStart = _ref3.onDragStart,
onDrag = _ref3.onDrag,
_onDrop = _ref3.onDrop,
intl = _ref3.intl,
nodeViewPortalProviderAPI = _ref3.nodeViewPortalProviderAPI;
var state = 'default';
var isLeftResizeHandleDisabled = (0, _expValEquals.expValEquals)('platform_editor_lovability_resize_dividers_panels', 'isEnabled', true) || (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_lovability_resize_extensions') || (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_remove_left_resize_handle');
var createHandle = function createHandle(side) {
var handle = document.createElement('div');
handle.contentEditable = 'false';
handle.classList.add('pm-breakout-resize-handle-container');
handle.style.gridColumn = side === 'left' ? '1' : '3';
var rail = document.createElement('div');
rail.classList.add('pm-breakout-resize-handle-rail');
if (side === 'left') {
handle.classList.add('pm-breakout-resize-handle-container--left');
handle.setAttribute('data-testid', 'pragmatic-resizer-handle-left');
} else {
handle.classList.add('pm-breakout-resize-handle-container--right');
handle.setAttribute('data-testid', 'pragmatic-resizer-handle-right');
}
var handleHitBox = document.createElement('div');
handleHitBox.classList.add('pm-breakout-resize-handle-hit-box');
var thumb = document.createElement('div');
thumb.classList.add('pm-breakout-resize-handle-thumb');
thumb.style.pointerEvents = 'none';
rail.appendChild(thumb);
// Marks the point the pointer entered the rail, and anchors the tooltip there: zero height
// so it is a point not a box, full rail width so the tooltip centres like the control arm.
//
// Must stay a child of `rail` — `openTooltipAt` sets `top` from the rail's `offsetY`, which
// only lines up while the rail is the containing block. jsdom cannot catch a wrong parent.
//
// Not built on the control arm, which must keep byte-identical DOM.
var tooltipAnchor;
if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_use_vanilla_components')) {
// Positioned by `.pm-breakout-resize-tooltip-anchor`; only the pointer's Y is passed in.
tooltipAnchor = document.createElement('div');
tooltipAnchor.classList.add('pm-breakout-resize-tooltip-anchor');
rail.appendChild(tooltipAnchor);
}
var tooltipContainer = document.createElement('div');
tooltipContainer.classList.add('pm-breakout-resize-handle-rail-wrapper');
handle.appendChild(tooltipContainer);
handle.appendChild(handleHitBox);
var key = crypto.randomUUID();
var renderTooltip = function renderTooltip() {
return nodeViewPortalProviderAPI.render(function () {
return /*#__PURE__*/_react.default.createElement(RailWithTooltip, {
rail: rail,
target: target,
intl: intl
});
}, tooltipContainer, key);
};
var vanillaTooltip;
// Tracks whether there is anything to tear down: the tooltip mounts on first hover and so
// may never mount at all.
var isTooltipMounted = false;
var mountTooltip = function mountTooltip() {
if (isTooltipMounted) {
return;
}
isTooltipMounted = true;
// `tooltipAnchor` is always set when the experiment is on; the check also narrows it.
if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_use_vanilla_components') && tooltipAnchor) {
vanillaTooltip = createHandleTooltip({
anchor: tooltipAnchor,
target: target,
intl: intl
});
return;
}
// `RailWithTooltip`'s ref moves `rail` into the tooltip wrapper, so the DOM ends up
// identical to the pre-mount state below.
renderTooltip();
};
// React 18 registers ~130 native event listeners on every portal container (see
// `listenToAllSupportedEvents`), and never removes them. Mounting this tooltip up front
// costs that for every resizable node in the document — two containers per node when the
// left handle is enabled — even though only the hovered handle can ever show a tooltip.
//
// Instead, park `rail` directly in the wrapper and mount the tooltip on the first
// `mouseenter` of the node (see `registerEvents` below). The rail has `opacity: 0` until
// the pointer is inside the node, so the pre-mount DOM is never visible, and the
// `mouseenter` fires on the whole node — long before the pointer reaches the 7px handle.
//
// Parking the rail also needs `width: 100%`: without the ADS tooltip's wrappers (stretched
// by `pragmaticResizerStylesForTooltip`) it would size to its content as a flex item.
rail.style.width = '100%';
tooltipContainer.appendChild(rail);
return {
handle: handle,
rail: rail,
// Exposed so the rail's hover listeners can position and open the tooltip.
tooltipAnchor: tooltipAnchor,
handleHitBox: handleHitBox,
mountTooltip: mountTooltip,
destroyTooltip: function destroyTooltip() {
// Re-read is safe: the gate is fixed for the page, so it matches what `mountTooltip`
// saw. At cleanup, keep this body and delete the control arm below.
if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_use_vanilla_components')) {
// May never have mounted, and there is no portal to remove either way.
if (vanillaTooltip) {
vanillaTooltip.destroy();
vanillaTooltip = undefined;
}
isTooltipMounted = false;
return;
}
// The tooltip mounts on first hover, so `destroyTooltip` can be reached without
// anything having been mounted.
if (!isTooltipMounted) {
return;
}
nodeViewPortalProviderAPI.remove(key);
isTooltipMounted = false;
}
};
};
var rightHandle = createHandle('right');
// Remove const leftHandle during 'platform_editor_lovability_resize_dividers_panels' cleanup
var leftHandle = isLeftResizeHandleDisabled ? undefined : createHandle('left');
var registerHandle = function registerHandle(handleElement, handleSide) {
return (0, _elementAdapter.draggable)({
element: handleElement,
onGenerateDragPreview: function onGenerateDragPreview(_ref4) {
var nativeSetDragImage = _ref4.nativeSetDragImage;
(0, _disableNativeDragPreview.disableNativeDragPreview)({
nativeSetDragImage: nativeSetDragImage
});
_preventUnhandled.preventUnhandled.start();
},
getInitialData: function getInitialData() {
return {
handleSide: handleSide
};
},
onDragStart: function onDragStart(args) {
state = 'resizing';
handleElement.classList.add('pm-breakout-resize-handle-container--active');
// The ADS tooltip hides itself on press; the vanilla one has no pointer-press
// listener, and its anchor moves with the rail, so it would follow the drag.
// Tearing it down hides it immediately; the next hover rebuilds it.
if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_use_vanilla_components')) {
rightHandle.destroyTooltip();
leftHandle === null || leftHandle === void 0 || leftHandle.destroyTooltip();
}
_onDragStart(args);
},
onDrag: onDrag,
onDrop: function onDrop(args) {
_preventUnhandled.preventUnhandled.stop();
state = 'default';
handleElement.classList.remove('pm-breakout-resize-handle-container--active');
_onDrop(args);
}
});
};
/**
* Parks the anchor where the pointer entered the rail, then opens the tooltip. The anchor is
* `pointer-events: none`, so the rail's hover listeners drive it. `offsetY` is already in the
* anchor's coordinate space, so no layout read is needed.
*
* Positions on enter only, matching the ADS tooltip as observed — though `Tooltip` does bind an
* `onMouseMove`, so start here if the two arms disagree on placement.
*/
var openTooltipAt = function openTooltipAt(anchor, event) {
// Only the rail bindings pass an anchor; `target` and the hit boxes do not.
if (!anchor) {
return;
}
anchor.style.setProperty(tooltipAnchorTopVar, "".concat(event.offsetY, "px"));
anchor.dispatchEvent(new MouseEvent('mouseenter'));
};
var closeTooltip = function closeTooltip(anchor) {
if (!anchor) {
return;
}
anchor.dispatchEvent(new MouseEvent('mouseleave'));
};
var registerEvents = function registerEvents(element, hoverAnchor) {
return [(0, _bindEventListener.bind)(element, {
type: 'mouseenter',
listener: function listener(event) {
// Mounted on first hover rather than up front, so a handle that is never
// hovered costs no portal and no listeners.
rightHandle.mountTooltip();
leftHandle === null || leftHandle === void 0 || leftHandle.mountTooltip();
if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_use_vanilla_components')) {
// After the mount above, so the tooltip always exists to be opened.
openTooltipAt(hoverAnchor, event);
}
rightHandle.rail.style.setProperty('opacity', '1');
leftHandle === null || leftHandle === void 0 || leftHandle.rail.style.setProperty('opacity', '1');
}
}), (0, _bindEventListener.bind)(element, {
type: 'mouseleave',
listener: function listener() {
// Ahead of the resize guard below: the ADS tooltip hides whenever the pointer
// leaves the rail, including part-way through a resize.
if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_use_vanilla_components')) {
closeTooltip(hoverAnchor);
}
if (state === 'resizing') {
return;
}
rightHandle.rail.style.removeProperty('opacity');
leftHandle === null || leftHandle === void 0 || leftHandle.rail.style.removeProperty('opacity');
}
})];
};
var unbindFns = leftHandle ? // old code - left + right
[].concat((0, _toConsumableArray2.default)(registerEvents(target)), (0, _toConsumableArray2.default)(registerEvents(rightHandle.handleHitBox)), (0, _toConsumableArray2.default)(registerEvents(leftHandle.handleHitBox)), (0, _toConsumableArray2.default)(registerEvents(rightHandle.rail, rightHandle.tooltipAnchor)), (0, _toConsumableArray2.default)(registerEvents(leftHandle.rail, leftHandle.tooltipAnchor))) : // new code - right only
[].concat((0, _toConsumableArray2.default)(registerEvents(target)), (0, _toConsumableArray2.default)(registerEvents(rightHandle.handleHitBox)), (0, _toConsumableArray2.default)(registerEvents(rightHandle.rail, rightHandle.tooltipAnchor)));
var handleElement = 'rail';
var destroyFns = leftHandle ?
// old code - left + right
[registerHandle(rightHandle[handleElement], 'right'), registerHandle(leftHandle[handleElement], 'left'), rightHandle.destroyTooltip, leftHandle.destroyTooltip] :
// new code - right only
[registerHandle(rightHandle[handleElement], 'right'), rightHandle.destroyTooltip];
return {
rightHandle: rightHandle.handle,
leftHandle: leftHandle === null || leftHandle === void 0 ? void 0 : leftHandle.handle,
destroy: function destroy(isChangeToViewMode) {
destroyFns.forEach(function (destroyFn) {
return destroyFn();
});
unbindFns.forEach(function (unbindFn) {
return unbindFn();
});
if (isChangeToViewMode) {
var _rightHandle$handle$p, _leftHandle$handle$pa;
(_rightHandle$handle$p = rightHandle.handle.parentElement) === null || _rightHandle$handle$p === void 0 || _rightHandle$handle$p.removeChild(rightHandle.handle);
leftHandle === null || leftHandle === void 0 || (_leftHandle$handle$pa = leftHandle.handle.parentElement) === null || _leftHandle$handle$pa === void 0 || _leftHandle$handle$pa.removeChild(leftHandle.handle);
}
}
};
};