@ckeditor/ckeditor5-emoji 0.0.0-nightly-20250129.0
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/CHANGELOG.md +4 -0
- package/LICENSE.md +28 -0
- package/README.md +31 -0
- package/build/emoji.js +4 -0
- package/ckeditor5-metadata.json +32 -0
- package/dist/index-content.css +4 -0
- package/dist/index-editor.css +111 -0
- package/dist/index.css +143 -0
- package/dist/index.css.map +1 -0
- package/dist/index.js +1477 -0
- package/dist/index.js.map +1 -0
- package/lang/contexts.json +24 -0
- package/package.json +67 -0
- package/src/augmentation.d.ts +24 -0
- package/src/augmentation.js +5 -0
- package/src/emoji.d.ts +32 -0
- package/src/emoji.js +38 -0
- package/src/emojicommand.d.ts +24 -0
- package/src/emojicommand.js +33 -0
- package/src/emojiconfig.d.ts +80 -0
- package/src/emojiconfig.js +5 -0
- package/src/emojimention.d.ts +68 -0
- package/src/emojimention.js +193 -0
- package/src/emojipicker.d.ts +97 -0
- package/src/emojipicker.js +255 -0
- package/src/emojirepository.d.ts +139 -0
- package/src/emojirepository.js +267 -0
- package/src/index.d.ts +14 -0
- package/src/index.js +13 -0
- package/src/ui/emojicategoriesview.d.ts +68 -0
- package/src/ui/emojicategoriesview.js +131 -0
- package/src/ui/emojigridview.d.ts +140 -0
- package/src/ui/emojigridview.js +183 -0
- package/src/ui/emojipickerview.d.ts +91 -0
- package/src/ui/emojipickerview.js +172 -0
- package/src/ui/emojisearchview.d.ts +51 -0
- package/src/ui/emojisearchview.js +89 -0
- package/src/ui/emojitoneview.d.ts +46 -0
- package/src/ui/emojitoneview.js +89 -0
- package/theme/emojicategories.css +29 -0
- package/theme/emojigrid.css +55 -0
- package/theme/emojipicker.css +32 -0
- package/theme/emojitone.css +21 -0
|
@@ -0,0 +1,267 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* @module emoji/emojirepository
|
|
7
|
+
*/
|
|
8
|
+
import Fuse from 'fuse.js';
|
|
9
|
+
import { groupBy } from 'lodash-es';
|
|
10
|
+
import { Plugin } from 'ckeditor5/src/core.js';
|
|
11
|
+
import { logWarning } from 'ckeditor5/src/utils.js';
|
|
12
|
+
// An endpoint from which the emoji database will be downloaded during plugin initialization.
|
|
13
|
+
// The `{version}` placeholder is replaced with the value from editor config.
|
|
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;
|
|
24
|
+
/**
|
|
25
|
+
* The emoji repository plugin.
|
|
26
|
+
*
|
|
27
|
+
* Loads the emoji database from URL during plugin initialization and provides utility methods to search it.
|
|
28
|
+
*/
|
|
29
|
+
class EmojiRepository extends Plugin {
|
|
30
|
+
/**
|
|
31
|
+
* @inheritDoc
|
|
32
|
+
*/
|
|
33
|
+
static get pluginName() {
|
|
34
|
+
return 'EmojiRepository';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @inheritDoc
|
|
38
|
+
*/
|
|
39
|
+
static get isOfficialPlugin() {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @inheritDoc
|
|
44
|
+
*/
|
|
45
|
+
constructor(editor) {
|
|
46
|
+
super(editor);
|
|
47
|
+
this.editor.config.define('emoji', {
|
|
48
|
+
version: 16,
|
|
49
|
+
skinTone: 'default'
|
|
50
|
+
});
|
|
51
|
+
this._database = [];
|
|
52
|
+
this._databasePromise = new Promise(resolve => {
|
|
53
|
+
this._databasePromiseResolveCallback = resolve;
|
|
54
|
+
});
|
|
55
|
+
this._fuseSearch = null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* @inheritDoc
|
|
59
|
+
*/
|
|
60
|
+
async init() {
|
|
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
|
+
}
|
|
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();
|
|
76
|
+
// Create instance of the Fuse.js library with configured weighted search keys and disabled fuzzy search.
|
|
77
|
+
this._fuseSearch = new Fuse(this._database, {
|
|
78
|
+
keys: [
|
|
79
|
+
{ name: 'emoticon', weight: 5 },
|
|
80
|
+
{ name: 'annotation', weight: 3 },
|
|
81
|
+
{ name: 'tags', weight: 1 }
|
|
82
|
+
],
|
|
83
|
+
minMatchCharLength: 2,
|
|
84
|
+
threshold: 0,
|
|
85
|
+
ignoreLocation: true
|
|
86
|
+
});
|
|
87
|
+
return this._databasePromiseResolveCallback(true);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns an array of emoji entries that match the search query.
|
|
91
|
+
* If the emoji database is not loaded, the [Fuse.js](https://www.fusejs.io/) instance is not created,
|
|
92
|
+
* hence this method returns an empty array.
|
|
93
|
+
*
|
|
94
|
+
* @param searchQuery A search query to match emoji.
|
|
95
|
+
* @returns An array of emoji entries that match the search query.
|
|
96
|
+
*/
|
|
97
|
+
getEmojiByQuery(searchQuery) {
|
|
98
|
+
if (!this._fuseSearch) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const searchQueryTokens = searchQuery.split(/\s/).filter(Boolean);
|
|
102
|
+
// Perform the search only if there is at least two non-white characters next to each other.
|
|
103
|
+
const shouldSearch = searchQueryTokens.some(token => token.length >= 2);
|
|
104
|
+
if (!shouldSearch) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
return this._fuseSearch
|
|
108
|
+
.search({
|
|
109
|
+
'$or': [
|
|
110
|
+
{
|
|
111
|
+
emoticon: searchQuery
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
'$and': searchQueryTokens.map(token => ({ annotation: token }))
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
'$and': searchQueryTokens.map(token => ({ tags: token }))
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
})
|
|
121
|
+
.map(result => result.item);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Groups all emojis by categories.
|
|
125
|
+
* If the emoji database is not loaded, it returns an empty array.
|
|
126
|
+
*
|
|
127
|
+
* @returns An array of emoji entries grouped by categories.
|
|
128
|
+
*/
|
|
129
|
+
getEmojiCategories() {
|
|
130
|
+
if (!this._database.length) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const { t } = this.editor.locale;
|
|
134
|
+
const categories = [
|
|
135
|
+
{ title: t('Smileys & Expressions'), icon: '😀', groupId: 0 },
|
|
136
|
+
{ title: t('Gestures & People'), icon: '👋', groupId: 1 },
|
|
137
|
+
{ title: t('Animals & Nature'), icon: '🐻', groupId: 3 },
|
|
138
|
+
{ title: t('Food & Drinks'), icon: '🍎', groupId: 4 },
|
|
139
|
+
{ title: t('Travel & Places'), icon: '🚘', groupId: 5 },
|
|
140
|
+
{ title: t('Activities'), icon: '🏀', groupId: 6 },
|
|
141
|
+
{ title: t('Objects'), icon: '💡', groupId: 7 },
|
|
142
|
+
{ title: t('Symbols'), icon: '🟢', groupId: 8 },
|
|
143
|
+
{ title: t('Flags'), icon: '🏁', groupId: 9 }
|
|
144
|
+
];
|
|
145
|
+
const groups = groupBy(this._database, 'group');
|
|
146
|
+
return categories.map(category => {
|
|
147
|
+
return {
|
|
148
|
+
...category,
|
|
149
|
+
items: groups[category.groupId]
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Returns an array of available skin tones.
|
|
155
|
+
*/
|
|
156
|
+
getSkinTones() {
|
|
157
|
+
const { t } = this.editor.locale;
|
|
158
|
+
return [
|
|
159
|
+
{ id: 'default', icon: '👋', tooltip: t('Default skin tone') },
|
|
160
|
+
{ id: 'light', icon: '👋🏻', tooltip: t('Light skin tone') },
|
|
161
|
+
{ id: 'medium-light', icon: '👋🏼', tooltip: t('Medium Light skin tone') },
|
|
162
|
+
{ id: 'medium', icon: '👋🏽', tooltip: t('Medium skin tone') },
|
|
163
|
+
{ id: 'medium-dark', icon: '👋🏾', tooltip: t('Medium Dark skin tone') },
|
|
164
|
+
{ id: 'dark', icon: '👋🏿', tooltip: t('Dark skin tone') }
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Indicates whether the emoji database has been successfully downloaded and the plugin is operational.
|
|
169
|
+
*/
|
|
170
|
+
isReady() {
|
|
171
|
+
return this._databasePromise;
|
|
172
|
+
}
|
|
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 [];
|
|
189
|
+
}
|
|
190
|
+
return response.json();
|
|
191
|
+
})
|
|
192
|
+
.catch(() => {
|
|
193
|
+
return [];
|
|
194
|
+
});
|
|
195
|
+
if (!result.length) {
|
|
196
|
+
/**
|
|
197
|
+
* Unable to load the emoji database from CDN.
|
|
198
|
+
*
|
|
199
|
+
* TODO: It could be a problem of CKEditor 5 CDN, but also, Content Security Policy that disallow the request.
|
|
200
|
+
* It would be good to explain what to do in such a case.
|
|
201
|
+
*
|
|
202
|
+
* @error emoji-database-load-failed
|
|
203
|
+
*/
|
|
204
|
+
logWarning('emoji-database-load-failed');
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Creates a div for emoji width testing purposes.
|
|
210
|
+
*/
|
|
211
|
+
function createEmojiWidthTestingContainer() {
|
|
212
|
+
const container = document.createElement('div');
|
|
213
|
+
container.setAttribute('aria-hidden', 'true');
|
|
214
|
+
container.style.position = 'absolute';
|
|
215
|
+
container.style.left = '-9999px';
|
|
216
|
+
container.style.whiteSpace = 'nowrap';
|
|
217
|
+
container.style.fontSize = BASELINE_EMOJI_WIDTH + 'px';
|
|
218
|
+
document.body.appendChild(container);
|
|
219
|
+
return container;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Returns the width of the provided node.
|
|
223
|
+
*/
|
|
224
|
+
function getNodeWidth(container, node) {
|
|
225
|
+
const span = document.createElement('span');
|
|
226
|
+
span.textContent = node;
|
|
227
|
+
container.appendChild(span);
|
|
228
|
+
const nodeWidth = span.offsetWidth;
|
|
229
|
+
container.removeChild(span);
|
|
230
|
+
return nodeWidth;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Checks whether the emoji is supported in the operating system.
|
|
234
|
+
*/
|
|
235
|
+
function isEmojiSupported(item, container) {
|
|
236
|
+
const emojiWidth = getNodeWidth(container, item.emoji);
|
|
237
|
+
// On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard
|
|
238
|
+
// against are the ones that are 2x the size, because those are truly broken (person with red hair = person with
|
|
239
|
+
// floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.)
|
|
240
|
+
// So here we set the threshold at 1.8 times the size of the baseline emoji.
|
|
241
|
+
return (emojiWidth / 1.8 < BASELINE_EMOJI_WIDTH) && (emojiWidth >= BASELINE_EMOJI_WIDTH);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Adds default skin tone property to each emoji. If emoji defines other skin tones, they are added as well.
|
|
245
|
+
*/
|
|
246
|
+
function normalizeEmojiSkinTone(item) {
|
|
247
|
+
const entry = {
|
|
248
|
+
...item,
|
|
249
|
+
skins: {
|
|
250
|
+
default: item.emoji
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
if (item.skins) {
|
|
254
|
+
item.skins.forEach(skin => {
|
|
255
|
+
const skinTone = SKIN_TONE_MAP[skin.tone];
|
|
256
|
+
entry.skins[skinTone] = skin.emoji;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return entry;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Checks whether the emoji belongs to a group that is allowed.
|
|
263
|
+
*/
|
|
264
|
+
function isEmojiCategoryAllowed(item) {
|
|
265
|
+
// Category group=2 contains skin tones only, which we do not want to render.
|
|
266
|
+
return item.group !== 2;
|
|
267
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* @module emoji
|
|
7
|
+
*/
|
|
8
|
+
export { default as Emoji } from './emoji.js';
|
|
9
|
+
export { default as EmojiMention } from './emojimention.js';
|
|
10
|
+
export { default as EmojiPicker } from './emojipicker.js';
|
|
11
|
+
export { default as EmojiRepository } from './emojirepository.js';
|
|
12
|
+
export { default as EmojiCommand } from './emojicommand.js';
|
|
13
|
+
export type { EmojiConfig } from './emojiconfig.js';
|
|
14
|
+
import './augmentation.js';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* @module emoji
|
|
7
|
+
*/
|
|
8
|
+
export { default as Emoji } from './emoji.js';
|
|
9
|
+
export { default as EmojiMention } from './emojimention.js';
|
|
10
|
+
export { default as EmojiPicker } from './emojipicker.js';
|
|
11
|
+
export { default as EmojiRepository } from './emojirepository.js';
|
|
12
|
+
export { default as EmojiCommand } from './emojicommand.js';
|
|
13
|
+
import './augmentation.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* @module emoji/ui/emojicategoriesview
|
|
7
|
+
*/
|
|
8
|
+
import { ButtonView, View, FocusCycler, type ViewCollection } from 'ckeditor5/src/ui.js';
|
|
9
|
+
import { FocusTracker, KeystrokeHandler, type Locale } from 'ckeditor5/src/utils.js';
|
|
10
|
+
import type { EmojiCategory } from '../emojirepository.js';
|
|
11
|
+
import '../../theme/emojicategories.css';
|
|
12
|
+
/**
|
|
13
|
+
* A class representing the navigation part of the emoji UI.
|
|
14
|
+
* It is responsible allowing the user to select a particular emoji category.
|
|
15
|
+
*/
|
|
16
|
+
export default class EmojiCategoriesView extends View {
|
|
17
|
+
/**
|
|
18
|
+
* Currently selected emoji category name.
|
|
19
|
+
*/
|
|
20
|
+
categoryName: string;
|
|
21
|
+
/**
|
|
22
|
+
* Tracks information about the DOM focus in the grid.
|
|
23
|
+
*/
|
|
24
|
+
readonly focusTracker: FocusTracker;
|
|
25
|
+
/**
|
|
26
|
+
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
|
|
27
|
+
*/
|
|
28
|
+
readonly keystrokes: KeystrokeHandler;
|
|
29
|
+
/**
|
|
30
|
+
* Helps cycling over focusable children in the input view.
|
|
31
|
+
*/
|
|
32
|
+
readonly focusCycler: FocusCycler;
|
|
33
|
+
/**
|
|
34
|
+
* A collection of the categories buttons.
|
|
35
|
+
*/
|
|
36
|
+
readonly buttonViews: ViewCollection<ButtonView>;
|
|
37
|
+
/**
|
|
38
|
+
* @inheritDoc
|
|
39
|
+
*/
|
|
40
|
+
constructor(locale: Locale, { emojiCategories, categoryName }: {
|
|
41
|
+
emojiCategories: Array<EmojiCategory>;
|
|
42
|
+
categoryName: string;
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* @inheritDoc
|
|
46
|
+
*/
|
|
47
|
+
render(): void;
|
|
48
|
+
/**
|
|
49
|
+
* @inheritDoc
|
|
50
|
+
*/
|
|
51
|
+
destroy(): void;
|
|
52
|
+
/**
|
|
53
|
+
* @inheritDoc
|
|
54
|
+
*/
|
|
55
|
+
focus(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Marks all categories buttons as enabled (clickable).
|
|
58
|
+
*/
|
|
59
|
+
enableCategories(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Marks all categories buttons as disabled (non-clickable).
|
|
62
|
+
*/
|
|
63
|
+
disableCategories(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Creates a button representing a category item.
|
|
66
|
+
*/
|
|
67
|
+
private _createCategoryButton;
|
|
68
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* @module emoji/ui/emojicategoriesview
|
|
7
|
+
*/
|
|
8
|
+
import { ButtonView, View, FocusCycler } from 'ckeditor5/src/ui.js';
|
|
9
|
+
import { FocusTracker, KeystrokeHandler } from 'ckeditor5/src/utils.js';
|
|
10
|
+
import '../../theme/emojicategories.css';
|
|
11
|
+
/**
|
|
12
|
+
* A class representing the navigation part of the emoji UI.
|
|
13
|
+
* It is responsible allowing the user to select a particular emoji category.
|
|
14
|
+
*/
|
|
15
|
+
export default class EmojiCategoriesView extends View {
|
|
16
|
+
/**
|
|
17
|
+
* @inheritDoc
|
|
18
|
+
*/
|
|
19
|
+
constructor(locale, { emojiCategories, categoryName }) {
|
|
20
|
+
super(locale);
|
|
21
|
+
this.buttonViews = this.createCollection(emojiCategories.map(emojiCategory => this._createCategoryButton(emojiCategory)));
|
|
22
|
+
this.focusTracker = new FocusTracker();
|
|
23
|
+
this.keystrokes = new KeystrokeHandler();
|
|
24
|
+
this.focusCycler = new FocusCycler({
|
|
25
|
+
focusables: this.buttonViews,
|
|
26
|
+
focusTracker: this.focusTracker,
|
|
27
|
+
keystrokeHandler: this.keystrokes,
|
|
28
|
+
actions: {
|
|
29
|
+
focusPrevious: 'arrowleft',
|
|
30
|
+
focusNext: 'arrowright'
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
this.setTemplate({
|
|
34
|
+
tag: 'div',
|
|
35
|
+
attributes: {
|
|
36
|
+
class: ['ck', 'ck-emoji__categories-list'],
|
|
37
|
+
role: 'tablist'
|
|
38
|
+
},
|
|
39
|
+
children: this.buttonViews
|
|
40
|
+
});
|
|
41
|
+
this.on('change:categoryName', (event, name, newValue, oldValue) => {
|
|
42
|
+
const oldCategoryButton = this.buttonViews.find(button => button.tooltip === oldValue);
|
|
43
|
+
if (oldCategoryButton) {
|
|
44
|
+
oldCategoryButton.isOn = false;
|
|
45
|
+
}
|
|
46
|
+
const newCategoryButton = this.buttonViews.find(button => button.tooltip === newValue);
|
|
47
|
+
newCategoryButton.isOn = true;
|
|
48
|
+
});
|
|
49
|
+
this.set('categoryName', categoryName);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* @inheritDoc
|
|
53
|
+
*/
|
|
54
|
+
render() {
|
|
55
|
+
super.render();
|
|
56
|
+
this.buttonViews.forEach(buttonView => {
|
|
57
|
+
this.focusTracker.add(buttonView);
|
|
58
|
+
});
|
|
59
|
+
this.keystrokes.listenTo(this.element);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @inheritDoc
|
|
63
|
+
*/
|
|
64
|
+
destroy() {
|
|
65
|
+
super.destroy();
|
|
66
|
+
this.focusTracker.destroy();
|
|
67
|
+
this.keystrokes.destroy();
|
|
68
|
+
this.buttonViews.destroy();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* @inheritDoc
|
|
72
|
+
*/
|
|
73
|
+
focus() {
|
|
74
|
+
this.buttonViews.first.focus();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Marks all categories buttons as enabled (clickable).
|
|
78
|
+
*/
|
|
79
|
+
enableCategories() {
|
|
80
|
+
this.buttonViews.forEach(buttonView => {
|
|
81
|
+
buttonView.isEnabled = true;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Marks all categories buttons as disabled (non-clickable).
|
|
86
|
+
*/
|
|
87
|
+
disableCategories() {
|
|
88
|
+
this.buttonViews.forEach(buttonView => {
|
|
89
|
+
buttonView.set({
|
|
90
|
+
class: '',
|
|
91
|
+
isEnabled: false,
|
|
92
|
+
isOn: false
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Creates a button representing a category item.
|
|
98
|
+
*/
|
|
99
|
+
_createCategoryButton(emojiCategory) {
|
|
100
|
+
const buttonView = new ButtonView();
|
|
101
|
+
const bind = buttonView.bindTemplate;
|
|
102
|
+
// A `[role="tab"]` element requires also the `[aria-selected]` attribute with its state.
|
|
103
|
+
buttonView.extendTemplate({
|
|
104
|
+
attributes: {
|
|
105
|
+
'aria-selected': bind.to('isOn', value => value.toString()),
|
|
106
|
+
class: [
|
|
107
|
+
'ck-emoji__category-item'
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
buttonView.set({
|
|
112
|
+
ariaLabel: emojiCategory.title,
|
|
113
|
+
label: emojiCategory.icon,
|
|
114
|
+
role: 'tab',
|
|
115
|
+
tooltip: emojiCategory.title,
|
|
116
|
+
withText: true,
|
|
117
|
+
// To improve accessibility, disconnect a button and its label connection so that screen
|
|
118
|
+
// readers can read the `[aria-label]` attribute directly from the more descriptive button.
|
|
119
|
+
ariaLabelledBy: undefined
|
|
120
|
+
});
|
|
121
|
+
buttonView.on('execute', () => {
|
|
122
|
+
this.categoryName = emojiCategory.title;
|
|
123
|
+
});
|
|
124
|
+
buttonView.on('change:isEnabled', () => {
|
|
125
|
+
if (buttonView.isEnabled && buttonView.tooltip === this.categoryName) {
|
|
126
|
+
buttonView.isOn = true;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return buttonView;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* @module emoji/ui/emojigridview
|
|
7
|
+
*/
|
|
8
|
+
import { ButtonView, type FilteredView, View, type ViewCollection } from 'ckeditor5/src/ui.js';
|
|
9
|
+
import { FocusTracker, KeystrokeHandler, type Locale } from 'ckeditor5/src/utils.js';
|
|
10
|
+
import type { EmojiCategory, EmojiEntry } from '../emojirepository.js';
|
|
11
|
+
import type { SkinToneId } from '../emojiconfig.js';
|
|
12
|
+
import '../../theme/emojigrid.css';
|
|
13
|
+
/**
|
|
14
|
+
* A grid of emoji tiles. It allows browsing emojis and selecting them to be inserted into the content.
|
|
15
|
+
*/
|
|
16
|
+
export default class EmojiGridView extends View<HTMLDivElement> implements FilteredView {
|
|
17
|
+
/**
|
|
18
|
+
* Defines the active category name.
|
|
19
|
+
*
|
|
20
|
+
* @observable
|
|
21
|
+
*/
|
|
22
|
+
categoryName: string;
|
|
23
|
+
/**
|
|
24
|
+
* Active skin tone.
|
|
25
|
+
*
|
|
26
|
+
* @observable
|
|
27
|
+
*/
|
|
28
|
+
skinTone: SkinToneId;
|
|
29
|
+
/**
|
|
30
|
+
* Set to `true` when the {@link #tiles} collection does not contain items to display.
|
|
31
|
+
*
|
|
32
|
+
* @observable
|
|
33
|
+
*/
|
|
34
|
+
isEmpty: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* A collection of the child tile views. Each tile represents a particular emoji.
|
|
37
|
+
*/
|
|
38
|
+
readonly tiles: ViewCollection<ButtonView>;
|
|
39
|
+
/**
|
|
40
|
+
* Tracks information about the DOM focus in the grid.
|
|
41
|
+
*/
|
|
42
|
+
readonly focusTracker: FocusTracker;
|
|
43
|
+
/**
|
|
44
|
+
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
|
|
45
|
+
*/
|
|
46
|
+
readonly keystrokes: KeystrokeHandler;
|
|
47
|
+
/**
|
|
48
|
+
* An array containing all emojis grouped by their categories.
|
|
49
|
+
*/
|
|
50
|
+
readonly emojiCategories: Array<EmojiCategory>;
|
|
51
|
+
/**
|
|
52
|
+
* A collection of all already created tile views. Each tile represents a particular emoji.
|
|
53
|
+
* The cached tiles collection is used for efficiency purposes to avoid re-creating a particular
|
|
54
|
+
* tile again when the grid view has changed.
|
|
55
|
+
*/
|
|
56
|
+
readonly cachedTiles: ViewCollection<ButtonView>;
|
|
57
|
+
/**
|
|
58
|
+
* A callback used to filter grid items by a specified query.
|
|
59
|
+
*/
|
|
60
|
+
private readonly _getEmojiByQuery;
|
|
61
|
+
/**
|
|
62
|
+
* @inheritDoc
|
|
63
|
+
*/
|
|
64
|
+
constructor(locale: Locale, { categoryName, emojiCategories, getEmojiByQuery, skinTone }: {
|
|
65
|
+
categoryName: string;
|
|
66
|
+
emojiCategories: Array<EmojiCategory>;
|
|
67
|
+
getEmojiByQuery: EmojiSearchQueryCallback;
|
|
68
|
+
skinTone: SkinToneId;
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* @inheritDoc
|
|
72
|
+
*/
|
|
73
|
+
render(): void;
|
|
74
|
+
/**
|
|
75
|
+
* @inheritDoc
|
|
76
|
+
*/
|
|
77
|
+
destroy(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Focuses the first focusable in {@link ~EmojiGridView#tiles} if available.
|
|
80
|
+
*/
|
|
81
|
+
focus(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Filters the grid view by the given regular expression.
|
|
84
|
+
*
|
|
85
|
+
* It filters either by the pattern or an emoji category, but never both.
|
|
86
|
+
*
|
|
87
|
+
* @param pattern Expression to search or `null` when filter by category name.
|
|
88
|
+
*/
|
|
89
|
+
filter(pattern: RegExp | null): {
|
|
90
|
+
resultsCount: number;
|
|
91
|
+
totalItemsCount: number;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Filters emojis to show based on the specified query phrase.
|
|
95
|
+
*
|
|
96
|
+
* @param query A query used to filter the grid.
|
|
97
|
+
*/
|
|
98
|
+
private _getItemsByQuery;
|
|
99
|
+
/**
|
|
100
|
+
* Returns emojis that belong to the specified category.
|
|
101
|
+
*/
|
|
102
|
+
private _getItemsByCategory;
|
|
103
|
+
/**
|
|
104
|
+
* Updates the grid by removing the existing items and insert the new ones.
|
|
105
|
+
*
|
|
106
|
+
* @param items An array of items to insert.
|
|
107
|
+
*/
|
|
108
|
+
private _updateGrid;
|
|
109
|
+
/**
|
|
110
|
+
* Creates a new tile for the grid. Created tile is added to the {@link #cachedTiles} collection for further usage, if needed.
|
|
111
|
+
*
|
|
112
|
+
* @param emoji The emoji itself.
|
|
113
|
+
* @param name The name of the emoji (e.g. "Smiling Face with Smiling Eyes").
|
|
114
|
+
*/
|
|
115
|
+
private _createTile;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A callback used to filter grid items by a specified query.
|
|
119
|
+
*/
|
|
120
|
+
export type EmojiSearchQueryCallback = (query: string) => Array<EmojiEntry>;
|
|
121
|
+
/**
|
|
122
|
+
* Fired when any of {@link ~EmojiGridView#tiles grid tiles} is clicked.
|
|
123
|
+
*
|
|
124
|
+
* @eventName ~EmojiGridView#execute
|
|
125
|
+
* @param data Additional information about the event.
|
|
126
|
+
*/
|
|
127
|
+
export type EmojiGridViewExecuteEvent = {
|
|
128
|
+
name: 'execute';
|
|
129
|
+
args: [data: EmojiGridViewEventData];
|
|
130
|
+
};
|
|
131
|
+
export type EmojiGridViewEventData = {
|
|
132
|
+
/**
|
|
133
|
+
* The name of the emoji (e.g. "Smiling Face with Smiling Eyes").
|
|
134
|
+
*/
|
|
135
|
+
name: string;
|
|
136
|
+
/**
|
|
137
|
+
* The emoji itself.
|
|
138
|
+
*/
|
|
139
|
+
emoji: string;
|
|
140
|
+
};
|