Skip to content

Commit 69c48eb

Browse files
authored
Merge pull request #17864 from ckeditor/ck/17834-fix-emoji-filtering
Internal: Fixed emoji filtering and improved the filtering performance. Closes #17834.
2 parents 5110f6f + 1d88927 commit 69c48eb

File tree

16 files changed

+766
-267
lines changed

16 files changed

+766
-267
lines changed

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ module.exports = {
1717
// The CKEditor 5 core DLL build is created from JavaScript files.
1818
// ESLint should not process compiled TypeScript.
1919
'src/*.js',
20-
'**/*.d.ts'
20+
'**/*.d.ts',
21+
'packages/ckeditor5-emoji/src/utils/isemojisupported.ts'
2122
],
2223
rules: {
2324
'ckeditor5-rules/ckeditor-imports': 'error',

LICENSE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The following libraries are included in CKEditor under the [MIT license](https:/
2323
* color-parse - Copyright (c) 2015 Dmitry Ivanov.
2424
* emoji-picker-element-data - Copyright (c) 2020 Nolan Lawson.
2525
* Fuse.js - Copyright (c) 2017 Kirollos Risk.
26+
* is-emoji-supported - Copyright (c) 2016-2020 Koala Interactive, Inc.
2627
* Lodash - Copyright (c) JS Foundation and other contributors https://js.foundation/. Based on Underscore.js, copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors http://underscorejs.org/.
2728
* Marked - Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/), copyright (c) 2011–2018, Christopher Jeffrey (https://github.com/chjj/).
2829
* Turndown - Copyright (c) 2017+ Dom Christie.

packages/ckeditor5-emoji/LICENSE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The following libraries are included in CKEditor 5 emoji feature under the
2020

2121
* emoji-picker-element-data - Copyright (c) 2020 Nolan Lawson.
2222
* Fuse.js - Copyright (c) 2017 Kirollos Risk.
23+
* is-emoji-supported - Copyright (c) 2016-2020 Koala Interactive, Inc.
2324
* Lodash - Copyright (c) JS Foundation and other contributors https://js.foundation/. Based on Underscore.js, copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors http://underscorejs.org/.
2425

2526
Trademarks

packages/ckeditor5-emoji/src/augmentation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
EmojiMention,
1010
EmojiPicker,
1111
EmojiRepository,
12+
EmojiUtils,
1213
EmojiCommand
1314
} from './index.js';
1415

@@ -28,6 +29,7 @@ declare module '@ckeditor/ckeditor5-core' {
2829
[ EmojiMention.pluginName ]: EmojiMention;
2930
[ EmojiPicker.pluginName ]: EmojiPicker;
3031
[ EmojiRepository.pluginName ]: EmojiRepository;
32+
[ EmojiUtils.pluginName ]: EmojiUtils;
3133
}
3234

3335
interface CommandsMap {

packages/ckeditor5-emoji/src/emojimention.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ export default class EmojiMention extends Plugin {
2929
/**
3030
* An instance of the {@link module:emoji/emojipicker~EmojiPicker} plugin if it is loaded in the editor.
3131
*/
32-
declare private _emojiPickerPlugin: EmojiPicker | null;
32+
declare public emojiPickerPlugin: EmojiPicker | null;
3333

3434
/**
3535
* An instance of the {@link module:emoji/emojirepository~EmojiRepository} plugin.
3636
*/
37-
declare private _emojiRepositoryPlugin: EmojiRepository;
37+
declare public emojiRepositoryPlugin: EmojiRepository;
3838

3939
/**
4040
* A flag that informs if the {@link module:emoji/emojirepository~EmojiRepository} plugin is loaded correctly.
@@ -139,9 +139,9 @@ export default class EmojiMention extends Plugin {
139139
public async init(): Promise<void> {
140140
const editor = this.editor;
141141

142-
this._emojiPickerPlugin = editor.plugins.has( 'EmojiPicker' ) ? editor.plugins.get( 'EmojiPicker' ) : null;
143-
this._emojiRepositoryPlugin = editor.plugins.get( 'EmojiRepository' );
144-
this._isEmojiRepositoryAvailable = await this._emojiRepositoryPlugin.isReady();
142+
this.emojiPickerPlugin = editor.plugins.has( 'EmojiPicker' ) ? editor.plugins.get( 'EmojiPicker' ) : null;
143+
this.emojiRepositoryPlugin = editor.plugins.get( 'EmojiRepository' );
144+
this._isEmojiRepositoryAvailable = await this.emojiRepositoryPlugin.isReady();
145145

146146
// Override the `mention` command listener if the emoji repository is ready.
147147
if ( this._isEmojiRepositoryAvailable ) {
@@ -218,7 +218,7 @@ export default class EmojiMention extends Plugin {
218218
editor.model.deleteContent( writer.createSelection( eventData.range ) );
219219
} );
220220

221-
const emojiPickerPlugin = this._emojiPickerPlugin!;
221+
const emojiPickerPlugin = this.emojiPickerPlugin!;
222222

223223
emojiPickerPlugin.showUI( text.slice( 1 ) );
224224

@@ -251,12 +251,12 @@ export default class EmojiMention extends Plugin {
251251
return [];
252252
}
253253

254-
const emojis: Array<MentionFeedObjectItem> = this._emojiRepositoryPlugin.getEmojiByQuery( searchQuery )
254+
const emojis: Array<MentionFeedObjectItem> = this.emojiRepositoryPlugin.getEmojiByQuery( searchQuery )
255255
.map( emoji => {
256256
let text = emoji.skins[ this._skinTone ] || emoji.skins.default;
257257

258-
if ( this._emojiPickerPlugin ) {
259-
text = emoji.skins[ this._emojiPickerPlugin.skinTone ] || emoji.skins.default;
258+
if ( this.emojiPickerPlugin ) {
259+
text = emoji.skins[ this.emojiPickerPlugin.skinTone ] || emoji.skins.default;
260260
}
261261

262262
return {
@@ -265,7 +265,7 @@ export default class EmojiMention extends Plugin {
265265
};
266266
} );
267267

268-
if ( !this._emojiPickerPlugin ) {
268+
if ( !this.emojiPickerPlugin ) {
269269
return emojis.slice( 0, this._emojiDropdownLimit );
270270
}
271271

packages/ckeditor5-emoji/src/emojipicker.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ export default class EmojiPicker extends Plugin {
3636
/**
3737
* The contextual balloon plugin instance.
3838
*/
39-
declare public _balloonPlugin: ContextualBalloon;
39+
declare public balloonPlugin: ContextualBalloon;
4040

4141
/**
4242
* An instance of the {@link module:emoji/emojirepository~EmojiRepository} plugin.
4343
*/
44-
declare private _emojiRepositoryPlugin: EmojiRepository;
44+
declare public emojiRepositoryPlugin: EmojiRepository;
4545

4646
/**
4747
* @inheritDoc
@@ -70,11 +70,11 @@ export default class EmojiPicker extends Plugin {
7070
public async init(): Promise<void> {
7171
const editor = this.editor;
7272

73-
this._balloonPlugin = editor.plugins.get( 'ContextualBalloon' );
74-
this._emojiRepositoryPlugin = editor.plugins.get( 'EmojiRepository' );
73+
this.balloonPlugin = editor.plugins.get( 'ContextualBalloon' );
74+
this.emojiRepositoryPlugin = editor.plugins.get( 'EmojiRepository' );
7575

7676
// Skip registering a button in the toolbar and list item in the menu bar if the emoji repository is not ready.
77-
if ( !await this._emojiRepositoryPlugin.isReady() ) {
77+
if ( !await this.emojiRepositoryPlugin.isReady() ) {
7878
return;
7979
}
8080

@@ -144,8 +144,8 @@ export default class EmojiPicker extends Plugin {
144144

145145
this.emojiPickerView.searchView.search( searchValue );
146146

147-
if ( !this._balloonPlugin.hasView( this.emojiPickerView ) ) {
148-
this._balloonPlugin.add( {
147+
if ( !this.balloonPlugin.hasView( this.emojiPickerView ) ) {
148+
this.balloonPlugin.add( {
149149
view: this.emojiPickerView,
150150
position: this._getBalloonPositionData()
151151
} );
@@ -181,11 +181,11 @@ export default class EmojiPicker extends Plugin {
181181
*/
182182
private _createEmojiPickerView(): EmojiPickerView {
183183
const emojiPickerView = new EmojiPickerView( this.editor.locale, {
184-
emojiCategories: this._emojiRepositoryPlugin.getEmojiCategories(),
184+
emojiCategories: this.emojiRepositoryPlugin.getEmojiCategories(),
185185
skinTone: this.editor.config.get( 'emoji.skinTone' )!,
186-
skinTones: this._emojiRepositoryPlugin.getSkinTones(),
186+
skinTones: this.emojiRepositoryPlugin.getSkinTones(),
187187
getEmojiByQuery: ( query: string ) => {
188-
return this._emojiRepositoryPlugin.getEmojiByQuery( query );
188+
return this.emojiRepositoryPlugin.getEmojiByQuery( query );
189189
}
190190
} );
191191

@@ -200,8 +200,8 @@ export default class EmojiPicker extends Plugin {
200200

201201
// Update the balloon position when layout is changed.
202202
this.listenTo<EmojiPickerViewUpdateEvent>( emojiPickerView, 'update', () => {
203-
if ( this._balloonPlugin.visibleView === emojiPickerView ) {
204-
this._balloonPlugin.updatePosition();
203+
if ( this.balloonPlugin.visibleView === emojiPickerView ) {
204+
this.balloonPlugin.updatePosition();
205205
}
206206
} );
207207

@@ -214,9 +214,9 @@ export default class EmojiPicker extends Plugin {
214214
// Close the dialog when clicking outside of it.
215215
clickOutsideHandler( {
216216
emitter: emojiPickerView,
217-
contextElements: [ this._balloonPlugin.view.element! ],
217+
contextElements: [ this.balloonPlugin.view.element! ],
218218
callback: () => this._hideUI(),
219-
activator: () => this._balloonPlugin.visibleView === emojiPickerView
219+
activator: () => this.balloonPlugin.visibleView === emojiPickerView
220220
} );
221221

222222
return emojiPickerView;
@@ -226,7 +226,7 @@ export default class EmojiPicker extends Plugin {
226226
* Hides the balloon with the emoji picker.
227227
*/
228228
private _hideUI(): void {
229-
this._balloonPlugin.remove( this.emojiPickerView! );
229+
this.balloonPlugin.remove( this.emojiPickerView! );
230230
this.emojiPickerView!.searchView.setInputValue( '' );
231231
this.editor.editing.view.focus();
232232
this._hideFakeVisualSelection();
@@ -267,7 +267,7 @@ export default class EmojiPicker extends Plugin {
267267
}
268268

269269
/**
270-
* Returns positioning options for the {@link #_balloonPlugin}. They control the way the balloon is attached
270+
* Returns positioning options for the {@link #balloonPlugin}. They control the way the balloon is attached
271271
* to the target element or selection.
272272
*/
273273
private _getBalloonPositionData(): Partial<PositionOptions> {

packages/ckeditor5-emoji/src/emojirepository.ts

Lines changed: 25 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,31 @@
1010
import Fuse from 'fuse.js';
1111
import { groupBy } from 'lodash-es';
1212

13-
import { Plugin, type Editor } from 'ckeditor5/src/core.js';
13+
import { type Editor, Plugin } from 'ckeditor5/src/core.js';
1414
import { logWarning } from 'ckeditor5/src/utils.js';
15+
import EmojiUtils from './emojiutils.js';
1516
import type { SkinToneId } from './emojiconfig.js';
1617

1718
// An endpoint from which the emoji database will be downloaded during plugin initialization.
1819
// The `{version}` placeholder is replaced with the value from editor config.
1920
const EMOJI_DATABASE_URL = 'https://cdn.ckeditor.com/ckeditor5/data/emoji/{version}/en.json';
2021

21-
const SKIN_TONE_MAP: Record<number, SkinToneId> = {
22-
0: 'default',
23-
1: 'light',
24-
2: 'medium-light',
25-
3: 'medium',
26-
4: 'medium-dark',
27-
5: 'dark'
28-
};
29-
30-
const BASELINE_EMOJI_WIDTH = 24;
31-
3222
/**
3323
* The emoji repository plugin.
3424
*
3525
* Loads the emoji database from URL during plugin initialization and provides utility methods to search it.
3626
*/
3727
export default class EmojiRepository extends Plugin {
28+
/**
29+
* A callback to resolve the {@link #_databasePromise} to control the return value of this promise.
30+
*/
31+
declare private _databasePromiseResolveCallback: ( value: boolean ) => void;
32+
33+
/**
34+
* An instance of the [Fuse.js](https://www.fusejs.io/) library.
35+
*/
36+
private _fuseSearch: Fuse<EmojiEntry> | null;
37+
3838
/**
3939
* Emoji database.
4040
*/
@@ -44,17 +44,14 @@ export default class EmojiRepository extends Plugin {
4444
* A promise resolved after downloading the emoji database.
4545
* The promise resolves with `true` when the database is successfully downloaded or `false` otherwise.
4646
*/
47-
private _databasePromise: Promise<boolean>;
48-
49-
/**
50-
* A callback to resolve the {@link #_databasePromise} to control the return value of this promise.
51-
*/
52-
declare private _databasePromiseResolveCallback: ( value: boolean ) => void;
47+
private readonly _databasePromise: Promise<boolean>;
5348

5449
/**
55-
* An instance of the [Fuse.js](https://www.fusejs.io/) library.
50+
* @inheritDoc
5651
*/
57-
private _fuseSearch: Fuse<EmojiEntry> | null;
52+
public static get requires() {
53+
return [ EmojiUtils ] as const;
54+
}
5855

5956
/**
6057
* @inheritDoc
@@ -93,23 +90,27 @@ export default class EmojiRepository extends Plugin {
9390
* @inheritDoc
9491
*/
9592
public async init(): Promise<void> {
93+
const emojiUtils = this.editor.plugins.get( 'EmojiUtils' );
9694
const emojiVersion = this.editor.config.get( 'emoji.version' )!;
95+
9796
const emojiDatabaseUrl = EMOJI_DATABASE_URL.replace( '{version}', `${ emojiVersion }` );
9897
const emojiDatabase = await loadEmojiDatabase( emojiDatabaseUrl );
98+
const emojiSupportedVersionByOs = emojiUtils.getEmojiSupportedVersionByOs();
9999

100100
// Skip the initialization if the emoji database download has failed.
101101
// An empty database prevents the initialization of other dependent plugins, such as `EmojiMention` and `EmojiPicker`.
102102
if ( !emojiDatabase.length ) {
103103
return this._databasePromiseResolveCallback( false );
104104
}
105105

106-
const container = createEmojiWidthTestingContainer();
106+
const container = emojiUtils.createEmojiWidthTestingContainer();
107+
document.body.appendChild( container );
107108

108109
// Store the emoji database after normalizing the raw data.
109110
this._database = emojiDatabase
110-
.filter( item => isEmojiCategoryAllowed( item ) )
111-
.filter( item => EmojiRepository._isEmojiSupported( item, container ) )
112-
.map( item => normalizeEmojiSkinTone( item ) );
111+
.filter( item => emojiUtils.isEmojiCategoryAllowed( item ) )
112+
.filter( item => emojiUtils.isEmojiSupported( item, emojiSupportedVersionByOs, container ) )
113+
.map( item => emojiUtils.normalizeEmojiSkinTone( item ) );
113114

114115
container.remove();
115116

@@ -224,13 +225,6 @@ export default class EmojiRepository extends Plugin {
224225
public isReady(): Promise<boolean> {
225226
return this._databasePromise;
226227
}
227-
228-
/**
229-
* A function used to check if the given emoji is supported in the operating system.
230-
*
231-
* Referenced for unit testing purposes.
232-
*/
233-
private static _isEmojiSupported = isEmojiSupported;
234228
}
235229

236230
/**
@@ -265,78 +259,6 @@ async function loadEmojiDatabase( emojiDatabaseUrl: string ): Promise<Array<Emoj
265259
return result;
266260
}
267261

268-
/**
269-
* Creates a div for emoji width testing purposes.
270-
*/
271-
function createEmojiWidthTestingContainer(): HTMLDivElement {
272-
const container = document.createElement( 'div' );
273-
274-
container.setAttribute( 'aria-hidden', 'true' );
275-
container.style.position = 'absolute';
276-
container.style.left = '-9999px';
277-
container.style.whiteSpace = 'nowrap';
278-
container.style.fontSize = BASELINE_EMOJI_WIDTH + 'px';
279-
document.body.appendChild( container );
280-
281-
return container;
282-
}
283-
284-
/**
285-
* Returns the width of the provided node.
286-
*/
287-
function getNodeWidth( container: HTMLDivElement, node: string ): number {
288-
const span = document.createElement( 'span' );
289-
span.textContent = node;
290-
container.appendChild( span );
291-
const nodeWidth = span.offsetWidth;
292-
container.removeChild( span );
293-
294-
return nodeWidth;
295-
}
296-
297-
/**
298-
* Checks whether the emoji is supported in the operating system.
299-
*/
300-
function isEmojiSupported( item: EmojiCdnResource, container: HTMLDivElement ): boolean {
301-
const emojiWidth = getNodeWidth( container, item.emoji );
302-
303-
// On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard
304-
// against are the ones that are 2x the size, because those are truly broken (person with red hair = person with
305-
// floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.)
306-
// So here we set the threshold at 1.8 times the size of the baseline emoji.
307-
return ( emojiWidth / 1.8 < BASELINE_EMOJI_WIDTH ) && ( emojiWidth >= BASELINE_EMOJI_WIDTH );
308-
}
309-
310-
/**
311-
* Adds default skin tone property to each emoji. If emoji defines other skin tones, they are added as well.
312-
*/
313-
function normalizeEmojiSkinTone( item: EmojiCdnResource ): EmojiEntry {
314-
const entry: EmojiEntry = {
315-
...item,
316-
skins: {
317-
default: item.emoji
318-
}
319-
};
320-
321-
if ( item.skins ) {
322-
item.skins.forEach( skin => {
323-
const skinTone = SKIN_TONE_MAP[ skin.tone ];
324-
325-
entry.skins[ skinTone ] = skin.emoji;
326-
} );
327-
}
328-
329-
return entry;
330-
}
331-
332-
/**
333-
* Checks whether the emoji belongs to a group that is allowed.
334-
*/
335-
function isEmojiCategoryAllowed( item: EmojiCdnResource ): boolean {
336-
// Category group=2 contains skin tones only, which we do not want to render.
337-
return item.group !== 2;
338-
}
339-
340262
/**
341263
* Represents a single group of the emoji category, e.g., "Smileys & Expressions".
342264
*/

0 commit comments

Comments
 (0)