@atlaskit/editor-plugin-quick-insert
Version:
Quick insert plugin for @atlaskit/editor-core
152 lines • 4.8 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';
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');
};