UNPKG

@atlaskit/editor-plugin-synced-block

Version:

SyncedBlock plugin for @atlaskit/editor-core

409 lines 15.3 kB
import { bind } from 'bind-event-listener'; import { ACTION, ACTION_SUBJECT_ID } from '@atlaskit/editor-common/analytics'; import { Experience, EXPERIENCE_ID, ExperienceCheckDomMutation, ExperienceCheckTimeout, getNodeQuery, getPopupContainerFromEditorView, popupWithNestedElement, getSelectionAncestorDOM } 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'; import { SYNCED_BLOCK_BUTTON_TEST_ID } from '../types'; const TIMEOUT_DURATION = 30000; const pluginKey = new PluginKey('syncedBlockMenuAndToolbarExperience'); const SYNCED_BLOCK_BUTTON_TEST_IDS = Object.values(SYNCED_BLOCK_BUTTON_TEST_ID); const syncedBlockButtonIds = new Set(SYNCED_BLOCK_BUTTON_TEST_IDS); let targetEl; export const getMenuAndToolbarExperiencesPlugin = ({ refs, dispatchAnalyticsEvent }) => { let popupsTargetEl; const editorViewRef = { current: undefined }; const getPopupsTarget = () => { if (!popupsTargetEl) { var _editorViewRef$curren; popupsTargetEl = refs.popupsMountPoint || refs.wrapperElement || getPopupContainerFromEditorView(editorViewRef === null || editorViewRef === void 0 ? void 0 : (_editorViewRef$curren = editorViewRef.current) === null || _editorViewRef$curren === void 0 ? void 0 : _editorViewRef$curren.dom); } return popupsTargetEl; }; const createSourcePrimaryToolbarExperience = new Experience(EXPERIENCE_ID.TOOLBAR_ACTION, { action: ACTION.SYNCED_BLOCK_CREATE, actionSubjectId: ACTION_SUBJECT_ID.PRIMARY_TOOLBAR, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), syncedBlockAddedToDomCheck(refs, editorViewRef)] }); const createSourceBlockMenuExperience = new Experience(EXPERIENCE_ID.MENU_ACTION, { action: ACTION.SYNCED_BLOCK_CREATE, actionSubjectId: ACTION_SUBJECT_ID.BLOCK_MENU, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), syncedBlockAddedToDomCheck(refs, editorViewRef)] }); const createSourceQuickInsertMenuExperience = new Experience(EXPERIENCE_ID.MENU_ACTION, { action: ACTION.SYNCED_BLOCK_CREATE, actionSubjectId: ACTION_SUBJECT_ID.QUICK_INSERT, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), syncedBlockAddedToDomCheck(refs, editorViewRef)] }); const deleteReferenceSyncedBlockExperience = new Experience(EXPERIENCE_ID.TOOLBAR_ACTION, { action: ACTION.REFERENCE_SYNCED_BLOCK_DELETE, actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_TOOLBAR, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), referenceSyncBlockRemovedFromDomCheck(refs, editorViewRef)] }); const unsyncReferenceSyncedBlockExperience = new Experience(EXPERIENCE_ID.TOOLBAR_ACTION, { action: ACTION.REFERENCE_SYNCED_BLOCK_UNSYNC, actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_TOOLBAR, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), referenceSyncBlockRemovedFromDomCheck(refs, editorViewRef)] }); const unsyncSourceSyncedBlockExperience = new Experience(EXPERIENCE_ID.TOOLBAR_ACTION, { action: ACTION.SYNCED_BLOCK_UNSYNC, actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_TOOLBAR, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), syncBlockDeleteConfirmationModalAddedCheck()] }); const deleteSourceSyncedBlockExperience = new Experience(EXPERIENCE_ID.TOOLBAR_ACTION, { action: ACTION.SYNCED_BLOCK_DELETE, actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_TOOLBAR, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), syncBlockDeleteConfirmationModalAddedCheck()] }); const syncedLocationsExperience = new Experience(EXPERIENCE_ID.TOOLBAR_ACTION, { action: ACTION.SYNCED_BLOCK_VIEW_SYNCED_LOCATIONS, actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_TOOLBAR, dispatchAnalyticsEvent, checks: [new ExperienceCheckTimeout({ durationMs: TIMEOUT_DURATION }), syncedLocationsDropdownOpenedCheck()] }); const unbindClickListener = bind(document, { type: 'click', listener: event => { const target = event.target; if (!target) { return; } const button = target.closest('button[data-testid]'); if (!button || !(button instanceof HTMLButtonElement)) { return; } const testId = button.dataset.testid; if (!isSyncedBlockButtonId(testId)) { return; } if (button.disabled) { return; } handleButtonClick({ testId, button, createSourcePrimaryToolbarExperience, createSourceBlockMenuExperience, createSourceQuickInsertMenuExperience, deleteReferenceSyncedBlockExperience, unsyncReferenceSyncedBlockExperience, unsyncSourceSyncedBlockExperience, deleteSourceSyncedBlockExperience, syncedLocationsExperience }); }, options: { capture: true } }); const unbindKeydownListener = bind(document, { type: 'keydown', listener: event => { if (isEnterKey(event.key)) { const typeaheadPopup = popupWithNestedElement(getPopupsTarget(), '.fabric-editor-typeahead'); if (!typeaheadPopup || !(typeaheadPopup instanceof HTMLElement)) { return; } const targetElement = fg('platform_synced_block_fix_experience_tracking') ? typeaheadPopup.querySelector('[role="option"][aria-selected="true"]') : typeaheadPopup.querySelector('[role="option"]'); if (!targetElement || !(targetElement instanceof HTMLElement)) { return; } const testId = targetElement.dataset.testid; if (testId === SYNCED_BLOCK_BUTTON_TEST_ID.quickInsertCreate) { createSourceQuickInsertMenuExperience.start(); } } }, options: { capture: true } }); return new SafePlugin({ key: pluginKey, view: view => { editorViewRef.current = view; return { destroy: () => { createSourcePrimaryToolbarExperience.abort({ reason: 'editorDestroyed' }); createSourceBlockMenuExperience.abort({ reason: 'editorDestroyed' }); createSourceQuickInsertMenuExperience.abort({ reason: 'editorDestroyed' }); deleteReferenceSyncedBlockExperience.abort({ reason: 'editorDestroyed' }); deleteSourceSyncedBlockExperience === null || deleteSourceSyncedBlockExperience === void 0 ? void 0 : deleteSourceSyncedBlockExperience.abort({ reason: 'editorDestroyed' }); unsyncReferenceSyncedBlockExperience === null || unsyncReferenceSyncedBlockExperience === void 0 ? void 0 : unsyncReferenceSyncedBlockExperience.abort({ reason: 'editorDestroyed' }); unsyncSourceSyncedBlockExperience === null || unsyncSourceSyncedBlockExperience === void 0 ? void 0 : unsyncSourceSyncedBlockExperience.abort({ reason: 'editorDestroyed' }); syncedLocationsExperience === null || syncedLocationsExperience === void 0 ? void 0 : syncedLocationsExperience.abort({ reason: 'editorDestroyed' }); unbindClickListener(); unbindKeydownListener(); } }; } }); }; const isSyncedBlockButtonId = value => { return !!value && syncedBlockButtonIds.has(value); }; const handleButtonClick = ({ testId, button, createSourcePrimaryToolbarExperience, createSourceBlockMenuExperience, createSourceQuickInsertMenuExperience, deleteReferenceSyncedBlockExperience, unsyncReferenceSyncedBlockExperience, unsyncSourceSyncedBlockExperience, deleteSourceSyncedBlockExperience, syncedLocationsExperience }) => { switch (testId) { case SYNCED_BLOCK_BUTTON_TEST_ID.primaryToolbarCreate: createSourcePrimaryToolbarExperience.start({ forceRestart: true }); break; case SYNCED_BLOCK_BUTTON_TEST_ID.blockMenuCreate: createSourceBlockMenuExperience.start({ forceRestart: true }); break; case SYNCED_BLOCK_BUTTON_TEST_ID.quickInsertCreate: createSourceQuickInsertMenuExperience.start({ forceRestart: true }); break; case SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarReferenceDelete: deleteReferenceSyncedBlockExperience.start({ forceRestart: true }); break; case SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarReferenceUnsync: unsyncReferenceSyncedBlockExperience === null || unsyncReferenceSyncedBlockExperience === void 0 ? void 0 : unsyncReferenceSyncedBlockExperience.start({ forceRestart: true }); break; case SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarSourceUnsync: unsyncSourceSyncedBlockExperience === null || unsyncSourceSyncedBlockExperience === void 0 ? void 0 : unsyncSourceSyncedBlockExperience.start({ forceRestart: true }); break; case SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarSourceDelete: deleteSourceSyncedBlockExperience === null || deleteSourceSyncedBlockExperience === void 0 ? void 0 : deleteSourceSyncedBlockExperience.start({ forceRestart: true }); break; case SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarSyncedLocationsTrigger: // Only track when opening the dropdown if (button.getAttribute('aria-pressed') === 'false') { syncedLocationsExperience === null || syncedLocationsExperience === void 0 ? void 0 : syncedLocationsExperience.start({ forceRestart: true }); } break; default: { // Exhaustiveness check: if a new SyncedBlockToolbarButtonId is added // but not handled above, TypeScript will error here. const _exhaustiveCheck = testId; return _exhaustiveCheck; } } }; const isEnterKey = key => { return key === 'Enter'; }; const getTarget = containerElement => { if (!targetEl) { const element = containerElement === null || containerElement === void 0 ? void 0 : containerElement.querySelector('.ProseMirror'); if (!element || !(element instanceof HTMLElement)) { return null; } targetEl = element; } return targetEl; }; const syncedBlockAddedToDomCheck = (refs, editorViewRef) => new ExperienceCheckDomMutation({ onDomMutation: ({ mutations }) => { if (mutations.some(isBodiedSyncBlockAddedInMutation)) { return { status: 'success' }; } return undefined; }, observeConfig: () => { var _editorViewRef$curren2; return [{ target: fg('platform_synced_block_fix_experience_tracking') ? editorViewRef === null || editorViewRef === void 0 ? void 0 : (_editorViewRef$curren2 = editorViewRef.current) === null || _editorViewRef$curren2 === void 0 ? void 0 : _editorViewRef$curren2.dom : getTarget(refs.containerElement), options: { childList: true } }, // When wrapping a node with breakout mark with sync block, breakout dom is reused // hence we need to observe subtree to catch sync block mutation ...(fg('platform_synced_block_fix_experience_tracking') ? [{ target: getSelectionAncestorDOM(editorViewRef === null || editorViewRef === void 0 ? void 0 : editorViewRef.current), options: { childList: true, subtree: true } }] : [])]; } }); const isBodiedSyncBlockAddedInMutation = ({ type, addedNodes }) => { return type === 'childList' && [...addedNodes].some(isBodiedSyncBlockWithinNode); }; const isBodiedSyncBlockWithinNode = node => getNodeQuery('[data-prosemirror-node-name="bodiedSyncBlock"]')(node); const referenceSyncBlockRemovedFromDomCheck = (refs, editorViewRef) => new ExperienceCheckDomMutation({ onDomMutation: ({ mutations }) => { if (mutations.some(isSyncBlockRemovedInMutation)) { return { status: 'success' }; } return undefined; }, observeConfig: () => { var _editorViewRef$curren3; return [{ target: fg('platform_synced_block_fix_experience_tracking') ? editorViewRef === null || editorViewRef === void 0 ? void 0 : (_editorViewRef$curren3 = editorViewRef.current) === null || _editorViewRef$curren3 === void 0 ? void 0 : _editorViewRef$curren3.dom : getTarget(refs.containerElement), options: { childList: true } }, ...(fg('platform_synced_block_fix_experience_tracking') ? [{ target: getSelectionAncestorDOM(editorViewRef === null || editorViewRef === void 0 ? void 0 : editorViewRef.current), options: { childList: true, subtree: true } }] : [])]; } }); const isSyncBlockRemovedInMutation = ({ type, removedNodes }) => { return type === 'childList' && [...removedNodes].some(isSyncBlockWithinNode); }; const isSyncBlockWithinNode = node => getNodeQuery('[data-prosemirror-node-name="syncBlock"]')(node); const syncBlockDeleteConfirmationModalAddedCheck = () => new ExperienceCheckDomMutation({ onDomMutation: ({ mutations }) => { if (mutations.some(isDeleteConfirmationModalAddedInMutation)) { return { status: 'success' }; } return undefined; }, observeConfig: () => { return { target: document.body, options: { childList: true, subtree: true } }; } }); const isDeleteConfirmationModalAddedInMutation = ({ type, addedNodes }) => { return type === 'childList' && [...addedNodes].some(isDeleteConfirmationModalWithinNode); }; const isDeleteConfirmationModalWithinNode = node => getNodeQuery('[data-testid="sync-block-delete-confirmation"]')(node); const syncedLocationsDropdownOpenedCheck = () => new ExperienceCheckDomMutation({ onDomMutation: ({ mutations }) => { if (mutations.some(isSyncedLocationsDropdownErrorInMutation)) { return { status: 'failure' }; } if (mutations.some(isSyncedLocationsDropdownAddedInMutation)) { return { status: 'success' }; } return undefined; }, observeConfig: () => { return { target: document.body, options: { childList: true, subtree: true } }; } }); const isSyncedLocationsDropdownAddedInMutation = ({ type, addedNodes }) => { return type === 'childList' && [...addedNodes].some(isSyncedLocationsDropdownWithinNode); }; const isSyncedLocationsDropdownErrorInMutation = ({ type, addedNodes }) => { return type === 'childList' && [...addedNodes].some(isSyncedLocationsDropdownErrorWithinNode); }; const isSyncedLocationsDropdownWithinNode = node => { return !!(getNodeQuery('[data-testid="synced-locations-dropdown-content"]')(node) || getNodeQuery('[data-testid="synced-locations-dropdown-content-no-results"]')(node)); }; const isSyncedLocationsDropdownErrorWithinNode = node => { return !!getNodeQuery('[data-testid="synced-locations-dropdown-content-error"]')(node); };