UNPKG

@atlaskit/editor-plugin-toolbar

Version:

Toolbar plugin for @atlaskit/editor-core

189 lines 6.11 kB
import { ACTION_SUBJECT_ID } from '@atlaskit/editor-common/analytics'; import { containsPopupWithNestedElement, Experience, EXPERIENCE_ID, ExperienceCheckDomMutation, ExperienceCheckTimeout, getPopupContainerFromEditorView } from '@atlaskit/editor-common/experiences'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { PluginKey } from '@atlaskit/editor-prosemirror/state'; import { fg } from '@atlaskit/platform-feature-flags'; const pluginKey = new PluginKey('selectionToolbarOpenExperience'); const START_METHOD = { MOUSE_UP: 'mouseUp', KEY_DOWN: 'keyDown' }; const ABORT_REASON = { SELECTION_CLEARED: 'selectionCleared', BLOCK_MENU_OPENED: 'blockMenuOpened', EDITOR_DESTROYED: 'editorDestroyed' }; /** * This experience tracks when the selection toolbar is opened. * * Start: When user makes a selection via mouseup or shift+arrow key down * Success: When the selection toolbar is added to the DOM within 1000ms of start * Failure: When 1000ms passes without the selection toolbar being added to the DOM * Abort: When selection transitions to empty or block menu is opened * * @see https://hello.atlassian.net/wiki/spaces/EDITOR/pages/6262117789/Experience+tracking+Selection+toolbar+open */ export const getSelectionToolbarOpenExperiencePlugin = ({ refs, dispatchAnalyticsEvent // eslint-disable-next-line @typescript-eslint/no-empty-object-type }) => { let editorView; let targetEl; let shiftArrowKeyPressed = false; let mouseDownPos; const getTarget = () => { if (!targetEl) { var _editorView; targetEl = refs.popupsMountPoint || getPopupContainerFromEditorView((_editorView = editorView) === null || _editorView === void 0 ? void 0 : _editorView.dom); } return targetEl; }; const experience = new Experience(EXPERIENCE_ID.TOOLBAR_OPEN, { actionSubjectId: ACTION_SUBJECT_ID.SELECTION_TOOLBAR, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: 1000, onTimeout: () => { var _editorView2; if (isBlockMenuWithinNode(getTarget())) { return { status: 'abort', reason: ABORT_REASON.BLOCK_MENU_OPENED }; } else if (isSelectionWithoutTextContent((_editorView2 = editorView) === null || _editorView2 === void 0 ? void 0 : _editorView2.state.selection)) { return { status: 'abort', reason: ABORT_REASON.SELECTION_CLEARED }; } } }), new ExperienceCheckDomMutation({ onDomMutation: ({ mutations }) => { if (mutations.some(isSelectionToolbarAddedInMutation)) { return { status: 'success' }; } }, observeConfig: () => ({ target: getTarget(), options: { childList: true } }) })] }); const shouldSkipExperienceStart = selection => { if (isSelectionWithoutTextContent(selection) || isSelectionWithinCodeBlock(selection)) { return true; } const target = getTarget(); return isSelectionToolbarWithinNode(target) || isBlockMenuWithinNode(target) && fg('platform_editor_toolbar_open_experience_fix'); }; return new SafePlugin({ key: pluginKey, state: { init: () => ({}), apply: (_tr, pluginState, oldState, newState) => { if (!oldState.selection.empty && isSelectionWithoutTextContent(newState.selection)) { experience.abort({ reason: ABORT_REASON.SELECTION_CLEARED }); } if (shiftArrowKeyPressed && !newState.selection.eq(oldState.selection) && !isSelectionWithoutTextContent(newState.selection)) { experience.start({ method: START_METHOD.KEY_DOWN }); shiftArrowKeyPressed = false; } return pluginState; } }, props: { handleDOMEvents: { mousedown: (_view, e) => { mouseDownPos = { x: e.clientX, y: e.clientY }; }, mouseup: (view, e) => { if (!mouseDownPos || shouldSkipExperienceStart(view.state.selection)) { return; } if (e.clientX !== mouseDownPos.x || e.clientY !== mouseDownPos.y) { experience.start({ method: START_METHOD.MOUSE_UP }); } }, dblclick: view => { if (shouldSkipExperienceStart(view.state.selection)) { return; } experience.start({ method: START_METHOD.MOUSE_UP }); }, keydown: (_view, { shiftKey, key }) => { shiftArrowKeyPressed = shiftKey && key.includes('Arrow') && !isSelectionToolbarWithinNode(getTarget()); }, keyup: () => { shiftArrowKeyPressed = false; } } }, view: view => { editorView = view; return { destroy: () => { experience.abort({ reason: ABORT_REASON.EDITOR_DESTROYED }); } }; } }); }; const isSelectionToolbarAddedInMutation = ({ type, addedNodes }) => { return type === 'childList' && [...addedNodes].some(isSelectionToolbarWithinNode); }; const isSelectionToolbarWithinNode = node => { return containsPopupWithNestedElement(node, '[data-testid="editor-floating-toolbar"]'); }; const isBlockMenuWithinNode = node => { return containsPopupWithNestedElement(node, '[data-testid="editor-block-menu"]'); }; const isSelectionWithoutTextContent = selection => { if (!selection || selection.empty) { return true; } let hasText = false; selection.$from.doc.nodesBetween(selection.from, selection.to, node => { if (hasText) { return false; } if (node.isText && node.text && node.text.length > 0) { hasText = true; return false; } return true; }); return !hasText; }; const isSelectionWithinCodeBlock = selection => { const { $from, $to } = selection; return $from.sameParent($to) && $from.parent.type.name === 'codeBlock'; };