@atlaskit/editor-plugin-synced-block
Version:
SyncedBlock plugin for @atlaskit/editor-core
409 lines • 15.3 kB
JavaScript
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);
};