@atlaskit/editor-plugin-type-ahead
Version:
Type-ahead plugin for @atlaskit/editor-core
192 lines (187 loc) • 7.57 kB
JavaScript
import { TypeAheadAvailableNodes, typeAheadListMessages } from '@atlaskit/editor-common/type-ahead';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { updateSelectedIndex } from './commands/update-selected-index';
import { itemIsDisabled } from './item-is-disabled';
import { pluginKey as typeAheadPluginKey } from './key';
import { StatsModifier } from './stats-modifier';
export const isTypeAheadHandler = handler => {
return handler && Object.values(TypeAheadAvailableNodes).includes(handler.id) && typeof handler.trigger === 'string' && typeof handler.selectItem === 'function' && typeof handler.getItems === 'function';
};
/** Is a typeahead plugin open? */
export const isTypeAheadOpen = editorState => {
var _typeAheadPluginKey$g, _typeAheadPluginKey$g2;
return !!(typeAheadPluginKey !== null && typeAheadPluginKey !== void 0 && (_typeAheadPluginKey$g = typeAheadPluginKey.getState(editorState)) !== null && _typeAheadPluginKey$g !== void 0 && (_typeAheadPluginKey$g2 = _typeAheadPluginKey$g.decorationSet) !== null && _typeAheadPluginKey$g2 !== void 0 && _typeAheadPluginKey$g2.find().length);
};
export const getPluginState = editorState => {
return typeAheadPluginKey.getState(editorState);
};
export const getTypeAheadHandler = editorState => {
var _typeAheadPluginKey$g3;
return (_typeAheadPluginKey$g3 = typeAheadPluginKey.getState(editorState)) === null || _typeAheadPluginKey$g3 === void 0 ? void 0 : _typeAheadPluginKey$g3.triggerHandler;
};
export const getTypeAheadQuery = editorState => {
var _typeAheadPluginKey$g4;
return (_typeAheadPluginKey$g4 = typeAheadPluginKey.getState(editorState)) === null || _typeAheadPluginKey$g4 === void 0 ? void 0 : _typeAheadPluginKey$g4.query;
};
export const isTypeAheadAllowed = state => {
const isOpen = isTypeAheadOpen(state);
// if the TypeAhead is open
// we should not allow it
return !isOpen;
};
export const findHandler = (id, state) => {
const pluginState = typeAheadPluginKey.getState(state);
if (!pluginState || !pluginState.typeAheadHandlers || pluginState.typeAheadHandlers.length === 0) {
return null;
}
const {
typeAheadHandlers
} = pluginState;
return typeAheadHandlers.find(h => h.id === id) || null;
};
export const skipForwardToSafeItem = ({
currentIndex,
nextIndex,
listSize,
itemIsDisabled
}) => {
// Use a loop to find the next selectable item
for (let idx = nextIndex; idx < listSize; idx++) {
if (!itemIsDisabled(idx)) {
return idx;
}
}
// We got to the end of the list ^, now try from the start
if (editorExperiment('platform_editor_offline_editing_web', true)) {
for (let idx = 0; idx < nextIndex; idx++) {
if (!itemIsDisabled(idx)) {
return idx;
}
}
}
// If no selectable items are found, return currentIndex
return currentIndex;
};
export const skipBackwardToSafeItem = ({
currentIndex,
nextIndex,
listSize,
itemIsDisabled
}) => {
// Use a loop to find the next non-selectable item when going backwards
for (let idx = nextIndex; idx >= 0; idx--) {
if (!itemIsDisabled(idx)) {
return idx;
}
}
// We got to the start of the list ^, now try from the end
if (editorExperiment('platform_editor_offline_editing_web', true)) {
for (let idx = listSize; idx > nextIndex; idx--) {
if (!itemIsDisabled(idx)) {
return idx;
}
}
}
// If no non-selectable items are found, return currentIndex
return currentIndex;
};
export const findHandlerByTrigger = ({
trigger,
editorState
}) => {
const pluginState = typeAheadPluginKey.getState(editorState);
if (!pluginState || !pluginState.typeAheadHandlers || pluginState.typeAheadHandlers.length === 0) {
return null;
}
const {
typeAheadHandlers
} = pluginState;
return typeAheadHandlers.find(h => h.trigger === trigger) || null;
};
export const moveSelectedIndex = ({
editorView,
direction,
api
}) => () => {
const typeAheadState = getPluginState(editorView.state);
if (!typeAheadState) {
return;
}
const {
selectedIndex,
items
} = typeAheadState;
const stats = typeAheadState.stats instanceof StatsModifier ? typeAheadState.stats : new StatsModifier();
let nextIndex;
const isDisabled = idx => itemIsDisabled(items[idx], api);
if (direction === 'next') {
stats.increaseArrowDown();
/**
* See: https://product-fabric.atlassian.net/browse/ED-17200
* `selectedIndex` is forced to -1 now to not immediately focus the typeahead
* and only do so when there is explicit logic to focus into the typeahead
* options.
*
* This check for "set index to 1 when -1"
* - is a temporary workaround to get back the previous behaviour without
* entirely reverting the a11y improvements
*
*/
if (selectedIndex === -1 && items.length > 1) {
// If the first item is disabled we actually want to skip to the 3rd item
// on the first arrow down
nextIndex = isDisabled(0) && items.length > 2 ? 2 : 1;
} else {
nextIndex = selectedIndex >= items.length - 1 ? 0 : selectedIndex + 1;
}
nextIndex = skipForwardToSafeItem({
currentIndex: selectedIndex,
nextIndex,
listSize: items.length,
itemIsDisabled: isDisabled
});
} else {
stats.increaseArrowUp();
nextIndex = selectedIndex <= 0 ? items.length - 1 : selectedIndex - 1;
nextIndex = skipBackwardToSafeItem({
currentIndex: selectedIndex,
nextIndex,
listSize: items.length,
itemIsDisabled: isDisabled
});
}
updateSelectedIndex(nextIndex, api)(editorView.state, editorView.dispatch);
};
export const getTypeAheadListAriaLabels = (trigger, intl, item) => {
var _item$mention, _item$mention2, _item$emoji, _item$emoji2, _item$emoji3;
switch (trigger) {
case '@':
return {
popupAriaLabel: intl.formatMessage(typeAheadListMessages.mentionPopupLabel),
listItemAriaLabel: intl.formatMessage(typeAheadListMessages.metionListItemLabel, {
name: (item === null || item === void 0 ? void 0 : (_item$mention = item.mention) === null || _item$mention === void 0 ? void 0 : _item$mention.name) || '',
shortName: (item === null || item === void 0 ? void 0 : (_item$mention2 = item.mention) === null || _item$mention2 === void 0 ? void 0 : _item$mention2.mentionName) || ''
})
};
case '/':
return {
popupAriaLabel: intl.formatMessage(typeAheadListMessages.quickInsertPopupLabel),
listItemAriaLabel: intl.formatMessage(typeAheadListMessages.emojiListItemLabel, {
name: (item === null || item === void 0 ? void 0 : item.title) || '',
shortcut: (item === null || item === void 0 ? void 0 : (_item$emoji = item.emoji) === null || _item$emoji === void 0 ? void 0 : _item$emoji.shortName) || ''
})
};
case ':':
return {
popupAriaLabel: intl.formatMessage(typeAheadListMessages.emojiPopupLabel),
listItemAriaLabel: intl.formatMessage(typeAheadListMessages.emojiListItemLabel, {
name: (item === null || item === void 0 ? void 0 : (_item$emoji2 = item.emoji) === null || _item$emoji2 === void 0 ? void 0 : _item$emoji2.name) || '',
shortcut: (item === null || item === void 0 ? void 0 : (_item$emoji3 = item.emoji) === null || _item$emoji3 === void 0 ? void 0 : _item$emoji3.shortName) || ''
})
};
default:
return {
popupAriaLabel: intl.formatMessage(typeAheadListMessages.typeAheadPopupLabel)
};
}
};