UNPKG

@atlaskit/editor-plugin-block-menu

Version:

BlockMenu plugin for @atlaskit/editor-core

248 lines (246 loc) 8.6 kB
import { bind } from 'bind-event-listener'; import { ACTION, ACTION_SUBJECT_ID } from '@atlaskit/editor-common/analytics'; import { BLOCK_MENU_ACTION_TEST_ID, BLOCK_MENU_TEST_ID, EXTENSION_MENU_ITEM_TEST_ID } from '@atlaskit/editor-common/block-menu'; import { Experience, EXPERIENCE_ID, ExperienceCheckDomMutation, ExperienceCheckPopupMutation, ExperienceCheckTimeout, getPopupContainerFromEditorView, getSelectionAncestorDOM } from '@atlaskit/editor-common/experiences'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { PluginKey } from '@atlaskit/editor-prosemirror/state'; import { handleDeleteDomMutation, handleMoveDomMutation, handleTransformDomMutation, isBlockMenuVisible, isDragHandleElement } from './experience-check-utils'; const TIMEOUT_DURATION = 1000; const PORTAL_TEST_ID = { LINK_COPIED_TO_CLIPBOARD: 'link-copied-to-clipboard', SYNC_BLOCK_DELETE_CONFIRMATION: 'sync-block-delete-confirmation' }; const pluginKey = new PluginKey('blockMenuExperiences'); const START_METHOD = { DRAG_HANDLE_CLICK: 'dragHandleClick', KEYBOARD: 'keyboard' }; const ABORT_REASON = { USER_CANCELED: 'userCanceled', EDITOR_DESTROYED: 'editorDestroyed' }; export const getBlockMenuExperiencesPlugin = ({ refs, dispatchAnalyticsEvent }) => { let popupTargetEl; let editorView; const getPopupsTarget = () => { if (!popupTargetEl) { var _editorView; popupTargetEl = refs.popupsMountPoint || getPopupContainerFromEditorView((_editorView = editorView) === null || _editorView === void 0 ? void 0 : _editorView.dom); } return popupTargetEl; }; const getEditorDom = () => { var _editorView2; if (((_editorView2 = editorView) === null || _editorView2 === void 0 ? void 0 : _editorView2.dom) instanceof HTMLElement) { return editorView.dom; } return null; }; const blockMenuOpenExperience = new Experience(EXPERIENCE_ID.MENU_OPEN, { actionSubjectId: ACTION_SUBJECT_ID.BLOCK_MENU, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), new ExperienceCheckPopupMutation({ nestedElementQuery: `[data-testid="${BLOCK_MENU_TEST_ID}"]`, getTarget: getPopupsTarget, type: 'editorContent' })] }); const observeConfigs = () => { const narrowTarget = getSelectionAncestorDOM(editorView); const editorDom = getEditorDom(); return [...(narrowTarget ? [{ target: narrowTarget, options: { childList: true, subtree: true } }] : []), ...(editorDom ? [{ target: editorDom, options: { childList: true } }] : [])]; }; const blockMoveUpExperience = new Experience(EXPERIENCE_ID.MENU_ACTION, { action: ACTION.MOVED, actionSubjectId: ACTION_SUBJECT_ID.MOVE_UP_BLOCK, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), new ExperienceCheckDomMutation({ onDomMutation: handleMoveDomMutation, observeConfig: observeConfigs })] }); const blockMoveDownExperience = new Experience(EXPERIENCE_ID.MENU_ACTION, { action: ACTION.MOVED, actionSubjectId: ACTION_SUBJECT_ID.MOVE_DOWN_BLOCK, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), new ExperienceCheckDomMutation({ onDomMutation: handleMoveDomMutation, observeConfig: observeConfigs })] }); const blockDeleteExperience = new Experience(EXPERIENCE_ID.MENU_ACTION, { action: ACTION.DELETED, actionSubjectId: ACTION_SUBJECT_ID.DELETE_BLOCK, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), new ExperienceCheckDomMutation({ onDomMutation: handleDeleteDomMutation, observeConfig: observeConfigs }), new ExperienceCheckPopupMutation({ nestedElementQuery: `[data-testid="${PORTAL_TEST_ID.SYNC_BLOCK_DELETE_CONFIRMATION}"]`, type: 'portalRoot' })] }); const blockTransformExperience = new Experience(EXPERIENCE_ID.MENU_ACTION, { action: ACTION.TRANSFORMED, actionSubjectId: ACTION_SUBJECT_ID.TRANSFORM_BLOCK, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), new ExperienceCheckDomMutation({ onDomMutation: handleTransformDomMutation, observeConfig: observeConfigs })] }); const blockCopyLinkExperience = new Experience(EXPERIENCE_ID.MENU_ACTION, { action: ACTION.COPIED, actionSubjectId: ACTION_SUBJECT_ID.COPY_LINK_TO_BLOCK, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), new ExperienceCheckPopupMutation({ nestedElementQuery: `[data-testid="${PORTAL_TEST_ID.LINK_COPIED_TO_CLIPBOARD}"]`, type: 'portalRoot' })] }); const handleMenuOpened = method => { // Don't start if block menu is already visible if (isBlockMenuVisible(getPopupsTarget())) { return; } blockMenuOpenExperience.start({ method }); }; const handleTransformActioned = target => { if (!target.closest('[data-testid="editor-turn-into-menu--content"]') || // Skip experience tracking when the clicked item is an extension menu item // (e.g. Jira macro, etc.) - they don't perform block transforms target.closest(`[data-testid="${EXTENSION_MENU_ITEM_TEST_ID}"]`)) { return false; } const turnIntoButton = target.closest('button'); if (turnIntoButton && turnIntoButton instanceof HTMLElement && !turnIntoButton.hasAttribute('disabled') && turnIntoButton.getAttribute('aria-disabled') !== 'true') { blockTransformExperience.start(); } return true; }; const handleItemActioned = target => { if (handleTransformActioned(target)) { return; } const button = target.closest('button[data-testid]'); if (!button || !(button instanceof HTMLButtonElement) || button.disabled || button.getAttribute('aria-disabled') === 'true') { return; } const testId = button.dataset.testid; if (!testId) { return; } switch (testId) { case BLOCK_MENU_ACTION_TEST_ID.MOVE_UP: blockMoveUpExperience.start(); break; case BLOCK_MENU_ACTION_TEST_ID.MOVE_DOWN: blockMoveDownExperience.start(); break; case BLOCK_MENU_ACTION_TEST_ID.DELETE: blockDeleteExperience.start(); break; case BLOCK_MENU_ACTION_TEST_ID.COPY_LINK: blockCopyLinkExperience.start(); break; } }; const unbindClickListener = bind(document, { type: 'click', listener: event => { const target = event.target; if (!(target instanceof HTMLElement)) { return; } if (isDragHandleElement(target)) { handleMenuOpened(START_METHOD.DRAG_HANDLE_CLICK); } else { handleItemActioned(target); } }, options: { capture: true } }); const unbindKeydownListener = bind(document, { type: 'keydown', listener: event => { const target = event.target; if (!(target instanceof HTMLElement)) { return; } // Check if Enter or Space is pressed on a drag handle if ((event.key === 'Enter' || event.key === ' ') && isDragHandleElement(target)) { handleMenuOpened(START_METHOD.KEYBOARD); } // Abort on Escape key if block menu is not yet visible if (event.key === 'Escape' && !isBlockMenuVisible(getPopupsTarget())) { blockMenuOpenExperience.abort({ reason: ABORT_REASON.USER_CANCELED }); } }, options: { capture: true } }); return new SafePlugin({ key: pluginKey, view: view => { editorView = view; return { destroy: () => { blockMenuOpenExperience.abort({ reason: ABORT_REASON.EDITOR_DESTROYED }); blockMoveUpExperience.abort({ reason: ABORT_REASON.EDITOR_DESTROYED }); blockMoveDownExperience.abort({ reason: ABORT_REASON.EDITOR_DESTROYED }); blockDeleteExperience.abort({ reason: ABORT_REASON.EDITOR_DESTROYED }); blockTransformExperience.abort({ reason: ABORT_REASON.EDITOR_DESTROYED }); blockCopyLinkExperience.abort({ reason: ABORT_REASON.EDITOR_DESTROYED }); editorView = undefined; unbindClickListener(); unbindKeydownListener(); } }; } }); };