UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

338 lines (326 loc) • 15.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isPossibleGutterElement = exports.createGutterBlockFinder = exports.bindSparseGutterHover = void 0; var _bindEventListener = require("bind-event-listener"); var _dragHandle = require("@atlaskit/editor-common/styles/drag-handle"); var _constants = require("@atlaskit/editor-shared-styles/constants"); var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled"); var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure"); var _consts = require("../ui/consts"); var _handleSparseHoverTarget = require("./handle-sparse-hover-target"); var _commands = require("./interaction-tracking/commands"); var _handleMouseMove = require("./interaction-tracking/handle-mouse-move"); var _pmPlugin = require("./interaction-tracking/pm-plugin"); var _visibleBox = require("./utils/visible-box"); var BLOCK_SELECTOR = '[data-prosemirror-node-block="true"]'; var TABLE_STRUCTURE_NODES = new Set(['table', 'tableRow', 'tableCell', 'tableHeader']); var LAYOUT_SECTION_SELECTOR = '[data-prosemirror-node-name="layoutSection"]'; var LAYOUT_COLUMN_SELECTOR = '[data-prosemirror-node-name="layoutColumn"]'; // `layoutColumnExtendedHoverZone` is a DRAG_HANDLE_WIDTH-tall strip centred on the column's top edge. var LAYOUT_COLUMN_STRIP_REACH = _dragHandle.DRAG_HANDLE_WIDTH / 2; var isBlockElement = function isBlockElement(element) { return element.getAttribute('data-prosemirror-node-block') === 'true'; }; var getVisibleLeft = function getVisibleLeft(block) { var _block$getAttribute; return (0, _visibleBox.getVisibleBox)(block, (_block$getAttribute = block.getAttribute('data-prosemirror-node-name')) !== null && _block$getAttribute !== void 0 ? _block$getAttribute : '').getBoundingClientRect().left; }; /** * Whether the element under the pointer can be part of a block's left gutter: a container block's * own padding (panel, layout, list…) or an editor wrapper around the content. Text and leaf blocks * have no gutter inside them. Table and cell padding shows the table's controls, but containers * nested inside cells can own gutters for their children, as in the legacy hover zones. Elements * rendered beside the editor (title, popups, toolbars) cover any gutter beneath them. */ var isPossibleGutterElement = exports.isPossibleGutterElement = function isPossibleGutterElement(view, hoveredElement) { if (!(hoveredElement instanceof Element)) { return false; } var owner = hoveredElement.closest("".concat(BLOCK_SELECTOR, ", ").concat(_consts.BLOCK_CONTROLS_SURFACE_SELECTOR)); if (owner !== null && owner !== void 0 && owner.matches(_consts.BLOCK_CONTROLS_SURFACE_SELECTOR)) { return false; } if (owner && view.dom.contains(owner)) { var _owner$getAttribute, _owner$getAttribute2; if (TABLE_STRUCTURE_NODES.has((_owner$getAttribute = owner.getAttribute('data-prosemirror-node-name')) !== null && _owner$getAttribute !== void 0 ? _owner$getAttribute : '')) { return false; } var nodeType = view.state.schema.nodes[(_owner$getAttribute2 = owner.getAttribute('data-prosemirror-node-name')) !== null && _owner$getAttribute2 !== void 0 ? _owner$getAttribute2 : '']; return !(nodeType !== null && nodeType !== void 0 && nodeType.isTextblock) && !(nodeType !== null && nodeType !== void 0 && nodeType.isLeaf); } return view.dom.contains(hoveredElement) || hoveredElement.contains(view.dom); }; var getGutterWidths = function getGutterWidths() { return { topLevel: (0, _constants.akEditorGutterPaddingDynamic)(), narrowTopLevel: (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_preview_panel_responsiveness', 'isEnabled', true) ? _constants.akEditorGutterPaddingReduced : undefined }; }; var getTopLevelGutterWidth = function getTopLevelGutterWidth(api, widths) { var _api$width; var editorWidth = api === null || api === void 0 || (_api$width = api.width) === null || _api$width === void 0 || (_api$width = _api$width.sharedState.currentState()) === null || _api$width === void 0 ? void 0 : _api$width.width; return widths.narrowTopLevel !== undefined && editorWidth !== undefined && editorWidth <= _constants.akEditorFullPageNarrowBreakout ? widths.narrowTopLevel : widths.topLevel; }; var getEditorElementAt = function getEditorElementAt(view, clientX, clientY) { var element = view.dom.ownerDocument.elementFromPoint(clientX, clientY); return element && element !== view.dom && view.dom.contains(element) ? element : null; }; /** The outermost block around `element` that does not contain the pointer owns that gutter. */ var findGutterOwner = function findGutterOwner(view, element, pointerTarget) { var owner = null; for (var current = element; current && current !== view.dom;) { if (isBlockElement(current) && !current.contains(pointerTarget) && // Layout columns have a top hover strip instead of a left gutter. current.getAttribute('data-prosemirror-node-name') !== 'layoutColumn') { owner = current; } current = current.parentElement; } return owner; }; var findBlockBesidePointer = function findBlockBesidePointer(view, api, event, pointerTarget, widths) { var pointerBlock = pointerTarget.closest(BLOCK_SELECTOR); var gutterWidth = pointerBlock && view.dom.contains(pointerBlock) ? _consts.DRAG_HANDLE_MAX_WIDTH_PLUS_GAP : getTopLevelGutterWidth(api, widths); var sample = getEditorElementAt(view, event.clientX + gutterWidth, event.clientY); var owner = sample && findGutterOwner(view, sample, pointerTarget); if (!owner) { return null; } var left = getVisibleLeft(owner); return event.clientX < left && left - event.clientX <= gutterWidth ? owner : null; }; var findColumnBelowPointer = function findColumnBelowPointer(view, event, pointerTarget) { if (pointerTarget.closest(LAYOUT_COLUMN_SELECTOR) || pointerTarget !== view.dom && !pointerTarget.closest(LAYOUT_SECTION_SELECTOR)) { return null; } var sample = getEditorElementAt(view, event.clientX, event.clientY + LAYOUT_COLUMN_STRIP_REACH); var column = sample === null || sample === void 0 ? void 0 : sample.closest(LAYOUT_COLUMN_SELECTOR); return column && view.dom.contains(column) && event.clientY < column.getBoundingClientRect().top ? column : null; }; /** Returns a function that finds the block whose legacy hover zone contains the pointer. */ var createGutterBlockFinder = exports.createGutterBlockFinder = function createGutterBlockFinder(view, api) { var widths = getGutterWidths(); return function (event) { var _findBlockBesidePoint; if (!(event.target instanceof Element)) { return null; } return (_findBlockBesidePoint = findBlockBesidePointer(view, api, event, event.target, widths)) !== null && _findBlockBesidePoint !== void 0 ? _findBlockBesidePoint : view.dom.contains(event.target) ? findColumnBelowPointer(view, event, event.target) : null; }; }; /** The latest gutter event waiting to be handled after the frame renders. */ /** What gutter hover does with a pointer position. */ /** * Leaving block controls does not count as reaching a new element: a nested block's drag handle * overhangs the block, so leaving the handle can put the pointer on the container with no block * beside it, and that exit must keep the controls on the block. */ var isLeavingBlockControls = function isLeavingBlockControls(event) { return event.relatedTarget instanceof Element && Boolean(event.relatedTarget.closest(_consts.BLOCK_CONTROLS_SURFACE_SELECTOR)); }; var classifyGutterHover = function classifyGutterHover(view, findGutterBlock, _ref) { var event = _ref.event, reachedNewElement = _ref.reachedNewElement; var block = findGutterBlock(event); if (block) { return { kind: 'block', element: block }; } var hoveredElement = event.target; if (reachedNewElement && hoveredElement instanceof Element && view.dom.contains(hoveredElement)) { return { kind: 'container', element: hoveredElement }; } return { kind: 'none' }; }; /** * Runs `callback` once after the next frame renders (`requestAnimationFrame`, then `setTimeout`). * Layout is clean then, so reading positions never forces a synchronous layout. Calls made while a * run is already scheduled are ignored. */ var createAfterRenderScheduler = function createAfterRenderScheduler(win, callback) { var frame; var timeout; return { schedule: function schedule() { if (frame !== undefined || timeout !== undefined) { return; } frame = win.requestAnimationFrame(function () { frame = undefined; timeout = win.setTimeout(function () { timeout = undefined; callback(); }, 0); }); }, cancel: function cancel() { if (frame !== undefined) { win.cancelAnimationFrame(frame); frame = undefined; } if (timeout !== undefined) { win.clearTimeout(timeout); timeout = undefined; } } }; }; /** * Keeps interaction tracking in step with gutter hover, as the legacy hover zones did. They sat * inside the block tree, so hovering one cleared the typing state, counted as inside the editor even * where the zone reached past the content area, and put the pointer on the left side of the * right-side controls split. */ var createInteractionTrackingSync = function createInteractionTrackingSync(view, api, contentArea) { // Set after gutter hover calls mouseEnter for a gutter outside the content area, so that leaving // that gutter calls mouseLeave. var isHoldingMouseEnter = false; return function (event, isOverGutter) { var _api$blockControls$sh; var state = (0, _pmPlugin.getInteractionTrackingState)(view.state); var rightSideControlsEnabled = api === null || api === void 0 || (_api$blockControls$sh = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh === void 0 ? void 0 : _api$blockControls$sh.rightSideControlsEnabled; var side = rightSideControlsEnabled ? 'left' : undefined; if (isOverGutter && state !== null && state !== void 0 && state.isEditing) { (0, _commands.stopEditing)(view); } if (event.target instanceof Node && contentArea !== null && contentArea !== void 0 && contentArea.contains(event.target)) { isHoldingMouseEnter = false; } else if (isOverGutter && state !== null && state !== void 0 && state.isMouseOut) { isHoldingMouseEnter = true; (0, _commands.mouseEnter)(view, side); return; } else if (!isOverGutter && isHoldingMouseEnter) { isHoldingMouseEnter = false; (0, _handleMouseMove.handleMouseLeave)(view, rightSideControlsEnabled); return; } if (isOverGutter && side && (state === null || state === void 0 ? void 0 : state.hoverSide) !== side) { (0, _commands.setHoverSide)(view, side); } }; }; /** * Shows block controls for the block the pointer hovers under sparse surfaces, including the empty * space to a block's left (the gutter). This is the only hover handler for sparse surfaces. * * Reaching text or a leaf block shows its controls straight away. Over an element that can be part * of a gutter, only the latest pointer event is kept, and it is resolved once after the frame * renders so that reading positions never forces a layout. * * Returns an unbind function, or `undefined` when the editor's document has no window. */ var bindSparseGutterHover = exports.bindSparseGutterHover = function bindSparseGutterHover(_ref2) { var _ref3, _ref4, _view$dom$closest; var api = _ref2.api, view = _ref2.view; var win = view.dom.ownerDocument.defaultView; if (!win) { return undefined; } var contentArea = view.dom.closest('.ak-editor-content-area'); // A wide block's gutter can sit outside the padded content area. Editors without a scroll // container or content area still get hover, over as much of the gutter as their wrapper covers. var eventArea = (_ref3 = (_ref4 = (_view$dom$closest = view.dom.closest('[data-editor-scroll-container="true"]')) !== null && _view$dom$closest !== void 0 ? _view$dom$closest : contentArea) !== null && _ref4 !== void 0 ? _ref4 : view.dom.parentElement) !== null && _ref3 !== void 0 ? _ref3 : view.dom; var findGutterBlock = createGutterBlockFinder(view, api); var syncInteractionTracking = createInteractionTrackingSync(view, api, contentArea); var pending; var handlePendingPointer = function handlePendingPointer() { var pointer = pending; pending = undefined; if (!pointer || (0, _handleSparseHoverTarget.isSparseHoverSuppressed)(view, api)) { return; } var hover = classifyGutterHover(view, findGutterBlock, pointer); if (hover.kind !== 'none') { (0, _handleSparseHoverTarget.handleSparseHoverTarget)(view, hover.element, api); } syncInteractionTracking(pointer.event, hover.kind === 'block'); }; var scheduler = createAfterRenderScheduler(win, handlePendingPointer); var queueGutterEvent = function queueGutterEvent(event, reachedNewElement) { var _pending; pending = { event: event, reachedNewElement: Boolean((_pending = pending) === null || _pending === void 0 ? void 0 : _pending.reachedNewElement) || reachedNewElement }; scheduler.schedule(); }; // Classified on every mouseover and reused while the pointer moves within that element, so // movement over text costs one comparison. var hovered; var classifyHoveredElement = function classifyHoveredElement(element) { hovered = { element: element, isPossibleGutter: isPossibleGutterElement(view, element) }; return hovered.isPossibleGutter; }; var onMouseOver = function onMouseOver(event) { if (classifyHoveredElement(event.target)) { queueGutterEvent(event, !isLeavingBlockControls(event)); return; } pending = undefined; var hoveredElement = event.target; if (hoveredElement instanceof Element && view.dom.contains(hoveredElement) && !(0, _handleSparseHoverTarget.isSparseHoverSuppressed)(view, api)) { (0, _handleSparseHoverTarget.handleSparseHoverTarget)(view, hoveredElement, api); } }; var onMouseMove = function onMouseMove(event) { var _hovered; var isPossibleGutter = ((_hovered = hovered) === null || _hovered === void 0 ? void 0 : _hovered.element) === event.target ? hovered.isPossibleGutter : classifyHoveredElement(event.target); if (isPossibleGutter) { queueGutterEvent(event, false); } }; var bindMouseListener = function bindMouseListener(type, _listener) { return (0, _bindEventListener.bind)(eventArea, { type: type, listener: function listener(event) { if (event instanceof win.MouseEvent) { _listener(event); } }, options: { passive: true } }); }; var unbindMouseOver; var unbindMouseMove; if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_block_controls_cls_fix')) { // Match the legacy ignoreRestingPointerHover guard until the pointer first moves. var pointerHasMoved = false; unbindMouseOver = bindMouseListener('mouseover', function (event) { if (!pointerHasMoved) { return; } onMouseOver(event); }); unbindMouseMove = bindMouseListener('mousemove', function (event) { if (!pointerHasMoved) { pointerHasMoved = true; onMouseOver(event); } onMouseMove(event); }); } else { unbindMouseOver = bindMouseListener('mouseover', onMouseOver); unbindMouseMove = bindMouseListener('mousemove', onMouseMove); } return function () { unbindMouseOver(); unbindMouseMove(); scheduler.cancel(); pending = undefined; }; };