UNPKG

@atlaskit/editor-plugin-type-ahead

Version:

Type-ahead plugin for @atlaskit/editor-core

186 lines (183 loc) 7.29 kB
import { InsertTypeAheadStep } from '@atlaskit/adf-schema/steps'; import { INPUT_METHOD } from '@atlaskit/editor-common/analytics'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { closest } from '@atlaskit/editor-common/utils'; import { DecorationSet } from '@atlaskit/editor-prosemirror/view'; import { fg } from '@atlaskit/platform-feature-flags/fg'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { ACTIONS } from './actions'; import { openTypeAheadAtCursor } from './commands/open-typeahead-at-cursor'; import { TYPE_AHEAD_DECORATION_DATA_ATTRIBUTE } from './constants'; import { factoryDecorations } from './decorations'; import { isInsertionTransaction } from './isInsertionTransaction'; import { pluginKey } from './key'; import { createReducer } from './reducer'; // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp const ASCII_CHAR_REGEX = /^[\x00-\x7F]$/; const hasValidTypeAheadStep = tr => { const steps = tr.steps.filter(step => step instanceof InsertTypeAheadStep); // There are some cases, like collab rebase, where the steps are re-applied // We should not re open the type-ahead for those cases if (steps.length === 1) { return steps[0]; } return null; }; /** Creates the type-ahead ProseMirror plugin. */ export function createPlugin({ reactDispatch, popupMountRef, typeAheadHandlers, getIntl, nodeViewPortalProviderAPI, api }) { const intl = getIntl(); const { createDecorations, removeDecorations } = factoryDecorations({ intl, nodeViewPortalProviderAPI, popupMountRef, api }); const reducer = createReducer({ createDecorations, removeDecorations, typeAheadHandlers, popupMountRef }); // Tracks a wide-char trigger handler detected during IME composition (e.g. /). // Set by compositionupdate, cleared by compositionend. Used by handleKeyDown // to intercept Enter that confirms the composition. let pendingWideSlashHandler = null; return new SafePlugin({ key: pluginKey, state: { init() { return { typeAheadHandlers, query: '', decorationSet: DecorationSet.empty, decorationElement: null, items: [], sectionTitleUpdates: {}, sections: [], errorInfo: null, selectedIndex: -1, stats: null, inputMethod: null, removePrefixTriggerOnCancel: undefined }; }, apply(tr, currentPluginState, oldEditorState, state) { const customStep = hasValidTypeAheadStep(tr); const nextPluginState = reducer(tr, currentPluginState, customStep); if (currentPluginState !== nextPluginState) { reactDispatch(pluginKey, nextPluginState); } return nextPluginState; } }, appendTransaction(transactions, _oldState, newState) { const insertItemCallback = isInsertionTransaction(transactions, ACTIONS.INSERT_RAW_QUERY); if (insertItemCallback) { const tr = insertItemCallback(newState); if (tr) { if (fg('platform_editor_ease_of_use_metrics')) { var _api$metrics; api === null || api === void 0 ? void 0 : (_api$metrics = api.metrics) === null || _api$metrics === void 0 ? void 0 : _api$metrics.commands.startActiveSessionTimer()({ tr }); } return tr; } } }, view() { return { update(editorView) {} }; }, props: { decorations: state => { var _pluginKey$getState; return (_pluginKey$getState = pluginKey.getState(state)) === null || _pluginKey$getState === void 0 ? void 0 : _pluginKey$getState.decorationSet; }, handleKeyDown: (view, event) => { // When composing a wide-char trigger (e.g. /), intercept the Enter key // that confirms the composition so we can open the typeahead instead. if (pendingWideSlashHandler && event.isComposing && (event.key === 'Enter' || event.keyCode === 13)) { const handler = pendingWideSlashHandler; pendingWideSlashHandler = null; // Defer until ProseMirror has flushed the composed text into its state. setTimeout(() => { const command = openTypeAheadAtCursor({ triggerHandler: handler, inputMethod: INPUT_METHOD.KEYBOARD }); const tr = command({ tr: view.state.tr }); if (tr) { view.dispatch(tr); } }, 0); return true; } return false; }, handleDOMEvents: { compositionupdate: (view, event) => { // When the experiment is on, track whether the current composition // exactly matches a wide-char trigger (e.g. / from Japanese keyboard). // We can't open the typeahead yet because composition is still active, // but we record the matching handler so the next keydown (Enter) can use it. if (expValEquals('platform_editor_wide_slash_trigger', 'isEnabled', true)) { var _event$data, _typeAheadHandlers$fi; const pendingData = (_event$data = event.data) !== null && _event$data !== void 0 ? _event$data : ''; pendingWideSlashHandler = (_typeAheadHandlers$fi = typeAheadHandlers.find(handler => { if (!handler.customRegex) { return false; } // Only match if the composition is a NON-ASCII trigger character. // ASCII triggers (e.g. '/') are handled by the normal input rule // path and must NOT be intercepted here — otherwise typing '/' on // a macOS Japanese IME (which briefly fires compositionupdate with // data='/') would cause the Enter-confirm to open the typeahead. // Matches any single ASCII character (U+0000–U+007F). // No 'u' flag needed for ASCII-only ranges. if (ASCII_CHAR_REGEX.test(pendingData)) { return false; } const pattern = new RegExp(`^(${handler.customRegex})$`, 'u'); return pattern.test(pendingData); })) !== null && _typeAheadHandlers$fi !== void 0 ? _typeAheadHandlers$fi : null; } return false; }, compositionend: (view, event) => { // Clear the pending handler when composition ends (cancelled or committed // via a non-Enter key like Space, which we don't want to intercept). pendingWideSlashHandler = null; return false; }, click: (view, event) => { const { target } = event; // ProseMirror view listen to any click event inside of it // When this event is coming from the typeahead // we should tell to ProseMirror to sit down and relax // cuz we know what we are doing (I hope) if (target instanceof HTMLElement && closest(target, `[data-type-ahead=${TYPE_AHEAD_DECORATION_DATA_ATTRIBUTE}]`)) { return true; } return false; } } } }); }