@atlaskit/editor-plugin-emoji 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +24 -3
- package/dist/cjs/commands/insert-emoji.js +47 -0
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/messages.js +15 -0
- package/dist/cjs/nodeviews/emoji.js +34 -0
- package/dist/cjs/plugin.js +414 -0
- package/dist/cjs/pm-plugins/ascii-input-rules.js +235 -0
- package/dist/cjs/ui/Emoji/index.js +19 -0
- package/dist/es2019/commands/insert-emoji.js +40 -0
- package/dist/es2019/index.js +1 -1
- package/dist/es2019/messages.js +8 -0
- package/dist/es2019/nodeviews/emoji.js +29 -0
- package/dist/es2019/plugin.js +386 -0
- package/dist/es2019/pm-plugins/ascii-input-rules.js +168 -0
- package/dist/es2019/ui/Emoji/index.js +13 -0
- package/dist/esm/commands/insert-emoji.js +39 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/messages.js +8 -0
- package/dist/esm/nodeviews/emoji.js +27 -0
- package/dist/esm/plugin.js +396 -0
- package/dist/esm/pm-plugins/ascii-input-rules.js +224 -0
- package/dist/esm/ui/Emoji/index.js +12 -0
- package/dist/types/commands/insert-emoji.d.ts +4 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messages.d.ts +7 -0
- package/dist/types/nodeviews/emoji.d.ts +7 -0
- package/dist/types/plugin.d.ts +42 -0
- package/dist/types/pm-plugins/ascii-input-rules.d.ts +10 -0
- package/dist/types/ui/Emoji/index.d.ts +4 -0
- package/dist/types-ts4.5/commands/insert-emoji.d.ts +4 -0
- package/dist/types-ts4.5/index.d.ts +1 -0
- package/dist/types-ts4.5/messages.d.ts +7 -0
- package/dist/types-ts4.5/nodeviews/emoji.d.ts +7 -0
- package/dist/types-ts4.5/plugin.d.ts +42 -0
- package/dist/types-ts4.5/pm-plugins/ascii-input-rules.d.ts +10 -0
- package/dist/types-ts4.5/ui/Emoji/index.d.ts +4 -0
- package/package.json +31 -25
- package/report.api.md +3 -0
- package/tmp/api-report-tmp.d.ts +46 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { emoji } from '@atlaskit/adf-schema';
|
|
3
|
+
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
|
|
4
|
+
import { toolbarInsertBlockMessages as messages } from '@atlaskit/editor-common/messages';
|
|
5
|
+
import { IconEmoji } from '@atlaskit/editor-common/quick-insert';
|
|
6
|
+
import { getInlineNodeViewProducer } from '@atlaskit/editor-common/react-node-view';
|
|
7
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
8
|
+
import { TypeAheadAvailableNodes } from '@atlaskit/editor-common/type-ahead';
|
|
9
|
+
import { Fragment } from '@atlaskit/editor-prosemirror/model';
|
|
10
|
+
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
11
|
+
import { EmojiTypeAheadItem, recordSelectionFailedSli, recordSelectionSucceededSli, SearchSort } from '@atlaskit/emoji';
|
|
12
|
+
import { insertEmoji } from './commands/insert-emoji';
|
|
13
|
+
import { EmojiNodeView } from './nodeviews/emoji';
|
|
14
|
+
import { inputRulePlugin as asciiInputRulePlugin } from './pm-plugins/ascii-input-rules';
|
|
15
|
+
export const emojiToTypeaheadItem = (emoji, emojiProvider) => ({
|
|
16
|
+
title: emoji.shortName || '',
|
|
17
|
+
key: emoji.id || emoji.shortName,
|
|
18
|
+
render({
|
|
19
|
+
isSelected,
|
|
20
|
+
onClick,
|
|
21
|
+
onHover
|
|
22
|
+
}) {
|
|
23
|
+
return /*#__PURE__*/React.createElement(EmojiTypeAheadItem, {
|
|
24
|
+
emoji: emoji,
|
|
25
|
+
selected: isSelected,
|
|
26
|
+
onMouseMove: onHover,
|
|
27
|
+
onSelection: onClick,
|
|
28
|
+
emojiProvider: emojiProvider
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
emoji
|
|
32
|
+
});
|
|
33
|
+
export function memoize(fn) {
|
|
34
|
+
// Cache results here
|
|
35
|
+
const seen = new Map();
|
|
36
|
+
function memoized(emoji, emojiProvider) {
|
|
37
|
+
// Check cache for hits
|
|
38
|
+
const hit = seen.get(emoji.id || emoji.shortName);
|
|
39
|
+
if (hit) {
|
|
40
|
+
return hit;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Generate new result and cache it
|
|
44
|
+
const result = fn(emoji, emojiProvider);
|
|
45
|
+
seen.set(emoji.id || emoji.shortName, result);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
call: memoized,
|
|
50
|
+
clear: seen.clear.bind(seen)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const memoizedToItem = memoize(emojiToTypeaheadItem);
|
|
54
|
+
export const defaultListLimit = 50;
|
|
55
|
+
const isFullShortName = query => query && query.length > 1 && query.charAt(0) === ':' && query.charAt(query.length - 1) === ':';
|
|
56
|
+
const TRIGGER = ':';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Emoji plugin to be added to an `EditorPresetBuilder` and used with `ComposableEditor`
|
|
60
|
+
* from `@atlaskit/editor-core`.
|
|
61
|
+
*/
|
|
62
|
+
export const emojiPlugin = ({
|
|
63
|
+
config: options,
|
|
64
|
+
api
|
|
65
|
+
}) => {
|
|
66
|
+
var _api$analytics5;
|
|
67
|
+
const typeAhead = {
|
|
68
|
+
id: TypeAheadAvailableNodes.EMOJI,
|
|
69
|
+
trigger: TRIGGER,
|
|
70
|
+
// Custom regex must have a capture group around trigger
|
|
71
|
+
// so it's possible to use it without needing to scan through all triggers again
|
|
72
|
+
customRegex: '\\(?(:)',
|
|
73
|
+
headless: options ? options.headless : undefined,
|
|
74
|
+
getItems({
|
|
75
|
+
query,
|
|
76
|
+
editorState
|
|
77
|
+
}) {
|
|
78
|
+
const pluginState = getEmojiPluginState(editorState);
|
|
79
|
+
const emojiProvider = pluginState.emojiProvider;
|
|
80
|
+
if (!emojiProvider) {
|
|
81
|
+
return Promise.resolve([]);
|
|
82
|
+
}
|
|
83
|
+
return new Promise(resolve => {
|
|
84
|
+
const emojiProviderChangeHandler = {
|
|
85
|
+
result(emojiResult) {
|
|
86
|
+
if (!emojiResult || !emojiResult.emojis) {
|
|
87
|
+
resolve([]);
|
|
88
|
+
} else {
|
|
89
|
+
const emojiItems = emojiResult.emojis.map(emoji => memoizedToItem.call(emoji, emojiProvider));
|
|
90
|
+
resolve(emojiItems);
|
|
91
|
+
}
|
|
92
|
+
emojiProvider.unsubscribe(emojiProviderChangeHandler);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
emojiProvider.subscribe(emojiProviderChangeHandler);
|
|
96
|
+
emojiProvider.filter(TRIGGER.concat(query), {
|
|
97
|
+
limit: defaultListLimit,
|
|
98
|
+
skinTone: emojiProvider.getSelectedTone(),
|
|
99
|
+
sort: !query.length ? SearchSort.UsageFrequency : SearchSort.Default
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
forceSelect({
|
|
104
|
+
query,
|
|
105
|
+
items,
|
|
106
|
+
editorState
|
|
107
|
+
}) {
|
|
108
|
+
const {
|
|
109
|
+
asciiMap
|
|
110
|
+
} = emojiPluginKey.getState(editorState) || {};
|
|
111
|
+
const normalizedQuery = TRIGGER.concat(query);
|
|
112
|
+
|
|
113
|
+
// if the query has space at the end
|
|
114
|
+
// check the ascii map for emojis
|
|
115
|
+
if (asciiMap && normalizedQuery.length >= 3 && normalizedQuery.endsWith(' ') && asciiMap.has(normalizedQuery.trim())) {
|
|
116
|
+
const emoji = asciiMap.get(normalizedQuery.trim());
|
|
117
|
+
return {
|
|
118
|
+
title: (emoji === null || emoji === void 0 ? void 0 : emoji.name) || '',
|
|
119
|
+
emoji
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const matchedItem = isFullShortName(normalizedQuery) ? items.find(item => item.title.toLowerCase() === normalizedQuery) : undefined;
|
|
123
|
+
return matchedItem;
|
|
124
|
+
},
|
|
125
|
+
selectItem(state, item, insert, {
|
|
126
|
+
mode
|
|
127
|
+
}) {
|
|
128
|
+
var _api$analytics3;
|
|
129
|
+
const {
|
|
130
|
+
id = '',
|
|
131
|
+
fallback,
|
|
132
|
+
shortName
|
|
133
|
+
} = item.emoji;
|
|
134
|
+
const text = fallback || shortName;
|
|
135
|
+
const emojiPluginState = emojiPluginKey.getState(state);
|
|
136
|
+
if (emojiPluginState.emojiProvider && emojiPluginState.emojiProvider.recordSelection && item.emoji) {
|
|
137
|
+
var _api$analytics$shared, _api$analytics, _api$analytics$shared2, _api$analytics$shared3, _api$analytics2, _api$analytics2$share;
|
|
138
|
+
emojiPluginState.emojiProvider.recordSelection(item.emoji).then(recordSelectionSucceededSli({
|
|
139
|
+
createAnalyticsEvent: (_api$analytics$shared = api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$shared2 = _api$analytics.sharedState.currentState()) === null || _api$analytics$shared2 === void 0 ? void 0 : _api$analytics$shared2.createAnalyticsEvent) !== null && _api$analytics$shared !== void 0 ? _api$analytics$shared : undefined
|
|
140
|
+
})).catch(recordSelectionFailedSli({
|
|
141
|
+
createAnalyticsEvent: (_api$analytics$shared3 = api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : (_api$analytics2$share = _api$analytics2.sharedState.currentState()) === null || _api$analytics2$share === void 0 ? void 0 : _api$analytics2$share.createAnalyticsEvent) !== null && _api$analytics$shared3 !== void 0 ? _api$analytics$shared3 : undefined
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
const emojiNode = state.schema.nodes.emoji.createChecked({
|
|
145
|
+
shortName,
|
|
146
|
+
id,
|
|
147
|
+
text
|
|
148
|
+
});
|
|
149
|
+
const space = state.schema.text(' ');
|
|
150
|
+
const tr = insert(Fragment.from([emojiNode, space]));
|
|
151
|
+
api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions.attachAnalyticsEvent({
|
|
152
|
+
action: ACTION.INSERTED,
|
|
153
|
+
actionSubject: ACTION_SUBJECT.DOCUMENT,
|
|
154
|
+
actionSubjectId: ACTION_SUBJECT_ID.EMOJI,
|
|
155
|
+
attributes: {
|
|
156
|
+
inputMethod: INPUT_METHOD.TYPEAHEAD
|
|
157
|
+
},
|
|
158
|
+
eventType: EVENT_TYPE.TRACK
|
|
159
|
+
})(tr);
|
|
160
|
+
return tr;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
name: 'emoji',
|
|
165
|
+
nodes() {
|
|
166
|
+
return [{
|
|
167
|
+
name: 'emoji',
|
|
168
|
+
node: emoji
|
|
169
|
+
}];
|
|
170
|
+
},
|
|
171
|
+
pmPlugins() {
|
|
172
|
+
return [{
|
|
173
|
+
name: 'emoji',
|
|
174
|
+
plugin: pmPluginFactoryParams => createEmojiPlugin(pmPluginFactoryParams)
|
|
175
|
+
}, {
|
|
176
|
+
name: 'emojiAsciiInputRule',
|
|
177
|
+
plugin: ({
|
|
178
|
+
schema,
|
|
179
|
+
providerFactory,
|
|
180
|
+
featureFlags
|
|
181
|
+
}) => {
|
|
182
|
+
var _api$analytics4;
|
|
183
|
+
return asciiInputRulePlugin(schema, providerFactory, featureFlags, api === null || api === void 0 ? void 0 : (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 ? void 0 : _api$analytics4.actions);
|
|
184
|
+
}
|
|
185
|
+
}];
|
|
186
|
+
},
|
|
187
|
+
getSharedState(editorState) {
|
|
188
|
+
if (!editorState) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
return emojiPluginKey.getState(editorState);
|
|
192
|
+
},
|
|
193
|
+
commands: {
|
|
194
|
+
insertEmoji: insertEmoji(api === null || api === void 0 ? void 0 : (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions)
|
|
195
|
+
},
|
|
196
|
+
pluginsOptions: {
|
|
197
|
+
quickInsert: ({
|
|
198
|
+
formatMessage
|
|
199
|
+
}) => [{
|
|
200
|
+
id: 'emoji',
|
|
201
|
+
title: formatMessage(messages.emoji),
|
|
202
|
+
description: formatMessage(messages.emojiDescription),
|
|
203
|
+
priority: 500,
|
|
204
|
+
keyshortcut: ':',
|
|
205
|
+
icon: () => /*#__PURE__*/React.createElement(IconEmoji, null),
|
|
206
|
+
action(insert, state) {
|
|
207
|
+
var _api$analytics6;
|
|
208
|
+
const tr = insert(undefined);
|
|
209
|
+
api === null || api === void 0 ? void 0 : api.typeAhead.commands.openTypeAheadAtCursor({
|
|
210
|
+
triggerHandler: typeAhead,
|
|
211
|
+
inputMethod: INPUT_METHOD.QUICK_INSERT
|
|
212
|
+
})({
|
|
213
|
+
tr
|
|
214
|
+
});
|
|
215
|
+
api === null || api === void 0 ? void 0 : (_api$analytics6 = api.analytics) === null || _api$analytics6 === void 0 ? void 0 : _api$analytics6.actions.attachAnalyticsEvent({
|
|
216
|
+
action: ACTION.INVOKED,
|
|
217
|
+
actionSubject: ACTION_SUBJECT.TYPEAHEAD,
|
|
218
|
+
actionSubjectId: ACTION_SUBJECT_ID.TYPEAHEAD_EMOJI,
|
|
219
|
+
attributes: {
|
|
220
|
+
inputMethod: INPUT_METHOD.QUICK_INSERT
|
|
221
|
+
},
|
|
222
|
+
eventType: EVENT_TYPE.UI
|
|
223
|
+
})(tr);
|
|
224
|
+
return tr;
|
|
225
|
+
}
|
|
226
|
+
}],
|
|
227
|
+
typeAhead
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Actions
|
|
234
|
+
*/
|
|
235
|
+
|
|
236
|
+
export const ACTIONS = {
|
|
237
|
+
SET_PROVIDER: 'SET_PROVIDER',
|
|
238
|
+
SET_RESULTS: 'SET_RESULTS',
|
|
239
|
+
SET_ASCII_MAP: 'SET_ASCII_MAP'
|
|
240
|
+
};
|
|
241
|
+
const setAsciiMap = asciiMap => (state, dispatch) => {
|
|
242
|
+
if (dispatch) {
|
|
243
|
+
dispatch(state.tr.setMeta(emojiPluginKey, {
|
|
244
|
+
action: ACTIONS.SET_ASCII_MAP,
|
|
245
|
+
params: {
|
|
246
|
+
asciiMap
|
|
247
|
+
}
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
*
|
|
255
|
+
* Wrapper to call `onLimitReached` when a specified number of calls of that function
|
|
256
|
+
* have been made within a time period.
|
|
257
|
+
*
|
|
258
|
+
* Note: It does not rate limit
|
|
259
|
+
*
|
|
260
|
+
* @param fn Function to wrap
|
|
261
|
+
* @param limitTime Time limit in milliseconds
|
|
262
|
+
* @param limitCount Number of function calls before `onRateReached` is called (per time period)
|
|
263
|
+
* @returns Wrapped function
|
|
264
|
+
*/
|
|
265
|
+
export function createRateLimitReachedFunction(fn, limitTime, limitCount, onLimitReached) {
|
|
266
|
+
let lastCallTime = 0;
|
|
267
|
+
let callCount = 0;
|
|
268
|
+
return function wrappedFn(...args) {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
if (now - lastCallTime < limitTime) {
|
|
271
|
+
if (++callCount > limitCount) {
|
|
272
|
+
onLimitReached === null || onLimitReached === void 0 ? void 0 : onLimitReached();
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
lastCallTime = now;
|
|
276
|
+
callCount = 1;
|
|
277
|
+
}
|
|
278
|
+
return fn(...args);
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// At this stage console.error only
|
|
283
|
+
const logRateWarning = () => {
|
|
284
|
+
if (process.env.NODE_ENV === 'development') {
|
|
285
|
+
// eslint-disable-next-line no-console
|
|
286
|
+
console.error('The emoji provider injected in the Editor is being reloaded frequently, this will cause a slow Editor experience.');
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
export const setProvider = createRateLimitReachedFunction(provider => (state, dispatch) => {
|
|
290
|
+
if (dispatch) {
|
|
291
|
+
dispatch(state.tr.setMeta(emojiPluginKey, {
|
|
292
|
+
action: ACTIONS.SET_PROVIDER,
|
|
293
|
+
params: {
|
|
294
|
+
provider
|
|
295
|
+
}
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
},
|
|
300
|
+
// If we change the emoji provider more than three times every 5 seconds we should warn.
|
|
301
|
+
// This seems like a really long time but the performance can be that laggy that we don't
|
|
302
|
+
// even get 3 events in 3 seconds and miss this indicator.
|
|
303
|
+
5000, 3, logRateWarning);
|
|
304
|
+
export const emojiPluginKey = new PluginKey('emojiPlugin');
|
|
305
|
+
export function getEmojiPluginState(state) {
|
|
306
|
+
return emojiPluginKey.getState(state) || {};
|
|
307
|
+
}
|
|
308
|
+
export function createEmojiPlugin(pmPluginFactoryParams) {
|
|
309
|
+
return new SafePlugin({
|
|
310
|
+
key: emojiPluginKey,
|
|
311
|
+
state: {
|
|
312
|
+
init() {
|
|
313
|
+
return {};
|
|
314
|
+
},
|
|
315
|
+
apply(tr, pluginState) {
|
|
316
|
+
const {
|
|
317
|
+
action,
|
|
318
|
+
params
|
|
319
|
+
} = tr.getMeta(emojiPluginKey) || {
|
|
320
|
+
action: null,
|
|
321
|
+
params: null
|
|
322
|
+
};
|
|
323
|
+
let newPluginState = pluginState;
|
|
324
|
+
switch (action) {
|
|
325
|
+
case ACTIONS.SET_PROVIDER:
|
|
326
|
+
newPluginState = {
|
|
327
|
+
...pluginState,
|
|
328
|
+
emojiProvider: params.provider
|
|
329
|
+
};
|
|
330
|
+
pmPluginFactoryParams.dispatch(emojiPluginKey, newPluginState);
|
|
331
|
+
return newPluginState;
|
|
332
|
+
case ACTIONS.SET_ASCII_MAP:
|
|
333
|
+
newPluginState = {
|
|
334
|
+
...pluginState,
|
|
335
|
+
asciiMap: params.asciiMap
|
|
336
|
+
};
|
|
337
|
+
pmPluginFactoryParams.dispatch(emojiPluginKey, newPluginState);
|
|
338
|
+
return newPluginState;
|
|
339
|
+
}
|
|
340
|
+
return newPluginState;
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
props: {
|
|
344
|
+
nodeViews: {
|
|
345
|
+
emoji: getInlineNodeViewProducer({
|
|
346
|
+
pmPluginFactoryParams,
|
|
347
|
+
Component: EmojiNodeView,
|
|
348
|
+
extraComponentProps: {
|
|
349
|
+
providerFactory: pmPluginFactoryParams.providerFactory
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
view(editorView) {
|
|
355
|
+
const providerHandler = (name, providerPromise) => {
|
|
356
|
+
switch (name) {
|
|
357
|
+
case 'emojiProvider':
|
|
358
|
+
if (!providerPromise) {
|
|
359
|
+
var _setProvider;
|
|
360
|
+
return setProvider === null || setProvider === void 0 ? void 0 : (_setProvider = setProvider(undefined)) === null || _setProvider === void 0 ? void 0 : _setProvider(editorView.state, editorView.dispatch);
|
|
361
|
+
}
|
|
362
|
+
providerPromise.then(provider => {
|
|
363
|
+
var _setProvider2;
|
|
364
|
+
setProvider === null || setProvider === void 0 ? void 0 : (_setProvider2 = setProvider(provider)) === null || _setProvider2 === void 0 ? void 0 : _setProvider2(editorView.state, editorView.dispatch);
|
|
365
|
+
provider.getAsciiMap().then(asciiMap => {
|
|
366
|
+
setAsciiMap(asciiMap)(editorView.state, editorView.dispatch);
|
|
367
|
+
});
|
|
368
|
+
}).catch(() => {
|
|
369
|
+
var _setProvider3;
|
|
370
|
+
return setProvider === null || setProvider === void 0 ? void 0 : (_setProvider3 = setProvider(undefined)) === null || _setProvider3 === void 0 ? void 0 : _setProvider3(editorView.state, editorView.dispatch);
|
|
371
|
+
});
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
};
|
|
376
|
+
pmPluginFactoryParams.providerFactory.subscribe('emojiProvider', providerHandler);
|
|
377
|
+
return {
|
|
378
|
+
destroy() {
|
|
379
|
+
if (pmPluginFactoryParams.providerFactory) {
|
|
380
|
+
pmPluginFactoryParams.providerFactory.unsubscribe('emojiProvider', providerHandler);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
|
|
3
|
+
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
4
|
+
import { createPlugin, createRule, leafNodeReplacementCharacter } from '@atlaskit/prosemirror-input-rules';
|
|
5
|
+
let matcher;
|
|
6
|
+
export function inputRulePlugin(schema, providerFactory, featureFlags, editorAnalyticsAPI) {
|
|
7
|
+
if (schema.nodes.emoji && providerFactory) {
|
|
8
|
+
initMatcher(providerFactory);
|
|
9
|
+
const asciiEmojiRule = createRule(AsciiEmojiMatcher.REGEX, inputRuleHandler(editorAnalyticsAPI));
|
|
10
|
+
return createPlugin('emoji', [asciiEmojiRule]);
|
|
11
|
+
}
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
function initMatcher(providerFactory) {
|
|
15
|
+
const handleProvider = (_name, provider) => {
|
|
16
|
+
if (!provider) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
provider.then(emojiProvider => {
|
|
20
|
+
emojiProvider.getAsciiMap().then(map => {
|
|
21
|
+
matcher = new RecordingAsciiEmojiMatcher(emojiProvider, map);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
providerFactory.subscribe('emojiProvider', handleProvider);
|
|
26
|
+
}
|
|
27
|
+
const inputRuleHandler = editorAnalyticsAPI => (state, matchParts, start, end) => {
|
|
28
|
+
if (!matcher) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const match = matcher.match(matchParts);
|
|
32
|
+
if (match) {
|
|
33
|
+
const transactionCreator = new AsciiEmojiTransactionCreator(state, match, start, end, editorAnalyticsAPI);
|
|
34
|
+
return transactionCreator.create();
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
38
|
+
const REGEX_LEADING_CAPTURE_INDEX = 1;
|
|
39
|
+
const REGEX_EMOJI_LEADING_PARENTHESES = 2;
|
|
40
|
+
const REGEX_EMOJI_ASCII_CAPTURE_INDEX = 3;
|
|
41
|
+
const REGEX_TRAILING_CAPTURE_INDEX = 4;
|
|
42
|
+
const getLeadingString = (match, withParenthesis = true) => match[REGEX_LEADING_CAPTURE_INDEX] + (withParenthesis ? match[REGEX_EMOJI_LEADING_PARENTHESES] : '');
|
|
43
|
+
const getLeadingStringWithoutParentheses = match => getLeadingString(match, false);
|
|
44
|
+
const getAscii = (match, withParentheses = false) => (withParentheses ? match[REGEX_EMOJI_LEADING_PARENTHESES] : '') + match[REGEX_EMOJI_ASCII_CAPTURE_INDEX].trim();
|
|
45
|
+
const getAsciiWithParentheses = matchParts => getAscii(matchParts, true);
|
|
46
|
+
const getTrailingString = match => match[REGEX_TRAILING_CAPTURE_INDEX] || '';
|
|
47
|
+
class AsciiEmojiMatcher {
|
|
48
|
+
constructor(asciiToEmojiMap) {
|
|
49
|
+
this.asciiToEmojiMap = asciiToEmojiMap;
|
|
50
|
+
}
|
|
51
|
+
match(matchParts) {
|
|
52
|
+
return this.getAsciiEmojiMatch(getLeadingStringWithoutParentheses(matchParts), getAsciiWithParentheses(matchParts), getTrailingString(matchParts)) || this.getAsciiEmojiMatch(getLeadingString(matchParts), getAscii(matchParts), getTrailingString(matchParts));
|
|
53
|
+
}
|
|
54
|
+
getAsciiEmojiMatch(leading, ascii, trailing) {
|
|
55
|
+
const emoji = this.asciiToEmojiMap.get(ascii);
|
|
56
|
+
return emoji ? {
|
|
57
|
+
emoji,
|
|
58
|
+
leadingString: leading,
|
|
59
|
+
trailingString: trailing
|
|
60
|
+
} : undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A matcher that will record ascii matches as usages of the matched emoji.
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* This regex matches 2 scenarios:
|
|
69
|
+
* 1. an emoticon starting with a colon character (e.g. :D => 😃)
|
|
70
|
+
* 2. an emoticon not starting with a colon character (e.g. 8-D => 😎)
|
|
71
|
+
*
|
|
72
|
+
* Explanation (${leafNodeReplacementCharacter} is replaced with character \ufffc)
|
|
73
|
+
*
|
|
74
|
+
* 1st Capturing Group ((?:^|[\s\ufffc])(?:\(*?))
|
|
75
|
+
* Non-capturing group (?:^|[\s\ufffc])
|
|
76
|
+
* 1st Alternative ^
|
|
77
|
+
* ^ asserts position at start of the string
|
|
78
|
+
* 2nd Alternative [\s\ufffc]
|
|
79
|
+
* matches a single character present in [\s\ufffc]
|
|
80
|
+
* Non-capturing group (?:\(*?)
|
|
81
|
+
* matches the character ( literally between zero and unlimited times, as few times as possible, expanding as needed (lazy)
|
|
82
|
+
* 2nd Capturing Group (\(?)
|
|
83
|
+
* matches a single ( if present
|
|
84
|
+
* 3rd Capturing Group ([^:\s\ufffc\(]\S{1,3}|:\S{1,3}( ))
|
|
85
|
+
* 1st Alternative [^:\s\ufffc\(]\S{1,3}
|
|
86
|
+
* matches a single character not present in [^:\s\ufffc\(] between 1 and 3 times, as many times as possible, giving back as needed (greedy)
|
|
87
|
+
* 2nd Alternative :\S{1,3}( )
|
|
88
|
+
* : matches the character : literally
|
|
89
|
+
* \S{1,3} matches any non-whitespace character between 1 and 3 times, as many times as possible, giving back as needed (greedy)
|
|
90
|
+
* 4th Capturing Group ( )
|
|
91
|
+
*
|
|
92
|
+
* See https://regex101.com/r/HRS9O2/4
|
|
93
|
+
*/
|
|
94
|
+
_defineProperty(AsciiEmojiMatcher, "REGEX", new RegExp(`((?:^|[\\s${leafNodeReplacementCharacter}])(?:\\(*?))(\\(?)([^:\\s${leafNodeReplacementCharacter}\\(]\\S{1,3}|:\\S{1,3}( ))$`));
|
|
95
|
+
class RecordingAsciiEmojiMatcher extends AsciiEmojiMatcher {
|
|
96
|
+
constructor(emojiProvider, asciiToEmojiMap) {
|
|
97
|
+
super(asciiToEmojiMap);
|
|
98
|
+
this.emojiProvider = emojiProvider;
|
|
99
|
+
}
|
|
100
|
+
match(matchParts) {
|
|
101
|
+
const match = super.match(matchParts);
|
|
102
|
+
if (match && this.emojiProvider.recordSelection) {
|
|
103
|
+
this.emojiProvider.recordSelection(match.emoji);
|
|
104
|
+
}
|
|
105
|
+
return match;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
class AsciiEmojiTransactionCreator {
|
|
109
|
+
constructor(state, match, start, end, editorAnalyticsAPI) {
|
|
110
|
+
this.state = state;
|
|
111
|
+
this.match = match;
|
|
112
|
+
this.start = start;
|
|
113
|
+
this.end = end;
|
|
114
|
+
this.editorAnalyticsAPI = editorAnalyticsAPI;
|
|
115
|
+
}
|
|
116
|
+
create() {
|
|
117
|
+
var _this$editorAnalytics;
|
|
118
|
+
const tr = this.state.tr.replaceWith(this.from, this.to, this.createNodes());
|
|
119
|
+
(_this$editorAnalytics = this.editorAnalyticsAPI) === null || _this$editorAnalytics === void 0 ? void 0 : _this$editorAnalytics.attachAnalyticsEvent({
|
|
120
|
+
action: ACTION.INSERTED,
|
|
121
|
+
actionSubject: ACTION_SUBJECT.DOCUMENT,
|
|
122
|
+
actionSubjectId: ACTION_SUBJECT_ID.EMOJI,
|
|
123
|
+
attributes: {
|
|
124
|
+
inputMethod: INPUT_METHOD.ASCII
|
|
125
|
+
},
|
|
126
|
+
eventType: EVENT_TYPE.TRACK
|
|
127
|
+
})(tr);
|
|
128
|
+
return tr;
|
|
129
|
+
}
|
|
130
|
+
get from() {
|
|
131
|
+
return this.start + this.match.leadingString.length;
|
|
132
|
+
}
|
|
133
|
+
get to() {
|
|
134
|
+
return this.end;
|
|
135
|
+
}
|
|
136
|
+
createNodes() {
|
|
137
|
+
const nodes = [this.createEmojiNode()];
|
|
138
|
+
if (this.trailingTextNodeRequired()) {
|
|
139
|
+
nodes.push(this.createTrailingTextNode());
|
|
140
|
+
}
|
|
141
|
+
return nodes;
|
|
142
|
+
}
|
|
143
|
+
createEmojiNode() {
|
|
144
|
+
const {
|
|
145
|
+
emoji: emojiTypeNode
|
|
146
|
+
} = this.state.schema.nodes;
|
|
147
|
+
return emojiTypeNode.create(this.getEmojiNodeAttrs());
|
|
148
|
+
}
|
|
149
|
+
getEmojiNodeAttrs() {
|
|
150
|
+
const emoji = this.match.emoji;
|
|
151
|
+
return {
|
|
152
|
+
id: emoji.id,
|
|
153
|
+
shortName: emoji.shortName,
|
|
154
|
+
text: emoji.fallback || emoji.shortName
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
trailingTextNodeRequired() {
|
|
158
|
+
return this.match.trailingString.length > 0;
|
|
159
|
+
}
|
|
160
|
+
createTrailingTextNode() {
|
|
161
|
+
return this.state.schema.text(this.match.trailingString);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export const stateKey = new PluginKey('asciiEmojiPlugin');
|
|
165
|
+
const plugins = (schema, providerFactory, featureFlags, editorAnalyticsAPI) => {
|
|
166
|
+
return [inputRulePlugin(schema, providerFactory, featureFlags, editorAnalyticsAPI)].filter(plugin => !!plugin);
|
|
167
|
+
};
|
|
168
|
+
export default plugins;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** @jsx jsx */
|
|
2
|
+
import { css, jsx } from '@emotion/react';
|
|
3
|
+
import { Emoji } from '@atlaskit/editor-common/emoji';
|
|
4
|
+
// eslint-disable-next-line
|
|
5
|
+
const clickSelectWrapperStyle = css`
|
|
6
|
+
// eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage
|
|
7
|
+
user-select: all;
|
|
8
|
+
`;
|
|
9
|
+
export default function EmojiNode(props) {
|
|
10
|
+
return jsx("span", {
|
|
11
|
+
css: clickSelectWrapperStyle
|
|
12
|
+
}, jsx(Emoji, props));
|
|
13
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
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; }
|
|
3
|
+
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; }
|
|
4
|
+
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
|
|
5
|
+
import { Fragment } from '@atlaskit/editor-prosemirror/model';
|
|
6
|
+
import { Selection } from '@atlaskit/editor-prosemirror/state';
|
|
7
|
+
import { safeInsert } from '@atlaskit/editor-prosemirror/utils';
|
|
8
|
+
export var insertEmoji = function insertEmoji(editorAnalyticsAPI) {
|
|
9
|
+
return function (emojiId, inputMethod) {
|
|
10
|
+
return function (_ref) {
|
|
11
|
+
var tr = _ref.tr;
|
|
12
|
+
var doc = tr.doc,
|
|
13
|
+
selection = tr.selection;
|
|
14
|
+
var emoji = tr.doc.type.schema.nodes.emoji;
|
|
15
|
+
if (emoji && emojiId) {
|
|
16
|
+
var node = emoji.createChecked(_objectSpread(_objectSpread({}, emojiId), {}, {
|
|
17
|
+
text: emojiId.fallback || emojiId.shortName
|
|
18
|
+
}));
|
|
19
|
+
var textNode = doc.type.schema.text(' ');
|
|
20
|
+
var fragment = Fragment.fromArray([node, textNode]);
|
|
21
|
+
var newTr = safeInsert(fragment)(tr);
|
|
22
|
+
if (inputMethod) {
|
|
23
|
+
editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({
|
|
24
|
+
action: ACTION.INSERTED,
|
|
25
|
+
actionSubject: ACTION_SUBJECT.DOCUMENT,
|
|
26
|
+
actionSubjectId: ACTION_SUBJECT_ID.EMOJI,
|
|
27
|
+
attributes: {
|
|
28
|
+
inputMethod: inputMethod
|
|
29
|
+
},
|
|
30
|
+
eventType: EVENT_TYPE.TRACK
|
|
31
|
+
})(newTr);
|
|
32
|
+
}
|
|
33
|
+
newTr.setSelection(Selection.near(newTr.doc.resolve(selection.$from.pos + fragment.size)));
|
|
34
|
+
return newTr;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
};
|
package/dist/esm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export { emojiPlugin } from './plugin';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useIntl } from 'react-intl-next';
|
|
3
|
+
import { messages } from '../messages';
|
|
4
|
+
import Emoji from '../ui/Emoji';
|
|
5
|
+
var EmojiAssistiveTextComponent = /*#__PURE__*/React.memo(function (_ref) {
|
|
6
|
+
var emojiShortName = _ref.emojiShortName;
|
|
7
|
+
var intl = useIntl();
|
|
8
|
+
return /*#__PURE__*/React.createElement("span", {
|
|
9
|
+
className: 'assistive'
|
|
10
|
+
}, "".concat(intl.formatMessage(messages.emojiNodeLabel), " ").concat(emojiShortName));
|
|
11
|
+
});
|
|
12
|
+
export function EmojiNodeView(props) {
|
|
13
|
+
var _props$node$attrs = props.node.attrs,
|
|
14
|
+
shortName = _props$node$attrs.shortName,
|
|
15
|
+
id = _props$node$attrs.id,
|
|
16
|
+
text = _props$node$attrs.text;
|
|
17
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(EmojiAssistiveTextComponent, {
|
|
18
|
+
emojiShortName: shortName
|
|
19
|
+
}), /*#__PURE__*/React.createElement("span", {
|
|
20
|
+
"aria-hidden": "true"
|
|
21
|
+
}, /*#__PURE__*/React.createElement(Emoji, {
|
|
22
|
+
providers: props.providerFactory,
|
|
23
|
+
id: id,
|
|
24
|
+
shortName: shortName,
|
|
25
|
+
fallback: text
|
|
26
|
+
})));
|
|
27
|
+
}
|