UNPKG

@atlaskit/editor-plugin-quick-insert

Version:

Quick insert plugin for @atlaskit/editor-core

152 lines 4.8 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'; const pluginKey = new PluginKey('quickInsertOpenExperience'); const TIMEOUT_DURATION = 1000; const START_METHOD = { QUICK_INSERT_BUTTON: 'quickInsertButton', TYPEAHEAD: 'typeahead' }; const ABORT_REASON = { USER_CANCELED: 'userCanceled', EDITOR_DESTROYED: 'editorDestroyed' }; /** * This experience tracks when the quick insert is opened. * * Start: When user types `/` to open quick insert or clicks the quick insert button * Success: When the quick insert menu is added to the DOM within 500ms of start * Failure: When 500ms passes without the quick insert menu being added to the DOM * Abort: When user presses escape or backspace */ export const getQuickInsertOpenExperiencePlugin = ({ refs, dispatchAnalyticsEvent }) => { let targetEl; let editorViewEl; let mouseDownPos; const getTarget = () => { if (!targetEl) { targetEl = refs.popupsMountPoint || refs.wrapperElement || getPopupContainerFromEditorView(editorViewEl); } return targetEl; }; const experience = new Experience(EXPERIENCE_ID.MENU_OPEN, { actionSubjectId: ACTION_SUBJECT_ID.QUICK_INSERT, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), new ExperienceCheckDomMutation({ onDomMutation: ({ mutations }) => { if (mutations.some(isQuickInsertMenuAddedInMutation)) { return { status: 'success' }; } }, observeConfig: () => ({ target: getTarget(), options: { childList: true } }) })] }); return new SafePlugin({ key: pluginKey, props: { handleDOMEvents: { mousedown: (_view, event) => { if (isTargetQuickInsertButton(event.target)) { mouseDownPos = { x: event.clientX, y: event.clientY }; } }, mouseup: (_view, event) => { if (mouseDownPos && isTargetQuickInsertButton(event.target) && event.clientX === mouseDownPos.x && event.clientY === mouseDownPos.y) { experience.start({ method: START_METHOD.QUICK_INSERT_BUTTON }); } mouseDownPos = undefined; }, beforeinput: (view, event) => { if (isQuickInsertTrigger(event) && isSelectionWhichSupportsTypeahead(view) && !isQuickInsertMenuWithinNode(getTarget())) { experience.start({ method: START_METHOD.TYPEAHEAD, forceRestart: true }); } }, keydown: (_view, event) => { if (isCancelKey(event.key) && !isQuickInsertMenuWithinNode(getTarget())) { experience.abort({ reason: ABORT_REASON.USER_CANCELED }); } } } }, view: editorView => { editorViewEl = editorView.dom; return { destroy: () => { experience.abort({ reason: ABORT_REASON.EDITOR_DESTROYED }); } }; } }); }; const isQuickInsertTrigger = ({ inputType, data }) => { return inputType === 'insertText' && data === '/'; }; const isSelectionWhichSupportsTypeahead = ({ state }) => { var _nodeBefore$textConte; const { from, $from } = state.selection; if ($from.parent.type.name === 'codeBlock') { return false; } if ($from.marks().some(mark => mark.type.name === 'code')) { return false; } if (from === 0) { return true; } const nodeBefore = state.doc.resolve(from).nodeBefore; if (!nodeBefore) { return true; } const charBefore = ((_nodeBefore$textConte = nodeBefore.textContent) === null || _nodeBefore$textConte === void 0 ? void 0 : _nodeBefore$textConte.slice(-1)) || ''; return charBefore.trim().length === 0; }; const isCancelKey = key => { return key === 'Escape' || key === 'Backspace'; }; const isTargetQuickInsertButton = target => { return target instanceof HTMLElement && (target.dataset.testid === 'editor-quick-insert-button' || !!target.closest('[data-testid="editor-quick-insert-button"]')); }; const isQuickInsertMenuAddedInMutation = ({ type, addedNodes }) => { return type === 'childList' && [...addedNodes].some(isQuickInsertMenuWithinNode); }; const isQuickInsertMenuWithinNode = node => { return containsPopupWithNestedElement(node, '.fabric-editor-typeahead'); };