@ckeditor/ckeditor5-emoji 44.2.0-alpha.7 → 44.2.0-alpha.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-emoji",
3
- "version": "44.2.0-alpha.7",
3
+ "version": "44.2.0-alpha.8",
4
4
  "description": "Emoji feature for CKEditor 5.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -13,12 +13,12 @@
13
13
  "type": "module",
14
14
  "main": "src/index.js",
15
15
  "dependencies": {
16
- "@ckeditor/ckeditor5-core": "44.2.0-alpha.7",
17
- "@ckeditor/ckeditor5-mention": "44.2.0-alpha.7",
18
- "@ckeditor/ckeditor5-typing": "44.2.0-alpha.7",
19
- "@ckeditor/ckeditor5-ui": "44.2.0-alpha.7",
20
- "@ckeditor/ckeditor5-utils": "44.2.0-alpha.7",
21
- "ckeditor5": "44.2.0-alpha.7",
16
+ "@ckeditor/ckeditor5-core": "44.2.0-alpha.8",
17
+ "@ckeditor/ckeditor5-mention": "44.2.0-alpha.8",
18
+ "@ckeditor/ckeditor5-typing": "44.2.0-alpha.8",
19
+ "@ckeditor/ckeditor5-ui": "44.2.0-alpha.8",
20
+ "@ckeditor/ckeditor5-utils": "44.2.0-alpha.8",
21
+ "ckeditor5": "44.2.0-alpha.8",
22
22
  "fuse.js": "7.0.0",
23
23
  "lodash-es": "4.17.21"
24
24
  },
@@ -2,7 +2,7 @@
2
2
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
- import type { Emoji, EmojiConfig, EmojiMention, EmojiPicker, EmojiRepository, EmojiCommand } from './index.js';
5
+ import type { Emoji, EmojiConfig, EmojiMention, EmojiPicker, EmojiRepository, EmojiUtils, EmojiCommand } from './index.js';
6
6
  declare module '@ckeditor/ckeditor5-core' {
7
7
  interface EditorConfig {
8
8
  /**
@@ -17,6 +17,7 @@ declare module '@ckeditor/ckeditor5-core' {
17
17
  [EmojiMention.pluginName]: EmojiMention;
18
18
  [EmojiPicker.pluginName]: EmojiPicker;
19
19
  [EmojiRepository.pluginName]: EmojiRepository;
20
+ [EmojiUtils.pluginName]: EmojiUtils;
20
21
  }
21
22
  interface CommandsMap {
22
23
  emoji: EmojiCommand;
@@ -5,6 +5,7 @@
5
5
  import { Plugin, type Editor } from 'ckeditor5/src/core.js';
6
6
  import { Typing } from 'ckeditor5/src/typing.js';
7
7
  import EmojiRepository from './emojirepository.js';
8
+ import type EmojiPicker from './emojipicker.js';
8
9
  /**
9
10
  * The emoji mention plugin.
10
11
  *
@@ -14,11 +15,11 @@ export default class EmojiMention extends Plugin {
14
15
  /**
15
16
  * An instance of the {@link module:emoji/emojipicker~EmojiPicker} plugin if it is loaded in the editor.
16
17
  */
17
- private _emojiPickerPlugin;
18
+ emojiPickerPlugin: EmojiPicker | null;
18
19
  /**
19
20
  * An instance of the {@link module:emoji/emojirepository~EmojiRepository} plugin.
20
21
  */
21
- private _emojiRepositoryPlugin;
22
+ emojiRepositoryPlugin: EmojiRepository;
22
23
  /**
23
24
  * A flag that informs if the {@link module:emoji/emojirepository~EmojiRepository} plugin is loaded correctly.
24
25
  */
@@ -88,9 +88,9 @@ export default class EmojiMention extends Plugin {
88
88
  */
89
89
  async init() {
90
90
  const editor = this.editor;
91
- this._emojiPickerPlugin = editor.plugins.has('EmojiPicker') ? editor.plugins.get('EmojiPicker') : null;
92
- this._emojiRepositoryPlugin = editor.plugins.get('EmojiRepository');
93
- this._isEmojiRepositoryAvailable = await this._emojiRepositoryPlugin.isReady();
91
+ this.emojiPickerPlugin = editor.plugins.has('EmojiPicker') ? editor.plugins.get('EmojiPicker') : null;
92
+ this.emojiRepositoryPlugin = editor.plugins.get('EmojiRepository');
93
+ this._isEmojiRepositoryAvailable = await this.emojiRepositoryPlugin.isReady();
94
94
  // Override the `mention` command listener if the emoji repository is ready.
95
95
  if (this._isEmojiRepositoryAvailable) {
96
96
  editor.once('ready', this._overrideMentionExecuteListener.bind(this));
@@ -152,7 +152,7 @@ export default class EmojiMention extends Plugin {
152
152
  editor.model.change(writer => {
153
153
  editor.model.deleteContent(writer.createSelection(eventData.range));
154
154
  });
155
- const emojiPickerPlugin = this._emojiPickerPlugin;
155
+ const emojiPickerPlugin = this.emojiPickerPlugin;
156
156
  emojiPickerPlugin.showUI(text.slice(1));
157
157
  setTimeout(() => {
158
158
  emojiPickerPlugin.emojiPickerView.focus();
@@ -180,18 +180,18 @@ export default class EmojiMention extends Plugin {
180
180
  if (!this._isEmojiRepositoryAvailable) {
181
181
  return [];
182
182
  }
183
- const emojis = this._emojiRepositoryPlugin.getEmojiByQuery(searchQuery)
183
+ const emojis = this.emojiRepositoryPlugin.getEmojiByQuery(searchQuery)
184
184
  .map(emoji => {
185
185
  let text = emoji.skins[this._skinTone] || emoji.skins.default;
186
- if (this._emojiPickerPlugin) {
187
- text = emoji.skins[this._emojiPickerPlugin.skinTone] || emoji.skins.default;
186
+ if (this.emojiPickerPlugin) {
187
+ text = emoji.skins[this.emojiPickerPlugin.skinTone] || emoji.skins.default;
188
188
  }
189
189
  return {
190
190
  id: `:${emoji.annotation}:`,
191
191
  text
192
192
  };
193
193
  });
194
- if (!this._emojiPickerPlugin) {
194
+ if (!this.emojiPickerPlugin) {
195
195
  return emojis.slice(0, this._emojiDropdownLimit);
196
196
  }
197
197
  const actionItem = {
@@ -25,11 +25,11 @@ export default class EmojiPicker extends Plugin {
25
25
  /**
26
26
  * The contextual balloon plugin instance.
27
27
  */
28
- _balloonPlugin: ContextualBalloon;
28
+ balloonPlugin: ContextualBalloon;
29
29
  /**
30
30
  * An instance of the {@link module:emoji/emojirepository~EmojiRepository} plugin.
31
31
  */
32
- private _emojiRepositoryPlugin;
32
+ emojiRepositoryPlugin: EmojiRepository;
33
33
  /**
34
34
  * @inheritDoc
35
35
  */
@@ -80,7 +80,7 @@ export default class EmojiPicker extends Plugin {
80
80
  */
81
81
  private _setupConversion;
82
82
  /**
83
- * Returns positioning options for the {@link #_balloonPlugin}. They control the way the balloon is attached
83
+ * Returns positioning options for the {@link #balloonPlugin}. They control the way the balloon is attached
84
84
  * to the target element or selection.
85
85
  */
86
86
  private _getBalloonPositionData;
@@ -42,10 +42,10 @@ export default class EmojiPicker extends Plugin {
42
42
  */
43
43
  async init() {
44
44
  const editor = this.editor;
45
- this._balloonPlugin = editor.plugins.get('ContextualBalloon');
46
- this._emojiRepositoryPlugin = editor.plugins.get('EmojiRepository');
45
+ this.balloonPlugin = editor.plugins.get('ContextualBalloon');
46
+ this.emojiRepositoryPlugin = editor.plugins.get('EmojiRepository');
47
47
  // Skip registering a button in the toolbar and list item in the menu bar if the emoji repository is not ready.
48
- if (!await this._emojiRepositoryPlugin.isReady()) {
48
+ if (!await this.emojiRepositoryPlugin.isReady()) {
49
49
  return;
50
50
  }
51
51
  const command = new EmojiCommand(editor);
@@ -99,8 +99,8 @@ export default class EmojiPicker extends Plugin {
99
99
  this.emojiPickerView.searchView.setInputValue(searchValue);
100
100
  }
101
101
  this.emojiPickerView.searchView.search(searchValue);
102
- if (!this._balloonPlugin.hasView(this.emojiPickerView)) {
103
- this._balloonPlugin.add({
102
+ if (!this.balloonPlugin.hasView(this.emojiPickerView)) {
103
+ this.balloonPlugin.add({
104
104
  view: this.emojiPickerView,
105
105
  position: this._getBalloonPositionData()
106
106
  });
@@ -129,11 +129,11 @@ export default class EmojiPicker extends Plugin {
129
129
  */
130
130
  _createEmojiPickerView() {
131
131
  const emojiPickerView = new EmojiPickerView(this.editor.locale, {
132
- emojiCategories: this._emojiRepositoryPlugin.getEmojiCategories(),
132
+ emojiCategories: this.emojiRepositoryPlugin.getEmojiCategories(),
133
133
  skinTone: this.editor.config.get('emoji.skinTone'),
134
- skinTones: this._emojiRepositoryPlugin.getSkinTones(),
134
+ skinTones: this.emojiRepositoryPlugin.getSkinTones(),
135
135
  getEmojiByQuery: (query) => {
136
- return this._emojiRepositoryPlugin.getEmojiByQuery(query);
136
+ return this.emojiRepositoryPlugin.getEmojiByQuery(query);
137
137
  }
138
138
  });
139
139
  // Insert an emoji on a tile click.
@@ -145,8 +145,8 @@ export default class EmojiPicker extends Plugin {
145
145
  });
146
146
  // Update the balloon position when layout is changed.
147
147
  this.listenTo(emojiPickerView, 'update', () => {
148
- if (this._balloonPlugin.visibleView === emojiPickerView) {
149
- this._balloonPlugin.updatePosition();
148
+ if (this.balloonPlugin.visibleView === emojiPickerView) {
149
+ this.balloonPlugin.updatePosition();
150
150
  }
151
151
  });
152
152
  // Close the panel on `Esc` key press when the **actions have focus**.
@@ -157,9 +157,9 @@ export default class EmojiPicker extends Plugin {
157
157
  // Close the dialog when clicking outside of it.
158
158
  clickOutsideHandler({
159
159
  emitter: emojiPickerView,
160
- contextElements: [this._balloonPlugin.view.element],
160
+ contextElements: [this.balloonPlugin.view.element],
161
161
  callback: () => this._hideUI(),
162
- activator: () => this._balloonPlugin.visibleView === emojiPickerView
162
+ activator: () => this.balloonPlugin.visibleView === emojiPickerView
163
163
  });
164
164
  return emojiPickerView;
165
165
  }
@@ -167,7 +167,7 @@ export default class EmojiPicker extends Plugin {
167
167
  * Hides the balloon with the emoji picker.
168
168
  */
169
169
  _hideUI() {
170
- this._balloonPlugin.remove(this.emojiPickerView);
170
+ this.balloonPlugin.remove(this.emojiPickerView);
171
171
  this.emojiPickerView.searchView.setInputValue('');
172
172
  this.editor.editing.view.focus();
173
173
  this._hideFakeVisualSelection();
@@ -198,7 +198,7 @@ export default class EmojiPicker extends Plugin {
198
198
  });
199
199
  }
200
200
  /**
201
- * Returns positioning options for the {@link #_balloonPlugin}. They control the way the balloon is attached
201
+ * Returns positioning options for the {@link #balloonPlugin}. They control the way the balloon is attached
202
202
  * to the target element or selection.
203
203
  */
204
204
  _getBalloonPositionData() {
@@ -2,7 +2,8 @@
2
2
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
- import { Plugin, type Editor } from 'ckeditor5/src/core.js';
5
+ import { type Editor, Plugin } from 'ckeditor5/src/core.js';
6
+ import EmojiUtils from './emojiutils.js';
6
7
  import type { SkinToneId } from './emojiconfig.js';
7
8
  /**
8
9
  * The emoji repository plugin.
@@ -10,6 +11,14 @@ import type { SkinToneId } from './emojiconfig.js';
10
11
  * Loads the emoji database from URL during plugin initialization and provides utility methods to search it.
11
12
  */
12
13
  export default class EmojiRepository extends Plugin {
14
+ /**
15
+ * A callback to resolve the {@link #_databasePromise} to control the return value of this promise.
16
+ */
17
+ private _databasePromiseResolveCallback;
18
+ /**
19
+ * An instance of the [Fuse.js](https://www.fusejs.io/) library.
20
+ */
21
+ private _fuseSearch;
13
22
  /**
14
23
  * Emoji database.
15
24
  */
@@ -18,15 +27,11 @@ export default class EmojiRepository extends Plugin {
18
27
  * A promise resolved after downloading the emoji database.
19
28
  * The promise resolves with `true` when the database is successfully downloaded or `false` otherwise.
20
29
  */
21
- private _databasePromise;
22
- /**
23
- * A callback to resolve the {@link #_databasePromise} to control the return value of this promise.
24
- */
25
- private _databasePromiseResolveCallback;
30
+ private readonly _databasePromise;
26
31
  /**
27
- * An instance of the [Fuse.js](https://www.fusejs.io/) library.
32
+ * @inheritDoc
28
33
  */
29
- private _fuseSearch;
34
+ static get requires(): readonly [typeof EmojiUtils];
30
35
  /**
31
36
  * @inheritDoc
32
37
  */
@@ -67,12 +72,6 @@ export default class EmojiRepository extends Plugin {
67
72
  * Indicates whether the emoji database has been successfully downloaded and the plugin is operational.
68
73
  */
69
74
  isReady(): Promise<boolean>;
70
- /**
71
- * A function used to check if the given emoji is supported in the operating system.
72
- *
73
- * Referenced for unit testing purposes.
74
- */
75
- private static _isEmojiSupported;
76
75
  }
77
76
  /**
78
77
  * Represents a single group of the emoji category, e.g., "Smileys & Expressions".
@@ -9,24 +9,22 @@ import Fuse from 'fuse.js';
9
9
  import { groupBy } from 'lodash-es';
10
10
  import { Plugin } from 'ckeditor5/src/core.js';
11
11
  import { logWarning } from 'ckeditor5/src/utils.js';
12
+ import EmojiUtils from './emojiutils.js';
12
13
  // An endpoint from which the emoji database will be downloaded during plugin initialization.
13
14
  // The `{version}` placeholder is replaced with the value from editor config.
14
15
  const EMOJI_DATABASE_URL = 'https://cdn.ckeditor.com/ckeditor5/data/emoji/{version}/en.json';
15
- const SKIN_TONE_MAP = {
16
- 0: 'default',
17
- 1: 'light',
18
- 2: 'medium-light',
19
- 3: 'medium',
20
- 4: 'medium-dark',
21
- 5: 'dark'
22
- };
23
- const BASELINE_EMOJI_WIDTH = 24;
24
16
  /**
25
17
  * The emoji repository plugin.
26
18
  *
27
19
  * Loads the emoji database from URL during plugin initialization and provides utility methods to search it.
28
20
  */
29
- class EmojiRepository extends Plugin {
21
+ export default class EmojiRepository extends Plugin {
22
+ /**
23
+ * @inheritDoc
24
+ */
25
+ static get requires() {
26
+ return [EmojiUtils];
27
+ }
30
28
  /**
31
29
  * @inheritDoc
32
30
  */
@@ -58,20 +56,23 @@ class EmojiRepository extends Plugin {
58
56
  * @inheritDoc
59
57
  */
60
58
  async init() {
59
+ const emojiUtils = this.editor.plugins.get('EmojiUtils');
61
60
  const emojiVersion = this.editor.config.get('emoji.version');
62
61
  const emojiDatabaseUrl = EMOJI_DATABASE_URL.replace('{version}', `${emojiVersion}`);
63
62
  const emojiDatabase = await loadEmojiDatabase(emojiDatabaseUrl);
63
+ const emojiSupportedVersionByOs = emojiUtils.getEmojiSupportedVersionByOs();
64
64
  // Skip the initialization if the emoji database download has failed.
65
65
  // An empty database prevents the initialization of other dependent plugins, such as `EmojiMention` and `EmojiPicker`.
66
66
  if (!emojiDatabase.length) {
67
67
  return this._databasePromiseResolveCallback(false);
68
68
  }
69
- const container = createEmojiWidthTestingContainer();
69
+ const container = emojiUtils.createEmojiWidthTestingContainer();
70
+ document.body.appendChild(container);
70
71
  // Store the emoji database after normalizing the raw data.
71
72
  this._database = emojiDatabase
72
- .filter(item => isEmojiCategoryAllowed(item))
73
- .filter(item => EmojiRepository._isEmojiSupported(item, container))
74
- .map(item => normalizeEmojiSkinTone(item));
73
+ .filter(item => emojiUtils.isEmojiCategoryAllowed(item))
74
+ .filter(item => emojiUtils.isEmojiSupported(item, emojiSupportedVersionByOs, container))
75
+ .map(item => emojiUtils.normalizeEmojiSkinTone(item));
75
76
  container.remove();
76
77
  // Create instance of the Fuse.js library with configured weighted search keys and disabled fuzzy search.
77
78
  this._fuseSearch = new Fuse(this._database, {
@@ -171,13 +172,6 @@ class EmojiRepository extends Plugin {
171
172
  return this._databasePromise;
172
173
  }
173
174
  }
174
- /**
175
- * A function used to check if the given emoji is supported in the operating system.
176
- *
177
- * Referenced for unit testing purposes.
178
- */
179
- EmojiRepository._isEmojiSupported = isEmojiSupported;
180
- export default EmojiRepository;
181
175
  /**
182
176
  * Makes the HTTP request to download the emoji database.
183
177
  */
@@ -206,63 +200,3 @@ async function loadEmojiDatabase(emojiDatabaseUrl) {
206
200
  }
207
201
  return result;
208
202
  }
209
- /**
210
- * Creates a div for emoji width testing purposes.
211
- */
212
- function createEmojiWidthTestingContainer() {
213
- const container = document.createElement('div');
214
- container.setAttribute('aria-hidden', 'true');
215
- container.style.position = 'absolute';
216
- container.style.left = '-9999px';
217
- container.style.whiteSpace = 'nowrap';
218
- container.style.fontSize = BASELINE_EMOJI_WIDTH + 'px';
219
- document.body.appendChild(container);
220
- return container;
221
- }
222
- /**
223
- * Returns the width of the provided node.
224
- */
225
- function getNodeWidth(container, node) {
226
- const span = document.createElement('span');
227
- span.textContent = node;
228
- container.appendChild(span);
229
- const nodeWidth = span.offsetWidth;
230
- container.removeChild(span);
231
- return nodeWidth;
232
- }
233
- /**
234
- * Checks whether the emoji is supported in the operating system.
235
- */
236
- function isEmojiSupported(item, container) {
237
- const emojiWidth = getNodeWidth(container, item.emoji);
238
- // On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard
239
- // against are the ones that are 2x the size, because those are truly broken (person with red hair = person with
240
- // floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.)
241
- // So here we set the threshold at 1.8 times the size of the baseline emoji.
242
- return (emojiWidth / 1.8 < BASELINE_EMOJI_WIDTH) && (emojiWidth >= BASELINE_EMOJI_WIDTH);
243
- }
244
- /**
245
- * Adds default skin tone property to each emoji. If emoji defines other skin tones, they are added as well.
246
- */
247
- function normalizeEmojiSkinTone(item) {
248
- const entry = {
249
- ...item,
250
- skins: {
251
- default: item.emoji
252
- }
253
- };
254
- if (item.skins) {
255
- item.skins.forEach(skin => {
256
- const skinTone = SKIN_TONE_MAP[skin.tone];
257
- entry.skins[skinTone] = skin.emoji;
258
- });
259
- }
260
- return entry;
261
- }
262
- /**
263
- * Checks whether the emoji belongs to a group that is allowed.
264
- */
265
- function isEmojiCategoryAllowed(item) {
266
- // Category group=2 contains skin tones only, which we do not want to render.
267
- return item.group !== 2;
268
- }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
+ */
5
+ import { Plugin } from 'ckeditor5/src/core.js';
6
+ import type { EmojiCdnResource, EmojiEntry } from './emojirepository.js';
7
+ /**
8
+ * The Emoji utilities plugin.
9
+ */
10
+ export default class EmojiUtils extends Plugin {
11
+ /**
12
+ * @inheritDoc
13
+ */
14
+ static get pluginName(): "EmojiUtils";
15
+ /**
16
+ * @inheritDoc
17
+ */
18
+ static get isOfficialPlugin(): true;
19
+ /**
20
+ * Checks if the emoji is supported by verifying the emoji version supported by the system first.
21
+ * Then checks if emoji contains a zero width joiner (ZWJ), and if yes, then checks if it is supported by the system.
22
+ */
23
+ isEmojiSupported(item: EmojiCdnResource, emojiSupportedVersionByOs: number, container: HTMLDivElement): boolean;
24
+ /**
25
+ * Checks the supported emoji version by the OS, by sampling some representatives from different emoji releases.
26
+ */
27
+ getEmojiSupportedVersionByOs(): number;
28
+ /**
29
+ * Check for ZWJ (zero width joiner) character.
30
+ */
31
+ hasZwj(emoji: string): boolean;
32
+ /**
33
+ * Checks whether the emoji is supported in the operating system.
34
+ */
35
+ isEmojiZwjSupported(item: EmojiCdnResource, container: HTMLDivElement): boolean;
36
+ /**
37
+ * Returns the width of the provided node.
38
+ */
39
+ getNodeWidth(container: HTMLDivElement, node: string): number;
40
+ /**
41
+ * Creates a div for emoji width testing purposes.
42
+ */
43
+ createEmojiWidthTestingContainer(): HTMLDivElement;
44
+ /**
45
+ * Adds default skin tone property to each emoji. If emoji defines other skin tones, they are added as well.
46
+ */
47
+ normalizeEmojiSkinTone(item: EmojiCdnResource): EmojiEntry;
48
+ /**
49
+ * Checks whether the emoji belongs to a group that is allowed.
50
+ */
51
+ isEmojiCategoryAllowed(item: EmojiCdnResource): boolean;
52
+ /**
53
+ * A function used to determine if emoji is supported by detecting pixels.
54
+ *
55
+ * Referenced for unit testing purposes. Kept in a separate file because of licensing.
56
+ */
57
+ private static _isEmojiSupported;
58
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
+ */
5
+ import { Plugin } from 'ckeditor5/src/core.js';
6
+ import isEmojiSupported from './utils/isemojisupported.js';
7
+ /**
8
+ * @module emoji/emojiutils
9
+ */
10
+ const SKIN_TONE_MAP = {
11
+ 0: 'default',
12
+ 1: 'light',
13
+ 2: 'medium-light',
14
+ 3: 'medium',
15
+ 4: 'medium-dark',
16
+ 5: 'dark'
17
+ };
18
+ /**
19
+ * A map representing an emoji and its release version.
20
+ * It's used to identify a user's minimal supported emoji level.
21
+ */
22
+ const EMOJI_SUPPORT_LEVEL = {
23
+ '🫩': 16,
24
+ '🫨': 15.1 // Shaking head. Although the version of emoji is 15, it is used to detect versions 15 and 15.1.
25
+ };
26
+ const BASELINE_EMOJI_WIDTH = 24;
27
+ /**
28
+ * The Emoji utilities plugin.
29
+ */
30
+ class EmojiUtils extends Plugin {
31
+ /**
32
+ * @inheritDoc
33
+ */
34
+ static get pluginName() {
35
+ return 'EmojiUtils';
36
+ }
37
+ /**
38
+ * @inheritDoc
39
+ */
40
+ static get isOfficialPlugin() {
41
+ return true;
42
+ }
43
+ /**
44
+ * Checks if the emoji is supported by verifying the emoji version supported by the system first.
45
+ * Then checks if emoji contains a zero width joiner (ZWJ), and if yes, then checks if it is supported by the system.
46
+ */
47
+ isEmojiSupported(item, emojiSupportedVersionByOs, container) {
48
+ const isEmojiVersionSupported = item.version <= emojiSupportedVersionByOs;
49
+ if (!isEmojiVersionSupported) {
50
+ return false;
51
+ }
52
+ if (!this.hasZwj(item.emoji)) {
53
+ return true;
54
+ }
55
+ return this.isEmojiZwjSupported(item, container);
56
+ }
57
+ /**
58
+ * Checks the supported emoji version by the OS, by sampling some representatives from different emoji releases.
59
+ */
60
+ getEmojiSupportedVersionByOs() {
61
+ return Object.entries(EMOJI_SUPPORT_LEVEL)
62
+ .reduce((currentVersion, [emoji, newVersion]) => {
63
+ if (newVersion > currentVersion && EmojiUtils._isEmojiSupported(emoji)) {
64
+ return newVersion;
65
+ }
66
+ return currentVersion;
67
+ }, 0);
68
+ }
69
+ /**
70
+ * Check for ZWJ (zero width joiner) character.
71
+ */
72
+ hasZwj(emoji) {
73
+ return emoji.includes('\u200d');
74
+ }
75
+ /**
76
+ * Checks whether the emoji is supported in the operating system.
77
+ */
78
+ isEmojiZwjSupported(item, container) {
79
+ const emojiWidth = this.getNodeWidth(container, item.emoji);
80
+ // On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard
81
+ // against are the ones that are 2x the size, because those are truly broken (person with red hair = person with
82
+ // floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.)
83
+ // So here we set the threshold at 1.8 times the size of the baseline emoji.
84
+ return emojiWidth < BASELINE_EMOJI_WIDTH * 1.8;
85
+ }
86
+ /**
87
+ * Returns the width of the provided node.
88
+ */
89
+ getNodeWidth(container, node) {
90
+ const span = document.createElement('span');
91
+ span.textContent = node;
92
+ container.appendChild(span);
93
+ const nodeWidth = span.offsetWidth;
94
+ container.removeChild(span);
95
+ return nodeWidth;
96
+ }
97
+ /**
98
+ * Creates a div for emoji width testing purposes.
99
+ */
100
+ createEmojiWidthTestingContainer() {
101
+ const container = document.createElement('div');
102
+ container.setAttribute('aria-hidden', 'true');
103
+ container.style.position = 'absolute';
104
+ container.style.left = '-9999px';
105
+ container.style.whiteSpace = 'nowrap';
106
+ container.style.fontSize = BASELINE_EMOJI_WIDTH + 'px';
107
+ return container;
108
+ }
109
+ /**
110
+ * Adds default skin tone property to each emoji. If emoji defines other skin tones, they are added as well.
111
+ */
112
+ normalizeEmojiSkinTone(item) {
113
+ const entry = {
114
+ ...item,
115
+ skins: {
116
+ default: item.emoji
117
+ }
118
+ };
119
+ if (item.skins) {
120
+ item.skins.forEach(skin => {
121
+ const skinTone = SKIN_TONE_MAP[skin.tone];
122
+ entry.skins[skinTone] = skin.emoji;
123
+ });
124
+ }
125
+ return entry;
126
+ }
127
+ /**
128
+ * Checks whether the emoji belongs to a group that is allowed.
129
+ */
130
+ isEmojiCategoryAllowed(item) {
131
+ // Category group=2 contains skin tones only, which we do not want to render.
132
+ return item.group !== 2;
133
+ }
134
+ }
135
+ /**
136
+ * A function used to determine if emoji is supported by detecting pixels.
137
+ *
138
+ * Referenced for unit testing purposes. Kept in a separate file because of licensing.
139
+ */
140
+ EmojiUtils._isEmojiSupported = isEmojiSupported;
141
+ export default EmojiUtils;
package/src/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export { default as Emoji } from './emoji.js';
9
9
  export { default as EmojiMention } from './emojimention.js';
10
10
  export { default as EmojiPicker } from './emojipicker.js';
11
11
  export { default as EmojiRepository } from './emojirepository.js';
12
+ export { default as EmojiUtils } from './emojiutils.js';
12
13
  export { default as EmojiCommand } from './emojicommand.js';
13
14
  export type { EmojiConfig } from './emojiconfig.js';
14
15
  import './augmentation.js';
package/src/index.js CHANGED
@@ -9,5 +9,6 @@ export { default as Emoji } from './emoji.js';
9
9
  export { default as EmojiMention } from './emojimention.js';
10
10
  export { default as EmojiPicker } from './emojipicker.js';
11
11
  export { default as EmojiRepository } from './emojirepository.js';
12
+ export { default as EmojiUtils } from './emojiutils.js';
12
13
  export { default as EmojiCommand } from './emojicommand.js';
13
14
  import './augmentation.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @license Copyright (c) 2023, Koala Interactive SAS
3
+ * For licensing, see https://github.com/koala-interactive/is-emoji-supported/blob/master/LICENSE.md
4
+ */
5
+ /**
6
+ * @module emoji/utils/isemojisupported
7
+ */
8
+ /**
9
+ * Checks if the two pixels parts are the same using canvas.
10
+ */
11
+ export default function isEmojiSupported(unicode: string): boolean;