UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

200 lines (187 loc) 9.69 kB
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { handleMouseOver } from '../handle-mouse-over'; import { clearHoverSide, mouseEnter, mouseLeave, setHoverSide, stopEditing } from './commands'; import { RIGHT_MARGIN_ROVO_GAP_PX } from './constants'; import { getInteractionTrackingState } from './pm-plugin'; /** Per-view pending hover state; avoids cross-editor singleton. */ var pendingByView = new WeakMap(); /** Per-view RAF handle so clearPendingHoverSide cancels only that view's callback. */ var rafIdByView = new WeakMap(); var cancelScheduledProcessForView = function cancelScheduledProcessForView(view) { var id = rafIdByView.get(view); if (id !== undefined) { cancelAnimationFrame(id); rafIdByView.delete(view); } }; var clearPendingHoverSide = function clearPendingHoverSide(view) { pendingByView.delete(view); cancelScheduledProcessForView(view); }; var BLOCK_SELECTORS = '[data-node-anchor], [data-drag-handler-anchor-name]'; var RIGHT_EDGE_SELECTOR = '[data-blocks-right-edge-button-container]'; // Top-level blocks (no block ancestor), matched by anchor attribute so it works under both the // legacy and native-anchor schemes. var getRootBlocks = function getRootBlocks(view) { return Array.from(view.dom.querySelectorAll(BLOCK_SELECTORS)).filter(function (el) { var _el$parentElement; return !((_el$parentElement = el.parentElement) !== null && _el$parentElement !== void 0 && _el$parentElement.closest(BLOCK_SELECTORS)); }); }; // Find the root block whose vertical bounds contain clientY. Returns the measured rect so callers // can reuse it without re-measuring. var findBlockAtY = function findBlockAtY(view, clientY) { var _iterator = _createForOfIteratorHelper(getRootBlocks(view)), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var el = _step.value; var rect = el.getBoundingClientRect(); if (clientY >= rect.top && clientY <= rect.bottom) { return { block: el, rect: rect }; } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return null; }; var getRightMarginBoundary = function getRightMarginBoundary(view) { var _view$dom$ownerDocume, _view$dom$ownerDocume2; return ((_view$dom$ownerDocume = (_view$dom$ownerDocume2 = view.dom.ownerDocument.defaultView) === null || _view$dom$ownerDocume2 === void 0 ? void 0 : _view$dom$ownerDocume2.innerWidth) !== null && _view$dom$ownerDocume !== void 0 ? _view$dom$ownerDocume : Number.POSITIVE_INFINITY) - RIGHT_MARGIN_ROVO_GAP_PX; }; // not in the right margin // Classify where the cursor sits in the right margin beside the block at its height, running the // block lookup once so the caller doesn't traverse the DOM twice per mousemove. var classifyRightMarginPosition = function classifyRightMarginPosition(view, event) { var found = findBlockAtY(view, event.clientY); if (!found) { return null; } if (event.clientX <= found.rect.right) { return null; } return event.clientX > getRightMarginBoundary(view) ? { type: 'gap' } : { type: 'active', block: found.block }; }; /** * Process hover position and set left/right side. Only invoked when right-side controls are * enabled (confluence_remix_button_right_side_block_fg); handleMouseMove returns early otherwise. */ var processHoverSide = function processHoverSide(view, api) { var event = pendingByView.get(view); if (!event) { return; } pendingByView.delete(view); rafIdByView.delete(view); var editorContentArea = view.dom.closest('.ak-editor-content-area'); if (!(editorContentArea instanceof HTMLElement)) { return; } var state = getInteractionTrackingState(view.state); var target = event.target instanceof HTMLElement ? event.target : null; // When hovering over block controls directly, infer side from which control we're over. // This is more reliable than bounds when controls are in portals outside the editor DOM. var rightEdgeElement = target === null || target === void 0 ? void 0 : target.closest(RIGHT_EDGE_SELECTOR); if (rightEdgeElement) { if ((state === null || state === void 0 ? void 0 : state.hoverSide) !== 'right') { setHoverSide(view, 'right'); } return; } var leftControlElement = target === null || target === void 0 ? void 0 : target.closest('[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"]'); if (leftControlElement) { if ((state === null || state === void 0 ? void 0 : state.hoverSide) !== 'left') { setHoverSide(view, 'left'); } return; } // Added right-margin hover, gated so it can be rolled back. When off, fall through to midpoint. if (editorExperiment('remix_button_right_margin_hover', true)) { var closestBlock = target === null || target === void 0 ? void 0 : target.closest(BLOCK_SELECTORS); var blockElement = closestBlock instanceof HTMLElement ? closestBlock : null; // Not over a block: the cursor may be in the right margin beside content. var marginZone = blockElement ? null : classifyRightMarginPosition(view, event); if ((marginZone === null || marginZone === void 0 ? void 0 : marginZone.type) === 'active' && api) { // handleMouseOver only reads event.target, so a target-only stand-in is enough here. handleMouseOver(view, { target: marginZone.block }, api); // mouseenter doesn't fire over the click overlay, so clear isMouseOut here to re-show. if (state !== null && state !== void 0 && state.isMouseOut) { mouseEnter(view); } if ((state === null || state === void 0 ? void 0 : state.hoverSide) !== 'right') { setHoverSide(view, 'right'); } return; } // In the Rovo gap, dismiss the controls so the button doesn't linger over the Rovo button. if ((marginZone === null || marginZone === void 0 ? void 0 : marginZone.type) === 'gap' || event.clientX > getRightMarginBoundary(view)) { if (!(state !== null && state !== void 0 && state.isMouseOut)) { clearHoverSide(view); mouseLeave(view); } return; } // Over a block: fall through to the midpoint split so the left half keeps the drag handle and // the right half shows the Remix button, matching the experiment-off behaviour. } // Pick the side from the content midpoint, keeping the original left/right halves. var _editorContentArea$ge = editorContentArea.getBoundingClientRect(), left = _editorContentArea$ge.left, right = _editorContentArea$ge.right; var midpoint = (left + right) / 2; var nextHoverSide = event.clientX > midpoint ? 'right' : 'left'; if ((state === null || state === void 0 ? void 0 : state.hoverSide) !== nextHoverSide) { setHoverSide(view, nextHoverSide); } }; export var handleMouseMove = function handleMouseMove(view, event) { var rightSideControlsEnabled = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var api = arguments.length > 3 ? arguments[3] : undefined; var state = getInteractionTrackingState(view.state); // if user has stopped editing and moved their mouse, show block controls again if (state !== null && state !== void 0 && state.isEditing) { stopEditing(view); } // Only track hover side when right-side controls are enabled (single source: confluence_remix_button_right_side_block_fg via config) if (!rightSideControlsEnabled) { return false; } if (!(event instanceof MouseEvent)) { return false; } pendingByView.set(view, event); cancelScheduledProcessForView(view); var id = requestAnimationFrame(function () { processHoverSide(view, api); }); rafIdByView.set(view, id); return false; }; export var handleMouseLeave = function handleMouseLeave(view) { var rightSideControlsEnabled = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; if (rightSideControlsEnabled) { clearPendingHoverSide(view); } mouseLeave(view); return false; }; export var handleMouseEnter = function handleMouseEnter(view) { mouseEnter(view); return false; };