@atlaskit/editor-plugin-type-ahead
Version:
Type-ahead plugin for @atlaskit/editor-core
189 lines (186 loc) • 8.12 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createPlugin = createPlugin;
var _steps = require("@atlaskit/adf-schema/steps");
var _analytics = require("@atlaskit/editor-common/analytics");
var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
var _utils = require("@atlaskit/editor-common/utils");
var _view = require("@atlaskit/editor-prosemirror/view");
var _fg = require("@atlaskit/platform-feature-flags/fg");
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
var _actions = require("./actions");
var _openTypeaheadAtCursor = require("./commands/open-typeahead-at-cursor");
var _constants = require("./constants");
var _decorations = require("./decorations");
var _isInsertionTransaction = require("./isInsertionTransaction");
var _key = require("./key");
var _reducer = require("./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 _steps.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. */
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 = (0, _decorations.factoryDecorations)({
intl: intl,
nodeViewPortalProviderAPI: nodeViewPortalProviderAPI,
popupMountRef: popupMountRef,
api: api
}),
createDecorations = _factoryDecorations.createDecorations,
removeDecorations = _factoryDecorations.removeDecorations;
var reducer = (0, _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.SafePlugin({
key: _key.pluginKey,
state: {
init: function init() {
return {
typeAheadHandlers: typeAheadHandlers,
query: '',
decorationSet: _view.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(_key.pluginKey, nextPluginState);
}
return nextPluginState;
}
},
appendTransaction: function appendTransaction(transactions, _oldState, newState) {
var insertItemCallback = (0, _isInsertionTransaction.isInsertionTransaction)(transactions, _actions.ACTIONS.INSERT_RAW_QUERY);
if (insertItemCallback) {
var tr = insertItemCallback(newState);
if (tr) {
if ((0, _fg.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 = _key.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 = (0, _openTypeaheadAtCursor.openTypeAheadAtCursor)({
triggerHandler: handler,
inputMethod: _analytics.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 ((0, _expValEquals.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 && (0, _utils.closest)(target, "[data-type-ahead=".concat(_constants.TYPE_AHEAD_DECORATION_DATA_ATTRIBUTE, "]"))) {
return true;
}
return false;
}
}
}
});
}