@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
338 lines (326 loc) • 15.9 kB
JavaScript
"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;
};
};