@ckeditor/ckeditor5-emoji 44.2.0-alpha.16 → 44.2.0-alpha.3
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/LICENSE.md +0 -1
- package/build/emoji.js +1 -1
- package/ckeditor5-metadata.json +2 -15
- package/dist/index-editor.css +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.js +151 -371
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/augmentation.d.ts +1 -2
- package/src/emojiconfig.d.ts +2 -20
- package/src/emojimention.d.ts +6 -13
- package/src/emojimention.js +26 -43
- package/src/emojipicker.d.ts +3 -3
- package/src/emojipicker.js +14 -14
- package/src/emojirepository.d.ts +17 -47
- package/src/emojirepository.js +125 -147
- package/src/index.d.ts +0 -1
- package/src/index.js +0 -1
- package/src/ui/emojipickerview.js +0 -1
- package/theme/emojigrid.css +1 -1
- package/src/emojiutils.d.ts +0 -58
- package/src/emojiutils.js +0 -141
- package/src/utils/isemojisupported.d.ts +0 -11
- package/src/utils/isemojisupported.js +0 -68
package/src/emojirepository.js
CHANGED
|
@@ -8,24 +8,25 @@
|
|
|
8
8
|
import Fuse from 'fuse.js';
|
|
9
9
|
import { groupBy } from 'lodash-es';
|
|
10
10
|
import { Plugin } from 'ckeditor5/src/core.js';
|
|
11
|
-
import { logWarning
|
|
12
|
-
|
|
13
|
-
// An endpoint from which the emoji data will be downloaded during plugin initialization.
|
|
11
|
+
import { logWarning } from 'ckeditor5/src/utils.js';
|
|
12
|
+
// An endpoint from which the emoji database will be downloaded during plugin initialization.
|
|
14
13
|
// The `{version}` placeholder is replaced with the value from editor config.
|
|
15
|
-
const
|
|
16
|
-
const
|
|
14
|
+
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;
|
|
17
24
|
/**
|
|
18
25
|
* The emoji repository plugin.
|
|
19
26
|
*
|
|
20
|
-
* Loads the emoji
|
|
27
|
+
* Loads the emoji database from URL during plugin initialization and provides utility methods to search it.
|
|
21
28
|
*/
|
|
22
29
|
class EmojiRepository extends Plugin {
|
|
23
|
-
/**
|
|
24
|
-
* @inheritDoc
|
|
25
|
-
*/
|
|
26
|
-
static get requires() {
|
|
27
|
-
return [EmojiUtils];
|
|
28
|
-
}
|
|
29
30
|
/**
|
|
30
31
|
* @inheritDoc
|
|
31
32
|
*/
|
|
@@ -43,14 +44,13 @@ class EmojiRepository extends Plugin {
|
|
|
43
44
|
*/
|
|
44
45
|
constructor(editor) {
|
|
45
46
|
super(editor);
|
|
46
|
-
editor.config.define('emoji', {
|
|
47
|
-
version:
|
|
48
|
-
skinTone: 'default'
|
|
49
|
-
definitionsUrl: undefined
|
|
47
|
+
this.editor.config.define('emoji', {
|
|
48
|
+
version: 16,
|
|
49
|
+
skinTone: 'default'
|
|
50
50
|
});
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
53
|
-
this.
|
|
51
|
+
this._database = [];
|
|
52
|
+
this._databasePromise = new Promise(resolve => {
|
|
53
|
+
this._databasePromiseResolveCallback = resolve;
|
|
54
54
|
});
|
|
55
55
|
this._fuseSearch = null;
|
|
56
56
|
}
|
|
@@ -58,16 +58,23 @@ class EmojiRepository extends Plugin {
|
|
|
58
58
|
* @inheritDoc
|
|
59
59
|
*/
|
|
60
60
|
async init() {
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
// Skip
|
|
65
|
-
//
|
|
66
|
-
if (!
|
|
67
|
-
return this.
|
|
61
|
+
const emojiVersion = this.editor.config.get('emoji.version');
|
|
62
|
+
const emojiDatabaseUrl = EMOJI_DATABASE_URL.replace('{version}', `${emojiVersion}`);
|
|
63
|
+
const emojiDatabase = await loadEmojiDatabase(emojiDatabaseUrl);
|
|
64
|
+
// Skip the initialization if the emoji database download has failed.
|
|
65
|
+
// An empty database prevents the initialization of other dependent plugins, such as `EmojiMention` and `EmojiPicker`.
|
|
66
|
+
if (!emojiDatabase.length) {
|
|
67
|
+
return this._databasePromiseResolveCallback(false);
|
|
68
68
|
}
|
|
69
|
+
const container = createEmojiWidthTestingContainer();
|
|
70
|
+
// Store the emoji database after normalizing the raw data.
|
|
71
|
+
this._database = emojiDatabase
|
|
72
|
+
.filter(item => isEmojiCategoryAllowed(item))
|
|
73
|
+
.filter(item => EmojiRepository._isEmojiSupported(item, container))
|
|
74
|
+
.map(item => normalizeEmojiSkinTone(item));
|
|
75
|
+
container.remove();
|
|
69
76
|
// Create instance of the Fuse.js library with configured weighted search keys and disabled fuzzy search.
|
|
70
|
-
this._fuseSearch = new Fuse(
|
|
77
|
+
this._fuseSearch = new Fuse(this._database, {
|
|
71
78
|
keys: [
|
|
72
79
|
{ name: 'emoticon', weight: 5 },
|
|
73
80
|
{ name: 'annotation', weight: 3 },
|
|
@@ -77,11 +84,11 @@ class EmojiRepository extends Plugin {
|
|
|
77
84
|
threshold: 0,
|
|
78
85
|
ignoreLocation: true
|
|
79
86
|
});
|
|
80
|
-
return this.
|
|
87
|
+
return this._databasePromiseResolveCallback(true);
|
|
81
88
|
}
|
|
82
89
|
/**
|
|
83
90
|
* Returns an array of emoji entries that match the search query.
|
|
84
|
-
* If the emoji
|
|
91
|
+
* If the emoji database is not loaded, the [Fuse.js](https://www.fusejs.io/) instance is not created,
|
|
85
92
|
* hence this method returns an empty array.
|
|
86
93
|
*
|
|
87
94
|
* @param searchQuery A search query to match emoji.
|
|
@@ -115,13 +122,12 @@ class EmojiRepository extends Plugin {
|
|
|
115
122
|
}
|
|
116
123
|
/**
|
|
117
124
|
* Groups all emojis by categories.
|
|
118
|
-
* If the emoji
|
|
125
|
+
* If the emoji database is not loaded, it returns an empty array.
|
|
119
126
|
*
|
|
120
127
|
* @returns An array of emoji entries grouped by categories.
|
|
121
128
|
*/
|
|
122
129
|
getEmojiCategories() {
|
|
123
|
-
|
|
124
|
-
if (!repository) {
|
|
130
|
+
if (!this._database.length) {
|
|
125
131
|
return [];
|
|
126
132
|
}
|
|
127
133
|
const { t } = this.editor.locale;
|
|
@@ -136,7 +142,7 @@ class EmojiRepository extends Plugin {
|
|
|
136
142
|
{ title: t('Symbols'), icon: '🟢', groupId: 8 },
|
|
137
143
|
{ title: t('Flags'), icon: '🏁', groupId: 9 }
|
|
138
144
|
];
|
|
139
|
-
const groups = groupBy(
|
|
145
|
+
const groups = groupBy(this._database, 'group');
|
|
140
146
|
return categories.map(category => {
|
|
141
147
|
return {
|
|
142
148
|
...category,
|
|
@@ -159,132 +165,104 @@ class EmojiRepository extends Plugin {
|
|
|
159
165
|
];
|
|
160
166
|
}
|
|
161
167
|
/**
|
|
162
|
-
* Indicates whether the emoji
|
|
168
|
+
* Indicates whether the emoji database has been successfully downloaded and the plugin is operational.
|
|
163
169
|
*/
|
|
164
170
|
isReady() {
|
|
165
|
-
return this.
|
|
171
|
+
return this._databasePromise;
|
|
166
172
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Both {@link module:emoji/emojiconfig~EmojiConfig#definitionsUrl `emoji.definitionsUrl`} and
|
|
185
|
-
* {@link module:emoji/emojiconfig~EmojiConfig#version `emoji.version`} configuration options
|
|
186
|
-
* are set. Only the `emoji.definitionsUrl` option will be used.
|
|
187
|
-
*
|
|
188
|
-
* The `emoji.version` option will be ignored and should be removed from the configuration.
|
|
189
|
-
*
|
|
190
|
-
* @error emoji-repository-redundant-version
|
|
191
|
-
*/
|
|
192
|
-
logWarning('emoji-repository-redundant-version');
|
|
193
|
-
}
|
|
194
|
-
return new URL(definitionsUrl);
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Warn users on self-hosted installations that this plugin uses a CDN to fetch the emoji repository.
|
|
198
|
-
*/
|
|
199
|
-
_warnAboutCdnUse() {
|
|
200
|
-
const editor = this.editor;
|
|
201
|
-
const config = editor.config.get('emoji');
|
|
202
|
-
const licenseKey = editor.config.get('licenseKey');
|
|
203
|
-
const distributionChannel = window[Symbol.for('cke distribution')];
|
|
204
|
-
if (licenseKey === 'GPL') {
|
|
205
|
-
// Don't warn GPL users.
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
if (distributionChannel === 'cloud') {
|
|
209
|
-
// Don't warn cloud users, because they already use our CDN.
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (config && config.definitionsUrl) {
|
|
213
|
-
// Don't warn users who have configured their own definitions URL.
|
|
214
|
-
return;
|
|
173
|
+
}
|
|
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
|
+
/**
|
|
182
|
+
* Makes the HTTP request to download the emoji database.
|
|
183
|
+
*/
|
|
184
|
+
async function loadEmojiDatabase(emojiDatabaseUrl) {
|
|
185
|
+
const result = await fetch(emojiDatabaseUrl)
|
|
186
|
+
.then(response => {
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
return [];
|
|
215
189
|
}
|
|
190
|
+
return response.json();
|
|
191
|
+
})
|
|
192
|
+
.catch(() => {
|
|
193
|
+
return [];
|
|
194
|
+
});
|
|
195
|
+
if (!result.length) {
|
|
216
196
|
/**
|
|
217
|
-
*
|
|
218
|
-
* you can use the {@link module:emoji/emojiconfig~EmojiConfig#definitionsUrl `emoji.definitionsUrl`}
|
|
219
|
-
* configuration option to provide a URL to your own emoji repository.
|
|
197
|
+
* Unable to load the emoji database from CDN.
|
|
220
198
|
*
|
|
221
|
-
* If
|
|
199
|
+
* If the CDN works properly and there is no disruption of communication, please check your
|
|
200
|
+
* {@glink getting-started/setup/csp Content Security Policy (CSP)} setting and make sure
|
|
201
|
+
* the CDN connection is allowed by the editor.
|
|
222
202
|
*
|
|
223
|
-
* @error emoji-
|
|
203
|
+
* @error emoji-database-load-failed
|
|
224
204
|
*/
|
|
225
|
-
logWarning('emoji-
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Returns the emoji repository in a configured version if it is a non-empty array. Returns `null` otherwise.
|
|
229
|
-
*/
|
|
230
|
-
_getItems() {
|
|
231
|
-
const repository = EmojiRepository._results[this._url.href];
|
|
232
|
-
return repository && repository.length ? repository : null;
|
|
205
|
+
logWarning('emoji-database-load-failed');
|
|
233
206
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
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
|
|
242
252
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return response.json();
|
|
249
|
-
})
|
|
250
|
-
.catch(() => {
|
|
251
|
-
return [];
|
|
253
|
+
};
|
|
254
|
+
if (item.skins) {
|
|
255
|
+
item.skins.forEach(skin => {
|
|
256
|
+
const skinTone = SKIN_TONE_MAP[skin.tone];
|
|
257
|
+
entry.skins[skinTone] = skin.emoji;
|
|
252
258
|
});
|
|
253
|
-
if (!result.length) {
|
|
254
|
-
/**
|
|
255
|
-
* Unable to load the emoji repository from the URL.
|
|
256
|
-
*
|
|
257
|
-
* If the URL works properly and there is no disruption of communication, please check your
|
|
258
|
-
* {@glink getting-started/setup/csp Content Security Policy (CSP)} setting and make sure
|
|
259
|
-
* the URL connection is allowed by the editor.
|
|
260
|
-
*
|
|
261
|
-
* @error emoji-repository-load-failed
|
|
262
|
-
*/
|
|
263
|
-
logWarning('emoji-repository-load-failed');
|
|
264
|
-
}
|
|
265
|
-
EmojiRepository._results[this._url.href] = this._normalizeEmoji(result);
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Normalizes the raw data fetched from CDN. By normalization, we meant:
|
|
269
|
-
*
|
|
270
|
-
* * Filter out unsupported emoji (these that will not render correctly),
|
|
271
|
-
* * Prepare skin tone variants if an emoji defines them.
|
|
272
|
-
*/
|
|
273
|
-
_normalizeEmoji(data) {
|
|
274
|
-
const emojiUtils = this.editor.plugins.get('EmojiUtils');
|
|
275
|
-
const emojiSupportedVersionByOs = emojiUtils.getEmojiSupportedVersionByOs();
|
|
276
|
-
const container = emojiUtils.createEmojiWidthTestingContainer();
|
|
277
|
-
document.body.appendChild(container);
|
|
278
|
-
const results = data
|
|
279
|
-
.filter(item => emojiUtils.isEmojiCategoryAllowed(item))
|
|
280
|
-
.filter(item => emojiUtils.isEmojiSupported(item, emojiSupportedVersionByOs, container))
|
|
281
|
-
.map(item => emojiUtils.normalizeEmojiSkinTone(item));
|
|
282
|
-
container.remove();
|
|
283
|
-
return results;
|
|
284
259
|
}
|
|
260
|
+
return entry;
|
|
285
261
|
}
|
|
286
262
|
/**
|
|
287
|
-
*
|
|
263
|
+
* Checks whether the emoji belongs to a group that is allowed.
|
|
288
264
|
*/
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -9,7 +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';
|
|
13
12
|
export { default as EmojiCommand } from './emojicommand.js';
|
|
14
13
|
export type { EmojiConfig } from './emojiconfig.js';
|
|
15
14
|
import './augmentation.js';
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,5 @@ 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';
|
|
13
12
|
export { default as EmojiCommand } from './emojicommand.js';
|
|
14
13
|
import './augmentation.js';
|
|
@@ -156,7 +156,6 @@ export default class EmojiPickerView extends View {
|
|
|
156
156
|
// Emit an update event to react to balloon dimensions changes.
|
|
157
157
|
this.searchView.on('search', () => {
|
|
158
158
|
this.fire('update');
|
|
159
|
-
this.gridView.element.scrollTo(0, 0);
|
|
160
159
|
});
|
|
161
160
|
// Update the grid of emojis when the selected category is changed.
|
|
162
161
|
this.categoriesView.on('change:categoryName', (ev, args, categoryName) => {
|
package/theme/emojigrid.css
CHANGED
package/src/emojiutils.d.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
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
|
-
}
|
package/src/emojiutils.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
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;
|
|
@@ -1,11 +0,0 @@
|
|
|
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;
|
|
@@ -1,68 +0,0 @@
|
|
|
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) {
|
|
12
|
-
const ctx = getCanvas();
|
|
13
|
-
/* istanbul ignore next -- @preserve */
|
|
14
|
-
if (!ctx) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
const CANVAS_HEIGHT = 25;
|
|
18
|
-
const CANVAS_WIDTH = 20;
|
|
19
|
-
const textSize = Math.floor(CANVAS_HEIGHT / 2);
|
|
20
|
-
// Initialize canvas context.
|
|
21
|
-
ctx.font = textSize + 'px Arial, Sans-Serif';
|
|
22
|
-
ctx.textBaseline = 'top';
|
|
23
|
-
ctx.canvas.width = CANVAS_WIDTH * 2;
|
|
24
|
-
ctx.canvas.height = CANVAS_HEIGHT;
|
|
25
|
-
ctx.clearRect(0, 0, CANVAS_WIDTH * 2, CANVAS_HEIGHT);
|
|
26
|
-
// Draw in red on the left.
|
|
27
|
-
ctx.fillStyle = '#FF0000';
|
|
28
|
-
ctx.fillText(unicode, 0, 22);
|
|
29
|
-
// Draw in blue on right.
|
|
30
|
-
ctx.fillStyle = '#0000FF';
|
|
31
|
-
ctx.fillText(unicode, CANVAS_WIDTH, 22);
|
|
32
|
-
const a = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT).data;
|
|
33
|
-
const count = a.length;
|
|
34
|
-
let i = 0;
|
|
35
|
-
// Search the first visible pixel.
|
|
36
|
-
for (; i < count && !a[i + 3]; i += 4)
|
|
37
|
-
;
|
|
38
|
-
// No visible pixel.
|
|
39
|
-
/* istanbul ignore next -- @preserve */
|
|
40
|
-
if (i >= count) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
// Emoji has immutable color, so we check the color of the emoji in two different colors.
|
|
44
|
-
// the result show be the same.
|
|
45
|
-
const x = CANVAS_WIDTH + ((i / 4) % CANVAS_WIDTH);
|
|
46
|
-
const y = Math.floor(i / 4 / CANVAS_WIDTH);
|
|
47
|
-
const b = ctx.getImageData(x, y, 1, 1).data;
|
|
48
|
-
if (a[i] !== b[0] || a[i + 2] !== b[2]) {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
//Some emojis consist of different ones, so they will show multiple characters if they are not supported.
|
|
52
|
-
/* istanbul ignore next -- @preserve */
|
|
53
|
-
if (ctx.measureText(unicode).width >= CANVAS_WIDTH) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
// Supported.
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
;
|
|
60
|
-
function getCanvas() {
|
|
61
|
-
try {
|
|
62
|
-
return document.createElement('canvas').getContext('2d', { willReadFrequently: true });
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
/* istanbul ignore next -- @preserve */
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|