UNPKG

@atlaskit/editor-plugin-emoji

Version:

Emoji plugin for @atlaskit/editor-core

566 lines (559 loc) 25.9 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.createRateLimitReachedFunction = createRateLimitReachedFunction; exports.emojiToTypeaheadItem = exports.emojiPluginKey = exports.emojiPlugin = void 0; exports.memoize = memoize; var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); var _react = _interopRequireWildcard(require("react")); var _analytics = require("@atlaskit/editor-common/analytics"); var _keymaps = require("@atlaskit/editor-common/keymaps"); var _messages = require("@atlaskit/editor-common/messages"); var _quickInsert = require("@atlaskit/editor-common/quick-insert"); var _safePlugin = require("@atlaskit/editor-common/safe-plugin"); var _typeAhead = require("@atlaskit/editor-common/type-ahead"); var _utils = require("@atlaskit/editor-common/utils"); var _state = require("@atlaskit/editor-prosemirror/state"); var _emoji2 = require("@atlaskit/emoji"); var _comment = _interopRequireDefault(require("@atlaskit/icon/core/comment")); var _experiments = require("@atlaskit/tmp-editor-statsig/experiments"); var _insertEmoji = require("./editor-commands/insert-emoji"); var _emojiNodeSpec = require("./nodeviews/emojiNodeSpec"); var _EmojiNodeView = require("./nodeviews/EmojiNodeView"); var _actions = require("./pm-plugins/actions"); var _asciiInputRules = require("./pm-plugins/ascii-input-rules"); var _InlineEmojiPopup = require("./ui/InlineEmojiPopup"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } var emojiToTypeaheadItem = exports.emojiToTypeaheadItem = function emojiToTypeaheadItem(emoji, emojiProvider) { return { title: emoji.shortName || '', key: emoji.id || emoji.shortName, render: function render(_ref) { var isSelected = _ref.isSelected, onClick = _ref.onClick, onHover = _ref.onHover; return /*#__PURE__*/_react.default.createElement(_emoji2.EmojiTypeAheadItem, { emoji: emoji, selected: isSelected, onMouseMove: onHover, onSelection: onClick, emojiProvider: emojiProvider }); }, emoji: emoji }; }; function memoize(fn) { // Cache results here var seen = new Map(); function memoized(emoji, emojiProvider) { // Check cache for hits var hit = seen.get(emoji.id || emoji.shortName); if (hit) { return hit; } // Generate new result and cache it var result = fn(emoji, emojiProvider); seen.set(emoji.id || emoji.shortName, result); return result; } return { call: memoized, clear: seen.clear.bind(seen) }; } var memoizedToItem = memoize(emojiToTypeaheadItem); var defaultListLimit = 50; var isFullShortName = function isFullShortName(query) { return query && query.length > 1 && query.charAt(0) === ':' && query.charAt(query.length - 1) === ':'; }; var TRIGGER = ':'; function delayUntilIdle(cb) { if (typeof window === 'undefined') { return; } // eslint-disable-next-line compat/compat if (window.requestIdleCallback !== undefined) { // eslint-disable-next-line compat/compat return window.requestIdleCallback(function () { return cb(); }, { timeout: 500 }); } return window.requestAnimationFrame(function () { return cb(); }); } /** * Emoji plugin to be added to an `EditorPresetBuilder` and used with `ComposableEditor` * from `@atlaskit/editor-core`. */ var emojiPlugin = exports.emojiPlugin = function emojiPlugin(_ref2) { var _api$base, _api$analytics5; var options = _ref2.config, api = _ref2.api; var previousEmojiProvider; var typeAhead = { id: _typeAhead.TypeAheadAvailableNodes.EMOJI, trigger: TRIGGER, // Custom regex must have a capture group around trigger // so it's possible to use it without needing to scan through all triggers again customRegex: '\\(?(:)', headless: options ? options.headless : undefined, getItems: function getItems(_ref3) { var query = _ref3.query, editorState = _ref3.editorState; var pluginState = getEmojiPluginState(editorState); var emojiProvider = pluginState.emojiProvider; if (!emojiProvider) { return Promise.resolve([]); } return new Promise(function (resolve) { var emojiProviderChangeHandler = { result: function result(emojiResult) { if (!emojiResult || !emojiResult.emojis) { resolve([]); } else { var emojiItems = emojiResult.emojis.map(function (emoji) { return memoizedToItem.call(emoji, emojiProvider); }); resolve(emojiItems); } emojiProvider.unsubscribe(emojiProviderChangeHandler); } }; emojiProvider.subscribe(emojiProviderChangeHandler); emojiProvider.filter(TRIGGER.concat(query), { limit: defaultListLimit, skinTone: emojiProvider.getSelectedTone(), sort: !query.length ? _emoji2.SearchSort.UsageFrequency : _emoji2.SearchSort.Default }); }); }, forceSelect: function forceSelect(_ref4) { var query = _ref4.query, items = _ref4.items, editorState = _ref4.editorState; var _ref5 = emojiPluginKey.getState(editorState) || {}, asciiMap = _ref5.asciiMap; var normalizedQuery = TRIGGER.concat(query); // if the query has space at the end // check the ascii map for emojis if (!(options !== null && options !== void 0 && options.disableAutoformat) && asciiMap && normalizedQuery.length >= 3 && normalizedQuery.endsWith(' ') && asciiMap.has(normalizedQuery.trim())) { var _emoji = asciiMap.get(normalizedQuery.trim()); return { title: (_emoji === null || _emoji === void 0 ? void 0 : _emoji.name) || '', emoji: _emoji }; } var matchedItem = isFullShortName(normalizedQuery) ? items.find(function (item) { return item.title.toLowerCase() === normalizedQuery; }) : undefined; return matchedItem; }, selectItem: function selectItem(state, item, insert, _ref6) { var _api$analytics3; var mode = _ref6.mode; var emojiPluginState = emojiPluginKey.getState(state); if (emojiPluginState.emojiProvider && emojiPluginState.emojiProvider.recordSelection && item.emoji) { var _api$analytics$shared, _api$analytics, _api$analytics$shared2, _api$analytics2; emojiPluginState.emojiProvider.recordSelection(item.emoji).then((0, _emoji2.recordSelectionSucceededSli)(item.emoji, { createAnalyticsEvent: (_api$analytics$shared = api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.sharedState.currentState()) === null || _api$analytics === void 0 ? void 0 : _api$analytics.createAnalyticsEvent) !== null && _api$analytics$shared !== void 0 ? _api$analytics$shared : undefined })).catch((0, _emoji2.recordSelectionFailedSli)(item.emoji, { createAnalyticsEvent: (_api$analytics$shared2 = api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 || (_api$analytics2 = _api$analytics2.sharedState.currentState()) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.createAnalyticsEvent) !== null && _api$analytics$shared2 !== void 0 ? _api$analytics$shared2 : undefined })); } var fragment = (0, _insertEmoji.createEmojiFragment)(state.doc, state.selection.$head, item.emoji); var tr = insert(fragment); api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 || _api$analytics3.actions.attachAnalyticsEvent({ action: _analytics.ACTION.INSERTED, actionSubject: _analytics.ACTION_SUBJECT.DOCUMENT, actionSubjectId: _analytics.ACTION_SUBJECT_ID.EMOJI, attributes: { inputMethod: _analytics.INPUT_METHOD.TYPEAHEAD }, eventType: _analytics.EVENT_TYPE.TRACK })(tr); return tr; } }; api === null || api === void 0 || (_api$base = api.base) === null || _api$base === void 0 || _api$base.actions.registerMarks(function (_ref7) { var tr = _ref7.tr, node = _ref7.node, pos = _ref7.pos; var doc = tr.doc; var schema = doc.type.schema; var emojiNodeType = schema.nodes.emoji; if (node.type === emojiNodeType) { var newText = node.attrs.text; var currentPos = tr.mapping.map(pos); tr.replaceWith(currentPos, currentPos + node.nodeSize, schema.text(newText, node.marks)); } }); return { name: 'emoji', nodes: function nodes() { return [{ name: 'emoji', node: (0, _emojiNodeSpec.emojiNodeSpec)() }]; }, usePluginHook: function usePluginHook() { (0, _react.useEffect)(function () { delayUntilIdle(function () { (0, _emoji2.preloadEmojiPicker)(); }); }, []); }, pmPlugins: function pmPlugins() { return [{ name: 'emoji', plugin: function plugin(pmPluginFactoryParams) { return createEmojiPlugin(pmPluginFactoryParams, options, api); } }, { name: 'emojiAsciiInputRule', plugin: function plugin(_ref8) { var _api$analytics4; var schema = _ref8.schema; return (0, _asciiInputRules.inputRulePlugin)(schema, api === null || api === void 0 || (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 ? void 0 : _api$analytics4.actions, api, options === null || options === void 0 ? void 0 : options.disableAutoformat); } }]; }, getSharedState: function getSharedState(editorState) { var _emojiPluginKey$getSt; if (!editorState) { return undefined; } var _ref9 = (_emojiPluginKey$getSt = emojiPluginKey.getState(editorState)) !== null && _emojiPluginKey$getSt !== void 0 ? _emojiPluginKey$getSt : {}, emojiResourceConfig = _ref9.emojiResourceConfig, asciiMap = _ref9.asciiMap, emojiProvider = _ref9.emojiProvider, inlineEmojiPopupOpen = _ref9.inlineEmojiPopupOpen, emojiProviderPromise = _ref9.emojiProviderPromise; return { emojiResourceConfig: emojiResourceConfig, asciiMap: asciiMap, typeAheadHandler: typeAhead, emojiProvider: emojiProvider, emojiProviderPromise: emojiProviderPromise, inlineEmojiPopupOpen: inlineEmojiPopupOpen }; }, actions: { openTypeAhead: (0, _actions.openTypeAhead)(typeAhead, api), setProvider: function () { var _setProvider = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(providerPromise) { var _api$core$actions$exe; var provider; return _regenerator.default.wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: _context.next = 2; return providerPromise; case 2: provider = _context.sent; if (!(previousEmojiProvider === provider || (options === null || options === void 0 ? void 0 : options.emojiProvider) === providerPromise)) { _context.next = 5; break; } return _context.abrupt("return", false); case 5: previousEmojiProvider = provider; return _context.abrupt("return", (_api$core$actions$exe = api === null || api === void 0 ? void 0 : api.core.actions.execute(function (_ref0) { var tr = _ref0.tr; return setProviderTr(provider)(tr); })) !== null && _api$core$actions$exe !== void 0 ? _api$core$actions$exe : false); case 7: case "end": return _context.stop(); } }, _callee); })); function setProvider(_x) { return _setProvider.apply(this, arguments); } return setProvider; }() }, commands: { insertEmoji: (0, _insertEmoji.insertEmoji)(api === null || api === void 0 || (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions) }, contentComponent: function contentComponent(_ref1) { var editorView = _ref1.editorView, popupsBoundariesElement = _ref1.popupsBoundariesElement, popupsMountPoint = _ref1.popupsMountPoint, popupsScrollableElement = _ref1.popupsScrollableElement; if (!api || (0, _experiments.editorExperiment)('platform_editor_controls', 'control') || !editorView) { return null; } return /*#__PURE__*/_react.default.createElement(_InlineEmojiPopup.InlineEmojiPopup, { api: api, editorView: editorView, popupsBoundariesElement: popupsBoundariesElement, popupsMountPoint: popupsMountPoint, popupsScrollableElement: popupsScrollableElement }); }, pluginsOptions: { quickInsert: function quickInsert(_ref10) { var formatMessage = _ref10.formatMessage; return [{ id: 'emoji', title: formatMessage(_messages.toolbarInsertBlockMessages.emoji), description: formatMessage(_messages.toolbarInsertBlockMessages.emojiDescription), priority: 500, keyshortcut: ':', isDisabledOffline: false, icon: function icon() { return /*#__PURE__*/_react.default.createElement(_quickInsert.IconEmoji, null); }, action: function action(insert) { var _api$typeAhead; if ((0, _experiments.editorExperiment)('platform_editor_controls', 'variant1', { exposure: true })) { // Clear slash var _tr = insert(''); _tr = (0, _actions.setInlineEmojiPopupOpen)(true)(_tr); return _tr; } var tr = insert(undefined); api === null || api === void 0 || (_api$typeAhead = api.typeAhead) === null || _api$typeAhead === void 0 || _api$typeAhead.actions.openAtTransaction({ triggerHandler: typeAhead, inputMethod: _analytics.INPUT_METHOD.QUICK_INSERT })(tr); return tr; } }]; }, typeAhead: typeAhead, floatingToolbar: function floatingToolbar(state, intl) { var isViewMode = function isViewMode() { var _api$editorViewMode; return (api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode) === 'view'; }; if (!isViewMode()) { return undefined; } var onClick = function onClick(stateFromClickEvent, dispatch) { var _api$analytics6, _api$annotation; if (!(api !== null && api !== void 0 && api.annotation)) { return true; } if (api !== null && api !== void 0 && (_api$analytics6 = api.analytics) !== null && _api$analytics6 !== void 0 && _api$analytics6.actions) { var _api$analytics7; api === null || api === void 0 || (_api$analytics7 = api.analytics) === null || _api$analytics7 === void 0 || (_api$analytics7 = _api$analytics7.actions) === null || _api$analytics7 === void 0 || _api$analytics7.fireAnalyticsEvent({ action: _analytics.ACTION.CLICKED, actionSubject: _analytics.ACTION_SUBJECT.BUTTON, actionSubjectId: _analytics.ACTION_SUBJECT_ID.CREATE_INLINE_COMMENT_FROM_HIGHLIGHT_ACTIONS_MENU, eventType: _analytics.EVENT_TYPE.UI, attributes: { source: 'highlightActionsMenu', pageMode: 'edit', sourceNode: 'emoji' } }); } var command = (_api$annotation = api.annotation) === null || _api$annotation === void 0 || (_api$annotation = _api$annotation.actions) === null || _api$annotation === void 0 ? void 0 : _api$annotation.setInlineCommentDraftState(true, _analytics.INPUT_METHOD.TOOLBAR); return command(stateFromClickEvent, dispatch); }; return { title: 'Emoji floating toolbar', nodeType: [state.schema.nodes.emoji], onPositionCalculated: (0, _utils.calculateToolbarPositionAboveSelection)('Emoji floating toolbar'), items: function items(node) { var _api$annotation2; var annotationState = api === null || api === void 0 || (_api$annotation2 = api.annotation) === null || _api$annotation2 === void 0 ? void 0 : _api$annotation2.sharedState.currentState(); var activeCommentMark = node.marks.find(function (mark) { return mark.type.name === 'annotation' && (annotationState === null || annotationState === void 0 ? void 0 : annotationState.annotations[mark.attrs.id]) === false; }); var showAnnotation = annotationState && annotationState.isVisible && !annotationState.bookmark && !annotationState.mouseData.isSelecting && !activeCommentMark && isViewMode(); if (showAnnotation) { return [{ type: 'button', showTitle: true, testId: 'add-comment-emoji-button', icon: _comment.default, title: intl.formatMessage(_messages.annotationMessages.createComment), onClick: onClick, tooltipContent: /*#__PURE__*/_react.default.createElement(_keymaps.ToolTipContent, { description: intl.formatMessage(_messages.annotationMessages.createComment) }), supportsViewMode: true }]; } return []; } }; } } }; }; /** * Actions */ var setAsciiMap = function setAsciiMap(asciiMap) { return function (state, dispatch) { if (dispatch) { var tr = (0, _actions.setAsciiMap)(asciiMap)(state.tr); dispatch(tr); } return true; }; }; /** * * Wrapper to call `onLimitReached` when a specified number of calls of that function * have been made within a time period. * * Note: It does not rate limit * * @param fn Function to wrap * @param limitTime Time limit in milliseconds * @param limitCount Number of function calls before `onRateReached` is called (per time period) * @returns Wrapped function */ function createRateLimitReachedFunction(fn, limitTime, limitCount, onLimitReached) { var lastCallTime = 0; var callCount = 0; return function wrappedFn() { var now = Date.now(); if (now - lastCallTime < limitTime) { if (++callCount > limitCount) { onLimitReached === null || onLimitReached === void 0 || onLimitReached(); } } else { lastCallTime = now; callCount = 1; } return fn.apply(void 0, arguments); }; } // At this stage console.error only var logRateWarning = function logRateWarning() { if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console console.error('The emoji provider injected in the Editor is being reloaded frequently, this will cause a slow Editor experience.'); } }; var setProviderTr = createRateLimitReachedFunction(function (provider) { return function (tr) { return (0, _actions.setProvider)(provider)(tr); }; }, // If we change the emoji provider more than three times every 5 seconds we should warn. // This seems like a really long time but the performance can be that laggy that we don't // even get 3 events in 3 seconds and miss this indicator. 5000, 3, logRateWarning); var setProvider = function setProvider(provider) { return function (state, dispatch) { if (dispatch) { var tr = setProviderTr(provider)(state.tr); dispatch(tr); } return true; }; }; var emojiPluginKey = exports.emojiPluginKey = new _state.PluginKey('emojiPlugin'); function getEmojiPluginState(state) { return emojiPluginKey.getState(state) || {}; } function createEmojiPlugin(pmPluginFactoryParams, options, api) { return new _safePlugin.SafePlugin({ key: emojiPluginKey, state: { init: function init() { if (options !== null && options !== void 0 && options.emojiProvider && (0, _experiments.editorExperiment)('platform_editor_prevent_toolbar_layout_shifts', true)) { return { emojiProviderPromise: options.emojiProvider }; } return {}; }, apply: function apply(tr, pluginState) { var _ref11 = tr.getMeta(emojiPluginKey) || { action: null, params: null }, action = _ref11.action, params = _ref11.params; var newPluginState = pluginState; switch (action) { case _actions.ACTIONS.SET_PROVIDER: newPluginState = _objectSpread(_objectSpread({}, pluginState), {}, { emojiProvider: params.provider, emojiProviderPromise: (0, _experiments.editorExperiment)('platform_editor_prevent_toolbar_layout_shifts', true) ? Promise.resolve(params.provider) : undefined }); pmPluginFactoryParams.dispatch(emojiPluginKey, newPluginState); return newPluginState; case _actions.ACTIONS.SET_ASCII_MAP: newPluginState = _objectSpread(_objectSpread({}, pluginState), {}, { asciiMap: params.asciiMap }); pmPluginFactoryParams.dispatch(emojiPluginKey, newPluginState); return newPluginState; case _actions.ACTIONS.SET_INLINE_POPUP: newPluginState = _objectSpread(_objectSpread({}, pluginState), {}, { inlineEmojiPopupOpen: params.open }); pmPluginFactoryParams.dispatch(emojiPluginKey, newPluginState); return newPluginState; } return newPluginState; } }, props: { nodeViews: { emoji: function emoji(node) { return new _EmojiNodeView.EmojiNodeView(node, { intl: pmPluginFactoryParams.getIntl(), api: api, emojiNodeDataProvider: options === null || options === void 0 ? void 0 : options.emojiNodeDataProvider }); } } }, view: function view(editorView) { var providerHandler = function providerHandler(name, providerPromise) { switch (name) { case 'emojiProvider': if (!providerPromise) { var _setProvider2; return setProvider === null || setProvider === void 0 || (_setProvider2 = setProvider(undefined)) === null || _setProvider2 === void 0 ? void 0 : _setProvider2(editorView.state, editorView.dispatch); } providerPromise.then(function (provider) { var _setProvider3; setProvider === null || setProvider === void 0 || (_setProvider3 = setProvider(provider)) === null || _setProvider3 === void 0 || _setProvider3(editorView.state, editorView.dispatch); provider.getAsciiMap().then(function (asciiMap) { setAsciiMap(asciiMap)(editorView.state, editorView.dispatch); }); }).catch(function () { var _setProvider4; return setProvider === null || setProvider === void 0 || (_setProvider4 = setProvider(undefined)) === null || _setProvider4 === void 0 ? void 0 : _setProvider4(editorView.state, editorView.dispatch); }); break; } return; }; if (options !== null && options !== void 0 && options.emojiProvider) { providerHandler('emojiProvider', options.emojiProvider); } return { destroy: function destroy() { if (pmPluginFactoryParams.providerFactory) { pmPluginFactoryParams.providerFactory.unsubscribe('emojiProvider', providerHandler); } } }; } }); }