@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
165 lines (162 loc) • 6.5 kB
JavaScript
import { bind } from 'bind-event-listener';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
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.) */
const 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"]';
const MOUSE_LEAVE_DEBOUNCE_MS = 200;
const isMovingToBlockControlsArea = target => target instanceof Element && !!target.closest(BLOCK_CONTROLS_HOVER_AREA_SELECTOR);
export const interactionTrackingPluginKey = new PluginKey('interactionTrackingPlugin');
export const createInteractionTrackingPlugin = (rightSideControlsEnabled = false) => {
return new SafePlugin({
key: interactionTrackingPluginKey,
state: {
init() {
const state = {
isEditing: false
};
if (editorExperiment('platform_editor_controls', 'variant1')) {
state.isMouseOut = false;
}
return state;
},
apply(tr, pluginState) {
const meta = tr.getMeta(interactionTrackingPluginKey);
const 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;
break;
case 'setHoverSide':
newState.hoverSide = meta.side;
break;
case 'clearHoverSide':
newState.hoverSide = undefined;
break;
}
return {
...pluginState,
...newState
};
}
},
props: {
handleKeyDown,
handleDOMEvents: {
mousemove: (view, event) => handleMouseMove(view, event, rightSideControlsEnabled)
}
},
view: editorExperiment('platform_editor_controls', 'variant1') ? view => {
const editorContentArea = view.dom.closest('.ak-editor-content-area');
// rightSideControlsEnabled is the single source of truth (confluence_remix_button_right_side_block_fg from preset)
let unbindMouseEnter;
let unbindMouseLeave;
let unbindDocumentMouseMove;
let mouseLeaveTimeoutId = null;
let lastMousePosition = {
x: 0,
y: 0
};
const scheduleMouseLeave = event => {
if (mouseLeaveTimeoutId) {
clearTimeout(mouseLeaveTimeoutId);
mouseLeaveTimeoutId = null;
}
// Don't set isMouseOut when moving to block controls (right-edge button, drag handle, etc.)
if (rightSideControlsEnabled && isMovingToBlockControlsArea(event.relatedTarget)) {
return;
}
mouseLeaveTimeoutId = setTimeout(() => {
mouseLeaveTimeoutId = null;
// Before dispatching, check if mouse has moved to block controls (e.g. through empty space)
if (rightSideControlsEnabled && typeof document !== 'undefined') {
const el = document.elementFromPoint(lastMousePosition.x, lastMousePosition.y);
if (el && isMovingToBlockControlsArea(el)) {
return;
}
}
handleMouseLeave(view, rightSideControlsEnabled);
}, MOUSE_LEAVE_DEBOUNCE_MS);
};
const cancelScheduledMouseLeave = () => {
if (mouseLeaveTimeoutId) {
clearTimeout(mouseLeaveTimeoutId);
mouseLeaveTimeoutId = null;
}
};
if (editorContentArea) {
if (rightSideControlsEnabled && typeof document !== 'undefined') {
unbindDocumentMouseMove = bind(document, {
type: 'mousemove',
listener: event => {
lastMousePosition = {
x: event.clientX,
y: event.clientY
};
// Use document-level mousemove so we get events when hovering over block
// controls (which may be in portals outside the editor DOM). Without this,
// handleDOMEvents.mousemove only fires when over the editor content.
if (editorContentArea.contains(event.target) || isMovingToBlockControlsArea(event.target)) {
handleMouseMove(view, event, rightSideControlsEnabled);
}
},
options: {
passive: true
}
});
}
unbindMouseEnter = bind(editorContentArea, {
type: 'mouseenter',
listener: () => {
if (rightSideControlsEnabled) {
cancelScheduledMouseLeave();
}
handleMouseEnter(view);
}
});
unbindMouseLeave = bind(editorContentArea, {
type: 'mouseleave',
listener: event => {
const e = event;
lastMousePosition = {
x: e.clientX,
y: e.clientY
};
if (rightSideControlsEnabled) {
scheduleMouseLeave(e);
} else {
handleMouseLeave(view, false);
}
}
});
}
return {
destroy: () => {
var _unbindMouseEnter, _unbindMouseLeave;
if (rightSideControlsEnabled) {
var _unbindDocumentMouseM;
cancelScheduledMouseLeave();
(_unbindDocumentMouseM = unbindDocumentMouseMove) === null || _unbindDocumentMouseM === void 0 ? void 0 : _unbindDocumentMouseM();
}
(_unbindMouseEnter = unbindMouseEnter) === null || _unbindMouseEnter === void 0 ? void 0 : _unbindMouseEnter();
(_unbindMouseLeave = unbindMouseLeave) === null || _unbindMouseLeave === void 0 ? void 0 : _unbindMouseLeave();
}
};
} : undefined
});
};
export const getInteractionTrackingState = state => {
return interactionTrackingPluginKey.getState(state);
};