@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
250 lines (245 loc) • 12.2 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
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) { _defineProperty(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; }
import { bind } from 'bind-event-listener';
import { isBlockControlsSuppressionTarget } from '@atlaskit/editor-common/block-controls-suppression';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { UNSAFE_expValNoExposure } from '@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/editor-experiment';
import { RIGHT_MARGIN_ROVO_GAP_PX } from './constants';
import { handleKeyDown } from './handle-key-down';
import { handleMouseEnter, handleMouseLeave, handleMouseMove } from './handle-mouse-move';
/** Elements that extend the editor hover area (block controls, right-edge button, etc.) */
var LEGACY_BLOCK_CONTROLS_HOVER_AREA_SELECTOR = '[data-blocks-right-edge-button-container], [data-blocks-drag-handle-container], [data-testid="block-ctrl-drag-handle"], [data-testid="block-ctrl-drag-handle-container"], [data-testid="block-ctrl-decorator-widget"], [data-testid="block-ctrl-quick-insert-button"]';
var getBlockControlsHoverAreaSelector = function getBlockControlsHoverAreaSelector() {
return isExperimentEnabled('platform_editor_block_control_migration') ? "[data-editor-block-controls-surface], [data-editor-block-ctrl-drag-handle], ".concat(LEGACY_BLOCK_CONTROLS_HOVER_AREA_SELECTOR) : LEGACY_BLOCK_CONTROLS_HOVER_AREA_SELECTOR;
};
var MOUSE_LEAVE_DEBOUNCE_MS = 200;
/** ClickAreaBlock overlay that wraps the editor content and covers the right margin. */
var CLICK_AREA_SELECTOR = '[data-editor-click-wrapper]';
var isMovingToBlockControlsArea = function isMovingToBlockControlsArea(target) {
return target instanceof Element && !!target.closest(getBlockControlsHoverAreaSelector());
};
/**
* The right margin is covered by the ClickAreaBlock overlay, which sits outside .ak-editor-content-area.
* Hovering there should still surface the right-side Remix button, so keep controls alive — but only on
* the right (past the content's right edge), and not in the far-right Rovo gap. The left gutter must
* dismiss like the experiment-off path, so it is treated as inactive.
*/
var isOverActiveClickArea = function isOverActiveClickArea(target, clientX) {
var _clickArea$querySelec, _target$ownerDocument, _target$ownerDocument2;
if (!(target instanceof Element)) {
return false;
}
var clickArea = target.closest(CLICK_AREA_SELECTOR);
if (!clickArea) {
return false;
}
var contentRight = (_clickArea$querySelec = clickArea.querySelector('.ak-editor-content-area')) === null || _clickArea$querySelec === void 0 ? void 0 : _clickArea$querySelec.getBoundingClientRect().right;
if (contentRight !== undefined && clientX <= contentRight) {
return false;
}
var innerWidth = (_target$ownerDocument = (_target$ownerDocument2 = target.ownerDocument.defaultView) === null || _target$ownerDocument2 === void 0 ? void 0 : _target$ownerDocument2.innerWidth) !== null && _target$ownerDocument !== void 0 ? _target$ownerDocument : Number.POSITIVE_INFINITY;
return clientX <= innerWidth - RIGHT_MARGIN_ROVO_GAP_PX;
};
var isOverTreatmentClickArea = function isOverTreatmentClickArea(target, editorContentArea, clientX) {
if (!(target instanceof Element) || !(editorContentArea instanceof HTMLElement)) {
return false;
}
if (!target.closest(CLICK_AREA_SELECTOR)) {
return false;
}
return clientX > editorContentArea.getBoundingClientRect().right;
};
export var interactionTrackingPluginKey = new PluginKey('interactionTrackingPlugin');
export var createInteractionTrackingPlugin = function createInteractionTrackingPlugin() {
var rightSideControlsEnabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
var api = arguments.length > 1 ? arguments[1] : undefined;
return new SafePlugin({
key: interactionTrackingPluginKey,
state: {
init: function init() {
var state = {
isEditing: false
};
if (editorExperiment('platform_editor_controls', 'variant1')) {
state.isMouseOut = false;
}
return state;
},
apply: function apply(tr, pluginState) {
var meta = tr.getMeta(interactionTrackingPluginKey);
var newState = {};
switch (meta === null || meta === void 0 ? void 0 : meta.type) {
case 'startEditing':
newState.isEditing = true;
break;
case 'stopEditing':
newState.isEditing = false;
break;
case 'mouseLeave':
newState.isMouseOut = true;
newState.hoverSide = undefined;
break;
case 'mouseEnter':
newState.isMouseOut = false;
if (meta.side) {
newState.hoverSide = meta.side;
}
break;
case 'setHoverSide':
newState.hoverSide = meta.side;
break;
case 'clearHoverSide':
newState.hoverSide = undefined;
break;
}
return _objectSpread(_objectSpread({}, pluginState), newState);
}
},
props: {
handleKeyDown: handleKeyDown,
handleDOMEvents: {
mousemove: function mousemove(view, event) {
return handleMouseMove(view, event, rightSideControlsEnabled, api);
}
}
},
view: editorExperiment('platform_editor_controls', 'variant1') ? function (view) {
var editorContentArea = view.dom.closest('.ak-editor-content-area');
// rightSideControlsEnabled is the single source of truth (confluence_remix_button_right_side_block_fg from preset)
var unbindMouseEnter;
var unbindMouseLeave;
var unbindDocumentMouseMove;
var mouseLeaveTimeoutId = null;
var lastMousePosition = {
x: 0,
y: 0
};
var scheduleMouseLeave = function scheduleMouseLeave(event) {
if (mouseLeaveTimeoutId) {
clearTimeout(mouseLeaveTimeoutId);
mouseLeaveTimeoutId = null;
}
// Preserve the existing hover targets for the control cohort.
if (rightSideControlsEnabled && (isMovingToBlockControlsArea(event.relatedTarget) || isOverActiveClickArea(event.relatedTarget, event.clientX))) {
return;
}
if (rightSideControlsEnabled && isExperimentEnabled('cc_maui_remix_button_hover_corridor') && isOverTreatmentClickArea(event.relatedTarget, editorContentArea, event.clientX)) {
return;
}
mouseLeaveTimeoutId = setTimeout(function () {
mouseLeaveTimeoutId = null;
// Re-check the control cohort's existing hover targets after the debounce.
if (rightSideControlsEnabled && typeof document !== 'undefined') {
var el = document.elementFromPoint(lastMousePosition.x, lastMousePosition.y);
if (el && (isMovingToBlockControlsArea(el) || isOverActiveClickArea(el, lastMousePosition.x))) {
return;
}
if (el && isExperimentEnabled('cc_maui_remix_button_hover_corridor') && isOverTreatmentClickArea(el, editorContentArea, lastMousePosition.x)) {
return;
}
}
handleMouseLeave(view, rightSideControlsEnabled);
}, MOUSE_LEAVE_DEBOUNCE_MS);
};
var cancelScheduledMouseLeave = function cancelScheduledMouseLeave() {
if (mouseLeaveTimeoutId) {
clearTimeout(mouseLeaveTimeoutId);
mouseLeaveTimeoutId = null;
}
};
var handleDocumentMouseMove = function handleDocumentMouseMove(event) {
lastMousePosition = {
x: event.clientX,
y: event.clientY
};
// Catches block controls in portals that handleDOMEvents.mousemove misses.
var isOverKnownHoverTarget = (editorContentArea === null || editorContentArea === void 0 ? void 0 : editorContentArea.contains(event.target)) || isMovingToBlockControlsArea(event.target);
var overClickArea = event.target instanceof Element && !!event.target.closest(CLICK_AREA_SELECTOR);
if (isOverKnownHoverTarget || overClickArea) {
handleMouseMove(view, event, rightSideControlsEnabled, api);
}
};
var handleTreatmentDocumentMouseMove = function handleTreatmentDocumentMouseMove(event) {
if (isBlockControlsSuppressionTarget(event.target)) {
var _interactionTrackingP;
lastMousePosition = {
x: event.clientX,
y: event.clientY
};
cancelScheduledMouseLeave();
if (!((_interactionTrackingP = interactionTrackingPluginKey.getState(view.state)) !== null && _interactionTrackingP !== void 0 && _interactionTrackingP.isMouseOut)) {
handleMouseLeave(view, rightSideControlsEnabled);
}
return;
}
handleDocumentMouseMove(event);
};
if (editorContentArea) {
if (rightSideControlsEnabled && typeof document !== 'undefined') {
// RemixButtonDecoration fires exposure when the control is visible; this
// mount-time split only selects the appropriate interaction listener.
if (UNSAFE_expValNoExposure('cc_maui_remix_button_hover_corridor', 'isEnabled', false) === true) {
unbindDocumentMouseMove = bind(document, {
type: 'mousemove',
listener: handleTreatmentDocumentMouseMove,
options: {
passive: true
}
});
} else {
unbindDocumentMouseMove = bind(document, {
type: 'mousemove',
listener: handleDocumentMouseMove,
options: {
passive: true
}
});
}
}
unbindMouseEnter = bind(editorContentArea, {
type: 'mouseenter',
listener: function listener() {
if (rightSideControlsEnabled) {
cancelScheduledMouseLeave();
}
handleMouseEnter(view);
}
});
unbindMouseLeave = bind(editorContentArea, {
type: 'mouseleave',
listener: function listener(event) {
var e = event;
lastMousePosition = {
x: e.clientX,
y: e.clientY
};
if (rightSideControlsEnabled) {
scheduleMouseLeave(e);
} else {
handleMouseLeave(view, false);
}
}
});
}
return {
destroy: function destroy() {
var _unbindMouseEnter, _unbindMouseLeave;
if (rightSideControlsEnabled) {
var _unbindDocumentMouseM;
cancelScheduledMouseLeave();
(_unbindDocumentMouseM = unbindDocumentMouseMove) === null || _unbindDocumentMouseM === void 0 || _unbindDocumentMouseM();
}
(_unbindMouseEnter = unbindMouseEnter) === null || _unbindMouseEnter === void 0 || _unbindMouseEnter();
(_unbindMouseLeave = unbindMouseLeave) === null || _unbindMouseLeave === void 0 || _unbindMouseLeave();
}
};
} : undefined
});
};
export var getInteractionTrackingState = function getInteractionTrackingState(state) {
return interactionTrackingPluginKey.getState(state);
};