Skip to content

Commit 24d8796

Browse files
authored
Merge pull request #17867 from ckeditor/ck/17838
Internal: Download the emoji database only once and make it available for other editors. Closes #17838.
2 parents cf1ac30 + 07865bd commit 24d8796

File tree

7 files changed

+533
-330
lines changed

7 files changed

+533
-330
lines changed

packages/ckeditor5-emoji/src/emojiconfig.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ export interface EmojiConfig {
8080
*
8181
* @default 16
8282
*/
83-
version?: 15 | 16;
83+
version?: EmojiVersion;
8484
}
8585

8686
export type SkinToneId = 'default' | 'light' | 'medium-light' | 'medium' | 'medium-dark' | 'dark';
87+
88+
export type EmojiVersion = 15 | 16;

packages/ckeditor5-emoji/src/emojirepository.ts

Lines changed: 104 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,40 @@ import Fuse from 'fuse.js';
1111
import { groupBy } from 'lodash-es';
1212

1313
import { type Editor, Plugin } from 'ckeditor5/src/core.js';
14-
import { logWarning } from 'ckeditor5/src/utils.js';
14+
import { logWarning, version } from 'ckeditor5/src/utils.js';
1515
import EmojiUtils from './emojiutils.js';
16-
import type { SkinToneId } from './emojiconfig.js';
16+
import type { EmojiVersion, SkinToneId } from './emojiconfig.js';
1717

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

2222
/**
2323
* The emoji repository plugin.
2424
*
25-
* Loads the emoji database from URL during plugin initialization and provides utility methods to search it.
25+
* Loads the emoji repository from URL during plugin initialization and provides utility methods to search it.
2626
*/
2727
export default class EmojiRepository extends Plugin {
2828
/**
29-
* A callback to resolve the {@link #_databasePromise} to control the return value of this promise.
29+
* A callback to resolve the {@link #_repositoryPromise} to control the return value of this promise.
3030
*/
31-
declare private _databasePromiseResolveCallback: ( value: boolean ) => void;
31+
declare private _repositoryPromiseResolveCallback: ( value: boolean ) => void;
3232

3333
/**
3434
* An instance of the [Fuse.js](https://www.fusejs.io/) library.
3535
*/
3636
private _fuseSearch: Fuse<EmojiEntry> | null;
3737

3838
/**
39-
* Emoji database.
39+
* The emoji version that is used to prepare the emoji repository.
4040
*/
41-
private _database: Array<EmojiEntry>;
41+
private readonly _version: EmojiVersion;
4242

4343
/**
44-
* A promise resolved after downloading the emoji database.
45-
* The promise resolves with `true` when the database is successfully downloaded or `false` otherwise.
44+
* A promise resolved after downloading the emoji collection.
45+
* The promise resolves with `true` when the repository is successfully downloaded or `false` otherwise.
4646
*/
47-
private readonly _databasePromise: Promise<boolean>;
47+
private readonly _repositoryPromise: Promise<boolean>;
4848

4949
/**
5050
* @inheritDoc
@@ -73,14 +73,15 @@ export default class EmojiRepository extends Plugin {
7373
constructor( editor: Editor ) {
7474
super( editor );
7575

76-
this.editor.config.define( 'emoji', {
76+
editor.config.define( 'emoji', {
7777
version: 16,
7878
skinTone: 'default'
7979
} );
8080

81-
this._database = [];
82-
this._databasePromise = new Promise<boolean>( resolve => {
83-
this._databasePromiseResolveCallback = resolve;
81+
this._version = editor.config.get( 'emoji.version' )!;
82+
83+
this._repositoryPromise = new Promise<boolean>( resolve => {
84+
this._repositoryPromiseResolveCallback = resolve;
8485
} );
8586

8687
this._fuseSearch = null;
@@ -90,32 +91,22 @@ export default class EmojiRepository extends Plugin {
9091
* @inheritDoc
9192
*/
9293
public async init(): Promise<void> {
93-
const emojiUtils = this.editor.plugins.get( 'EmojiUtils' );
94-
const emojiVersion = this.editor.config.get( 'emoji.version' )!;
95-
96-
const emojiDatabaseUrl = EMOJI_DATABASE_URL.replace( '{version}', `${ emojiVersion }` );
97-
const emojiDatabase = await loadEmojiDatabase( emojiDatabaseUrl );
98-
const emojiSupportedVersionByOs = emojiUtils.getEmojiSupportedVersionByOs();
94+
if ( !( this._version in EmojiRepository._results ) ) {
95+
const cdnResult = await this._loadItemsFromCdn();
9996

100-
// Skip the initialization if the emoji database download has failed.
101-
// An empty database prevents the initialization of other dependent plugins, such as `EmojiMention` and `EmojiPicker`.
102-
if ( !emojiDatabase.length ) {
103-
return this._databasePromiseResolveCallback( false );
97+
EmojiRepository._results[ this._version ] = this._normalizeEmoji( cdnResult );
10498
}
10599

106-
const container = emojiUtils.createEmojiWidthTestingContainer();
107-
document.body.appendChild( container );
108-
109-
// Store the emoji database after normalizing the raw data.
110-
this._database = emojiDatabase
111-
.filter( item => emojiUtils.isEmojiCategoryAllowed( item ) )
112-
.filter( item => emojiUtils.isEmojiSupported( item, emojiSupportedVersionByOs, container ) )
113-
.map( item => emojiUtils.normalizeEmojiSkinTone( item ) );
100+
const items = this._getItems();
114101

115-
container.remove();
102+
// Skip plugin initialization if the emoji repository is not available.
103+
// The initialization of other dependent plugins, such as `EmojiMention` and `EmojiPicker`, is prevented as well.
104+
if ( !items ) {
105+
return this._repositoryPromiseResolveCallback( false );
106+
}
116107

117108
// Create instance of the Fuse.js library with configured weighted search keys and disabled fuzzy search.
118-
this._fuseSearch = new Fuse( this._database, {
109+
this._fuseSearch = new Fuse( items, {
119110
keys: [
120111
{ name: 'emoticon', weight: 5 },
121112
{ name: 'annotation', weight: 3 },
@@ -126,12 +117,12 @@ export default class EmojiRepository extends Plugin {
126117
ignoreLocation: true
127118
} );
128119

129-
return this._databasePromiseResolveCallback( true );
120+
return this._repositoryPromiseResolveCallback( true );
130121
}
131122

132123
/**
133124
* Returns an array of emoji entries that match the search query.
134-
* If the emoji database is not loaded, the [Fuse.js](https://www.fusejs.io/) instance is not created,
125+
* If the emoji repository is not loaded, the [Fuse.js](https://www.fusejs.io/) instance is not created,
135126
* hence this method returns an empty array.
136127
*
137128
* @param searchQuery A search query to match emoji.
@@ -170,12 +161,14 @@ export default class EmojiRepository extends Plugin {
170161

171162
/**
172163
* Groups all emojis by categories.
173-
* If the emoji database is not loaded, it returns an empty array.
164+
* If the emoji repository is not loaded, it returns an empty array.
174165
*
175166
* @returns An array of emoji entries grouped by categories.
176167
*/
177168
public getEmojiCategories(): Array<EmojiCategory> {
178-
if ( !this._database.length ) {
169+
const repository = this._getItems();
170+
171+
if ( !repository ) {
179172
return [];
180173
}
181174

@@ -193,7 +186,7 @@ export default class EmojiRepository extends Plugin {
193186
{ title: t( 'Flags' ), icon: '🏁', groupId: 9 }
194187
];
195188

196-
const groups = groupBy( this._database, 'group' );
189+
const groups = groupBy( repository, 'group' );
197190

198191
return categories.map( category => {
199192
return {
@@ -220,43 +213,86 @@ export default class EmojiRepository extends Plugin {
220213
}
221214

222215
/**
223-
* Indicates whether the emoji database has been successfully downloaded and the plugin is operational.
216+
* Indicates whether the emoji repository has been successfully downloaded and the plugin is operational.
224217
*/
225218
public isReady(): Promise<boolean> {
226-
return this._databasePromise;
219+
return this._repositoryPromise;
227220
}
228-
}
229221

230-
/**
231-
* Makes the HTTP request to download the emoji database.
232-
*/
233-
async function loadEmojiDatabase( emojiDatabaseUrl: string ): Promise<Array<EmojiCdnResource>> {
234-
const result = await fetch( emojiDatabaseUrl )
235-
.then( response => {
236-
if ( !response.ok ) {
222+
/**
223+
* Returns the emoji repository in a configured version if it is a non-empty array. Returns `null` otherwise.
224+
*/
225+
private _getItems(): Array<EmojiEntry> | null {
226+
const repository = EmojiRepository._results[ this._version ];
227+
228+
return repository && repository.length ? repository : null;
229+
}
230+
231+
/**
232+
* Makes the HTTP request to download the emoji repository in a configured version.
233+
*/
234+
private async _loadItemsFromCdn(): Promise<Array<EmojiCdnResource>> {
235+
const repositoryUrl = new URL( EMOJI_DATABASE_URL.replace( '{version}', `${ this._version }` ) );
236+
237+
repositoryUrl.searchParams.set( 'editorVersion', version );
238+
239+
const result: Array<EmojiCdnResource> = await fetch( repositoryUrl )
240+
.then( response => {
241+
if ( !response.ok ) {
242+
return [];
243+
}
244+
245+
return response.json();
246+
} )
247+
.catch( () => {
237248
return [];
238-
}
249+
} );
250+
251+
if ( !result.length ) {
252+
/**
253+
* Unable to load the emoji repository from CDN.
254+
*
255+
* If the CDN works properly and there is no disruption of communication, please check your
256+
* {@glink getting-started/setup/csp Content Security Policy (CSP)} setting and make sure
257+
* the CDN connection is allowed by the editor.
258+
*
259+
* @error emoji-repository-load-failed
260+
*/
261+
logWarning( 'emoji-repository-load-failed' );
262+
}
239263

240-
return response.json();
241-
} )
242-
.catch( () => {
243-
return [];
244-
} );
264+
return result;
265+
}
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+
private _normalizeEmoji( data: Array<EmojiCdnResource> ): Array<EmojiEntry> {
274+
const emojiUtils = this.editor.plugins.get( 'EmojiUtils' );
275+
const emojiSupportedVersionByOs = emojiUtils.getEmojiSupportedVersionByOs();
276+
277+
const container = emojiUtils.createEmojiWidthTestingContainer();
278+
document.body.appendChild( container );
279+
280+
const results = data
281+
.filter( item => emojiUtils.isEmojiCategoryAllowed( item ) )
282+
.filter( item => emojiUtils.isEmojiSupported( item, emojiSupportedVersionByOs, container ) )
283+
.map( item => emojiUtils.normalizeEmojiSkinTone( item ) );
284+
285+
container.remove();
245286

246-
if ( !result.length ) {
247-
/**
248-
* Unable to load the emoji database from CDN.
249-
*
250-
* If the CDN works properly and there is no disruption of communication, please check your
251-
* {@glink getting-started/setup/csp Content Security Policy (CSP)} setting and make sure
252-
* the CDN connection is allowed by the editor.
253-
*
254-
* @error emoji-database-load-failed
255-
*/
256-
logWarning( 'emoji-database-load-failed' );
287+
return results;
257288
}
258289

259-
return result;
290+
/**
291+
* Versioned emoji repository.
292+
*/
293+
private static _results: {
294+
[ key in EmojiVersion ]?: Array<EmojiEntry>
295+
} = {};
260296
}
261297

262298
/**

packages/ckeditor5-emoji/src/utils/isemojisupported.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export default function isEmojiSupported( unicode: string ): boolean {
7373

7474
function getCanvas(): CanvasRenderingContext2D | null {
7575
try {
76-
return document.createElement( 'canvas' ).getContext( '2d' );
76+
return document.createElement( 'canvas' ).getContext( '2d', { willReadFrequently: true } );
7777
} catch {
7878
/* istanbul ignore next -- @preserve */
7979
return null;

packages/ckeditor5-emoji/tests/emojimention.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ describe( 'EmojiMention', () => {
615615
expect( queryEmoji( ' see' ) ).to.deep.equal( [] );
616616
} );
617617

618-
it( 'should return an empty array when the repository plugin is not loaded correctly', async () => {
618+
it( 'should return an empty array when the repository plugin is not available', async () => {
619619
testUtils.sinon.stub( console, 'warn' );
620620
fetchStub.rejects( 'Failed to load CDN.' );
621621

@@ -626,6 +626,8 @@ describe( 'EmojiMention', () => {
626626
plugins: [ EmojiMention, Paragraph, Essentials, Mention ]
627627
} );
628628

629+
editor.plugins.get( 'EmojiMention' )._isEmojiRepositoryAvailable = false;
630+
629631
const queryEmoji = editor.plugins.get( 'EmojiMention' )._queryEmojiCallbackFactory();
630632

631633
expect( queryEmoji( '' ) ).to.deep.equal( [] );

packages/ckeditor5-emoji/tests/emojipicker.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ describe( 'EmojiPicker', () => {
5353
let editor, editorElement, emojiPicker, fetchStub;
5454

5555
beforeEach( async () => {
56+
EmojiRepository._results = {};
57+
5658
editorElement = document.createElement( 'div' );
5759
document.body.appendChild( editorElement );
5860

@@ -129,6 +131,9 @@ describe( 'EmojiPicker', () => {
129131
const editorElement = document.createElement( 'div' );
130132
document.body.appendChild( editorElement );
131133

134+
// As the data are shored between editors creation, let's manually clear it before creating a new editor.
135+
EmojiRepository._results = {};
136+
132137
const editor = await ClassicTestEditor.create( editorElement, {
133138
plugins: [ EmojiPicker, Essentials, Paragraph ],
134139
emoji: {
@@ -148,6 +153,9 @@ describe( 'EmojiPicker', () => {
148153
const editorElement = document.createElement( 'div' );
149154
document.body.appendChild( editorElement );
150155

156+
// As the data are shored between editors creation, let's manually clear it before creating a new editor.
157+
EmojiRepository._results = {};
158+
151159
const editor = await ClassicTestEditor.create( editorElement, {
152160
plugins: [ EmojiPicker, Essentials, Paragraph ],
153161
emoji: {
@@ -197,6 +205,9 @@ describe( 'EmojiPicker', () => {
197205
testUtils.sinon.stub( console, 'warn' );
198206
fetchStub.rejects( 'Failed to load CDN.' );
199207

208+
// As the data are shored between editors creation, let's manually clear it before creating a new editor.
209+
EmojiRepository._results = {};
210+
200211
const editor = await ClassicTestEditor.create( editorElement, {
201212
plugins: [ EmojiPicker, Paragraph, Essentials ]
202213
} );
@@ -234,6 +245,9 @@ describe( 'EmojiPicker', () => {
234245
testUtils.sinon.stub( console, 'warn' );
235246
fetchStub.rejects( 'Failed to load CDN.' );
236247

248+
// As the data are shored between editors creation, let's manually clear it before creating a new editor.
249+
EmojiRepository._results = {};
250+
237251
const editor = await ClassicTestEditor.create( editorElement, {
238252
plugins: [ EmojiPicker, Paragraph, Essentials ]
239253
} );

0 commit comments

Comments
 (0)