@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
111 lines (105 loc) • 4.49 kB
JavaScript
import { clearHoverSide, mouseEnter, mouseLeave, setHoverSide, stopEditing } from './commands';
import { getInteractionTrackingState } from './pm-plugin';
/** Per-view pending hover state; avoids cross-editor singleton. */
const pendingByView = new WeakMap();
/** Per-view RAF handle so clearPendingHoverSide cancels only that view's callback. */
const rafIdByView = new WeakMap();
const cancelScheduledProcessForView = view => {
const id = rafIdByView.get(view);
if (id !== undefined) {
cancelAnimationFrame(id);
rafIdByView.delete(view);
}
};
const clearPendingHoverSide = view => {
pendingByView.delete(view);
cancelScheduledProcessForView(view);
};
const BLOCK_SELECTORS = '[data-node-anchor], [data-drag-handler-anchor-name]';
const RIGHT_EDGE_SELECTOR = '[data-blocks-right-edge-button-container]';
/**
* 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.
*/
const processHoverSide = view => {
const event = pendingByView.get(view);
if (!event) {
return;
}
pendingByView.delete(view);
rafIdByView.delete(view);
const editorContentArea = view.dom.closest('.ak-editor-content-area');
if (!(editorContentArea instanceof HTMLElement)) {
return;
}
const state = getInteractionTrackingState(view.state);
const 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.
const 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;
}
const 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;
}
// Primary path: depth-1 block (doc direct child). Decorations-anchor sets [data-drag-handler-anchor-depth="1"]
// on every root block (table, layoutSection, expand, etc.), so we get the whole block without per-type logic.
const blockElement = target === null || target === void 0 ? void 0 : target.closest(BLOCK_SELECTORS);
const depth1Block = blockElement instanceof HTMLElement ? blockElement.closest('[data-drag-handler-anchor-depth="1"]') : null;
const boundsElement = depth1Block instanceof HTMLElement ? depth1Block : editorContentArea;
if (!boundsElement) {
if ((state === null || state === void 0 ? void 0 : state.hoverSide) !== undefined) {
clearHoverSide(view);
}
return;
}
const {
left,
right
} = boundsElement.getBoundingClientRect();
const midpoint = (left + right) / 2;
const nextHoverSide = event.clientX > midpoint ? 'right' : 'left';
if ((state === null || state === void 0 ? void 0 : state.hoverSide) !== nextHoverSide) {
setHoverSide(view, nextHoverSide);
}
};
export const handleMouseMove = (view, event, rightSideControlsEnabled = false) => {
const 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);
const id = requestAnimationFrame(() => {
processHoverSide(view);
});
rafIdByView.set(view, id);
return false;
};
export const handleMouseLeave = (view, rightSideControlsEnabled = false) => {
if (rightSideControlsEnabled) {
clearPendingHoverSide(view);
}
mouseLeave(view);
return false;
};
export const handleMouseEnter = view => {
mouseEnter(view);
return false;
};