@atlaskit/editor-plugin-toolbar
Version:
Toolbar plugin for @atlaskit/editor-core
189 lines • 6.11 kB
JavaScript
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';
};