@atlaskit/editor-plugin-emoji
Version:
Emoji plugin for @atlaskit/editor-core
555 lines (549 loc) • 24.4 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
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) { _defineProperty(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; }
import _regeneratorRuntime from "@babel/runtime/regenerator";
import React, { useEffect } from 'react';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { ToolTipContent } from '@atlaskit/editor-common/keymaps';
import { annotationMessages, toolbarInsertBlockMessages as messages } from '@atlaskit/editor-common/messages';
import { IconEmoji } from '@atlaskit/editor-common/quick-insert';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { TypeAheadAvailableNodes } from '@atlaskit/editor-common/type-ahead';
import { calculateToolbarPositionAboveSelection } from '@atlaskit/editor-common/utils';
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
import { EmojiTypeAheadItem, preloadEmojiPicker, recordSelectionFailedSli, recordSelectionSucceededSli, SearchSort } from '@atlaskit/emoji';
import CommentIcon from '@atlaskit/icon/core/comment';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { createEmojiFragment, insertEmoji } from './editor-commands/insert-emoji';
import { emojiNodeSpec } from './nodeviews/emojiNodeSpec';
import { EmojiNodeView } from './nodeviews/EmojiNodeView';
import { ACTIONS, openTypeAhead as openTypeAheadAction, setAsciiMap as setAsciiMapAction, setInlineEmojiPopupOpen, setProvider as setProviderAction } from './pm-plugins/actions';
import { inputRulePlugin as asciiInputRulePlugin } from './pm-plugins/ascii-input-rules';
import { InlineEmojiPopup } from './ui/InlineEmojiPopup';
export var 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.createElement(EmojiTypeAheadItem, {
emoji: emoji,
selected: isSelected,
onMouseMove: onHover,
onSelection: onClick,
emojiProvider: emojiProvider
});
},
emoji: emoji
};
};
export 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`.
*/
export var emojiPlugin = function emojiPlugin(_ref2) {
var _api$base, _api$analytics5;
var options = _ref2.config,
api = _ref2.api;
var previousEmojiProvider;
var typeAhead = {
id: 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 ? SearchSort.UsageFrequency : 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(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(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 = 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: ACTION.INSERTED,
actionSubject: ACTION_SUBJECT.DOCUMENT,
actionSubjectId: ACTION_SUBJECT_ID.EMOJI,
attributes: {
inputMethod: INPUT_METHOD.TYPEAHEAD
},
eventType: 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: emojiNodeSpec()
}];
},
usePluginHook: function usePluginHook() {
useEffect(function () {
delayUntilIdle(function () {
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 asciiInputRulePlugin(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: openTypeAheadAction(typeAhead, api),
setProvider: function () {
var _setProvider = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(providerPromise) {
var _api$core$actions$exe;
var provider;
return _regeneratorRuntime.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: 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 || editorExperiment('platform_editor_controls', 'control') || !editorView) {
return null;
}
return /*#__PURE__*/React.createElement(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.emoji),
description: formatMessage(messages.emojiDescription),
priority: 500,
keyshortcut: ':',
isDisabledOffline: false,
icon: function icon() {
return /*#__PURE__*/React.createElement(IconEmoji, null);
},
action: function action(insert) {
var _api$typeAhead;
if (editorExperiment('platform_editor_controls', 'variant1', {
exposure: true
})) {
// Clear slash
var _tr = insert('');
_tr = 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: 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: ACTION.CLICKED,
actionSubject: ACTION_SUBJECT.BUTTON,
actionSubjectId: ACTION_SUBJECT_ID.CREATE_INLINE_COMMENT_FROM_HIGHLIGHT_ACTIONS_MENU,
eventType: 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, INPUT_METHOD.TOOLBAR);
return command(stateFromClickEvent, dispatch);
};
return {
title: 'Emoji floating toolbar',
nodeType: [state.schema.nodes.emoji],
onPositionCalculated: 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: CommentIcon,
title: intl.formatMessage(annotationMessages.createComment),
onClick: onClick,
tooltipContent: /*#__PURE__*/React.createElement(ToolTipContent, {
description: intl.formatMessage(annotationMessages.createComment)
}),
supportsViewMode: true
}];
}
return [];
}
};
}
}
};
};
/**
* Actions
*/
var setAsciiMap = function setAsciiMap(asciiMap) {
return function (state, dispatch) {
if (dispatch) {
var tr = setAsciiMapAction(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
*/
export 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 setProviderAction(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;
};
};
export var emojiPluginKey = new PluginKey('emojiPlugin');
function getEmojiPluginState(state) {
return emojiPluginKey.getState(state) || {};
}
function createEmojiPlugin(pmPluginFactoryParams, options, api) {
return new SafePlugin({
key: emojiPluginKey,
state: {
init: function init() {
if (options !== null && options !== void 0 && options.emojiProvider && 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.SET_PROVIDER:
newPluginState = _objectSpread(_objectSpread({}, pluginState), {}, {
emojiProvider: params.provider,
emojiProviderPromise: editorExperiment('platform_editor_prevent_toolbar_layout_shifts', true) ? Promise.resolve(params.provider) : undefined
});
pmPluginFactoryParams.dispatch(emojiPluginKey, newPluginState);
return newPluginState;
case ACTIONS.SET_ASCII_MAP:
newPluginState = _objectSpread(_objectSpread({}, pluginState), {}, {
asciiMap: params.asciiMap
});
pmPluginFactoryParams.dispatch(emojiPluginKey, newPluginState);
return newPluginState;
case 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(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);
}
}
};
}
});
}