Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ module.exports = {
// The CKEditor 5 core DLL build is created from JavaScript files.
// ESLint should not process compiled TypeScript.
'src/*.js',
'**/*.d.ts'
'**/*.d.ts',
'packages/ckeditor5-emoji/src/utils/isemojisupported.ts'
],
rules: {
'ckeditor5-rules/ckeditor-imports': 'error',
Expand Down
1 change: 1 addition & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The following libraries are included in CKEditor under the [MIT license](https:/
* color-parse - Copyright (c) 2015 Dmitry Ivanov.
* emoji-picker-element-data - Copyright (c) 2020 Nolan Lawson.
* Fuse.js - Copyright (c) 2017 Kirollos Risk.
* is-emoji-supported - Copyright (c) 2016-2020 Koala Interactive, Inc.
* 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/.
* Marked - Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/), copyright (c) 2011–2018, Christopher Jeffrey (https://github.com/chjj/).
* Turndown - Copyright (c) 2017+ Dom Christie.
Expand Down
1 change: 1 addition & 0 deletions packages/ckeditor5-emoji/LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The following libraries are included in CKEditor 5 emoji feature under the

* emoji-picker-element-data - Copyright (c) 2020 Nolan Lawson.
* Fuse.js - Copyright (c) 2017 Kirollos Risk.
* is-emoji-supported - Copyright (c) 2016-2020 Koala Interactive, Inc.
* 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/.

Trademarks
Expand Down
2 changes: 2 additions & 0 deletions packages/ckeditor5-emoji/src/augmentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
EmojiMention,
EmojiPicker,
EmojiRepository,
EmojiUtils,
EmojiCommand
} from './index.js';

Expand All @@ -28,6 +29,7 @@ declare module '@ckeditor/ckeditor5-core' {
[ EmojiMention.pluginName ]: EmojiMention;
[ EmojiPicker.pluginName ]: EmojiPicker;
[ EmojiRepository.pluginName ]: EmojiRepository;
[ EmojiUtils.pluginName ]: EmojiUtils;
}

interface CommandsMap {
Expand Down
20 changes: 10 additions & 10 deletions packages/ckeditor5-emoji/src/emojimention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ export default class EmojiMention extends Plugin {
/**
* An instance of the {@link module:emoji/emojipicker~EmojiPicker} plugin if it is loaded in the editor.
*/
declare private _emojiPickerPlugin: EmojiPicker | null;
declare public emojiPickerPlugin: EmojiPicker | null;

/**
* An instance of the {@link module:emoji/emojirepository~EmojiRepository} plugin.
*/
declare private _emojiRepositoryPlugin: EmojiRepository;
declare public emojiRepositoryPlugin: EmojiRepository;

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

this._emojiPickerPlugin = editor.plugins.has( 'EmojiPicker' ) ? editor.plugins.get( 'EmojiPicker' ) : null;
this._emojiRepositoryPlugin = editor.plugins.get( 'EmojiRepository' );
this._isEmojiRepositoryAvailable = await this._emojiRepositoryPlugin.isReady();
this.emojiPickerPlugin = editor.plugins.has( 'EmojiPicker' ) ? editor.plugins.get( 'EmojiPicker' ) : null;
this.emojiRepositoryPlugin = editor.plugins.get( 'EmojiRepository' );
this._isEmojiRepositoryAvailable = await this.emojiRepositoryPlugin.isReady();

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

const emojiPickerPlugin = this._emojiPickerPlugin!;
const emojiPickerPlugin = this.emojiPickerPlugin!;

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

Expand Down Expand Up @@ -251,12 +251,12 @@ export default class EmojiMention extends Plugin {
return [];
}

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

if ( this._emojiPickerPlugin ) {
text = emoji.skins[ this._emojiPickerPlugin.skinTone ] || emoji.skins.default;
if ( this.emojiPickerPlugin ) {
text = emoji.skins[ this.emojiPickerPlugin.skinTone ] || emoji.skins.default;
}

return {
Expand All @@ -265,7 +265,7 @@ export default class EmojiMention extends Plugin {
};
} );

if ( !this._emojiPickerPlugin ) {
if ( !this.emojiPickerPlugin ) {
return emojis.slice( 0, this._emojiDropdownLimit );
}

Expand Down
32 changes: 16 additions & 16 deletions packages/ckeditor5-emoji/src/emojipicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ export default class EmojiPicker extends Plugin {
/**
* The contextual balloon plugin instance.
*/
declare public _balloonPlugin: ContextualBalloon;
declare public balloonPlugin: ContextualBalloon;

/**
* An instance of the {@link module:emoji/emojirepository~EmojiRepository} plugin.
*/
declare private _emojiRepositoryPlugin: EmojiRepository;
declare public emojiRepositoryPlugin: EmojiRepository;

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

this._balloonPlugin = editor.plugins.get( 'ContextualBalloon' );
this._emojiRepositoryPlugin = editor.plugins.get( 'EmojiRepository' );
this.balloonPlugin = editor.plugins.get( 'ContextualBalloon' );
this.emojiRepositoryPlugin = editor.plugins.get( 'EmojiRepository' );

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

Expand Down Expand Up @@ -144,8 +144,8 @@ export default class EmojiPicker extends Plugin {

this.emojiPickerView.searchView.search( searchValue );

if ( !this._balloonPlugin.hasView( this.emojiPickerView ) ) {
this._balloonPlugin.add( {
if ( !this.balloonPlugin.hasView( this.emojiPickerView ) ) {
this.balloonPlugin.add( {
view: this.emojiPickerView,
position: this._getBalloonPositionData()
} );
Expand Down Expand Up @@ -181,11 +181,11 @@ export default class EmojiPicker extends Plugin {
*/
private _createEmojiPickerView(): EmojiPickerView {
const emojiPickerView = new EmojiPickerView( this.editor.locale, {
emojiCategories: this._emojiRepositoryPlugin.getEmojiCategories(),
emojiCategories: this.emojiRepositoryPlugin.getEmojiCategories(),
skinTone: this.editor.config.get( 'emoji.skinTone' )!,
skinTones: this._emojiRepositoryPlugin.getSkinTones(),
skinTones: this.emojiRepositoryPlugin.getSkinTones(),
getEmojiByQuery: ( query: string ) => {
return this._emojiRepositoryPlugin.getEmojiByQuery( query );
return this.emojiRepositoryPlugin.getEmojiByQuery( query );
}
} );

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

// Update the balloon position when layout is changed.
this.listenTo<EmojiPickerViewUpdateEvent>( emojiPickerView, 'update', () => {
if ( this._balloonPlugin.visibleView === emojiPickerView ) {
this._balloonPlugin.updatePosition();
if ( this.balloonPlugin.visibleView === emojiPickerView ) {
this.balloonPlugin.updatePosition();
}
} );

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

return emojiPickerView;
Expand All @@ -226,7 +226,7 @@ export default class EmojiPicker extends Plugin {
* Hides the balloon with the emoji picker.
*/
private _hideUI(): void {
this._balloonPlugin.remove( this.emojiPickerView! );
this.balloonPlugin.remove( this.emojiPickerView! );
this.emojiPickerView!.searchView.setInputValue( '' );
this.editor.editing.view.focus();
this._hideFakeVisualSelection();
Expand Down Expand Up @@ -267,7 +267,7 @@ export default class EmojiPicker extends Plugin {
}

/**
* Returns positioning options for the {@link #_balloonPlugin}. They control the way the balloon is attached
* Returns positioning options for the {@link #balloonPlugin}. They control the way the balloon is attached
* to the target element or selection.
*/
private _getBalloonPositionData(): Partial<PositionOptions> {
Expand Down
128 changes: 25 additions & 103 deletions packages/ckeditor5-emoji/src/emojirepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,31 @@
import Fuse from 'fuse.js';
import { groupBy } from 'lodash-es';

import { Plugin, type Editor } from 'ckeditor5/src/core.js';
import { type Editor, Plugin } from 'ckeditor5/src/core.js';
import { logWarning } from 'ckeditor5/src/utils.js';
import EmojiUtils from './emojiutils.js';
import type { SkinToneId } from './emojiconfig.js';

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

const SKIN_TONE_MAP: Record<number, SkinToneId> = {
0: 'default',
1: 'light',
2: 'medium-light',
3: 'medium',
4: 'medium-dark',
5: 'dark'
};

const BASELINE_EMOJI_WIDTH = 24;

/**
* The emoji repository plugin.
*
* Loads the emoji database from URL during plugin initialization and provides utility methods to search it.
*/
export default class EmojiRepository extends Plugin {
/**
* A callback to resolve the {@link #_databasePromise} to control the return value of this promise.
*/
declare private _databasePromiseResolveCallback: ( value: boolean ) => void;

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

/**
* Emoji database.
*/
Expand All @@ -44,17 +44,14 @@ export default class EmojiRepository extends Plugin {
* A promise resolved after downloading the emoji database.
* The promise resolves with `true` when the database is successfully downloaded or `false` otherwise.
*/
private _databasePromise: Promise<boolean>;

/**
* A callback to resolve the {@link #_databasePromise} to control the return value of this promise.
*/
declare private _databasePromiseResolveCallback: ( value: boolean ) => void;
private readonly _databasePromise: Promise<boolean>;

/**
* An instance of the [Fuse.js](https://www.fusejs.io/) library.
* @inheritDoc
*/
private _fuseSearch: Fuse<EmojiEntry> | null;
public static get requires() {
return [ EmojiUtils ] as const;
}

/**
* @inheritDoc
Expand Down Expand Up @@ -93,23 +90,27 @@ export default class EmojiRepository extends Plugin {
* @inheritDoc
*/
public async init(): Promise<void> {
const emojiUtils = this.editor.plugins.get( 'EmojiUtils' );
const emojiVersion = this.editor.config.get( 'emoji.version' )!;

const emojiDatabaseUrl = EMOJI_DATABASE_URL.replace( '{version}', `${ emojiVersion }` );
const emojiDatabase = await loadEmojiDatabase( emojiDatabaseUrl );
const emojiSupportedVersionByOs = emojiUtils.getEmojiSupportedVersionByOs();

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

const container = createEmojiWidthTestingContainer();
const container = emojiUtils.createEmojiWidthTestingContainer();
document.body.appendChild( container );

// Store the emoji database after normalizing the raw data.
this._database = emojiDatabase
.filter( item => isEmojiCategoryAllowed( item ) )
.filter( item => EmojiRepository._isEmojiSupported( item, container ) )
.map( item => normalizeEmojiSkinTone( item ) );
.filter( item => emojiUtils.isEmojiCategoryAllowed( item ) )
.filter( item => emojiUtils.isEmojiSupported( item, emojiSupportedVersionByOs, container ) )
.map( item => emojiUtils.normalizeEmojiSkinTone( item ) );

container.remove();

Expand Down Expand Up @@ -224,13 +225,6 @@ export default class EmojiRepository extends Plugin {
public isReady(): Promise<boolean> {
return this._databasePromise;
}

/**
* A function used to check if the given emoji is supported in the operating system.
*
* Referenced for unit testing purposes.
*/
private static _isEmojiSupported = isEmojiSupported;
}

/**
Expand Down Expand Up @@ -265,78 +259,6 @@ async function loadEmojiDatabase( emojiDatabaseUrl: string ): Promise<Array<Emoj
return result;
}

/**
* Creates a div for emoji width testing purposes.
*/
function createEmojiWidthTestingContainer(): HTMLDivElement {
const container = document.createElement( 'div' );

container.setAttribute( 'aria-hidden', 'true' );
container.style.position = 'absolute';
container.style.left = '-9999px';
container.style.whiteSpace = 'nowrap';
container.style.fontSize = BASELINE_EMOJI_WIDTH + 'px';
document.body.appendChild( container );

return container;
}

/**
* Returns the width of the provided node.
*/
function getNodeWidth( container: HTMLDivElement, node: string ): number {
const span = document.createElement( 'span' );
span.textContent = node;
container.appendChild( span );
const nodeWidth = span.offsetWidth;
container.removeChild( span );

return nodeWidth;
}

/**
* Checks whether the emoji is supported in the operating system.
*/
function isEmojiSupported( item: EmojiCdnResource, container: HTMLDivElement ): boolean {
const emojiWidth = getNodeWidth( container, item.emoji );

// On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard
// against are the ones that are 2x the size, because those are truly broken (person with red hair = person with
// floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.)
// So here we set the threshold at 1.8 times the size of the baseline emoji.
return ( emojiWidth / 1.8 < BASELINE_EMOJI_WIDTH ) && ( emojiWidth >= BASELINE_EMOJI_WIDTH );
}

/**
* Adds default skin tone property to each emoji. If emoji defines other skin tones, they are added as well.
*/
function normalizeEmojiSkinTone( item: EmojiCdnResource ): EmojiEntry {
const entry: EmojiEntry = {
...item,
skins: {
default: item.emoji
}
};

if ( item.skins ) {
item.skins.forEach( skin => {
const skinTone = SKIN_TONE_MAP[ skin.tone ];

entry.skins[ skinTone ] = skin.emoji;
} );
}

return entry;
}

/**
* Checks whether the emoji belongs to a group that is allowed.
*/
function isEmojiCategoryAllowed( item: EmojiCdnResource ): boolean {
// Category group=2 contains skin tones only, which we do not want to render.
return item.group !== 2;
}

/**
* Represents a single group of the emoji category, e.g., "Smileys & Expressions".
*/
Expand Down
Loading