@atlaskit/editor-plugin-emoji
Version:
Emoji plugin for @atlaskit/editor-core
566 lines (559 loc) • 25.9 kB
JavaScript
"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);
}
}
};
}
});
}