UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

165 lines (162 loc) 6.5 kB
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); };