UNPKG

@atlaskit/editor-plugin-type-ahead

Version:

Type-ahead plugin for @atlaskit/editor-core

184 lines (181 loc) 7.88 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 var ASCII_CHAR_REGEX = /^[\x00-\x7F]$/; var hasValidTypeAheadStep = function hasValidTypeAheadStep(tr) { var steps = tr.steps.filter(function (step) { return 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(_ref) { var reactDispatch = _ref.reactDispatch, popupMountRef = _ref.popupMountRef, typeAheadHandlers = _ref.typeAheadHandlers, getIntl = _ref.getIntl, nodeViewPortalProviderAPI = _ref.nodeViewPortalProviderAPI, api = _ref.api; var intl = getIntl(); var _factoryDecorations = factoryDecorations({ intl: intl, nodeViewPortalProviderAPI: nodeViewPortalProviderAPI, popupMountRef: popupMountRef, api: api }), createDecorations = _factoryDecorations.createDecorations, removeDecorations = _factoryDecorations.removeDecorations; var reducer = createReducer({ createDecorations: createDecorations, removeDecorations: removeDecorations, typeAheadHandlers: typeAheadHandlers, popupMountRef: 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. var pendingWideSlashHandler = null; return new SafePlugin({ key: pluginKey, state: { init: function init() { return { typeAheadHandlers: typeAheadHandlers, query: '', decorationSet: DecorationSet.empty, decorationElement: null, items: [], sectionTitleUpdates: {}, sections: [], errorInfo: null, selectedIndex: -1, stats: null, inputMethod: null, removePrefixTriggerOnCancel: undefined }; }, apply: function apply(tr, currentPluginState, oldEditorState, state) { var customStep = hasValidTypeAheadStep(tr); var nextPluginState = reducer(tr, currentPluginState, customStep); if (currentPluginState !== nextPluginState) { reactDispatch(pluginKey, nextPluginState); } return nextPluginState; } }, appendTransaction: function appendTransaction(transactions, _oldState, newState) { var insertItemCallback = isInsertionTransaction(transactions, ACTIONS.INSERT_RAW_QUERY); if (insertItemCallback) { var tr = insertItemCallback(newState); if (tr) { if (fg('platform_editor_ease_of_use_metrics')) { var _api$metrics; api === null || api === void 0 || (_api$metrics = api.metrics) === null || _api$metrics === void 0 || _api$metrics.commands.startActiveSessionTimer()({ tr: tr }); } return tr; } } }, view: function view() { return { update: function update(editorView) {} }; }, props: { decorations: function decorations(state) { var _pluginKey$getState; return (_pluginKey$getState = pluginKey.getState(state)) === null || _pluginKey$getState === void 0 ? void 0 : _pluginKey$getState.decorationSet; }, handleKeyDown: function 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)) { var handler = pendingWideSlashHandler; pendingWideSlashHandler = null; // Defer until ProseMirror has flushed the composed text into its state. setTimeout(function () { var command = openTypeAheadAtCursor({ triggerHandler: handler, inputMethod: INPUT_METHOD.KEYBOARD }); var tr = command({ tr: view.state.tr }); if (tr) { view.dispatch(tr); } }, 0); return true; } return false; }, handleDOMEvents: { compositionupdate: function 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; var pendingData = (_event$data = event.data) !== null && _event$data !== void 0 ? _event$data : ''; pendingWideSlashHandler = (_typeAheadHandlers$fi = typeAheadHandlers.find(function (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; } var pattern = new RegExp("^(".concat(handler.customRegex, ")$"), 'u'); return pattern.test(pendingData); })) !== null && _typeAheadHandlers$fi !== void 0 ? _typeAheadHandlers$fi : null; } return false; }, compositionend: function 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: function click(view, event) { var target = event.target; // 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=".concat(TYPE_AHEAD_DECORATION_DATA_ATTRIBUTE, "]"))) { return true; } return false; } } } }); }