@ckeditor/ckeditor5-emoji 48.2.0 → 48.3.0-alpha.1

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/dist/index.js CHANGED
@@ -2,1879 +2,1954 @@
2
2
  * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
- import { Plugin, Command } from '@ckeditor/ckeditor5-core/dist/index.js';
6
- import { logWarning, version, FocusTracker, KeystrokeHandler, global, Collection } from '@ckeditor/ckeditor5-utils/dist/index.js';
7
- import { Typing } from '@ckeditor/ckeditor5-typing/dist/index.js';
8
- import { Mention } from '@ckeditor/ckeditor5-mention/dist/index.js';
9
- import fuzzysort from 'fuzzysort';
10
- import { groupBy, escapeRegExp } from 'es-toolkit/compat';
11
- import { View, addKeyboardHandlingForGrid, ButtonView, FocusCycler, SearchTextView, createLabeledInputText, createDropdown, UIModel, addListToDropdown, SearchInfoView, ViewCollection, isFocusable, isViewWithFocusCycler, FormHeaderView, ContextualBalloon, Dialog, MenuBarMenuListItemButtonView, clickOutsideHandler } from '@ckeditor/ckeditor5-ui/dist/index.js';
12
- import { IconPreviousArrow, IconEmoji } from '@ckeditor/ckeditor5-icons/dist/index.js';
5
+ import { Command, Plugin } from "@ckeditor/ckeditor5-core";
6
+ import { Collection, FocusTracker, KeystrokeHandler, global, logWarning, version } from "@ckeditor/ckeditor5-utils";
7
+ import { Typing } from "@ckeditor/ckeditor5-typing";
8
+ import { Mention } from "@ckeditor/ckeditor5-mention";
9
+ import fuzzysort from "fuzzysort";
10
+ import { escapeRegExp, groupBy } from "es-toolkit/compat";
11
+ import { ButtonView, ContextualBalloon, Dialog, FocusCycler, FormHeaderView, MenuBarMenuListItemButtonView, SearchInfoView, SearchTextView, UIModel, View, ViewCollection, addKeyboardHandlingForGrid, addListToDropdown, clickOutsideHandler, createDropdown, createLabeledInputText, isFocusable, isViewWithFocusCycler } from "@ckeditor/ckeditor5-ui";
12
+ import { IconEmoji, IconPreviousArrow } from "@ckeditor/ckeditor5-icons";
13
13
 
14
14
  /**
15
- * @license Copyright (c) 2023, Koala Interactive SAS
16
- * For licensing, see https://github.com/koala-interactive/is-emoji-supported/blob/master/LICENSE.md
17
- */ /**
18
- * @module emoji/utils/isemojisupported
19
- */ /**
20
- * Checks if the two pixels parts are the same using canvas.
21
- *
22
- * @internal
23
- */ function isEmojiSupported(unicode) {
24
- const ctx = getCanvas();
25
- /* istanbul ignore next -- @preserve */ if (!ctx) {
26
- return false;
27
- }
28
- const CANVAS_HEIGHT = 25;
29
- const CANVAS_WIDTH = 20;
30
- const textSize = Math.floor(CANVAS_HEIGHT / 2);
31
- // Initialize canvas context.
32
- ctx.font = textSize + 'px Arial, Sans-Serif';
33
- ctx.textBaseline = 'top';
34
- ctx.canvas.width = CANVAS_WIDTH * 2;
35
- ctx.canvas.height = CANVAS_HEIGHT;
36
- ctx.clearRect(0, 0, CANVAS_WIDTH * 2, CANVAS_HEIGHT);
37
- // Draw in red on the left.
38
- ctx.fillStyle = '#FF0000';
39
- ctx.fillText(unicode, 0, 22);
40
- // Draw in blue on right.
41
- ctx.fillStyle = '#0000FF';
42
- ctx.fillText(unicode, CANVAS_WIDTH, 22);
43
- const a = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT).data;
44
- const count = a.length;
45
- let i = 0;
46
- // Search the first visible pixel.
47
- for(; i < count && !a[i + 3]; i += 4);
48
- // No visible pixel.
49
- /* istanbul ignore next -- @preserve */ if (i >= count) {
50
- return false;
51
- }
52
- // Emoji has immutable color, so we check the color of the emoji in two different colors.
53
- // the result show be the same.
54
- const x = CANVAS_WIDTH + i / 4 % CANVAS_WIDTH;
55
- const y = Math.floor(i / 4 / CANVAS_WIDTH);
56
- const b = ctx.getImageData(x, y, 1, 1).data;
57
- /* istanbul ignore next -- @preserve */ if (a[i] !== b[0] || a[i + 2] !== b[2]) {
58
- return false;
59
- }
60
- //Some emojis consist of different ones, so they will show multiple characters if they are not supported.
61
- /* istanbul ignore next -- @preserve */ if (ctx.measureText(unicode).width >= CANVAS_WIDTH) {
62
- return false;
63
- }
64
- // Supported.
65
- return true;
15
+ * @license Copyright (c) 2023, Koala Interactive SAS
16
+ * For licensing, see https://github.com/koala-interactive/is-emoji-supported/blob/master/LICENSE.md
17
+ */
18
+ /**
19
+ * @module emoji/utils/isemojisupported
20
+ */
21
+ /**
22
+ * Checks if the two pixels parts are the same using canvas.
23
+ *
24
+ * @internal
25
+ */
26
+ function isEmojiSupported(unicode) {
27
+ const ctx = getCanvas();
28
+ /* v8 ignore next -- @preserve */
29
+ if (!ctx) return false;
30
+ const CANVAS_HEIGHT = 25;
31
+ const CANVAS_WIDTH = 20;
32
+ ctx.font = "12px Arial, Sans-Serif";
33
+ ctx.textBaseline = "top";
34
+ ctx.canvas.width = CANVAS_WIDTH * 2;
35
+ ctx.canvas.height = CANVAS_HEIGHT;
36
+ ctx.clearRect(0, 0, CANVAS_WIDTH * 2, CANVAS_HEIGHT);
37
+ ctx.fillStyle = "#FF0000";
38
+ ctx.fillText(unicode, 0, 22);
39
+ ctx.fillStyle = "#0000FF";
40
+ ctx.fillText(unicode, CANVAS_WIDTH, 22);
41
+ const a = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT).data;
42
+ const count = a.length;
43
+ let i = 0;
44
+ for (; i < count && !a[i + 3]; i += 4);
45
+ /* v8 ignore next -- @preserve */
46
+ if (i >= count) return false;
47
+ const x = CANVAS_WIDTH + i / 4 % CANVAS_WIDTH;
48
+ const y = Math.floor(i / 4 / CANVAS_WIDTH);
49
+ const b = ctx.getImageData(x, y, 1, 1).data;
50
+ /* v8 ignore next -- @preserve */
51
+ if (a[i] !== b[0] || a[i + 2] !== b[2]) return false;
52
+ /* v8 ignore next -- @preserve */
53
+ if (ctx.measureText(unicode).width >= CANVAS_WIDTH) return false;
54
+ return true;
66
55
  }
67
56
  function getCanvas() {
68
- try {
69
- return document.createElement('canvas').getContext('2d', {
70
- willReadFrequently: true
71
- });
72
- } catch {
73
- /* istanbul ignore next -- @preserve */ return null;
74
- }
57
+ try {
58
+ return document.createElement("canvas").getContext("2d", { willReadFrequently: true });
59
+ } catch {
60
+ /* v8 ignore next -- @preserve */
61
+ return null;
62
+ }
75
63
  }
76
64
 
65
+ /**
66
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
67
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
68
+ */
69
+ /**
70
+ * @module emoji/emojiutils
71
+ */
77
72
  const SKIN_TONE_MAP = {
78
- 0: 'default',
79
- 1: 'light',
80
- 2: 'medium-light',
81
- 3: 'medium',
82
- 4: 'medium-dark',
83
- 5: 'dark'
73
+ 0: "default",
74
+ 1: "light",
75
+ 2: "medium-light",
76
+ 3: "medium",
77
+ 4: "medium-dark",
78
+ 5: "dark"
84
79
  };
85
80
  /**
86
- * A map representing an emoji and its release version.
87
- * It's used to identify a user's minimal supported emoji level.
88
- * We skip versions with older patches, such as 15.0 instead of 15.1 etc.
89
- */ const EMOJI_SUPPORT_LEVEL = {
90
- '🪎': 17,
91
- '🫩': 16,
92
- '🫨': 15.1,
93
- '🫠': 14,
94
- '😶‍🌫️': 13.1,
95
- '🧑‍💻': 12.1,
96
- '🥰': 11,
97
- '🤪': 5,
98
- '⚕️': 4,
99
- '🤣': 3,
100
- '👋🏽': 2,
101
- '😀': 1,
102
- '😐': 0.7,
103
- '😂': 0.6 // Face with Tears of Joy.
81
+ * A map representing an emoji and its release version.
82
+ * It's used to identify a user's minimal supported emoji level.
83
+ * We skip versions with older patches, such as 15.0 instead of 15.1 etc.
84
+ */
85
+ const EMOJI_SUPPORT_LEVEL = {
86
+ "🪎": 17,
87
+ "🫩": 16,
88
+ "🫨": 15.1,
89
+ "🫠": 14,
90
+ "😶‍🌫️": 13.1,
91
+ "🧑‍💻": 12.1,
92
+ "🥰": 11,
93
+ "🤪": 5,
94
+ "⚕️": 4,
95
+ "🤣": 3,
96
+ "👋🏽": 2,
97
+ "😀": 1,
98
+ "😐": .7,
99
+ "😂": .6
104
100
  };
105
101
  const BASELINE_EMOJI_WIDTH = 24;
106
102
  /**
107
- * The Emoji utilities plugin.
108
- */ class EmojiUtils extends Plugin {
109
- /**
110
- * Used for testing whether the environment supports the given emoji.
111
- */ _emojiCanvas = null;
112
- /**
113
- * @inheritDoc
114
- */ static get pluginName() {
115
- return 'EmojiUtils';
116
- }
117
- /**
118
- * @inheritDoc
119
- */ static get isOfficialPlugin() {
120
- return true;
121
- }
122
- /**
123
- * @inheritDoc
124
- */ init() {
125
- this._emojiCanvas = document.createElement('canvas').getContext('2d');
126
- }
127
- /**
128
- * Checks if the emoji is supported by verifying the emoji version supported by the system first.
129
- * Then checks if emoji contains a zero width joiner (ZWJ), and if yes, then checks if it is supported by the system.
130
- */ isEmojiSupported(item, emojiSupportedVersionByOs, container) {
131
- const isEmojiVersionSupported = item.version <= emojiSupportedVersionByOs;
132
- if (!isEmojiVersionSupported) {
133
- return false;
134
- }
135
- if (!this.hasZwj(item.emoji)) {
136
- return true;
137
- }
138
- return this.isEmojiZwjSupported(item, container);
139
- }
140
- /**
141
- * Checks the supported emoji version by the OS, by sampling some representatives from different emoji releases.
142
- */ getEmojiSupportedVersionByOs() {
143
- for (const [emoji, emojiVersion] of Object.entries(EMOJI_SUPPORT_LEVEL)){
144
- if (EmojiUtils._isEmojiSupported(emoji)) {
145
- return emojiVersion;
146
- }
147
- }
148
- return 0;
149
- }
150
- /**
151
- * Check for ZWJ (zero width joiner) character.
152
- */ hasZwj(emoji) {
153
- return emoji.includes('\u200d');
154
- }
155
- /**
156
- * Checks whether the emoji is supported in the operating system.
157
- */ isEmojiZwjSupported(item, container) {
158
- // On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard
159
- // against are the ones that are 2x the size, because those are truly broken (person with red hair = person with
160
- // floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.)
161
- // So here we set the threshold at 1.8 times the size of the baseline emoji.
162
- const canvasWidth = this.getNodeWidthUsingCanvas(container, item.emoji);
163
- // Checking emoji using canvas is much faster, so always try it first. Unfortunately canvas has worse emoji coverage.
164
- if (canvasWidth < BASELINE_EMOJI_WIDTH * 1.8) {
165
- return true;
166
- }
167
- const domWidth = this.getNodeWidth(container, item.emoji);
168
- // Checking emoji using DOM is much slower, so use it as a fallback.
169
- return domWidth < BASELINE_EMOJI_WIDTH * 1.8;
170
- }
171
- /**
172
- * Returns the width of the provided node.
173
- */ getNodeWidth(container, node) {
174
- const span = document.createElement('span');
175
- span.textContent = node;
176
- container.appendChild(span);
177
- const nodeWidth = span.offsetWidth;
178
- container.removeChild(span);
179
- return nodeWidth;
180
- }
181
- /**
182
- * Returns the width of the provided node.
183
- *
184
- * This is a faster alternative to `getNodeWidth` method, which works for great majority of emojis.
185
- */ getNodeWidthUsingCanvas(container, node) {
186
- const style = getComputedStyle(container);
187
- this._emojiCanvas.font = [
188
- style.fontStyle,
189
- style.fontVariant,
190
- style.fontWeight,
191
- `${BASELINE_EMOJI_WIDTH}px`,
192
- `"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", ${style.fontFamily}`
193
- ].join(' ');
194
- return Math.ceil(this._emojiCanvas.measureText(node).width);
195
- }
196
- /**
197
- * Creates a div for emoji width testing purposes.
198
- */ createEmojiWidthTestingContainer() {
199
- const container = document.createElement('div');
200
- container.setAttribute('aria-hidden', 'true');
201
- container.style.position = 'absolute';
202
- container.style.left = '-9999px';
203
- container.style.whiteSpace = 'nowrap';
204
- container.style.fontSize = BASELINE_EMOJI_WIDTH + 'px';
205
- return container;
206
- }
207
- /**
208
- * Adds default skin tone property to each emoji. If emoji defines other skin tones, they are added as well.
209
- */ normalizeEmojiSkinTone(item) {
210
- const entry = {
211
- ...item,
212
- skins: {
213
- default: item.emoji
214
- }
215
- };
216
- if (item.skins) {
217
- item.skins.forEach((skin)=>{
218
- const skinTone = SKIN_TONE_MAP[skin.tone];
219
- entry.skins[skinTone] = skin.emoji;
220
- });
221
- }
222
- return entry;
223
- }
224
- /**
225
- * Checks whether the emoji belongs to a group that is allowed.
226
- */ isEmojiCategoryAllowed(item) {
227
- // Category group=2 contains skin tones only, which we do not want to render.
228
- return item.group !== 2;
229
- }
230
- /**
231
- * A function used to determine if emoji is supported by detecting pixels.
232
- *
233
- * Referenced for unit testing purposes. Kept in a separate file because of licensing.
234
- */ static _isEmojiSupported = isEmojiSupported;
103
+ * The Emoji utilities plugin.
104
+ */
105
+ var EmojiUtils = class EmojiUtils extends Plugin {
106
+ /**
107
+ * Used for testing whether the environment supports the given emoji.
108
+ */
109
+ _emojiCanvas = null;
110
+ /**
111
+ * @inheritDoc
112
+ */
113
+ static get pluginName() {
114
+ return "EmojiUtils";
115
+ }
116
+ /**
117
+ * @inheritDoc
118
+ */
119
+ static get isOfficialPlugin() {
120
+ return true;
121
+ }
122
+ /**
123
+ * @inheritDoc
124
+ */
125
+ init() {
126
+ this._emojiCanvas = document.createElement("canvas").getContext("2d");
127
+ }
128
+ /**
129
+ * Checks if the emoji is supported by verifying the emoji version supported by the system first.
130
+ * Then checks if emoji contains a zero width joiner (ZWJ), and if yes, then checks if it is supported by the system.
131
+ */
132
+ isEmojiSupported(item, emojiSupportedVersionByOs, container) {
133
+ if (!(item.version <= emojiSupportedVersionByOs)) return false;
134
+ if (!this.hasZwj(item.emoji)) return true;
135
+ return this.isEmojiZwjSupported(item, container);
136
+ }
137
+ /**
138
+ * Checks the supported emoji version by the OS, by sampling some representatives from different emoji releases.
139
+ */
140
+ getEmojiSupportedVersionByOs() {
141
+ for (const [emoji, emojiVersion] of Object.entries(EMOJI_SUPPORT_LEVEL)) if (EmojiUtils._isEmojiSupported(emoji)) return emojiVersion;
142
+ return 0;
143
+ }
144
+ /**
145
+ * Check for ZWJ (zero width joiner) character.
146
+ */
147
+ hasZwj(emoji) {
148
+ return emoji.includes("‍");
149
+ }
150
+ /**
151
+ * Checks whether the emoji is supported in the operating system.
152
+ */
153
+ isEmojiZwjSupported(item, container) {
154
+ if (this.getNodeWidthUsingCanvas(container, item.emoji) < BASELINE_EMOJI_WIDTH * 1.8) return true;
155
+ return this.getNodeWidth(container, item.emoji) < BASELINE_EMOJI_WIDTH * 1.8;
156
+ }
157
+ /**
158
+ * Returns the width of the provided node.
159
+ */
160
+ getNodeWidth(container, node) {
161
+ const span = document.createElement("span");
162
+ span.textContent = node;
163
+ container.appendChild(span);
164
+ const nodeWidth = span.offsetWidth;
165
+ container.removeChild(span);
166
+ return nodeWidth;
167
+ }
168
+ /**
169
+ * Returns the width of the provided node.
170
+ *
171
+ * This is a faster alternative to `getNodeWidth` method, which works for great majority of emojis.
172
+ */
173
+ getNodeWidthUsingCanvas(container, node) {
174
+ const style = getComputedStyle(container);
175
+ this._emojiCanvas.font = [
176
+ style.fontStyle,
177
+ style.fontVariant,
178
+ style.fontWeight,
179
+ `${BASELINE_EMOJI_WIDTH}px`,
180
+ `"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", ${style.fontFamily}`
181
+ ].join(" ");
182
+ return Math.ceil(this._emojiCanvas.measureText(node).width);
183
+ }
184
+ /**
185
+ * Creates a div for emoji width testing purposes.
186
+ */
187
+ createEmojiWidthTestingContainer() {
188
+ const container = document.createElement("div");
189
+ container.setAttribute("aria-hidden", "true");
190
+ container.style.position = "absolute";
191
+ container.style.left = "-9999px";
192
+ container.style.whiteSpace = "nowrap";
193
+ container.style.fontSize = "24px";
194
+ return container;
195
+ }
196
+ /**
197
+ * Adds default skin tone property to each emoji. If emoji defines other skin tones, they are added as well.
198
+ */
199
+ normalizeEmojiSkinTone(item) {
200
+ const entry = {
201
+ ...item,
202
+ skins: { default: item.emoji }
203
+ };
204
+ if (item.skins) item.skins.forEach((skin) => {
205
+ const skinTone = SKIN_TONE_MAP[skin.tone];
206
+ entry.skins[skinTone] = skin.emoji;
207
+ });
208
+ return entry;
209
+ }
210
+ /**
211
+ * Checks whether the emoji belongs to a group that is allowed.
212
+ */
213
+ isEmojiCategoryAllowed(item) {
214
+ return item.group !== 2;
215
+ }
216
+ /**
217
+ * A function used to determine if emoji is supported by detecting pixels.
218
+ *
219
+ * Referenced for unit testing purposes. Kept in a separate file because of licensing.
220
+ */
221
+ static _isEmojiSupported = isEmojiSupported;
222
+ };
223
+
224
+ /**
225
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
226
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
227
+ */
228
+ /**
229
+ * Cache for emoji repository data.
230
+ *
231
+ * @internal
232
+ */
233
+ var EmojiRepositoryCache = class {
234
+ /**
235
+ * Fetch-and-transform promises, keyed by composite key.
236
+ */
237
+ _cache = /* @__PURE__ */ new Map();
238
+ /**
239
+ * Fetches emoji data for `url`, runs it through `transform`, and caches the result.
240
+ * At most one HTTP request is issued per unique `[url, ...cacheKeys]` combination.
241
+ * Returns `[]` on network or HTTP failure.
242
+ *
243
+ * @param params Fetch parameters.
244
+ * @param params.url URL of the emoji repository JSON file.
245
+ * @param params.cacheKeys Extra segments that differentiate results for the same URL.
246
+ * The URL itself is always the first segment of the composite key.
247
+ * @param params.transform Converts raw CDN resources into `EmojiEntry` objects.
248
+ */
249
+ async fetch({ url, cacheKeys, transform }) {
250
+ const key = compositeKey(url, cacheKeys);
251
+ if (this._cache.has(key)) return this._cache.get(key).promise;
252
+ const promise = fetch(url, { cache: "force-cache" }).then((response) => response.ok ? response.json() : Promise.resolve([])).then((raw) => transform(raw)).catch(() => {
253
+ this._cache.delete(key);
254
+ return [];
255
+ });
256
+ this._cache.set(key, createInspectablePromise(promise));
257
+ return promise;
258
+ }
259
+ /**
260
+ * Synchronously returns the already-transformed array for the given `url` + `cacheKeys`,
261
+ * or `null` if the result is not yet available.
262
+ */
263
+ getSync({ url, cacheKeys }) {
264
+ const entry = this._cache.get(compositeKey(url, cacheKeys));
265
+ if (!entry || entry.status !== "fulfilled") return null;
266
+ return entry.value;
267
+ }
268
+ /**
269
+ * Clears the cache.
270
+ */
271
+ clear() {
272
+ this._cache.clear();
273
+ }
274
+ };
275
+ /**
276
+ * Creates a composite cache key from a URL and additional key segments.
277
+ */
278
+ function compositeKey(url, cacheKeys) {
279
+ return [url, ...cacheKeys].join("|");
280
+ }
281
+ /**
282
+ * Wraps a `Promise` so its settled value is readable synchronously via `status` and `value`.
283
+ */
284
+ function createInspectablePromise(promise) {
285
+ const state = {
286
+ promise,
287
+ status: "pending",
288
+ value: void 0
289
+ };
290
+ promise.then((resolved) => {
291
+ Object.assign(state, {
292
+ status: "fulfilled",
293
+ value: resolved
294
+ });
295
+ });
296
+ return state;
235
297
  }
236
298
 
237
- // An endpoint from which the emoji data will be downloaded during plugin initialization.
238
- // The `{version}` placeholder is replaced with the value from editor config.
239
- const DEFAULT_EMOJI_DATABASE_URL = 'https://cdn.ckeditor.com/ckeditor5/data/emoji/{version}/en.json';
299
+ /**
300
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
301
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
302
+ */
303
+ /**
304
+ * @module emoji/emojirepository
305
+ */
306
+ const DEFAULT_EMOJI_DATABASE_URL = "https://cdn.ckeditor.com/ckeditor5/data/emoji/{version}/en.json";
240
307
  const DEFAULT_EMOJI_VERSION = 16;
241
308
  /**
242
- * The emoji repository plugin.
243
- *
244
- * Loads the emoji repository from URL during plugin initialization and provides utility methods to search it.
245
- */ class EmojiRepository extends Plugin {
246
- /**
247
- * Emoji repository in a configured version.
248
- */ _items;
249
- /**
250
- * The resolved URL from which the emoji repository is downloaded.
251
- */ _url;
252
- /**
253
- * A promise resolved after downloading the emoji collection.
254
- * The promise resolves with `true` when the repository is successfully downloaded or `false` otherwise.
255
- */ _repositoryPromise;
256
- /**
257
- * @inheritDoc
258
- */ static get requires() {
259
- return [
260
- EmojiUtils
261
- ];
262
- }
263
- /**
264
- * @inheritDoc
265
- */ static get pluginName() {
266
- return 'EmojiRepository';
267
- }
268
- /**
269
- * @inheritDoc
270
- */ static get isOfficialPlugin() {
271
- return true;
272
- }
273
- /**
274
- * @inheritDoc
275
- */ constructor(editor){
276
- super(editor);
277
- editor.config.define('emoji', {
278
- version: undefined,
279
- skinTone: 'default',
280
- definitionsUrl: undefined,
281
- useCustomFont: false
282
- });
283
- this._url = this._getUrl();
284
- this._repositoryPromise = new Promise((resolve)=>{
285
- this._repositoryPromiseResolveCallback = resolve;
286
- });
287
- this._items = null;
288
- }
289
- /**
290
- * @inheritDoc
291
- */ async init() {
292
- this._warnAboutCdnUse();
293
- await this._loadAndCacheEmoji();
294
- this._items = this._getItems();
295
- if (!this._items) {
296
- /**
297
- * Unable to identify the available emoji to display.
298
- *
299
- * See the {@glink features/emoji#troubleshooting troubleshooting} section in the {@glink features/emoji Emoji feature} guide
300
- * for more details.
301
- *
302
- * @error emoji-repository-empty
303
- */ logWarning('emoji-repository-empty');
304
- return this._repositoryPromiseResolveCallback(false);
305
- }
306
- return this._repositoryPromiseResolveCallback(true);
307
- }
308
- /**
309
- * Returns an array of emoji entries that match the search query.
310
- * If the emoji repository is not loaded this method returns an empty array.
311
- *
312
- * @param searchQuery A search query to match emoji.
313
- * @returns An array of emoji entries that match the search query.
314
- */ getEmojiByQuery(searchQuery) {
315
- if (!this._items) {
316
- return [];
317
- }
318
- const searchQueryTokens = searchQuery.split(/\s/).filter(Boolean);
319
- // Perform the search only if there is at least two non-white characters next to each other.
320
- const shouldSearch = searchQueryTokens.some((token)=>token.length >= 2);
321
- if (!shouldSearch) {
322
- return [];
323
- }
324
- return fuzzysort.go(searchQuery, this._items, {
325
- threshold: 0.6,
326
- keys: [
327
- 'emoticon',
328
- 'annotation',
329
- (emojiEntry)=>{
330
- // Instead of searching over all tags, let's use only those that matches the query.
331
- // It enables searching in tags with the space character in names.
332
- const searchQueryTokens = searchQuery.split(/\s/).filter(Boolean);
333
- const matchedTags = searchQueryTokens.flatMap((tok)=>{
334
- return emojiEntry.tags?.filter((t)=>t.startsWith(tok));
335
- });
336
- return matchedTags.join();
337
- }
338
- ]
339
- }).map((result)=>result.obj);
340
- }
341
- /**
342
- * Groups all emojis by categories.
343
- * If the emoji repository is not loaded, it returns an empty array.
344
- *
345
- * @returns An array of emoji entries grouped by categories.
346
- */ getEmojiCategories() {
347
- const repository = this._getItems();
348
- if (!repository) {
349
- return [];
350
- }
351
- const { t } = this.editor.locale;
352
- const categories = [
353
- {
354
- title: t('Smileys & Expressions'),
355
- icon: '😄',
356
- groupId: 0
357
- },
358
- {
359
- title: t('Gestures & People'),
360
- icon: '👋',
361
- groupId: 1
362
- },
363
- {
364
- title: t('Animals & Nature'),
365
- icon: '🐻',
366
- groupId: 3
367
- },
368
- {
369
- title: t('Food & Drinks'),
370
- icon: '🍎',
371
- groupId: 4
372
- },
373
- {
374
- title: t('Travel & Places'),
375
- icon: '🚘',
376
- groupId: 5
377
- },
378
- {
379
- title: t('Activities'),
380
- icon: '🏀',
381
- groupId: 6
382
- },
383
- {
384
- title: t('Objects'),
385
- icon: '💡',
386
- groupId: 7
387
- },
388
- {
389
- title: t('Symbols'),
390
- icon: '🔵',
391
- groupId: 8
392
- },
393
- {
394
- title: t('Flags'),
395
- icon: '🏁',
396
- groupId: 9
397
- }
398
- ];
399
- const groups = groupBy(repository, (item)=>item.group);
400
- return categories.map((category)=>{
401
- return {
402
- ...category,
403
- items: groups[category.groupId]
404
- };
405
- });
406
- }
407
- /**
408
- * Returns an array of available skin tones.
409
- */ getSkinTones() {
410
- const { t } = this.editor.locale;
411
- return [
412
- {
413
- id: 'default',
414
- icon: '👋',
415
- tooltip: t('Default skin tone')
416
- },
417
- {
418
- id: 'light',
419
- icon: '👋🏻',
420
- tooltip: t('Light skin tone')
421
- },
422
- {
423
- id: 'medium-light',
424
- icon: '👋🏼',
425
- tooltip: t('Medium Light skin tone')
426
- },
427
- {
428
- id: 'medium',
429
- icon: '👋🏽',
430
- tooltip: t('Medium skin tone')
431
- },
432
- {
433
- id: 'medium-dark',
434
- icon: '👋🏾',
435
- tooltip: t('Medium Dark skin tone')
436
- },
437
- {
438
- id: 'dark',
439
- icon: '👋🏿',
440
- tooltip: t('Dark skin tone')
441
- }
442
- ];
443
- }
444
- /**
445
- * Indicates whether the emoji repository has been successfully downloaded and the plugin is operational.
446
- */ isReady() {
447
- return this._repositoryPromise;
448
- }
449
- /**
450
- * Returns the URL from which the emoji repository is downloaded. If the URL is not provided
451
- * in the configuration, the default URL is used with the version from the configuration.
452
- *
453
- * If both the URL and version are provided, a warning is logged.
454
- */ _getUrl() {
455
- const { definitionsUrl, version: version$1 } = this.editor.config.get('emoji');
456
- if (!definitionsUrl || definitionsUrl === 'cdn') {
457
- // URL was not provided or is set to 'cdn', so we use the default CDN URL.
458
- const urlVersion = version$1 || DEFAULT_EMOJI_VERSION;
459
- const url = new URL(DEFAULT_EMOJI_DATABASE_URL.replace('{version}', urlVersion.toString()));
460
- url.searchParams.set('editorVersion', version);
461
- return url;
462
- }
463
- if (version$1) {
464
- /**
465
- * Both {@link module:emoji/emojiconfig~EmojiConfig#definitionsUrl `emoji.definitionsUrl`} and
466
- * {@link module:emoji/emojiconfig~EmojiConfig#version `emoji.version`} configuration options
467
- * are set. Only the `emoji.definitionsUrl` option will be used.
468
- *
469
- * The `emoji.version` option will be ignored and should be removed from the configuration.
470
- *
471
- * @error emoji-repository-redundant-version
472
- */ logWarning('emoji-repository-redundant-version');
473
- }
474
- return new URL(definitionsUrl);
475
- }
476
- /**
477
- * Warn users on self-hosted installations that this plugin uses a CDN to fetch the emoji repository.
478
- */ _warnAboutCdnUse() {
479
- const editor = this.editor;
480
- const config = editor.config.get('emoji');
481
- const licenseKey = editor.config.get('licenseKey');
482
- const distributionChannel = window[Symbol.for('cke distribution')];
483
- if (licenseKey === 'GPL') {
484
- // Don't warn GPL users.
485
- return;
486
- }
487
- if (distributionChannel === 'cloud') {
488
- // Don't warn cloud users, because they already use our CDN.
489
- return;
490
- }
491
- if (config && config.definitionsUrl) {
492
- // Don't warn users who have configured their own definitions URL.
493
- return;
494
- }
495
- /**
496
- * It was detected that your installation uses a commercial license key,
497
- * and the default {@glink features/emoji#emoji-source CKEditor CDN for Emoji plugin data}.
498
- *
499
- * To avoid this, you can use the {@link module:emoji/emojiconfig~EmojiConfig#definitionsUrl `emoji.definitionsUrl`}
500
- * configuration option to provide a URL to your own emoji repository.
501
- *
502
- * If you want to suppress this warning, while using the default CDN, set this configuration option to `cdn`.
503
- *
504
- * @error emoji-repository-cdn-use
505
- */ logWarning('emoji-repository-cdn-use');
506
- }
507
- /**
508
- * Returns the emoji repository in a configured version if it is a non-empty array. Returns `null` otherwise.
509
- */ _getItems() {
510
- const repository = EmojiRepository._results[this._url.href];
511
- return repository && repository.length ? repository : null;
512
- }
513
- /**
514
- * Loads the emoji repository. If the repository is already loaded, it returns the cached result.
515
- * Otherwise, it fetches the repository from the URL and adds it to the cache.
516
- */ async _loadAndCacheEmoji() {
517
- if (EmojiRepository._results[this._url.href]) {
518
- // The repository has already been downloaded.
519
- return;
520
- }
521
- const result = await fetch(this._url, {
522
- cache: 'force-cache'
523
- }).then((response)=>{
524
- if (!response.ok) {
525
- return [];
526
- }
527
- return response.json();
528
- }).catch(()=>{
529
- return [];
530
- });
531
- EmojiRepository._results[this._url.href] = this._normalizeEmoji(result);
532
- }
533
- /**
534
- * Normalizes the raw data fetched from CDN. By normalization, we meant:
535
- *
536
- * * Filter out unsupported emoji (these that will not render correctly),
537
- * * Prepare skin tone variants if an emoji defines them.
538
- */ _normalizeEmoji(data) {
539
- const editor = this.editor;
540
- const useCustomFont = editor.config.get('emoji.useCustomFont');
541
- const emojiUtils = editor.plugins.get('EmojiUtils');
542
- const insertableEmoji = data.filter((item)=>emojiUtils.isEmojiCategoryAllowed(item));
543
- // When using a custom font, the feature does not filter any emoji.
544
- if (useCustomFont) {
545
- return insertableEmoji.map((item)=>emojiUtils.normalizeEmojiSkinTone(item));
546
- }
547
- const emojiSupportedVersionByOs = emojiUtils.getEmojiSupportedVersionByOs();
548
- const container = emojiUtils.createEmojiWidthTestingContainer();
549
- document.body.appendChild(container);
550
- const results = insertableEmoji.filter((item)=>emojiUtils.isEmojiSupported(item, emojiSupportedVersionByOs, container)).map((item)=>emojiUtils.normalizeEmojiSkinTone(item));
551
- container.remove();
552
- return results;
553
- }
554
- /**
555
- * Versioned emoji repository.
556
- */ static _results = {};
557
- }
309
+ * The emoji repository plugin.
310
+ *
311
+ * Loads the emoji repository from URL during plugin initialization and provides utility methods to search it.
312
+ */
313
+ var EmojiRepository = class EmojiRepository extends Plugin {
314
+ /**
315
+ * The resolved URL from which the emoji repository is downloaded.
316
+ */
317
+ _url;
318
+ /**
319
+ * @inheritDoc
320
+ */
321
+ static get requires() {
322
+ return [EmojiUtils];
323
+ }
324
+ /**
325
+ * @inheritDoc
326
+ */
327
+ static get pluginName() {
328
+ return "EmojiRepository";
329
+ }
330
+ /**
331
+ * @inheritDoc
332
+ */
333
+ static get isOfficialPlugin() {
334
+ return true;
335
+ }
336
+ /**
337
+ * @inheritDoc
338
+ */
339
+ constructor(editor) {
340
+ super(editor);
341
+ editor.config.define("emoji", {
342
+ version: void 0,
343
+ skinTone: "default",
344
+ definitionsUrl: void 0,
345
+ useCustomFont: false
346
+ });
347
+ this.set("isRepositoryReady", null);
348
+ this._url = this._getUrl();
349
+ }
350
+ /**
351
+ * @inheritDoc
352
+ */
353
+ init() {
354
+ this._warnAboutCdnUse();
355
+ EmojiRepository._cache.fetch({
356
+ url: this._url.toString(),
357
+ cacheKeys: this._cacheKeys,
358
+ transform: (raw) => this._normalizeEmoji(raw)
359
+ }).then(() => {
360
+ if (this.editor.state === "destroyed") return;
361
+ const ready = !!this._getItems();
362
+ if (!ready)
558
363
  /**
559
- * Unable to load the emoji repository from the URL.
560
- *
561
- * If the URL works properly and there is no disruption of communication, please check your
562
- * {@glink getting-started/setup/csp Content Security Policy (CSP)} setting and make sure
563
- * the URL connection is allowed by the editor.
564
- *
565
- * @error emoji-repository-load-failed
566
- */
364
+ * Unable to identify the available emoji to display.
365
+ *
366
+ * See the {@glink features/emoji#troubleshooting troubleshooting} section in
367
+ * the {@glink features/emoji Emoji feature} guide for more details.
368
+ *
369
+ * @error emoji-repository-empty
370
+ */
371
+ logWarning("emoji-repository-empty");
372
+ this.isRepositoryReady = ready;
373
+ });
374
+ }
375
+ /**
376
+ * Calls `callback` once the repository finishes loading, passing the result as the argument.
377
+ *
378
+ * @param callback Receives `true` when the repository loaded successfully, `false` on failure.
379
+ * Note: if the editor is destroyed before loading completes, the callback may never be called.
380
+ * Use {@link #isReady} instead if you need a guaranteed resolution in all cases.
381
+ */
382
+ onReady(callback) {
383
+ if (this.editor.state === "destroyed") return;
384
+ if (this.isRepositoryReady !== null) {
385
+ callback(this.isRepositoryReady);
386
+ return;
387
+ }
388
+ this.once("change:isRepositoryReady", (evt, name, isReady) => {
389
+ callback(!!isReady);
390
+ });
391
+ }
392
+ /**
393
+ * Returns an array of emoji entries that match the search query.
394
+ * If the emoji repository is not loaded this method returns an empty array.
395
+ *
396
+ * @param searchQuery A search query to match emoji.
397
+ * @returns An array of emoji entries that match the search query.
398
+ */
399
+ getEmojiByQuery(searchQuery) {
400
+ const items = this._getItems();
401
+ if (!items) return [];
402
+ if (!searchQuery.split(/\s/).filter(Boolean).some((token) => token.length >= 2)) return [];
403
+ return fuzzysort.go(searchQuery, items, {
404
+ threshold: .6,
405
+ keys: [
406
+ "emoticon",
407
+ "annotation",
408
+ (emojiEntry) => {
409
+ return searchQuery.split(/\s/).filter(Boolean).flatMap((tok) => {
410
+ return emojiEntry.tags?.filter((t) => t.startsWith(tok));
411
+ }).join();
412
+ }
413
+ ]
414
+ }).map((result) => result.obj);
415
+ }
416
+ /**
417
+ * Groups all emojis by categories.
418
+ * If the emoji repository is not loaded, it returns an empty array.
419
+ *
420
+ * @returns An array of emoji entries grouped by categories.
421
+ */
422
+ getEmojiCategories() {
423
+ const repository = this._getItems();
424
+ if (!repository) return [];
425
+ const { t } = this.editor.locale;
426
+ const categories = [
427
+ {
428
+ title: t("Smileys & Expressions"),
429
+ icon: "😄",
430
+ groupId: 0
431
+ },
432
+ {
433
+ title: t("Gestures & People"),
434
+ icon: "👋",
435
+ groupId: 1
436
+ },
437
+ {
438
+ title: t("Animals & Nature"),
439
+ icon: "🐻",
440
+ groupId: 3
441
+ },
442
+ {
443
+ title: t("Food & Drinks"),
444
+ icon: "🍎",
445
+ groupId: 4
446
+ },
447
+ {
448
+ title: t("Travel & Places"),
449
+ icon: "🚘",
450
+ groupId: 5
451
+ },
452
+ {
453
+ title: t("Activities"),
454
+ icon: "🏀",
455
+ groupId: 6
456
+ },
457
+ {
458
+ title: t("Objects"),
459
+ icon: "💡",
460
+ groupId: 7
461
+ },
462
+ {
463
+ title: t("Symbols"),
464
+ icon: "🔵",
465
+ groupId: 8
466
+ },
467
+ {
468
+ title: t("Flags"),
469
+ icon: "🏁",
470
+ groupId: 9
471
+ }
472
+ ];
473
+ const groups = groupBy(repository, (item) => item.group);
474
+ return categories.map((category) => {
475
+ return {
476
+ ...category,
477
+ items: groups[category.groupId]
478
+ };
479
+ });
480
+ }
481
+ /**
482
+ * Returns an array of available skin tones.
483
+ */
484
+ getSkinTones() {
485
+ const { t } = this.editor.locale;
486
+ return [
487
+ {
488
+ id: "default",
489
+ icon: "👋",
490
+ tooltip: t("Default skin tone")
491
+ },
492
+ {
493
+ id: "light",
494
+ icon: "👋🏻",
495
+ tooltip: t("Light skin tone")
496
+ },
497
+ {
498
+ id: "medium-light",
499
+ icon: "👋🏼",
500
+ tooltip: t("Medium Light skin tone")
501
+ },
502
+ {
503
+ id: "medium",
504
+ icon: "👋🏽",
505
+ tooltip: t("Medium skin tone")
506
+ },
507
+ {
508
+ id: "medium-dark",
509
+ icon: "👋🏾",
510
+ tooltip: t("Medium Dark skin tone")
511
+ },
512
+ {
513
+ id: "dark",
514
+ icon: "👋🏿",
515
+ tooltip: t("Dark skin tone")
516
+ }
517
+ ];
518
+ }
519
+ /**
520
+ * Returns a promise that resolves once the repository finishes loading.
521
+ *
522
+ * Resolves with `true` if the repository loaded successfully, or `false` if loading failed.
523
+ * Rejects if the editor was destroyed before loading completed.
524
+ */
525
+ isReady() {
526
+ return new Promise((resolve, reject) => {
527
+ const fail = () => reject(/* @__PURE__ */ new Error("The editor was destroyed before the emoji repository finished loading."));
528
+ if (this.editor.state === "destroyed") fail();
529
+ else {
530
+ this.onReady(resolve);
531
+ this.editor.once("destroy", fail);
532
+ }
533
+ });
534
+ }
535
+ /**
536
+ * Returns the URL from which the emoji repository is downloaded. If the URL is not provided
537
+ * in the configuration, the default URL is used with the version from the configuration.
538
+ *
539
+ * If both the URL and version are provided, a warning is logged.
540
+ */
541
+ _getUrl() {
542
+ const { definitionsUrl, version: version$1 } = this.editor.config.get("emoji");
543
+ if (!definitionsUrl || definitionsUrl === "cdn") {
544
+ const urlVersion = version$1 || DEFAULT_EMOJI_VERSION;
545
+ const url = new URL(DEFAULT_EMOJI_DATABASE_URL.replace("{version}", urlVersion.toString()));
546
+ url.searchParams.set("editorVersion", version);
547
+ return url;
548
+ }
549
+ if (version$1)
550
+ /**
551
+ * Both {@link module:emoji/emojiconfig~EmojiConfig#definitionsUrl `emoji.definitionsUrl`} and
552
+ * {@link module:emoji/emojiconfig~EmojiConfig#version `emoji.version`} configuration options
553
+ * are set. Only the `emoji.definitionsUrl` option will be used.
554
+ *
555
+ * The `emoji.version` option will be ignored and should be removed from the configuration.
556
+ *
557
+ * @error emoji-repository-redundant-version
558
+ */
559
+ logWarning("emoji-repository-redundant-version");
560
+ return new URL(definitionsUrl);
561
+ }
562
+ /**
563
+ * Warn users on self-hosted installations that this plugin uses a CDN to fetch the emoji repository.
564
+ */
565
+ _warnAboutCdnUse() {
566
+ const editor = this.editor;
567
+ const config = editor.config.get("emoji");
568
+ const licenseKey = editor.config.get("licenseKey");
569
+ const distributionChannel = window[Symbol.for("cke distribution")];
570
+ if (licenseKey === "GPL") return;
571
+ if (distributionChannel === "cloud") return;
572
+ if (config && config.definitionsUrl) return;
573
+ /**
574
+ * It was detected that your installation uses a commercial license key,
575
+ * and the default {@glink features/emoji#emoji-source CKEditor CDN for Emoji plugin data}.
576
+ *
577
+ * To avoid this, you can use the {@link module:emoji/emojiconfig~EmojiConfig#definitionsUrl `emoji.definitionsUrl`}
578
+ * configuration option to provide a URL to your own emoji repository.
579
+ *
580
+ * If you want to suppress this warning, while using the default CDN, set this configuration option to `cdn`.
581
+ *
582
+ * @error emoji-repository-cdn-use
583
+ */
584
+ logWarning("emoji-repository-cdn-use");
585
+ }
586
+ /**
587
+ * Returns the normalised emoji repository for this editor instance if it is
588
+ * a non-empty array, or `null` otherwise.
589
+ */
590
+ _getItems() {
591
+ const repository = EmojiRepository._cache.getSync({
592
+ url: this._url.toString(),
593
+ cacheKeys: this._cacheKeys
594
+ });
595
+ return repository && repository.length ? repository : null;
596
+ }
597
+ /**
598
+ * Normalizes the raw data fetched from CDN. By normalization, we meant:
599
+ *
600
+ * * Filter out unsupported emoji (these that will not render correctly),
601
+ * * Prepare skin tone variants if an emoji defines them.
602
+ */
603
+ _normalizeEmoji(data) {
604
+ const editor = this.editor;
605
+ const useCustomFont = editor.config.get("emoji.useCustomFont");
606
+ const emojiUtils = editor.plugins.get("EmojiUtils");
607
+ const insertableEmoji = data.filter((item) => emojiUtils.isEmojiCategoryAllowed(item));
608
+ if (useCustomFont) return insertableEmoji.map((item) => emojiUtils.normalizeEmojiSkinTone(item));
609
+ const emojiSupportedVersionByOs = emojiUtils.getEmojiSupportedVersionByOs();
610
+ const container = emojiUtils.createEmojiWidthTestingContainer();
611
+ document.body.appendChild(container);
612
+ const results = insertableEmoji.filter((item) => emojiUtils.isEmojiSupported(item, emojiSupportedVersionByOs, container)).map((item) => emojiUtils.normalizeEmojiSkinTone(item));
613
+ container.remove();
614
+ return results;
615
+ }
616
+ /**
617
+ * Cache key segments that distinguish the transformation result for this editor instance.
618
+ */
619
+ get _cacheKeys() {
620
+ return [`useCustomFont:${!!this.editor.config.get("emoji.useCustomFont")}`];
621
+ }
622
+ /**
623
+ * Process-global cache that stores fetched emojis.
624
+ */
625
+ static _cache = new EmojiRepositoryCache();
626
+ };
627
+ /**
628
+ * Unable to load the emoji repository from the URL.
629
+ *
630
+ * If the URL works properly and there is no disruption of communication, please check your
631
+ * {@glink getting-started/setup/csp Content Security Policy (CSP)} setting and make sure
632
+ * the URL connection is allowed by the editor.
633
+ *
634
+ * @error emoji-repository-load-failed
635
+ */
567
636
 
568
- const EMOJI_MENTION_MARKER = ':';
569
- const EMOJI_SHOW_ALL_OPTION_ID = ':__EMOJI_SHOW_ALL:';
570
- const EMOJI_HINT_OPTION_ID = ':__EMOJI_HINT:';
571
637
  /**
572
- * The emoji mention plugin.
573
- *
574
- * Introduces the autocomplete of emojis while typing.
575
- */ class EmojiMention extends Plugin {
576
- /**
577
- * Defines a number of displayed items in the auto complete dropdown.
578
- *
579
- * It includes the "Show all emoji..." option if the `EmojiPicker` plugin is loaded.
580
- */ _emojiDropdownLimit;
581
- /**
582
- * Defines a skin tone that is set in the emoji config.
583
- */ _skinTone;
584
- /**
585
- * @inheritDoc
586
- */ static get requires() {
587
- return [
588
- EmojiRepository,
589
- Typing,
590
- Mention
591
- ];
592
- }
593
- /**
594
- * @inheritDoc
595
- */ static get pluginName() {
596
- return 'EmojiMention';
597
- }
598
- /**
599
- * @inheritDoc
600
- */ static get isOfficialPlugin() {
601
- return true;
602
- }
603
- /**
604
- * @inheritDoc
605
- */ constructor(editor){
606
- super(editor);
607
- this.editor.config.define('emoji', {
608
- dropdownLimit: 6
609
- });
610
- this._emojiDropdownLimit = editor.config.get('emoji.dropdownLimit');
611
- this._skinTone = editor.config.get('emoji.skinTone');
612
- this._setupMentionConfiguration(editor);
613
- }
614
- /**
615
- * Initializes the configuration for emojis in the mention feature.
616
- * If the marker used by emoji mention is already registered, it displays a warning.
617
- * If emoji mention configuration is detected, it does not register it for a second time.
618
- */ _setupMentionConfiguration(editor) {
619
- const mergeFieldsPrefix = editor.config.get('mergeFields.prefix');
620
- const mentionFeedsConfigs = editor.config.get('mention.feeds');
621
- const isEmojiMarkerUsedByMergeFields = mergeFieldsPrefix ? mergeFieldsPrefix[0] === EMOJI_MENTION_MARKER : false;
622
- const isEmojiMarkerUsedByMention = mentionFeedsConfigs.filter((config)=>!config._isEmojiMarker).some((config)=>config.marker === EMOJI_MENTION_MARKER);
623
- if (isEmojiMarkerUsedByMention || isEmojiMarkerUsedByMergeFields) {
624
- /**
625
- * The `marker` in the `emoji` config is already used by other plugin configuration.
626
- *
627
- * @error emoji-config-marker-already-used
628
- * @param {string} marker Used marker.
629
- */ logWarning('emoji-config-marker-already-used', {
630
- marker: EMOJI_MENTION_MARKER
631
- });
632
- return;
633
- }
634
- const isEmojiConfigDefined = mentionFeedsConfigs.some((config)=>config._isEmojiMarker);
635
- if (isEmojiConfigDefined) {
636
- return;
637
- }
638
- const emojiMentionFeedConfig = {
639
- _isEmojiMarker: true,
640
- marker: EMOJI_MENTION_MARKER,
641
- dropdownLimit: this._emojiDropdownLimit,
642
- itemRenderer: this._customItemRendererFactory(this.editor.t),
643
- feed: this._queryEmojiCallbackFactory()
644
- };
645
- this.editor.config.set('mention.feeds', [
646
- ...mentionFeedsConfigs,
647
- emojiMentionFeedConfig
648
- ]);
649
- }
650
- /**
651
- * @inheritDoc
652
- */ async init() {
653
- const editor = this.editor;
654
- this.emojiPickerPlugin = editor.plugins.has('EmojiPicker') ? editor.plugins.get('EmojiPicker') : null;
655
- this.emojiRepositoryPlugin = editor.plugins.get('EmojiRepository');
656
- this._isEmojiRepositoryAvailable = await this.emojiRepositoryPlugin.isReady();
657
- // Override the `mention` command listener if the emoji repository is ready.
658
- if (this._isEmojiRepositoryAvailable) {
659
- editor.once('ready', this._overrideMentionExecuteListener.bind(this));
660
- }
661
- }
662
- /**
663
- * Returns the `itemRenderer()` callback for mention config.
664
- */ _customItemRendererFactory(t) {
665
- return (item)=>{
666
- const itemElement = document.createElement('button');
667
- itemElement.classList.add('ck');
668
- itemElement.classList.add('ck-button');
669
- itemElement.classList.add('ck-button_with-text');
670
- itemElement.id = `mention-list-item-id${item.id.slice(0, -1)}`;
671
- itemElement.type = 'button';
672
- itemElement.tabIndex = -1;
673
- const labelElement = document.createElement('span');
674
- labelElement.classList.add('ck');
675
- labelElement.classList.add('ck-button__label');
676
- itemElement.appendChild(labelElement);
677
- if (item.id === EMOJI_HINT_OPTION_ID) {
678
- itemElement.classList.add('ck-list-item-button');
679
- itemElement.classList.add('ck-disabled');
680
- labelElement.textContent = t('Keep on typing to see the emoji.');
681
- } else if (item.id === EMOJI_SHOW_ALL_OPTION_ID) {
682
- labelElement.textContent = t('Show all emoji...');
683
- } else {
684
- labelElement.textContent = `${item.text} ${item.id}`;
685
- }
686
- return itemElement;
687
- };
688
- }
689
- /**
690
- * Overrides the default mention execute listener to insert an emoji as plain text instead.
691
- */ _overrideMentionExecuteListener() {
692
- const editor = this.editor;
693
- editor.commands.get('mention').on('execute', (event, data)=>{
694
- const eventData = data[0];
695
- // Ignore non-emoji auto-complete actions.
696
- if (eventData.marker !== EMOJI_MENTION_MARKER) {
697
- return;
698
- }
699
- // Do not propagate the event.
700
- event.stop();
701
- // Do nothing when executing after selecting a hint message.
702
- if (eventData.mention.id === EMOJI_HINT_OPTION_ID) {
703
- return;
704
- }
705
- // Trigger the picker UI.
706
- if (eventData.mention.id === EMOJI_SHOW_ALL_OPTION_ID) {
707
- const text = [
708
- ...eventData.range.getItems()
709
- ].filter((item)=>item.is('$textProxy')).map((item)=>item.data).reduce((result, text)=>result + text, '');
710
- editor.model.change((writer)=>{
711
- editor.model.deleteContent(writer.createSelection(eventData.range));
712
- });
713
- const emojiPickerPlugin = this.emojiPickerPlugin;
714
- emojiPickerPlugin.showUI(text.slice(1));
715
- setTimeout(()=>{
716
- emojiPickerPlugin.emojiPickerView.focus();
717
- });
718
- } else {
719
- editor.execute('insertText', {
720
- text: eventData.mention.text,
721
- range: eventData.range
722
- });
723
- }
724
- }, {
725
- priority: 'high'
726
- });
727
- }
728
- /**
729
- * Returns the `feed()` callback for mention config.
730
- */ _queryEmojiCallbackFactory() {
731
- return (searchQuery)=>{
732
- // Do not show anything when a query starts with a space.
733
- if (searchQuery.startsWith(' ')) {
734
- return [];
735
- }
736
- // Do not show anything when a query starts with a marker character.
737
- if (searchQuery.startsWith(EMOJI_MENTION_MARKER)) {
738
- return [];
739
- }
740
- // If the repository plugin is not available, return an empty feed to avoid confusion.
741
- // See: https://github.com/ckeditor/ckeditor5/issues/17842.
742
- if (!this._isEmojiRepositoryAvailable) {
743
- return [];
744
- }
745
- const emojis = this.emojiRepositoryPlugin.getEmojiByQuery(searchQuery).map((emoji)=>{
746
- let text = emoji.skins[this._skinTone] || emoji.skins.default;
747
- if (this.emojiPickerPlugin) {
748
- text = emoji.skins[this.emojiPickerPlugin.skinTone] || emoji.skins.default;
749
- }
750
- return {
751
- id: `:${emoji.annotation}:`,
752
- text
753
- };
754
- });
755
- if (!this.emojiPickerPlugin) {
756
- return emojis.slice(0, this._emojiDropdownLimit);
757
- }
758
- const actionItem = {
759
- id: searchQuery.length > 1 ? EMOJI_SHOW_ALL_OPTION_ID : EMOJI_HINT_OPTION_ID
760
- };
761
- return [
762
- ...emojis.slice(0, this._emojiDropdownLimit - 1),
763
- actionItem
764
- ];
765
- };
766
- }
767
- }
638
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
639
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
640
+ */
641
+ /**
642
+ * @module emoji/emojimention
643
+ */
644
+ const EMOJI_MENTION_MARKER = ":";
645
+ const EMOJI_SHOW_ALL_OPTION_ID = ":__EMOJI_SHOW_ALL:";
646
+ const EMOJI_HINT_OPTION_ID = ":__EMOJI_HINT:";
647
+ /**
648
+ * The emoji mention plugin.
649
+ *
650
+ * Introduces the autocomplete of emojis while typing.
651
+ */
652
+ var EmojiMention = class extends Plugin {
653
+ /**
654
+ * Defines a number of displayed items in the auto complete dropdown.
655
+ *
656
+ * It includes the "Show all emoji..." option if the `EmojiPicker` plugin is loaded.
657
+ */
658
+ _emojiDropdownLimit;
659
+ /**
660
+ * Defines a skin tone that is set in the emoji config.
661
+ */
662
+ _skinTone;
663
+ /**
664
+ * @inheritDoc
665
+ */
666
+ static get requires() {
667
+ return [
668
+ EmojiRepository,
669
+ Typing,
670
+ Mention
671
+ ];
672
+ }
673
+ /**
674
+ * @inheritDoc
675
+ */
676
+ static get pluginName() {
677
+ return "EmojiMention";
678
+ }
679
+ /**
680
+ * @inheritDoc
681
+ */
682
+ static get isOfficialPlugin() {
683
+ return true;
684
+ }
685
+ /**
686
+ * @inheritDoc
687
+ */
688
+ constructor(editor) {
689
+ super(editor);
690
+ this.editor.config.define("emoji", { dropdownLimit: 6 });
691
+ this._emojiDropdownLimit = editor.config.get("emoji.dropdownLimit");
692
+ this._skinTone = editor.config.get("emoji.skinTone");
693
+ this._setupMentionConfiguration(editor);
694
+ }
695
+ /**
696
+ * Initializes the configuration for emojis in the mention feature.
697
+ * If the marker used by emoji mention is already registered, it displays a warning.
698
+ * If emoji mention configuration is detected, it does not register it for a second time.
699
+ */
700
+ _setupMentionConfiguration(editor) {
701
+ const mergeFieldsPrefix = editor.config.get("mergeFields.prefix");
702
+ const mentionFeedsConfigs = editor.config.get("mention.feeds");
703
+ const isEmojiMarkerUsedByMergeFields = mergeFieldsPrefix ? mergeFieldsPrefix[0] === EMOJI_MENTION_MARKER : false;
704
+ if (mentionFeedsConfigs.filter((config) => !config._isEmojiMarker).some((config) => config.marker === EMOJI_MENTION_MARKER) || isEmojiMarkerUsedByMergeFields) {
705
+ /**
706
+ * The `marker` in the `emoji` config is already used by other plugin configuration.
707
+ *
708
+ * @error emoji-config-marker-already-used
709
+ * @param {string} marker Used marker.
710
+ */
711
+ logWarning("emoji-config-marker-already-used", { marker: EMOJI_MENTION_MARKER });
712
+ return;
713
+ }
714
+ if (mentionFeedsConfigs.some((config) => config._isEmojiMarker)) return;
715
+ const emojiMentionFeedConfig = {
716
+ _isEmojiMarker: true,
717
+ marker: EMOJI_MENTION_MARKER,
718
+ dropdownLimit: this._emojiDropdownLimit,
719
+ itemRenderer: this._customItemRendererFactory(this.editor.t),
720
+ feed: this._queryEmojiCallbackFactory()
721
+ };
722
+ this.editor.config.set("mention.feeds", [...mentionFeedsConfigs, emojiMentionFeedConfig]);
723
+ }
724
+ /**
725
+ * @inheritDoc
726
+ */
727
+ init() {
728
+ const { editor } = this;
729
+ this.emojiPickerPlugin = editor.plugins.has("EmojiPicker") ? editor.plugins.get("EmojiPicker") : null;
730
+ this.emojiRepositoryPlugin = editor.plugins.get("EmojiRepository");
731
+ this._overrideMentionExecuteListener();
732
+ }
733
+ /**
734
+ * Returns the `itemRenderer()` callback for mention config.
735
+ */
736
+ _customItemRendererFactory(t) {
737
+ return (item) => {
738
+ const itemElement = document.createElement("button");
739
+ itemElement.classList.add("ck");
740
+ itemElement.classList.add("ck-button");
741
+ itemElement.classList.add("ck-button_with-text");
742
+ itemElement.id = `mention-list-item-id${item.id.slice(0, -1)}`;
743
+ itemElement.type = "button";
744
+ itemElement.tabIndex = -1;
745
+ const labelElement = document.createElement("span");
746
+ labelElement.classList.add("ck");
747
+ labelElement.classList.add("ck-button__label");
748
+ itemElement.appendChild(labelElement);
749
+ if (item.id === EMOJI_HINT_OPTION_ID) {
750
+ itemElement.classList.add("ck-list-item-button");
751
+ itemElement.classList.add("ck-disabled");
752
+ labelElement.textContent = t("Keep on typing to see the emoji.");
753
+ } else if (item.id === EMOJI_SHOW_ALL_OPTION_ID) labelElement.textContent = t("Show all emoji...");
754
+ else labelElement.textContent = `${item.text} ${item.id}`;
755
+ return itemElement;
756
+ };
757
+ }
758
+ /**
759
+ * Overrides the default mention execute listener to insert an emoji as plain text instead.
760
+ */
761
+ _overrideMentionExecuteListener() {
762
+ const { editor } = this;
763
+ editor.commands.get("mention").on("execute", (event, data) => {
764
+ if (!this.emojiRepositoryPlugin.isRepositoryReady) return;
765
+ const eventData = data[0];
766
+ if (eventData.marker !== EMOJI_MENTION_MARKER) return;
767
+ event.stop();
768
+ if (eventData.mention.id === EMOJI_HINT_OPTION_ID) return;
769
+ if (eventData.mention.id === EMOJI_SHOW_ALL_OPTION_ID) {
770
+ const text = [...eventData.range.getItems()].filter((item) => item.is("$textProxy")).map((item) => item.data).reduce((result, text) => result + text, "");
771
+ editor.model.change((writer) => {
772
+ editor.model.deleteContent(writer.createSelection(eventData.range));
773
+ });
774
+ const emojiPickerPlugin = this.emojiPickerPlugin;
775
+ emojiPickerPlugin.showUI(text.slice(1));
776
+ setTimeout(() => {
777
+ emojiPickerPlugin.emojiPickerView.focus();
778
+ });
779
+ } else editor.execute("insertText", {
780
+ text: eventData.mention.text,
781
+ range: eventData.range
782
+ });
783
+ }, { priority: "high" });
784
+ }
785
+ /**
786
+ * Returns the `feed()` callback for mention config.
787
+ */
788
+ _queryEmojiCallbackFactory() {
789
+ return (searchQuery) => {
790
+ if (searchQuery.startsWith(" ")) return [];
791
+ if (searchQuery.startsWith(EMOJI_MENTION_MARKER)) return [];
792
+ if (!this.emojiRepositoryPlugin.isRepositoryReady) return [];
793
+ const emojis = this.emojiRepositoryPlugin.getEmojiByQuery(searchQuery).map((emoji) => {
794
+ let text = emoji.skins[this._skinTone] || emoji.skins.default;
795
+ if (this.emojiPickerPlugin) text = emoji.skins[this.emojiPickerPlugin.skinTone] || emoji.skins.default;
796
+ return {
797
+ id: `:${emoji.annotation}:`,
798
+ text
799
+ };
800
+ });
801
+ if (!this.emojiPickerPlugin) return emojis.slice(0, this._emojiDropdownLimit);
802
+ const actionItem = { id: searchQuery.length > 1 ? EMOJI_SHOW_ALL_OPTION_ID : EMOJI_HINT_OPTION_ID };
803
+ return [...emojis.slice(0, this._emojiDropdownLimit - 1), actionItem];
804
+ };
805
+ }
806
+ };
768
807
 
769
808
  /**
770
- * Command that shows the emoji user interface.
771
- */ class EmojiCommand extends Command {
772
- /**
773
- * Updates the command's {@link #isEnabled} based on the current selection.
774
- */ refresh() {
775
- const editor = this.editor;
776
- const model = editor.model;
777
- const schema = model.schema;
778
- const selection = model.document.selection;
779
- this.isEnabled = schema.checkChild(selection.getFirstPosition(), '$text');
780
- }
781
- /**
782
- * Opens emoji user interface for the current document selection.
783
- *
784
- * @fires execute
785
- * @param [searchValue=''] A default query used to filer the grid when opening the UI.
786
- */ execute(searchValue = '') {
787
- const emojiPickerPlugin = this.editor.plugins.get('EmojiPicker');
788
- emojiPickerPlugin.showUI(searchValue);
789
- }
790
- }
809
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
810
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
811
+ */
812
+ /**
813
+ * @module emoji/emojicommand
814
+ */
815
+ /**
816
+ * Command that shows the emoji user interface.
817
+ */
818
+ var EmojiCommand = class extends Command {
819
+ constructor(editor) {
820
+ super(editor);
821
+ const repository = editor.plugins.get("EmojiRepository");
822
+ this.listenTo(repository, "change:isRepositoryReady", () => this.refresh());
823
+ }
824
+ /**
825
+ * Updates the command's {@link #isEnabled} based on the current selection
826
+ * and whether the emoji repository has been loaded.
827
+ */
828
+ refresh() {
829
+ const editor = this.editor;
830
+ const repository = editor.plugins.get("EmojiRepository");
831
+ const model = editor.model;
832
+ const schema = model.schema;
833
+ const selection = model.document.selection;
834
+ this.isEnabled = !!repository.isRepositoryReady && schema.checkChild(selection.getFirstPosition(), "$text");
835
+ }
836
+ /**
837
+ * Opens emoji user interface for the current document selection.
838
+ *
839
+ * @fires execute
840
+ * @param [searchValue=''] A default query used to filer the grid when opening the UI.
841
+ */
842
+ execute(searchValue = "") {
843
+ this.editor.plugins.get("EmojiPicker").showUI(searchValue);
844
+ }
845
+ };
791
846
 
792
847
  /**
793
- * A grid of emoji tiles. It allows browsing emojis and selecting them to be inserted into the content.
794
- */ class EmojiGridView extends View {
795
- /**
796
- * A collection of the child tile views. Each tile represents a particular emoji.
797
- */ tiles;
798
- /**
799
- * Tracks information about the DOM focus in the grid.
800
- */ focusTracker;
801
- /**
802
- * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
803
- */ keystrokes;
804
- /**
805
- * An array containing all emojis grouped by their categories.
806
- */ emojiCategories;
807
- /**
808
- * A collection of all already created tile views. Each tile represents a particular emoji.
809
- * The cached tiles collection is used for efficiency purposes to avoid re-creating a particular
810
- * tile again when the grid view has changed.
811
- */ cachedTiles;
812
- /**
813
- * A callback used to filter grid items by a specified query.
814
- */ _getEmojiByQuery;
815
- /**
816
- * @inheritDoc
817
- */ constructor(locale, { categoryName, emojiCategories, getEmojiByQuery, skinTone }){
818
- super(locale);
819
- this.set('isEmpty', true);
820
- this.set('categoryName', categoryName);
821
- this.set('skinTone', skinTone);
822
- this.tiles = this.createCollection();
823
- this.cachedTiles = this.createCollection();
824
- this.focusTracker = new FocusTracker();
825
- this.keystrokes = new KeystrokeHandler();
826
- this._getEmojiByQuery = getEmojiByQuery;
827
- this.emojiCategories = emojiCategories;
828
- const bind = this.bindTemplate;
829
- this.setTemplate({
830
- tag: 'div',
831
- children: [
832
- {
833
- tag: 'div',
834
- attributes: {
835
- role: 'grid',
836
- class: [
837
- 'ck',
838
- 'ck-emoji__grid'
839
- ]
840
- },
841
- children: this.tiles
842
- }
843
- ],
844
- attributes: {
845
- role: 'tabpanel',
846
- class: [
847
- 'ck',
848
- 'ck-emoji__tiles',
849
- // To avoid issues with focus cycling, ignore a grid when it's empty.
850
- bind.if('isEmpty', 'ck-hidden', (value)=>value)
851
- ]
852
- }
853
- });
854
- addKeyboardHandlingForGrid({
855
- keystrokeHandler: this.keystrokes,
856
- focusTracker: this.focusTracker,
857
- gridItems: this.tiles,
858
- numberOfColumns: ()=>global.window.getComputedStyle(this.element.firstChild) // Responsive `.ck-emoji-grid__tiles`.
859
- .getPropertyValue('grid-template-columns').split(' ').length,
860
- uiLanguageDirection: this.locale && this.locale.uiLanguageDirection
861
- });
862
- }
863
- /**
864
- * @inheritDoc
865
- */ render() {
866
- super.render();
867
- this.keystrokes.listenTo(this.element);
868
- }
869
- /**
870
- * @inheritDoc
871
- */ destroy() {
872
- super.destroy();
873
- this.keystrokes.destroy();
874
- this.focusTracker.destroy();
875
- }
876
- /**
877
- * Focuses the first focusable in {@link ~EmojiGridView#tiles} if available.
878
- */ focus() {
879
- const firstTile = this.tiles.first;
880
- if (firstTile) {
881
- firstTile.focus();
882
- }
883
- }
884
- /**
885
- * Filters the grid view by the given regular expression.
886
- *
887
- * It filters either by the pattern or an emoji category, but never both.
888
- *
889
- * @param pattern Expression to search or `null` when filter by category name.
890
- */ filter(pattern) {
891
- const { matchingItems, allItems } = pattern ? this._getItemsByQuery(pattern.source) : this._getItemsByCategory();
892
- this._updateGrid(matchingItems);
893
- this.set('isEmpty', matchingItems.length === 0);
894
- return {
895
- resultsCount: matchingItems.length,
896
- totalItemsCount: allItems.length
897
- };
898
- }
899
- /**
900
- * Filters emojis to show based on the specified query phrase.
901
- *
902
- * @param query A query used to filter the grid.
903
- */ _getItemsByQuery(query) {
904
- return {
905
- matchingItems: this._getEmojiByQuery(query),
906
- allItems: this.emojiCategories.flatMap((group)=>group.items)
907
- };
908
- }
909
- /**
910
- * Returns emojis that belong to the specified category.
911
- */ _getItemsByCategory() {
912
- const emojiCategory = this.emojiCategories.find((item)=>item.title === this.categoryName);
913
- const { items } = emojiCategory;
914
- return {
915
- matchingItems: items,
916
- allItems: items
917
- };
918
- }
919
- /**
920
- * Updates the grid by removing the existing items and insert the new ones.
921
- *
922
- * @param items An array of items to insert.
923
- */ _updateGrid(items) {
924
- // Clean-up.
925
- [
926
- ...this.tiles
927
- ].forEach((item)=>{
928
- this.focusTracker.remove(item);
929
- this.tiles.remove(item);
930
- });
931
- items// Create tiles from matching results.
932
- .map((item)=>{
933
- const emoji = item.skins[this.skinTone] || item.skins.default;
934
- return this.cachedTiles.get(emoji) || this._createTile(emoji, item.annotation);
935
- })// Insert new elements.
936
- .forEach((item)=>{
937
- this.tiles.add(item);
938
- this.focusTracker.add(item);
939
- });
940
- }
941
- /**
942
- * Creates a new tile for the grid. Created tile is added to the {@link #cachedTiles} collection for further usage, if needed.
943
- *
944
- * @param emoji The emoji itself.
945
- * @param name The name of the emoji (e.g. "Smiling Face with Smiling Eyes").
946
- */ _createTile(emoji, name) {
947
- const tile = new ButtonView(this.locale);
948
- tile.viewUid = emoji;
949
- tile.extendTemplate({
950
- attributes: {
951
- class: [
952
- 'ck-emoji__tile'
953
- ]
954
- }
955
- });
956
- tile.set({
957
- label: emoji,
958
- tooltip: name,
959
- withText: true,
960
- ariaLabel: name,
961
- // To improve accessibility, disconnect a button and its label connection so that screen
962
- // readers can read the `[aria-label]` attribute directly from the more descriptive button.
963
- ariaLabelledBy: undefined
964
- });
965
- tile.on('execute', ()=>{
966
- this.fire('execute', {
967
- name,
968
- emoji
969
- });
970
- });
971
- this.cachedTiles.add(tile);
972
- return tile;
973
- }
974
- }
848
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
849
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
850
+ */
851
+ /**
852
+ * @module emoji/ui/emojigridview
853
+ */
854
+ /**
855
+ * A grid of emoji tiles. It allows browsing emojis and selecting them to be inserted into the content.
856
+ */
857
+ var EmojiGridView = class extends View {
858
+ /**
859
+ * A collection of the child tile views. Each tile represents a particular emoji.
860
+ */
861
+ tiles;
862
+ /**
863
+ * Tracks information about the DOM focus in the grid.
864
+ */
865
+ focusTracker;
866
+ /**
867
+ * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
868
+ */
869
+ keystrokes;
870
+ /**
871
+ * An array containing all emojis grouped by their categories.
872
+ */
873
+ emojiCategories;
874
+ /**
875
+ * A collection of all already created tile views. Each tile represents a particular emoji.
876
+ * The cached tiles collection is used for efficiency purposes to avoid re-creating a particular
877
+ * tile again when the grid view has changed.
878
+ */
879
+ cachedTiles;
880
+ /**
881
+ * A callback used to filter grid items by a specified query.
882
+ */
883
+ _getEmojiByQuery;
884
+ /**
885
+ * @inheritDoc
886
+ */
887
+ constructor(locale, { categoryName, emojiCategories, getEmojiByQuery, skinTone }) {
888
+ super(locale);
889
+ this.set("isEmpty", true);
890
+ this.set("categoryName", categoryName);
891
+ this.set("skinTone", skinTone);
892
+ this.tiles = this.createCollection();
893
+ this.cachedTiles = this.createCollection();
894
+ this.focusTracker = new FocusTracker();
895
+ this.keystrokes = new KeystrokeHandler();
896
+ this._getEmojiByQuery = getEmojiByQuery;
897
+ this.emojiCategories = emojiCategories;
898
+ const bind = this.bindTemplate;
899
+ this.setTemplate({
900
+ tag: "div",
901
+ children: [{
902
+ tag: "div",
903
+ attributes: {
904
+ role: "grid",
905
+ class: ["ck", "ck-emoji__grid"]
906
+ },
907
+ children: this.tiles
908
+ }],
909
+ attributes: {
910
+ role: "tabpanel",
911
+ class: [
912
+ "ck",
913
+ "ck-emoji__tiles",
914
+ bind.if("isEmpty", "ck-hidden", (value) => value)
915
+ ]
916
+ }
917
+ });
918
+ addKeyboardHandlingForGrid({
919
+ keystrokeHandler: this.keystrokes,
920
+ focusTracker: this.focusTracker,
921
+ gridItems: this.tiles,
922
+ numberOfColumns: () => global.window.getComputedStyle(this.element.firstChild).getPropertyValue("grid-template-columns").split(" ").length,
923
+ uiLanguageDirection: this.locale && this.locale.uiLanguageDirection
924
+ });
925
+ }
926
+ /**
927
+ * @inheritDoc
928
+ */
929
+ render() {
930
+ super.render();
931
+ this.keystrokes.listenTo(this.element);
932
+ }
933
+ /**
934
+ * @inheritDoc
935
+ */
936
+ destroy() {
937
+ super.destroy();
938
+ this.keystrokes.destroy();
939
+ this.focusTracker.destroy();
940
+ }
941
+ /**
942
+ * Focuses the first focusable in {@link ~EmojiGridView#tiles} if available.
943
+ */
944
+ focus() {
945
+ const firstTile = this.tiles.first;
946
+ if (firstTile) firstTile.focus();
947
+ }
948
+ /**
949
+ * Filters the grid view by the given regular expression.
950
+ *
951
+ * It filters either by the pattern or an emoji category, but never both.
952
+ *
953
+ * @param pattern Expression to search or `null` when filter by category name.
954
+ */
955
+ filter(pattern) {
956
+ const { matchingItems, allItems } = pattern ? this._getItemsByQuery(pattern.source) : this._getItemsByCategory();
957
+ this._updateGrid(matchingItems);
958
+ this.set("isEmpty", matchingItems.length === 0);
959
+ return {
960
+ resultsCount: matchingItems.length,
961
+ totalItemsCount: allItems.length
962
+ };
963
+ }
964
+ /**
965
+ * Filters emojis to show based on the specified query phrase.
966
+ *
967
+ * @param query A query used to filter the grid.
968
+ */
969
+ _getItemsByQuery(query) {
970
+ return {
971
+ matchingItems: this._getEmojiByQuery(query),
972
+ allItems: this.emojiCategories.flatMap((group) => group.items)
973
+ };
974
+ }
975
+ /**
976
+ * Returns emojis that belong to the specified category.
977
+ */
978
+ _getItemsByCategory() {
979
+ const emojiCategory = this.emojiCategories.find((item) => item.title === this.categoryName);
980
+ if (!emojiCategory) return {
981
+ matchingItems: [],
982
+ allItems: []
983
+ };
984
+ const { items } = emojiCategory;
985
+ return {
986
+ matchingItems: items,
987
+ allItems: items
988
+ };
989
+ }
990
+ /**
991
+ * Updates the grid by removing the existing items and insert the new ones.
992
+ *
993
+ * @param items An array of items to insert.
994
+ */
995
+ _updateGrid(items) {
996
+ [...this.tiles].forEach((item) => {
997
+ this.focusTracker.remove(item);
998
+ this.tiles.remove(item);
999
+ });
1000
+ items.map((item) => {
1001
+ const emoji = item.skins[this.skinTone] || item.skins.default;
1002
+ return this.cachedTiles.get(emoji) || this._createTile(emoji, item.annotation);
1003
+ }).forEach((item) => {
1004
+ this.tiles.add(item);
1005
+ this.focusTracker.add(item);
1006
+ });
1007
+ }
1008
+ /**
1009
+ * Creates a new tile for the grid. Created tile is added to the {@link #cachedTiles} collection for further usage, if needed.
1010
+ *
1011
+ * @param emoji The emoji itself.
1012
+ * @param name The name of the emoji (e.g. "Smiling Face with Smiling Eyes").
1013
+ */
1014
+ _createTile(emoji, name) {
1015
+ const tile = new ButtonView(this.locale);
1016
+ tile.viewUid = emoji;
1017
+ tile.extendTemplate({ attributes: { class: ["ck-emoji__tile"] } });
1018
+ tile.set({
1019
+ label: emoji,
1020
+ tooltip: name,
1021
+ withText: true,
1022
+ ariaLabel: name,
1023
+ ariaLabelledBy: void 0
1024
+ });
1025
+ tile.on("execute", () => {
1026
+ this.fire("execute", {
1027
+ name,
1028
+ emoji
1029
+ });
1030
+ });
1031
+ this.cachedTiles.add(tile);
1032
+ return tile;
1033
+ }
1034
+ };
975
1035
 
976
1036
  /**
977
- * A class representing the navigation part of the emoji UI.
978
- * It is responsible allowing the user to select a particular emoji category.
979
- */ class EmojiCategoriesView extends View {
980
- /**
981
- * Tracks information about the DOM focus in the grid.
982
- */ focusTracker;
983
- /**
984
- * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
985
- */ keystrokes;
986
- /**
987
- * Helps cycling over focusable children in the input view.
988
- */ focusCycler;
989
- /**
990
- * A collection of the categories buttons.
991
- */ buttonViews;
992
- /**
993
- * @inheritDoc
994
- */ constructor(locale, { emojiCategories, categoryName }){
995
- super(locale);
996
- this.buttonViews = this.createCollection(emojiCategories.map((emojiCategory)=>this._createCategoryButton(emojiCategory)));
997
- this.focusTracker = new FocusTracker();
998
- this.keystrokes = new KeystrokeHandler();
999
- this.focusCycler = new FocusCycler({
1000
- focusables: this.buttonViews,
1001
- focusTracker: this.focusTracker,
1002
- keystrokeHandler: this.keystrokes,
1003
- actions: {
1004
- focusPrevious: 'arrowleft',
1005
- focusNext: 'arrowright'
1006
- }
1007
- });
1008
- this.setTemplate({
1009
- tag: 'div',
1010
- attributes: {
1011
- class: [
1012
- 'ck',
1013
- 'ck-emoji__categories-list'
1014
- ],
1015
- role: 'tablist'
1016
- },
1017
- children: this.buttonViews
1018
- });
1019
- this.on('change:categoryName', (event, name, newValue, oldValue)=>{
1020
- const oldCategoryButton = this.buttonViews.find((button)=>button.tooltip === oldValue);
1021
- if (oldCategoryButton) {
1022
- oldCategoryButton.isOn = false;
1023
- }
1024
- const newCategoryButton = this.buttonViews.find((button)=>button.tooltip === newValue);
1025
- newCategoryButton.isOn = true;
1026
- });
1027
- this.set('categoryName', categoryName);
1028
- }
1029
- /**
1030
- * @inheritDoc
1031
- */ render() {
1032
- super.render();
1033
- this.buttonViews.forEach((buttonView)=>{
1034
- this.focusTracker.add(buttonView);
1035
- });
1036
- this.keystrokes.listenTo(this.element);
1037
- }
1038
- /**
1039
- * @inheritDoc
1040
- */ destroy() {
1041
- super.destroy();
1042
- this.focusTracker.destroy();
1043
- this.keystrokes.destroy();
1044
- this.buttonViews.destroy();
1045
- }
1046
- /**
1047
- * @inheritDoc
1048
- */ focus() {
1049
- this.buttonViews.first.focus();
1050
- }
1051
- /**
1052
- * Marks all categories buttons as enabled (clickable).
1053
- */ enableCategories() {
1054
- this.buttonViews.forEach((buttonView)=>{
1055
- buttonView.isEnabled = true;
1056
- });
1057
- }
1058
- /**
1059
- * Marks all categories buttons as disabled (non-clickable).
1060
- */ disableCategories() {
1061
- this.buttonViews.forEach((buttonView)=>{
1062
- buttonView.set({
1063
- class: '',
1064
- isEnabled: false,
1065
- isOn: false
1066
- });
1067
- });
1068
- }
1069
- /**
1070
- * Creates a button representing a category item.
1071
- */ _createCategoryButton(emojiCategory) {
1072
- const buttonView = new ButtonView();
1073
- const bind = buttonView.bindTemplate;
1074
- // A `[role="tab"]` element requires also the `[aria-selected]` attribute with its state.
1075
- buttonView.extendTemplate({
1076
- attributes: {
1077
- 'aria-selected': bind.to('isOn', (value)=>value.toString()),
1078
- class: [
1079
- 'ck-emoji__category-item'
1080
- ]
1081
- }
1082
- });
1083
- buttonView.set({
1084
- ariaLabel: emojiCategory.title,
1085
- label: emojiCategory.icon,
1086
- role: 'tab',
1087
- tooltip: emojiCategory.title,
1088
- withText: true,
1089
- // To improve accessibility, disconnect a button and its label connection so that screen
1090
- // readers can read the `[aria-label]` attribute directly from the more descriptive button.
1091
- ariaLabelledBy: undefined
1092
- });
1093
- buttonView.on('execute', ()=>{
1094
- this.categoryName = emojiCategory.title;
1095
- });
1096
- buttonView.on('change:isEnabled', ()=>{
1097
- if (buttonView.isEnabled && buttonView.tooltip === this.categoryName) {
1098
- buttonView.isOn = true;
1099
- }
1100
- });
1101
- return buttonView;
1102
- }
1103
- }
1037
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1038
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1039
+ */
1040
+ /**
1041
+ * @module emoji/ui/emojicategoriesview
1042
+ */
1043
+ /**
1044
+ * A class representing the navigation part of the emoji UI.
1045
+ * It is responsible allowing the user to select a particular emoji category.
1046
+ */
1047
+ var EmojiCategoriesView = class extends View {
1048
+ /**
1049
+ * Tracks information about the DOM focus in the grid.
1050
+ */
1051
+ focusTracker;
1052
+ /**
1053
+ * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
1054
+ */
1055
+ keystrokes;
1056
+ /**
1057
+ * Helps cycling over focusable children in the input view.
1058
+ */
1059
+ focusCycler;
1060
+ /**
1061
+ * A collection of the categories buttons.
1062
+ */
1063
+ buttonViews;
1064
+ /**
1065
+ * @inheritDoc
1066
+ */
1067
+ constructor(locale, { emojiCategories, categoryName }) {
1068
+ super(locale);
1069
+ this.buttonViews = this.createCollection(emojiCategories.map((emojiCategory) => this._createCategoryButton(emojiCategory)));
1070
+ this.focusTracker = new FocusTracker();
1071
+ this.keystrokes = new KeystrokeHandler();
1072
+ this.focusCycler = new FocusCycler({
1073
+ focusables: this.buttonViews,
1074
+ focusTracker: this.focusTracker,
1075
+ keystrokeHandler: this.keystrokes,
1076
+ actions: {
1077
+ focusPrevious: "arrowleft",
1078
+ focusNext: "arrowright"
1079
+ }
1080
+ });
1081
+ this.setTemplate({
1082
+ tag: "div",
1083
+ attributes: {
1084
+ class: ["ck", "ck-emoji__categories-list"],
1085
+ role: "tablist"
1086
+ },
1087
+ children: this.buttonViews
1088
+ });
1089
+ this.on("change:categoryName", (event, name, newValue, oldValue) => {
1090
+ const oldCategoryButton = this.buttonViews.find((button) => button.tooltip === oldValue);
1091
+ if (oldCategoryButton) oldCategoryButton.isOn = false;
1092
+ const newCategoryButton = this.buttonViews.find((button) => button.tooltip === newValue);
1093
+ if (newCategoryButton) newCategoryButton.isOn = true;
1094
+ });
1095
+ this.set("categoryName", categoryName);
1096
+ }
1097
+ /**
1098
+ * @inheritDoc
1099
+ */
1100
+ render() {
1101
+ super.render();
1102
+ this.buttonViews.forEach((buttonView) => {
1103
+ this.focusTracker.add(buttonView);
1104
+ });
1105
+ this.keystrokes.listenTo(this.element);
1106
+ }
1107
+ /**
1108
+ * @inheritDoc
1109
+ */
1110
+ destroy() {
1111
+ super.destroy();
1112
+ this.focusTracker.destroy();
1113
+ this.keystrokes.destroy();
1114
+ this.buttonViews.destroy();
1115
+ }
1116
+ /**
1117
+ * @inheritDoc
1118
+ */
1119
+ focus() {
1120
+ this.buttonViews.first.focus();
1121
+ }
1122
+ /**
1123
+ * Sets category buttons for the provided emoji categories.
1124
+ */
1125
+ setCategories(emojiCategories) {
1126
+ for (const button of this.buttonViews) {
1127
+ this.focusTracker.remove(button);
1128
+ button.destroy();
1129
+ }
1130
+ this.buttonViews.clear();
1131
+ for (const emojiCategory of emojiCategories) {
1132
+ const buttonView = this._createCategoryButton(emojiCategory);
1133
+ this.buttonViews.add(buttonView);
1134
+ if (this.isRendered) this.focusTracker.add(buttonView);
1135
+ }
1136
+ }
1137
+ /**
1138
+ * Marks all categories buttons as enabled (clickable).
1139
+ */
1140
+ enableCategories() {
1141
+ this.buttonViews.forEach((buttonView) => {
1142
+ buttonView.isEnabled = true;
1143
+ });
1144
+ }
1145
+ /**
1146
+ * Marks all categories buttons as disabled (non-clickable).
1147
+ */
1148
+ disableCategories() {
1149
+ this.buttonViews.forEach((buttonView) => {
1150
+ buttonView.set({
1151
+ class: "",
1152
+ isEnabled: false,
1153
+ isOn: false
1154
+ });
1155
+ });
1156
+ }
1157
+ /**
1158
+ * Creates a button representing a category item.
1159
+ */
1160
+ _createCategoryButton(emojiCategory) {
1161
+ const buttonView = new ButtonView();
1162
+ const bind = buttonView.bindTemplate;
1163
+ buttonView.extendTemplate({ attributes: {
1164
+ "aria-selected": bind.to("isOn", (value) => value.toString()),
1165
+ class: ["ck-emoji__category-item"]
1166
+ } });
1167
+ buttonView.set({
1168
+ ariaLabel: emojiCategory.title,
1169
+ label: emojiCategory.icon,
1170
+ role: "tab",
1171
+ tooltip: emojiCategory.title,
1172
+ withText: true,
1173
+ ariaLabelledBy: void 0
1174
+ });
1175
+ buttonView.on("execute", () => {
1176
+ this.categoryName = emojiCategory.title;
1177
+ });
1178
+ buttonView.on("change:isEnabled", () => {
1179
+ if (buttonView.isEnabled && buttonView.tooltip === this.categoryName) buttonView.isOn = true;
1180
+ });
1181
+ return buttonView;
1182
+ }
1183
+ };
1104
1184
 
1105
1185
  /**
1106
- * A view responsible for providing an input element that allows filtering emoji by the provided query.
1107
- */ class EmojiSearchView extends View {
1108
- /**
1109
- * The find in text input view that stores the searched string.
1110
- */ inputView;
1111
- /**
1112
- * An instance of the `EmojiGridView`.
1113
- */ gridView;
1114
- /**
1115
- * @inheritDoc
1116
- */ constructor(locale, { gridView, resultsView }){
1117
- super(locale);
1118
- this.gridView = gridView;
1119
- const t = locale.t;
1120
- this.inputView = new SearchTextView(this.locale, {
1121
- queryView: {
1122
- label: t('Find an emoji (min. 2 characters)'),
1123
- creator: createLabeledInputText
1124
- },
1125
- filteredView: this.gridView,
1126
- infoView: {
1127
- instance: resultsView
1128
- }
1129
- });
1130
- this.setTemplate({
1131
- tag: 'div',
1132
- attributes: {
1133
- class: [
1134
- 'ck',
1135
- 'ck-search'
1136
- ],
1137
- tabindex: '-1'
1138
- },
1139
- children: [
1140
- this.inputView.queryView
1141
- ]
1142
- });
1143
- // Pass through the `search` event to handle it by a parent view.
1144
- this.inputView.delegate('search').to(this);
1145
- }
1146
- /**
1147
- * @inheritDoc
1148
- */ destroy() {
1149
- super.destroy();
1150
- this.inputView.destroy();
1151
- }
1152
- /**
1153
- * Searches the {@link #gridView} for the given query.
1154
- *
1155
- * @param query The search query string.
1156
- */ search(query) {
1157
- const regExp = query ? new RegExp(escapeRegExp(query), 'ig') : null;
1158
- const filteringResults = this.gridView.filter(regExp);
1159
- this.inputView.fire('search', {
1160
- query,
1161
- ...filteringResults
1162
- });
1163
- }
1164
- /**
1165
- * Allows defining the default value in the search text field.
1166
- *
1167
- * @param value The new value.
1168
- */ setInputValue(value) {
1169
- if (!value) {
1170
- this.inputView.queryView.fieldView.reset();
1171
- } else {
1172
- this.inputView.queryView.fieldView.value = value;
1173
- }
1174
- }
1175
- /**
1176
- * Returns an input provided by a user in the search text field.
1177
- */ getInputValue() {
1178
- return this.inputView.queryView.fieldView.element.value;
1179
- }
1180
- /**
1181
- * @inheritDoc
1182
- */ focus() {
1183
- this.inputView.focus();
1184
- }
1185
- }
1186
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1187
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1188
+ */
1189
+ /**
1190
+ * @module emoji/ui/emojisearchview
1191
+ */
1192
+ /**
1193
+ * A view responsible for providing an input element that allows filtering emoji by the provided query.
1194
+ */
1195
+ var EmojiSearchView = class extends View {
1196
+ /**
1197
+ * The find in text input view that stores the searched string.
1198
+ */
1199
+ inputView;
1200
+ /**
1201
+ * An instance of the `EmojiGridView`.
1202
+ */
1203
+ gridView;
1204
+ /**
1205
+ * @inheritDoc
1206
+ */
1207
+ constructor(locale, { gridView, resultsView }) {
1208
+ super(locale);
1209
+ this.gridView = gridView;
1210
+ const t = locale.t;
1211
+ this.inputView = new SearchTextView(this.locale, {
1212
+ queryView: {
1213
+ label: t("Find an emoji (min. 2 characters)"),
1214
+ creator: createLabeledInputText
1215
+ },
1216
+ filteredView: this.gridView,
1217
+ infoView: { instance: resultsView }
1218
+ });
1219
+ this.setTemplate({
1220
+ tag: "div",
1221
+ attributes: {
1222
+ class: ["ck", "ck-search"],
1223
+ tabindex: "-1"
1224
+ },
1225
+ children: [this.inputView.queryView]
1226
+ });
1227
+ this.inputView.delegate("search").to(this);
1228
+ }
1229
+ /**
1230
+ * @inheritDoc
1231
+ */
1232
+ destroy() {
1233
+ super.destroy();
1234
+ this.inputView.destroy();
1235
+ }
1236
+ /**
1237
+ * Searches the {@link #gridView} for the given query.
1238
+ *
1239
+ * @param query The search query string.
1240
+ */
1241
+ search(query) {
1242
+ const regExp = query ? new RegExp(escapeRegExp(query), "ig") : null;
1243
+ const filteringResults = this.gridView.filter(regExp);
1244
+ this.inputView.fire("search", {
1245
+ query,
1246
+ ...filteringResults
1247
+ });
1248
+ }
1249
+ /**
1250
+ * Allows defining the default value in the search text field.
1251
+ *
1252
+ * @param value The new value.
1253
+ */
1254
+ setInputValue(value) {
1255
+ if (!value) this.inputView.queryView.fieldView.reset();
1256
+ else this.inputView.queryView.fieldView.value = value;
1257
+ }
1258
+ /**
1259
+ * Returns an input provided by a user in the search text field.
1260
+ */
1261
+ getInputValue() {
1262
+ return this.inputView.queryView.fieldView.element.value;
1263
+ }
1264
+ /**
1265
+ * @inheritDoc
1266
+ */
1267
+ focus() {
1268
+ this.inputView.focus();
1269
+ }
1270
+ };
1186
1271
 
1187
1272
  /**
1188
- * A view responsible for selecting a skin tone for an emoji.
1189
- */ class EmojiToneView extends View {
1190
- /**
1191
- * A dropdown element for selecting an active skin tone.
1192
- */ dropdownView;
1193
- /**
1194
- * An array of available skin tones.
1195
- */ _skinTones;
1196
- /**
1197
- * @inheritDoc
1198
- */ constructor(locale, { skinTone, skinTones }){
1199
- super(locale);
1200
- this.set('skinTone', skinTone);
1201
- this._skinTones = skinTones;
1202
- const t = locale.t;
1203
- const accessibleLabel = t('Select skin tone');
1204
- const dropdownView = createDropdown(locale);
1205
- const itemDefinitions = new Collection();
1206
- for (const { id, icon, tooltip } of this._skinTones){
1207
- const def = {
1208
- type: 'button',
1209
- model: new UIModel({
1210
- value: id,
1211
- label: icon,
1212
- ariaLabel: tooltip,
1213
- tooltip,
1214
- tooltipPosition: 'e',
1215
- role: 'menuitemradio',
1216
- withText: true,
1217
- // To improve accessibility, disconnect a button and its label connection so that screen
1218
- // readers can read the `[aria-label]` attribute directly from the more descriptive button.
1219
- ariaLabelledBy: undefined
1220
- })
1221
- };
1222
- def.model.bind('isOn').to(this, 'skinTone', (value)=>value === id);
1223
- itemDefinitions.add(def);
1224
- }
1225
- addListToDropdown(dropdownView, itemDefinitions, {
1226
- ariaLabel: accessibleLabel,
1227
- role: 'menu'
1228
- });
1229
- dropdownView.buttonView.set({
1230
- label: this._getSkinTone().icon,
1231
- ariaLabel: accessibleLabel,
1232
- ariaLabelledBy: undefined,
1233
- isOn: false,
1234
- withText: true,
1235
- tooltip: accessibleLabel
1236
- });
1237
- this.dropdownView = dropdownView;
1238
- // Execute command when an item from the dropdown is selected.
1239
- this.listenTo(dropdownView, 'execute', (evt)=>{
1240
- this.skinTone = evt.source.value;
1241
- });
1242
- dropdownView.buttonView.bind('label').to(this, 'skinTone', ()=>{
1243
- return this._getSkinTone().icon;
1244
- });
1245
- dropdownView.buttonView.bind('ariaLabel').to(this, 'skinTone', ()=>{
1246
- // Render a current state, but also what the dropdown does.
1247
- return `${this._getSkinTone().tooltip}, ${accessibleLabel}`;
1248
- });
1249
- this.setTemplate({
1250
- tag: 'div',
1251
- attributes: {
1252
- class: [
1253
- 'ck',
1254
- 'ck-emoji__skin-tone'
1255
- ]
1256
- },
1257
- children: [
1258
- dropdownView
1259
- ]
1260
- });
1261
- }
1262
- /**
1263
- * @inheritDoc
1264
- */ focus() {
1265
- this.dropdownView.buttonView.focus();
1266
- }
1267
- /**
1268
- * Helper method for receiving an object describing the active skin tone.
1269
- */ _getSkinTone() {
1270
- return this._skinTones.find((tone)=>tone.id === this.skinTone);
1271
- }
1272
- }
1273
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1274
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1275
+ */
1276
+ /**
1277
+ * @module emoji/ui/emojitoneview
1278
+ */
1279
+ /**
1280
+ * A view responsible for selecting a skin tone for an emoji.
1281
+ */
1282
+ var EmojiToneView = class extends View {
1283
+ /**
1284
+ * A dropdown element for selecting an active skin tone.
1285
+ */
1286
+ dropdownView;
1287
+ /**
1288
+ * An array of available skin tones.
1289
+ */
1290
+ _skinTones;
1291
+ /**
1292
+ * @inheritDoc
1293
+ */
1294
+ constructor(locale, { skinTone, skinTones }) {
1295
+ super(locale);
1296
+ this.set("skinTone", skinTone);
1297
+ this._skinTones = skinTones;
1298
+ const t = locale.t;
1299
+ const accessibleLabel = t("Select skin tone");
1300
+ const dropdownView = createDropdown(locale);
1301
+ const itemDefinitions = new Collection();
1302
+ for (const { id, icon, tooltip } of this._skinTones) {
1303
+ const def = {
1304
+ type: "button",
1305
+ model: new UIModel({
1306
+ value: id,
1307
+ label: icon,
1308
+ ariaLabel: tooltip,
1309
+ tooltip,
1310
+ tooltipPosition: "e",
1311
+ role: "menuitemradio",
1312
+ withText: true,
1313
+ ariaLabelledBy: void 0
1314
+ })
1315
+ };
1316
+ def.model.bind("isOn").to(this, "skinTone", (value) => value === id);
1317
+ itemDefinitions.add(def);
1318
+ }
1319
+ addListToDropdown(dropdownView, itemDefinitions, {
1320
+ ariaLabel: accessibleLabel,
1321
+ role: "menu"
1322
+ });
1323
+ dropdownView.buttonView.set({
1324
+ label: this._getSkinTone().icon,
1325
+ ariaLabel: accessibleLabel,
1326
+ ariaLabelledBy: void 0,
1327
+ isOn: false,
1328
+ withText: true,
1329
+ tooltip: accessibleLabel
1330
+ });
1331
+ this.dropdownView = dropdownView;
1332
+ this.listenTo(dropdownView, "execute", (evt) => {
1333
+ this.skinTone = evt.source.value;
1334
+ });
1335
+ dropdownView.buttonView.bind("label").to(this, "skinTone", () => {
1336
+ return this._getSkinTone().icon;
1337
+ });
1338
+ dropdownView.buttonView.bind("ariaLabel").to(this, "skinTone", () => {
1339
+ return `${this._getSkinTone().tooltip}, ${accessibleLabel}`;
1340
+ });
1341
+ this.setTemplate({
1342
+ tag: "div",
1343
+ attributes: { class: ["ck", "ck-emoji__skin-tone"] },
1344
+ children: [dropdownView]
1345
+ });
1346
+ }
1347
+ /**
1348
+ * @inheritDoc
1349
+ */
1350
+ focus() {
1351
+ this.dropdownView.buttonView.focus();
1352
+ }
1353
+ /**
1354
+ * Helper method for receiving an object describing the active skin tone.
1355
+ */
1356
+ _getSkinTone() {
1357
+ return this._skinTones.find((tone) => tone.id === this.skinTone);
1358
+ }
1359
+ };
1273
1360
 
1274
1361
  /**
1275
- * A view that glues pieces of the emoji panel together.
1276
- */ class EmojiPickerView extends View {
1277
- /**
1278
- * A collection of the focusable children of the view.
1279
- */ items;
1280
- /**
1281
- * Tracks information about the DOM focus in the view.
1282
- */ focusTracker;
1283
- /**
1284
- * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
1285
- */ keystrokes;
1286
- /**
1287
- * Helps cycling over focusable {@link #items} in the view.
1288
- */ focusCycler;
1289
- /**
1290
- * An instance of the `EmojiSearchView`.
1291
- */ searchView;
1292
- /**
1293
- * An instance of the `EmojiToneView`.
1294
- */ toneView;
1295
- /**
1296
- * An instance of the `EmojiCategoriesView`.
1297
- */ categoriesView;
1298
- /**
1299
- * An instance of the `EmojiGridView`.
1300
- */ gridView;
1301
- /**
1302
- * An instance of the `EmojiGridView`.
1303
- */ infoView;
1304
- /**
1305
- * @inheritDoc
1306
- */ constructor(locale, { emojiCategories, getEmojiByQuery, skinTone, skinTones }){
1307
- super(locale);
1308
- const categoryName = emojiCategories[0].title;
1309
- this.gridView = new EmojiGridView(locale, {
1310
- categoryName,
1311
- emojiCategories,
1312
- getEmojiByQuery,
1313
- skinTone
1314
- });
1315
- this.infoView = new SearchInfoView();
1316
- this.searchView = new EmojiSearchView(locale, {
1317
- gridView: this.gridView,
1318
- resultsView: this.infoView
1319
- });
1320
- this.categoriesView = new EmojiCategoriesView(locale, {
1321
- emojiCategories,
1322
- categoryName
1323
- });
1324
- this.toneView = new EmojiToneView(locale, {
1325
- skinTone,
1326
- skinTones
1327
- });
1328
- this.items = this.createCollection([
1329
- this.searchView,
1330
- this.toneView,
1331
- this.categoriesView,
1332
- this.gridView,
1333
- this.infoView
1334
- ]);
1335
- this.focusTracker = new FocusTracker();
1336
- this.keystrokes = new KeystrokeHandler();
1337
- this.focusCycler = new FocusCycler({
1338
- focusables: this.items,
1339
- focusTracker: this.focusTracker,
1340
- keystrokeHandler: this.keystrokes,
1341
- actions: {
1342
- focusPrevious: 'shift + tab',
1343
- focusNext: 'tab'
1344
- }
1345
- });
1346
- this.setTemplate({
1347
- tag: 'div',
1348
- children: [
1349
- {
1350
- tag: 'div',
1351
- children: [
1352
- this.searchView,
1353
- this.toneView
1354
- ],
1355
- attributes: {
1356
- class: [
1357
- 'ck',
1358
- 'ck-emoji__search'
1359
- ]
1360
- }
1361
- },
1362
- this.categoriesView,
1363
- this.gridView,
1364
- {
1365
- tag: 'div',
1366
- children: [
1367
- this.infoView
1368
- ],
1369
- attributes: {
1370
- class: [
1371
- 'ck',
1372
- 'ck-search__results'
1373
- ]
1374
- }
1375
- }
1376
- ],
1377
- attributes: {
1378
- tabindex: '-1',
1379
- class: [
1380
- 'ck',
1381
- 'ck-emoji',
1382
- 'ck-search'
1383
- ]
1384
- }
1385
- });
1386
- this._setupEventListeners();
1387
- }
1388
- /**
1389
- * @inheritDoc
1390
- */ render() {
1391
- super.render();
1392
- this.focusTracker.add(this.searchView.element);
1393
- this.focusTracker.add(this.toneView.element);
1394
- this.focusTracker.add(this.categoriesView.element);
1395
- this.focusTracker.add(this.gridView.element);
1396
- this.focusTracker.add(this.infoView.element);
1397
- // Start listening for the keystrokes coming from #element.
1398
- this.keystrokes.listenTo(this.element);
1399
- }
1400
- /**
1401
- * @inheritDoc
1402
- */ destroy() {
1403
- super.destroy();
1404
- this.focusTracker.destroy();
1405
- this.keystrokes.destroy();
1406
- }
1407
- /**
1408
- * Focuses the search input.
1409
- */ focus() {
1410
- this.searchView.focus();
1411
- }
1412
- /**
1413
- * Initializes interactions between sub-views.
1414
- */ _setupEventListeners() {
1415
- const t = this.locale.t;
1416
- // Disable the category switcher when filtering by a query.
1417
- this.searchView.on('search', (evt, data)=>{
1418
- if (data.query) {
1419
- this.categoriesView.disableCategories();
1420
- } else {
1421
- this.categoriesView.enableCategories();
1422
- }
1423
- });
1424
- // Show a user-friendly message depending on the search query.
1425
- this.searchView.on('search', (evt, data)=>{
1426
- if (data.query.length === 1) {
1427
- this.infoView.set({
1428
- primaryText: t('Keep on typing to see the emoji.'),
1429
- secondaryText: t('The query must contain at least two characters.'),
1430
- isVisible: true
1431
- });
1432
- } else if (!data.resultsCount) {
1433
- this.infoView.set({
1434
- primaryText: t('No emojis were found matching "%0".', data.query),
1435
- secondaryText: t('Please try a different phrase or check the spelling.'),
1436
- isVisible: true
1437
- });
1438
- } else {
1439
- this.infoView.set({
1440
- isVisible: false
1441
- });
1442
- }
1443
- });
1444
- // Emit an update event to react to balloon dimensions changes.
1445
- this.searchView.on('search', ()=>{
1446
- this.fire('update');
1447
- this.gridView.element.scrollTo(0, 0);
1448
- });
1449
- // Update the grid of emojis when the selected category is changed.
1450
- this.categoriesView.on('change:categoryName', (ev, args, categoryName)=>{
1451
- this.gridView.categoryName = categoryName;
1452
- this.searchView.search('');
1453
- });
1454
- // Update the grid of emojis when the selected skin tone is changed.
1455
- // In such a case, the displayed emoji should use an updated skin tone value.
1456
- this.toneView.on('change:skinTone', (evt, propertyName, newValue)=>{
1457
- this.gridView.skinTone = newValue;
1458
- this.searchView.search(this.searchView.getInputValue());
1459
- });
1460
- }
1461
- }
1362
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1363
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1364
+ */
1365
+ /**
1366
+ * @module emoji/ui/emojipickerview
1367
+ */
1368
+ /**
1369
+ * A view that glues pieces of the emoji panel together.
1370
+ */
1371
+ var EmojiPickerView = class extends View {
1372
+ /**
1373
+ * A collection of the focusable children of the view.
1374
+ */
1375
+ items;
1376
+ /**
1377
+ * Tracks information about the DOM focus in the view.
1378
+ */
1379
+ focusTracker;
1380
+ /**
1381
+ * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
1382
+ */
1383
+ keystrokes;
1384
+ /**
1385
+ * Helps cycling over focusable {@link #items} in the view.
1386
+ */
1387
+ focusCycler;
1388
+ /**
1389
+ * An instance of the `EmojiSearchView`.
1390
+ */
1391
+ searchView;
1392
+ /**
1393
+ * An instance of the `EmojiToneView`.
1394
+ */
1395
+ toneView;
1396
+ /**
1397
+ * An instance of the `EmojiCategoriesView`.
1398
+ */
1399
+ categoriesView;
1400
+ /**
1401
+ * An instance of the `EmojiGridView`.
1402
+ */
1403
+ gridView;
1404
+ /**
1405
+ * An instance of the `EmojiGridView`.
1406
+ */
1407
+ infoView;
1408
+ /**
1409
+ * @inheritDoc
1410
+ */
1411
+ constructor(locale, { emojiCategories, getEmojiByQuery, skinTone, skinTones }) {
1412
+ super(locale);
1413
+ const categoryName = emojiCategories[0]?.title ?? "";
1414
+ this.gridView = new EmojiGridView(locale, {
1415
+ categoryName,
1416
+ emojiCategories,
1417
+ getEmojiByQuery,
1418
+ skinTone
1419
+ });
1420
+ this.infoView = new SearchInfoView();
1421
+ this.searchView = new EmojiSearchView(locale, {
1422
+ gridView: this.gridView,
1423
+ resultsView: this.infoView
1424
+ });
1425
+ this.categoriesView = new EmojiCategoriesView(locale, {
1426
+ emojiCategories,
1427
+ categoryName
1428
+ });
1429
+ this.toneView = new EmojiToneView(locale, {
1430
+ skinTone,
1431
+ skinTones
1432
+ });
1433
+ this.items = this.createCollection([
1434
+ this.searchView,
1435
+ this.toneView,
1436
+ this.categoriesView,
1437
+ this.gridView,
1438
+ this.infoView
1439
+ ]);
1440
+ this.focusTracker = new FocusTracker();
1441
+ this.keystrokes = new KeystrokeHandler();
1442
+ this.focusCycler = new FocusCycler({
1443
+ focusables: this.items,
1444
+ focusTracker: this.focusTracker,
1445
+ keystrokeHandler: this.keystrokes,
1446
+ actions: {
1447
+ focusPrevious: "shift + tab",
1448
+ focusNext: "tab"
1449
+ }
1450
+ });
1451
+ this.setTemplate({
1452
+ tag: "div",
1453
+ children: [
1454
+ {
1455
+ tag: "div",
1456
+ children: [this.searchView, this.toneView],
1457
+ attributes: { class: ["ck", "ck-emoji__search"] }
1458
+ },
1459
+ this.categoriesView,
1460
+ this.gridView,
1461
+ {
1462
+ tag: "div",
1463
+ children: [this.infoView],
1464
+ attributes: { class: ["ck", "ck-search__results"] }
1465
+ }
1466
+ ],
1467
+ attributes: {
1468
+ tabindex: "-1",
1469
+ class: [
1470
+ "ck",
1471
+ "ck-emoji",
1472
+ "ck-search"
1473
+ ]
1474
+ }
1475
+ });
1476
+ this._setupEventListeners();
1477
+ }
1478
+ /**
1479
+ * @inheritDoc
1480
+ */
1481
+ render() {
1482
+ super.render();
1483
+ this.focusTracker.add(this.searchView.element);
1484
+ this.focusTracker.add(this.toneView.element);
1485
+ this.focusTracker.add(this.categoriesView.element);
1486
+ this.focusTracker.add(this.gridView.element);
1487
+ this.focusTracker.add(this.infoView.element);
1488
+ this.keystrokes.listenTo(this.element);
1489
+ }
1490
+ /**
1491
+ * @inheritDoc
1492
+ */
1493
+ destroy() {
1494
+ super.destroy();
1495
+ this.focusTracker.destroy();
1496
+ this.keystrokes.destroy();
1497
+ }
1498
+ /**
1499
+ * Focuses the search input.
1500
+ */
1501
+ focus() {
1502
+ this.searchView.focus();
1503
+ }
1504
+ /**
1505
+ * Sets certain list of categories in categories view and performs search.
1506
+ */
1507
+ setCategories(categories) {
1508
+ const { emojiCategories } = this.gridView;
1509
+ const categoryName = categories[0].title;
1510
+ emojiCategories.splice(0, emojiCategories.length, ...categories);
1511
+ this.categoriesView.setCategories(categories);
1512
+ this.categoriesView.categoryName = categoryName;
1513
+ this.gridView.categoryName = categoryName;
1514
+ this.searchView.search(this.searchView.getInputValue());
1515
+ }
1516
+ /**
1517
+ * Initializes interactions between sub-views.
1518
+ */
1519
+ _setupEventListeners() {
1520
+ const t = this.locale.t;
1521
+ this.searchView.on("search", (evt, data) => {
1522
+ if (data.query) this.categoriesView.disableCategories();
1523
+ else this.categoriesView.enableCategories();
1524
+ });
1525
+ this.searchView.on("search", (evt, data) => {
1526
+ if (!this.categoriesView.buttonViews.length) return;
1527
+ if (data.query.length === 1) this.infoView.set({
1528
+ primaryText: t("Keep on typing to see the emoji."),
1529
+ secondaryText: t("The query must contain at least two characters."),
1530
+ isVisible: true
1531
+ });
1532
+ else if (!data.resultsCount) this.infoView.set({
1533
+ primaryText: t("No emojis were found matching \"%0\".", data.query),
1534
+ secondaryText: t("Please try a different phrase or check the spelling."),
1535
+ isVisible: true
1536
+ });
1537
+ else this.infoView.set({ isVisible: false });
1538
+ });
1539
+ this.searchView.on("search", () => {
1540
+ this.fire("update");
1541
+ this.gridView.element.scrollTo(0, 0);
1542
+ });
1543
+ this.categoriesView.on("change:categoryName", (ev, args, categoryName) => {
1544
+ this.gridView.categoryName = categoryName;
1545
+ this.searchView.search("");
1546
+ });
1547
+ this.toneView.on("change:skinTone", (evt, propertyName, newValue) => {
1548
+ this.gridView.skinTone = newValue;
1549
+ this.searchView.search(this.searchView.getInputValue());
1550
+ });
1551
+ }
1552
+ };
1462
1553
 
1463
1554
  /**
1464
- * The emoji picker form view.
1465
- */ class EmojiPickerFormView extends View {
1466
- /**
1467
- * The Back button view displayed in the header.
1468
- */ backButtonView;
1469
- /**
1470
- * Tracks information about DOM focus in the form.
1471
- */ focusTracker = new FocusTracker();
1472
- /**
1473
- * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
1474
- */ keystrokes = new KeystrokeHandler();
1475
- /**
1476
- * A collection of child views.
1477
- */ children;
1478
- /**
1479
- * A collection of views that can be focused in the form.
1480
- */ _focusables = new ViewCollection();
1481
- /**
1482
- * Helps cycling over {@link #_focusables} in the form.
1483
- */ _focusCycler;
1484
- /**
1485
- * Creates an instance of the {@link module:emoji/ui/emojipickerformview~EmojiPickerFormView} class.
1486
- *
1487
- * Also see {@link #render}.
1488
- *
1489
- * @param locale The localization services instance.
1490
- */ constructor(locale){
1491
- super(locale);
1492
- this.backButtonView = this._createBackButton();
1493
- this.children = this.createCollection([
1494
- this._createHeaderView()
1495
- ]);
1496
- this._focusCycler = new FocusCycler({
1497
- focusables: this._focusables,
1498
- focusTracker: this.focusTracker,
1499
- keystrokeHandler: this.keystrokes,
1500
- actions: {
1501
- // Navigate form fields backward using the Shift + Tab keystroke.
1502
- focusPrevious: 'shift + tab',
1503
- // Navigate form fields forwards using the Tab key.
1504
- focusNext: 'tab'
1505
- }
1506
- });
1507
- this.setTemplate({
1508
- tag: 'div',
1509
- attributes: {
1510
- class: [
1511
- 'ck',
1512
- 'ck-form',
1513
- 'ck-emoji-picker-form'
1514
- ],
1515
- // https://github.com/ckeditor/ckeditor5-link/issues/90
1516
- tabindex: '-1'
1517
- },
1518
- children: this.children
1519
- });
1520
- }
1521
- /**
1522
- * @inheritDoc
1523
- */ render() {
1524
- super.render();
1525
- const childViews = [
1526
- ...this.children.filter(isFocusable),
1527
- this.backButtonView
1528
- ];
1529
- childViews.forEach((v)=>{
1530
- // Register the view as focusable.
1531
- this._focusables.add(v);
1532
- // Register the view in the focus tracker.
1533
- this.focusTracker.add(v.element);
1534
- // Register the view in the focus cycler to avoid nested focus cycles traps.
1535
- if (isViewWithFocusCycler(v)) {
1536
- this._focusCycler.chain(v.focusCycler);
1537
- }
1538
- });
1539
- // Start listening for the keystrokes coming from #element.
1540
- this.keystrokes.listenTo(this.element);
1541
- }
1542
- /**
1543
- * @inheritDoc
1544
- */ destroy() {
1545
- super.destroy();
1546
- this.focusTracker.destroy();
1547
- this.keystrokes.destroy();
1548
- }
1549
- /**
1550
- * Focuses the fist {@link #_focusables} in the form.
1551
- */ focus() {
1552
- this._focusCycler.focusFirst();
1553
- }
1554
- /**
1555
- * Creates a back button view that cancels the form.
1556
- */ _createBackButton() {
1557
- const t = this.locale.t;
1558
- const backButton = new ButtonView(this.locale);
1559
- backButton.set({
1560
- class: 'ck-button-back',
1561
- label: t('Back'),
1562
- icon: IconPreviousArrow,
1563
- tooltip: true
1564
- });
1565
- backButton.delegate('execute').to(this, 'cancel');
1566
- return backButton;
1567
- }
1568
- /**
1569
- * Creates a header view for the form.
1570
- */ _createHeaderView() {
1571
- const t = this.locale.t;
1572
- const header = new FormHeaderView(this.locale, {
1573
- label: t('Emoji picker')
1574
- });
1575
- header.children.add(this.backButtonView, 0);
1576
- return header;
1577
- }
1578
- }
1555
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1556
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1557
+ */
1558
+ /**
1559
+ * @module emoji/ui/emojipickerformview
1560
+ */
1561
+ /**
1562
+ * The emoji picker form view.
1563
+ */
1564
+ var EmojiPickerFormView = class extends View {
1565
+ /**
1566
+ * The Back button view displayed in the header.
1567
+ */
1568
+ backButtonView;
1569
+ /**
1570
+ * Tracks information about DOM focus in the form.
1571
+ */
1572
+ focusTracker = new FocusTracker();
1573
+ /**
1574
+ * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
1575
+ */
1576
+ keystrokes = new KeystrokeHandler();
1577
+ /**
1578
+ * A collection of child views.
1579
+ */
1580
+ children;
1581
+ /**
1582
+ * A collection of views that can be focused in the form.
1583
+ */
1584
+ _focusables = new ViewCollection();
1585
+ /**
1586
+ * Helps cycling over {@link #_focusables} in the form.
1587
+ */
1588
+ _focusCycler;
1589
+ /**
1590
+ * Creates an instance of the {@link module:emoji/ui/emojipickerformview~EmojiPickerFormView} class.
1591
+ *
1592
+ * Also see {@link #render}.
1593
+ *
1594
+ * @param locale The localization services instance.
1595
+ */
1596
+ constructor(locale) {
1597
+ super(locale);
1598
+ this.backButtonView = this._createBackButton();
1599
+ this.children = this.createCollection([this._createHeaderView()]);
1600
+ this._focusCycler = new FocusCycler({
1601
+ focusables: this._focusables,
1602
+ focusTracker: this.focusTracker,
1603
+ keystrokeHandler: this.keystrokes,
1604
+ actions: {
1605
+ focusPrevious: "shift + tab",
1606
+ focusNext: "tab"
1607
+ }
1608
+ });
1609
+ this.setTemplate({
1610
+ tag: "div",
1611
+ attributes: {
1612
+ class: [
1613
+ "ck",
1614
+ "ck-form",
1615
+ "ck-emoji-picker-form"
1616
+ ],
1617
+ tabindex: "-1"
1618
+ },
1619
+ children: this.children
1620
+ });
1621
+ }
1622
+ /**
1623
+ * @inheritDoc
1624
+ */
1625
+ render() {
1626
+ super.render();
1627
+ [...this.children.filter(isFocusable), this.backButtonView].forEach((v) => {
1628
+ this._focusables.add(v);
1629
+ this.focusTracker.add(v.element);
1630
+ if (isViewWithFocusCycler(v)) this._focusCycler.chain(v.focusCycler);
1631
+ });
1632
+ this.keystrokes.listenTo(this.element);
1633
+ }
1634
+ /**
1635
+ * @inheritDoc
1636
+ */
1637
+ destroy() {
1638
+ super.destroy();
1639
+ this.focusTracker.destroy();
1640
+ this.keystrokes.destroy();
1641
+ }
1642
+ /**
1643
+ * Focuses the fist {@link #_focusables} in the form.
1644
+ */
1645
+ focus() {
1646
+ this._focusCycler.focusFirst();
1647
+ }
1648
+ /**
1649
+ * Creates a back button view that cancels the form.
1650
+ */
1651
+ _createBackButton() {
1652
+ const t = this.locale.t;
1653
+ const backButton = new ButtonView(this.locale);
1654
+ backButton.set({
1655
+ class: "ck-button-back",
1656
+ label: t("Back"),
1657
+ icon: IconPreviousArrow,
1658
+ tooltip: true
1659
+ });
1660
+ backButton.delegate("execute").to(this, "cancel");
1661
+ return backButton;
1662
+ }
1663
+ /**
1664
+ * Creates a header view for the form.
1665
+ */
1666
+ _createHeaderView() {
1667
+ const t = this.locale.t;
1668
+ const header = new FormHeaderView(this.locale, { label: t("Emoji picker") });
1669
+ header.children.add(this.backButtonView, 0);
1670
+ return header;
1671
+ }
1672
+ };
1579
1673
 
1580
- const VISUAL_SELECTION_MARKER_NAME = 'emoji-picker';
1581
1674
  /**
1582
- * The emoji picker plugin.
1583
- *
1584
- * Introduces the `'emoji'` dropdown.
1585
- */ class EmojiPicker extends Plugin {
1586
- /**
1587
- * @inheritDoc
1588
- */ static get requires() {
1589
- return [
1590
- EmojiRepository,
1591
- ContextualBalloon,
1592
- Dialog,
1593
- Typing
1594
- ];
1595
- }
1596
- /**
1597
- * @inheritDoc
1598
- */ static get pluginName() {
1599
- return 'EmojiPicker';
1600
- }
1601
- /**
1602
- * @inheritDoc
1603
- */ static get isOfficialPlugin() {
1604
- return true;
1605
- }
1606
- /**
1607
- * @inheritDoc
1608
- */ async init() {
1609
- const editor = this.editor;
1610
- this.balloonPlugin = editor.plugins.get('ContextualBalloon');
1611
- this.emojiRepositoryPlugin = editor.plugins.get('EmojiRepository');
1612
- // Skip registering a button in the toolbar and list item in the menu bar if the emoji repository is not ready.
1613
- if (!await this.emojiRepositoryPlugin.isReady()) {
1614
- return;
1615
- }
1616
- const command = new EmojiCommand(editor);
1617
- editor.commands.add('emoji', command);
1618
- editor.ui.componentFactory.add('emoji', ()=>{
1619
- const button = this._createButton(ButtonView, command);
1620
- button.set({
1621
- tooltip: true
1622
- });
1623
- return button;
1624
- });
1625
- editor.ui.componentFactory.add('menuBar:emoji', ()=>{
1626
- return this._createButton(MenuBarMenuListItemButtonView, command);
1627
- });
1628
- this._setupConversion();
1629
- }
1630
- /**
1631
- * @inheritDoc
1632
- */ destroy() {
1633
- super.destroy();
1634
- if (this.emojiPickerView) {
1635
- this.emojiPickerView.destroy();
1636
- }
1637
- }
1638
- /**
1639
- * Represents an active skin tone. Its value depends on the emoji UI plugin.
1640
- *
1641
- * Before opening the UI for the first time, the returned value is read from the editor configuration.
1642
- * Otherwise, it reflects the user's intention.
1643
- */ get skinTone() {
1644
- if (!this.emojiPickerView) {
1645
- return this.editor.config.get('emoji.skinTone');
1646
- }
1647
- return this.emojiPickerView.gridView.skinTone;
1648
- }
1649
- /**
1650
- * Displays the balloon with the emoji picker.
1651
- *
1652
- * @param [searchValue=''] A default query used to filer the grid when opening the UI.
1653
- */ showUI(searchValue = '') {
1654
- // Show visual selection on a text when the contextual balloon is displayed.
1655
- // See https://github.com/ckeditor/ckeditor5/issues/17654.
1656
- this._showFakeVisualSelection();
1657
- if (!this.emojiPickerView) {
1658
- this.emojiPickerView = this._createEmojiPickerView();
1659
- }
1660
- if (searchValue) {
1661
- this.emojiPickerView.searchView.setInputValue(searchValue);
1662
- }
1663
- this.emojiPickerView.searchView.search(searchValue);
1664
- if (!this.emojiPickerFormView) {
1665
- this.emojiPickerFormView = this._createEmojiPickerFormView();
1666
- }
1667
- if (!this.balloonPlugin.hasView(this.emojiPickerFormView)) {
1668
- // Show back button if there is another balloon view visible.
1669
- this.emojiPickerFormView.backButtonView.isVisible = !!this.balloonPlugin.visibleView;
1670
- this.balloonPlugin.add({
1671
- view: this.emojiPickerFormView,
1672
- position: this._getBalloonPositionData(),
1673
- balloonClassName: 'ck-emoji-picker-balloon'
1674
- });
1675
- }
1676
- this.emojiPickerView.focus();
1677
- }
1678
- /**
1679
- * Creates a button for toolbar and menu bar that will show the emoji dialog.
1680
- */ _createButton(ViewClass, command) {
1681
- const buttonView = new ViewClass(this.editor.locale);
1682
- const t = this.editor.locale.t;
1683
- buttonView.bind('isEnabled').to(command, 'isEnabled');
1684
- buttonView.set({
1685
- label: t('Emoji'),
1686
- icon: IconEmoji,
1687
- isToggleable: true
1688
- });
1689
- buttonView.on('execute', ()=>{
1690
- this.editor.editing.view.scrollToTheSelection();
1691
- this.showUI();
1692
- });
1693
- return buttonView;
1694
- }
1695
- /**
1696
- * Creates an instance of the `EmojiPickerView` class that represents an emoji balloon.
1697
- */ _createEmojiPickerView() {
1698
- const emojiPickerView = new EmojiPickerView(this.editor.locale, {
1699
- emojiCategories: this.emojiRepositoryPlugin.getEmojiCategories(),
1700
- skinTone: this.editor.config.get('emoji.skinTone'),
1701
- skinTones: this.emojiRepositoryPlugin.getSkinTones(),
1702
- getEmojiByQuery: (query)=>{
1703
- return this.emojiRepositoryPlugin.getEmojiByQuery(query);
1704
- }
1705
- });
1706
- // Insert an emoji on a tile click.
1707
- this.listenTo(emojiPickerView.gridView, 'execute', (evt, data)=>{
1708
- const editor = this.editor;
1709
- const textToInsert = data.emoji;
1710
- this._hideUI();
1711
- editor.execute('insertText', {
1712
- text: textToInsert
1713
- });
1714
- });
1715
- return emojiPickerView;
1716
- }
1717
- /**
1718
- * Creates an instance of the `EmojiPickerFormView` class that represents a balloon with the emoji picker.
1719
- */ _createEmojiPickerFormView() {
1720
- const emojiPickerFormView = new EmojiPickerFormView(this.editor.locale);
1721
- emojiPickerFormView.children.add(this.emojiPickerView);
1722
- // Update the balloon position when layout is changed.
1723
- this.listenTo(this.emojiPickerView, 'update', ()=>{
1724
- if (this.balloonPlugin.visibleView === emojiPickerFormView) {
1725
- this.balloonPlugin.updatePosition();
1726
- }
1727
- });
1728
- // Close the dialog when the back button is clicked.
1729
- this.listenTo(emojiPickerFormView, 'cancel', ()=>{
1730
- this._hideUI();
1731
- });
1732
- // Close the panel on `Esc` key press when the **actions have focus**.
1733
- emojiPickerFormView.keystrokes.set('Esc', (data, cancel)=>{
1734
- this._hideUI();
1735
- cancel();
1736
- });
1737
- // Close the dialog when clicking outside of it.
1738
- clickOutsideHandler({
1739
- emitter: emojiPickerFormView,
1740
- contextElements: [
1741
- this.balloonPlugin.view.element
1742
- ],
1743
- callback: ()=>{
1744
- // Focusing on the editable during a click outside the balloon panel might
1745
- // cause the selection to move to the beginning of the editable, so we avoid
1746
- // focusing on it during this action.
1747
- // See: https://github.com/ckeditor/ckeditor5/issues/18253
1748
- this._hideUI(false);
1749
- },
1750
- activator: ()=>this.balloonPlugin.visibleView === emojiPickerFormView
1751
- });
1752
- return emojiPickerFormView;
1753
- }
1754
- /**
1755
- * Hides the balloon with the emoji picker.
1756
- *
1757
- * @param updateFocus Whether to focus the editor after closing the emoji picker.
1758
- */ _hideUI(updateFocus = true) {
1759
- this.balloonPlugin.remove(this.emojiPickerFormView);
1760
- this.emojiPickerView.searchView.setInputValue('');
1761
- if (updateFocus) {
1762
- this.editor.editing.view.focus();
1763
- }
1764
- this._hideFakeVisualSelection();
1765
- }
1766
- /**
1767
- * Registers converters.
1768
- */ _setupConversion() {
1769
- const editor = this.editor;
1770
- // Renders a fake visual selection marker on an expanded selection.
1771
- editor.conversion.for('editingDowncast').markerToHighlight({
1772
- model: VISUAL_SELECTION_MARKER_NAME,
1773
- view: {
1774
- classes: [
1775
- 'ck-fake-emoji-selection'
1776
- ]
1777
- }
1778
- });
1779
- // Renders a fake visual selection marker on a collapsed selection.
1780
- editor.conversion.for('editingDowncast').markerToElement({
1781
- model: VISUAL_SELECTION_MARKER_NAME,
1782
- view: (data, { writer })=>{
1783
- if (!data.markerRange.isCollapsed) {
1784
- return null;
1785
- }
1786
- const markerElement = writer.createUIElement('span');
1787
- writer.addClass([
1788
- 'ck-fake-emoji-selection',
1789
- 'ck-fake-emoji-selection_collapsed'
1790
- ], markerElement);
1791
- return markerElement;
1792
- }
1793
- });
1794
- }
1795
- /**
1796
- * Returns positioning options for the {@link #balloonPlugin}. They control the way the balloon is attached
1797
- * to the target element or selection.
1798
- */ _getBalloonPositionData() {
1799
- const view = this.editor.editing.view;
1800
- const viewDocument = view.document;
1801
- // Set a target position by converting view selection range to DOM.
1802
- const target = ()=>view.domConverter.viewRangeToDom(viewDocument.selection.getFirstRange());
1803
- return {
1804
- target
1805
- };
1806
- }
1807
- /**
1808
- * Displays a fake visual selection when the contextual balloon is displayed.
1809
- *
1810
- * This adds an 'emoji-picker' marker into the document that is rendered as a highlight on selected text fragment.
1811
- */ _showFakeVisualSelection() {
1812
- const model = this.editor.model;
1813
- model.change((writer)=>{
1814
- const range = model.document.selection.getFirstRange();
1815
- if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
1816
- writer.updateMarker(VISUAL_SELECTION_MARKER_NAME, {
1817
- range
1818
- });
1819
- } else {
1820
- if (range.start.isAtEnd) {
1821
- const startPosition = range.start.getLastMatchingPosition(({ item })=>!model.schema.isContent(item), {
1822
- boundaries: range
1823
- });
1824
- writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
1825
- usingOperation: false,
1826
- affectsData: false,
1827
- range: writer.createRange(startPosition, range.end)
1828
- });
1829
- } else {
1830
- writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
1831
- usingOperation: false,
1832
- affectsData: false,
1833
- range
1834
- });
1835
- }
1836
- }
1837
- });
1838
- }
1839
- /**
1840
- * Hides the fake visual selection.
1841
- */ _hideFakeVisualSelection() {
1842
- const model = this.editor.model;
1843
- if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
1844
- model.change((writer)=>{
1845
- writer.removeMarker(VISUAL_SELECTION_MARKER_NAME);
1846
- });
1847
- }
1848
- }
1849
- }
1675
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1676
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1677
+ */
1678
+ /**
1679
+ * @module emoji/emojipicker
1680
+ */
1681
+ const VISUAL_SELECTION_MARKER_NAME = "emoji-picker";
1682
+ /**
1683
+ * The emoji picker plugin.
1684
+ *
1685
+ * Introduces the `'emoji'` dropdown.
1686
+ */
1687
+ var EmojiPicker = class extends Plugin {
1688
+ /**
1689
+ * @inheritDoc
1690
+ */
1691
+ static get requires() {
1692
+ return [
1693
+ EmojiRepository,
1694
+ ContextualBalloon,
1695
+ Dialog,
1696
+ Typing
1697
+ ];
1698
+ }
1699
+ /**
1700
+ * @inheritDoc
1701
+ */
1702
+ static get pluginName() {
1703
+ return "EmojiPicker";
1704
+ }
1705
+ /**
1706
+ * @inheritDoc
1707
+ */
1708
+ static get isOfficialPlugin() {
1709
+ return true;
1710
+ }
1711
+ /**
1712
+ * @inheritDoc
1713
+ */
1714
+ init() {
1715
+ const { editor } = this;
1716
+ this.balloonPlugin = editor.plugins.get("ContextualBalloon");
1717
+ this.emojiRepositoryPlugin = editor.plugins.get("EmojiRepository");
1718
+ const command = new EmojiCommand(editor);
1719
+ editor.commands.add("emoji", command);
1720
+ editor.ui.componentFactory.add("emoji", () => {
1721
+ const button = this._createButton(ButtonView, command);
1722
+ button.set({ tooltip: true });
1723
+ return button;
1724
+ });
1725
+ editor.ui.componentFactory.add("menuBar:emoji", () => {
1726
+ return this._createButton(MenuBarMenuListItemButtonView, command);
1727
+ });
1728
+ this._setupConversion();
1729
+ }
1730
+ /**
1731
+ * @inheritDoc
1732
+ */
1733
+ destroy() {
1734
+ super.destroy();
1735
+ if (this.emojiPickerView) this.emojiPickerView.destroy();
1736
+ }
1737
+ /**
1738
+ * Represents an active skin tone. Its value depends on the emoji UI plugin.
1739
+ *
1740
+ * Before opening the UI for the first time, the returned value is read from the editor configuration.
1741
+ * Otherwise, it reflects the user's intention.
1742
+ */
1743
+ get skinTone() {
1744
+ if (!this.emojiPickerView) return this.editor.config.get("emoji.skinTone");
1745
+ return this.emojiPickerView.gridView.skinTone;
1746
+ }
1747
+ /**
1748
+ * Displays the balloon with the emoji picker.
1749
+ *
1750
+ * @param [searchValue=''] A default query used to filer the grid when opening the UI.
1751
+ */
1752
+ showUI(searchValue = "") {
1753
+ this._showFakeVisualSelection();
1754
+ if (!this.emojiPickerView) this.emojiPickerView = this._createEmojiPickerView();
1755
+ if (searchValue) this.emojiPickerView.searchView.setInputValue(searchValue);
1756
+ this.emojiPickerView.searchView.search(searchValue);
1757
+ if (!this.emojiPickerFormView) this.emojiPickerFormView = this._createEmojiPickerFormView();
1758
+ if (!this.balloonPlugin.hasView(this.emojiPickerFormView)) {
1759
+ this.emojiPickerFormView.backButtonView.isVisible = !!this.balloonPlugin.visibleView;
1760
+ this.balloonPlugin.add({
1761
+ view: this.emojiPickerFormView,
1762
+ position: this._getBalloonPositionData(),
1763
+ balloonClassName: "ck-emoji-picker-balloon"
1764
+ });
1765
+ }
1766
+ this.emojiPickerView.focus();
1767
+ }
1768
+ /**
1769
+ * Creates a button for toolbar and menu bar that will show the emoji dialog.
1770
+ */
1771
+ _createButton(ViewClass, command) {
1772
+ const buttonView = new ViewClass(this.editor.locale);
1773
+ const t = this.editor.locale.t;
1774
+ buttonView.bind("isEnabled").to(command, "isEnabled");
1775
+ buttonView.set({
1776
+ label: t("Emoji"),
1777
+ icon: IconEmoji,
1778
+ isToggleable: true
1779
+ });
1780
+ buttonView.on("execute", () => {
1781
+ this.editor.editing.view.scrollToTheSelection();
1782
+ this.showUI();
1783
+ });
1784
+ return buttonView;
1785
+ }
1786
+ /**
1787
+ * Creates an instance of the `EmojiPickerView` class that represents an emoji balloon.
1788
+ */
1789
+ _createEmojiPickerView() {
1790
+ const emojiCategories = [...this.emojiRepositoryPlugin.getEmojiCategories()];
1791
+ const emojiPickerView = new EmojiPickerView(this.editor.locale, {
1792
+ emojiCategories,
1793
+ skinTone: this.editor.config.get("emoji.skinTone"),
1794
+ skinTones: this.emojiRepositoryPlugin.getSkinTones(),
1795
+ getEmojiByQuery: (query) => {
1796
+ return this.emojiRepositoryPlugin.getEmojiByQuery(query);
1797
+ }
1798
+ });
1799
+ if (!this.emojiRepositoryPlugin.isRepositoryReady) this.emojiRepositoryPlugin.onReady((isReady) => {
1800
+ if (!isReady) return;
1801
+ const categories = this.emojiRepositoryPlugin.getEmojiCategories();
1802
+ if (categories.length) emojiPickerView.setCategories(categories);
1803
+ });
1804
+ this.listenTo(emojiPickerView.gridView, "execute", (evt, data) => {
1805
+ const editor = this.editor;
1806
+ const textToInsert = data.emoji;
1807
+ this._hideUI();
1808
+ editor.execute("insertText", { text: textToInsert });
1809
+ });
1810
+ return emojiPickerView;
1811
+ }
1812
+ /**
1813
+ * Creates an instance of the `EmojiPickerFormView` class that represents a balloon with the emoji picker.
1814
+ */
1815
+ _createEmojiPickerFormView() {
1816
+ const emojiPickerFormView = new EmojiPickerFormView(this.editor.locale);
1817
+ emojiPickerFormView.children.add(this.emojiPickerView);
1818
+ this.listenTo(this.emojiPickerView, "update", () => {
1819
+ if (this.balloonPlugin.visibleView === emojiPickerFormView) this.balloonPlugin.updatePosition();
1820
+ });
1821
+ this.listenTo(emojiPickerFormView, "cancel", () => {
1822
+ this._hideUI();
1823
+ });
1824
+ emojiPickerFormView.keystrokes.set("Esc", (data, cancel) => {
1825
+ this._hideUI();
1826
+ cancel();
1827
+ });
1828
+ clickOutsideHandler({
1829
+ emitter: emojiPickerFormView,
1830
+ contextElements: [this.balloonPlugin.view.element],
1831
+ callback: () => {
1832
+ this._hideUI(false);
1833
+ },
1834
+ activator: () => this.balloonPlugin.visibleView === emojiPickerFormView
1835
+ });
1836
+ return emojiPickerFormView;
1837
+ }
1838
+ /**
1839
+ * Hides the balloon with the emoji picker.
1840
+ *
1841
+ * @param updateFocus Whether to focus the editor after closing the emoji picker.
1842
+ */
1843
+ _hideUI(updateFocus = true) {
1844
+ this.balloonPlugin.remove(this.emojiPickerFormView);
1845
+ this.emojiPickerView.searchView.setInputValue("");
1846
+ if (updateFocus) this.editor.editing.view.focus();
1847
+ this._hideFakeVisualSelection();
1848
+ }
1849
+ /**
1850
+ * Registers converters.
1851
+ */
1852
+ _setupConversion() {
1853
+ const editor = this.editor;
1854
+ editor.conversion.for("editingDowncast").markerToHighlight({
1855
+ model: VISUAL_SELECTION_MARKER_NAME,
1856
+ view: { classes: ["ck-fake-emoji-selection"] }
1857
+ });
1858
+ editor.conversion.for("editingDowncast").markerToElement({
1859
+ model: VISUAL_SELECTION_MARKER_NAME,
1860
+ view: (data, { writer }) => {
1861
+ if (!data.markerRange.isCollapsed) return null;
1862
+ const markerElement = writer.createUIElement("span");
1863
+ writer.addClass(["ck-fake-emoji-selection", "ck-fake-emoji-selection_collapsed"], markerElement);
1864
+ return markerElement;
1865
+ }
1866
+ });
1867
+ }
1868
+ /**
1869
+ * Returns positioning options for the balloon plugin. They control the way the balloon is attached
1870
+ * to the target element or selection.
1871
+ */
1872
+ _getBalloonPositionData() {
1873
+ const view = this.editor.editing.view;
1874
+ const viewDocument = view.document;
1875
+ const target = () => view.domConverter.viewRangeToDom(viewDocument.selection.getFirstRange());
1876
+ return { target };
1877
+ }
1878
+ /**
1879
+ * Displays a fake visual selection when the contextual balloon is displayed.
1880
+ *
1881
+ * This adds an 'emoji-picker' marker into the document that is rendered as a highlight on selected text fragment.
1882
+ */
1883
+ _showFakeVisualSelection() {
1884
+ const model = this.editor.model;
1885
+ model.change((writer) => {
1886
+ const range = model.document.selection.getFirstRange();
1887
+ if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) writer.updateMarker(VISUAL_SELECTION_MARKER_NAME, { range });
1888
+ else if (range.start.isAtEnd) {
1889
+ const startPosition = range.start.getLastMatchingPosition(({ item }) => !model.schema.isContent(item), { boundaries: range });
1890
+ writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
1891
+ usingOperation: false,
1892
+ affectsData: false,
1893
+ range: writer.createRange(startPosition, range.end)
1894
+ });
1895
+ } else writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
1896
+ usingOperation: false,
1897
+ affectsData: false,
1898
+ range
1899
+ });
1900
+ });
1901
+ }
1902
+ /**
1903
+ * Hides the fake visual selection.
1904
+ */
1905
+ _hideFakeVisualSelection() {
1906
+ const model = this.editor.model;
1907
+ if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) model.change((writer) => {
1908
+ writer.removeMarker(VISUAL_SELECTION_MARKER_NAME);
1909
+ });
1910
+ }
1911
+ };
1850
1912
 
1851
1913
  /**
1852
- * The emoji plugin.
1853
- *
1854
- * This is a "glue" plugin which loads the following plugins:
1855
- *
1856
- * * {@link module:emoji/emojimention~EmojiMention},
1857
- * * {@link module:emoji/emojipicker~EmojiPicker},
1858
- */ class Emoji extends Plugin {
1859
- /**
1860
- * @inheritDoc
1861
- */ static get requires() {
1862
- return [
1863
- EmojiMention,
1864
- EmojiPicker
1865
- ];
1866
- }
1867
- /**
1868
- * @inheritDoc
1869
- */ static get pluginName() {
1870
- return 'Emoji';
1871
- }
1872
- /**
1873
- * @inheritDoc
1874
- */ static get isOfficialPlugin() {
1875
- return true;
1876
- }
1877
- }
1914
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1915
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1916
+ */
1917
+ /**
1918
+ * @module emoji/emoji
1919
+ */
1920
+ /**
1921
+ * The emoji plugin.
1922
+ *
1923
+ * This is a "glue" plugin which loads the following plugins:
1924
+ *
1925
+ * * {@link module:emoji/emojimention~EmojiMention},
1926
+ * * {@link module:emoji/emojipicker~EmojiPicker},
1927
+ */
1928
+ var Emoji = class extends Plugin {
1929
+ /**
1930
+ * @inheritDoc
1931
+ */
1932
+ static get requires() {
1933
+ return [EmojiMention, EmojiPicker];
1934
+ }
1935
+ /**
1936
+ * @inheritDoc
1937
+ */
1938
+ static get pluginName() {
1939
+ return "Emoji";
1940
+ }
1941
+ /**
1942
+ * @inheritDoc
1943
+ */
1944
+ static get isOfficialPlugin() {
1945
+ return true;
1946
+ }
1947
+ };
1948
+
1949
+ /**
1950
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1951
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1952
+ */
1878
1953
 
1879
1954
  export { Emoji, EmojiCategoriesView, EmojiCommand, EmojiGridView, EmojiMention, EmojiPicker, EmojiPickerFormView, EmojiPickerView, EmojiRepository, EmojiSearchView, EmojiToneView, EmojiUtils, isEmojiSupported as _isEmojiSupported };
1880
- //# sourceMappingURL=index.js.map
1955
+ //# sourceMappingURL=index.js.map