@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/augmentation.d.ts +23 -23
- package/dist/emoji.d.ts +27 -27
- package/dist/emojicommand.d.ts +21 -19
- package/dist/emojiconfig.d.ts +103 -103
- package/dist/emojimention.d.ts +68 -72
- package/dist/emojipicker.d.ts +103 -103
- package/dist/emojirepository.d.ts +155 -160
- package/dist/emojiutils.d.ts +70 -70
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +20 -20
- package/dist/index.js +1912 -1837
- package/dist/index.js.map +1 -1
- package/dist/ui/emojicategoriesview.d.ts +67 -63
- package/dist/ui/emojigridview.d.ts +128 -128
- package/dist/ui/emojipickerformview.d.ts +68 -68
- package/dist/ui/emojipickerview.d.ts +87 -83
- package/dist/ui/emojisearchview.d.ts +47 -47
- package/dist/ui/emojitoneview.d.ts +41 -41
- package/dist/utils/emojirepositorycache.d.ts +48 -0
- package/dist/utils/isemojisupported.d.ts +9 -9
- package/package.json +7 -7
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 {
|
|
6
|
-
import {
|
|
7
|
-
import { Typing } from
|
|
8
|
-
import { Mention } from
|
|
9
|
-
import fuzzysort from
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
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
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
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
|