@ckeditor/ckeditor5-emoji 0.0.1 → 44.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/LICENSE.md +15 -5
- package/README.md +30 -3
- package/build/emoji.js +5 -0
- package/build/translations/af.js +1 -0
- package/build/translations/ar.js +1 -0
- package/build/translations/ast.js +1 -0
- package/build/translations/az.js +1 -0
- package/build/translations/bg.js +1 -0
- package/build/translations/bn.js +1 -0
- package/build/translations/bs.js +1 -0
- package/build/translations/ca.js +1 -0
- package/build/translations/cs.js +1 -0
- package/build/translations/da.js +1 -0
- package/build/translations/de-ch.js +1 -0
- package/build/translations/de.js +1 -0
- package/build/translations/el.js +1 -0
- package/build/translations/en-au.js +1 -0
- package/build/translations/en-gb.js +1 -0
- package/build/translations/eo.js +1 -0
- package/build/translations/es-co.js +1 -0
- package/build/translations/es.js +1 -0
- package/build/translations/et.js +1 -0
- package/build/translations/eu.js +1 -0
- package/build/translations/fa.js +1 -0
- package/build/translations/fi.js +1 -0
- package/build/translations/fr.js +1 -0
- package/build/translations/gl.js +1 -0
- package/build/translations/gu.js +1 -0
- package/build/translations/he.js +1 -0
- package/build/translations/hi.js +1 -0
- package/build/translations/hr.js +1 -0
- package/build/translations/hu.js +1 -0
- package/build/translations/hy.js +1 -0
- package/build/translations/id.js +1 -0
- package/build/translations/it.js +1 -0
- package/build/translations/ja.js +1 -0
- package/build/translations/jv.js +1 -0
- package/build/translations/kk.js +1 -0
- package/build/translations/km.js +1 -0
- package/build/translations/kn.js +1 -0
- package/build/translations/ko.js +1 -0
- package/build/translations/ku.js +1 -0
- package/build/translations/lt.js +1 -0
- package/build/translations/lv.js +1 -0
- package/build/translations/ms.js +1 -0
- package/build/translations/nb.js +1 -0
- package/build/translations/ne.js +1 -0
- package/build/translations/nl.js +1 -0
- package/build/translations/no.js +1 -0
- package/build/translations/oc.js +1 -0
- package/build/translations/pl.js +1 -0
- package/build/translations/pt-br.js +1 -0
- package/build/translations/pt.js +1 -0
- package/build/translations/ro.js +1 -0
- package/build/translations/ru.js +1 -0
- package/build/translations/si.js +1 -0
- package/build/translations/sk.js +1 -0
- package/build/translations/sl.js +1 -0
- package/build/translations/sq.js +1 -0
- package/build/translations/sr-latn.js +1 -0
- package/build/translations/sr.js +1 -0
- package/build/translations/sv.js +1 -0
- package/build/translations/th.js +1 -0
- package/build/translations/ti.js +1 -0
- package/build/translations/tk.js +1 -0
- package/build/translations/tr.js +1 -0
- package/build/translations/tt.js +1 -0
- package/build/translations/ug.js +1 -0
- package/build/translations/uk.js +1 -0
- package/build/translations/ur.js +1 -0
- package/build/translations/uz.js +1 -0
- package/build/translations/vi.js +1 -0
- package/build/translations/zh-cn.js +1 -0
- package/build/translations/zh.js +1 -0
- package/ckeditor5-metadata.json +32 -0
- package/dist/index-content.css +4 -0
- package/dist/index-editor.css +111 -0
- package/dist/index.css +143 -0
- package/dist/index.css.map +1 -0
- package/dist/index.js +1477 -0
- package/dist/index.js.map +1 -0
- package/dist/translations/af.d.ts +8 -0
- package/dist/translations/af.js +5 -0
- package/dist/translations/af.umd.js +11 -0
- package/dist/translations/ar.d.ts +8 -0
- package/dist/translations/ar.js +5 -0
- package/dist/translations/ar.umd.js +11 -0
- package/dist/translations/ast.d.ts +8 -0
- package/dist/translations/ast.js +5 -0
- package/dist/translations/ast.umd.js +11 -0
- package/dist/translations/az.d.ts +8 -0
- package/dist/translations/az.js +5 -0
- package/dist/translations/az.umd.js +11 -0
- package/dist/translations/bg.d.ts +8 -0
- package/dist/translations/bg.js +5 -0
- package/dist/translations/bg.umd.js +11 -0
- package/dist/translations/bn.d.ts +8 -0
- package/dist/translations/bn.js +5 -0
- package/dist/translations/bn.umd.js +11 -0
- package/dist/translations/bs.d.ts +8 -0
- package/dist/translations/bs.js +5 -0
- package/dist/translations/bs.umd.js +11 -0
- package/dist/translations/ca.d.ts +8 -0
- package/dist/translations/ca.js +5 -0
- package/dist/translations/ca.umd.js +11 -0
- package/dist/translations/cs.d.ts +8 -0
- package/dist/translations/cs.js +5 -0
- package/dist/translations/cs.umd.js +11 -0
- package/dist/translations/da.d.ts +8 -0
- package/dist/translations/da.js +5 -0
- package/dist/translations/da.umd.js +11 -0
- package/dist/translations/de-ch.d.ts +8 -0
- package/dist/translations/de-ch.js +5 -0
- package/dist/translations/de-ch.umd.js +11 -0
- package/dist/translations/de.d.ts +8 -0
- package/dist/translations/de.js +5 -0
- package/dist/translations/de.umd.js +11 -0
- package/dist/translations/el.d.ts +8 -0
- package/dist/translations/el.js +5 -0
- package/dist/translations/el.umd.js +11 -0
- package/dist/translations/en-au.d.ts +8 -0
- package/dist/translations/en-au.js +5 -0
- package/dist/translations/en-au.umd.js +11 -0
- package/dist/translations/en-gb.d.ts +8 -0
- package/dist/translations/en-gb.js +5 -0
- package/dist/translations/en-gb.umd.js +11 -0
- package/dist/translations/en.d.ts +8 -0
- package/dist/translations/en.js +5 -0
- package/dist/translations/en.umd.js +11 -0
- package/dist/translations/eo.d.ts +8 -0
- package/dist/translations/eo.js +5 -0
- package/dist/translations/eo.umd.js +11 -0
- package/dist/translations/es-co.d.ts +8 -0
- package/dist/translations/es-co.js +5 -0
- package/dist/translations/es-co.umd.js +11 -0
- package/dist/translations/es.d.ts +8 -0
- package/dist/translations/es.js +5 -0
- package/dist/translations/es.umd.js +11 -0
- package/dist/translations/et.d.ts +8 -0
- package/dist/translations/et.js +5 -0
- package/dist/translations/et.umd.js +11 -0
- package/dist/translations/eu.d.ts +8 -0
- package/dist/translations/eu.js +5 -0
- package/dist/translations/eu.umd.js +11 -0
- package/dist/translations/fa.d.ts +8 -0
- package/dist/translations/fa.js +5 -0
- package/dist/translations/fa.umd.js +11 -0
- package/dist/translations/fi.d.ts +8 -0
- package/dist/translations/fi.js +5 -0
- package/dist/translations/fi.umd.js +11 -0
- package/dist/translations/fr.d.ts +8 -0
- package/dist/translations/fr.js +5 -0
- package/dist/translations/fr.umd.js +11 -0
- package/dist/translations/gl.d.ts +8 -0
- package/dist/translations/gl.js +5 -0
- package/dist/translations/gl.umd.js +11 -0
- package/dist/translations/gu.d.ts +8 -0
- package/dist/translations/gu.js +5 -0
- package/dist/translations/gu.umd.js +11 -0
- package/dist/translations/he.d.ts +8 -0
- package/dist/translations/he.js +5 -0
- package/dist/translations/he.umd.js +11 -0
- package/dist/translations/hi.d.ts +8 -0
- package/dist/translations/hi.js +5 -0
- package/dist/translations/hi.umd.js +11 -0
- package/dist/translations/hr.d.ts +8 -0
- package/dist/translations/hr.js +5 -0
- package/dist/translations/hr.umd.js +11 -0
- package/dist/translations/hu.d.ts +8 -0
- package/dist/translations/hu.js +5 -0
- package/dist/translations/hu.umd.js +11 -0
- package/dist/translations/hy.d.ts +8 -0
- package/dist/translations/hy.js +5 -0
- package/dist/translations/hy.umd.js +11 -0
- package/dist/translations/id.d.ts +8 -0
- package/dist/translations/id.js +5 -0
- package/dist/translations/id.umd.js +11 -0
- package/dist/translations/it.d.ts +8 -0
- package/dist/translations/it.js +5 -0
- package/dist/translations/it.umd.js +11 -0
- package/dist/translations/ja.d.ts +8 -0
- package/dist/translations/ja.js +5 -0
- package/dist/translations/ja.umd.js +11 -0
- package/dist/translations/jv.d.ts +8 -0
- package/dist/translations/jv.js +5 -0
- package/dist/translations/jv.umd.js +11 -0
- package/dist/translations/kk.d.ts +8 -0
- package/dist/translations/kk.js +5 -0
- package/dist/translations/kk.umd.js +11 -0
- package/dist/translations/km.d.ts +8 -0
- package/dist/translations/km.js +5 -0
- package/dist/translations/km.umd.js +11 -0
- package/dist/translations/kn.d.ts +8 -0
- package/dist/translations/kn.js +5 -0
- package/dist/translations/kn.umd.js +11 -0
- package/dist/translations/ko.d.ts +8 -0
- package/dist/translations/ko.js +5 -0
- package/dist/translations/ko.umd.js +11 -0
- package/dist/translations/ku.d.ts +8 -0
- package/dist/translations/ku.js +5 -0
- package/dist/translations/ku.umd.js +11 -0
- package/dist/translations/lt.d.ts +8 -0
- package/dist/translations/lt.js +5 -0
- package/dist/translations/lt.umd.js +11 -0
- package/dist/translations/lv.d.ts +8 -0
- package/dist/translations/lv.js +5 -0
- package/dist/translations/lv.umd.js +11 -0
- package/dist/translations/ms.d.ts +8 -0
- package/dist/translations/ms.js +5 -0
- package/dist/translations/ms.umd.js +11 -0
- package/dist/translations/nb.d.ts +8 -0
- package/dist/translations/nb.js +5 -0
- package/dist/translations/nb.umd.js +11 -0
- package/dist/translations/ne.d.ts +8 -0
- package/dist/translations/ne.js +5 -0
- package/dist/translations/ne.umd.js +11 -0
- package/dist/translations/nl.d.ts +8 -0
- package/dist/translations/nl.js +5 -0
- package/dist/translations/nl.umd.js +11 -0
- package/dist/translations/no.d.ts +8 -0
- package/dist/translations/no.js +5 -0
- package/dist/translations/no.umd.js +11 -0
- package/dist/translations/oc.d.ts +8 -0
- package/dist/translations/oc.js +5 -0
- package/dist/translations/oc.umd.js +11 -0
- package/dist/translations/pl.d.ts +8 -0
- package/dist/translations/pl.js +5 -0
- package/dist/translations/pl.umd.js +11 -0
- package/dist/translations/pt-br.d.ts +8 -0
- package/dist/translations/pt-br.js +5 -0
- package/dist/translations/pt-br.umd.js +11 -0
- package/dist/translations/pt.d.ts +8 -0
- package/dist/translations/pt.js +5 -0
- package/dist/translations/pt.umd.js +11 -0
- package/dist/translations/ro.d.ts +8 -0
- package/dist/translations/ro.js +5 -0
- package/dist/translations/ro.umd.js +11 -0
- package/dist/translations/ru.d.ts +8 -0
- package/dist/translations/ru.js +5 -0
- package/dist/translations/ru.umd.js +11 -0
- package/dist/translations/si.d.ts +8 -0
- package/dist/translations/si.js +5 -0
- package/dist/translations/si.umd.js +11 -0
- package/dist/translations/sk.d.ts +8 -0
- package/dist/translations/sk.js +5 -0
- package/dist/translations/sk.umd.js +11 -0
- package/dist/translations/sl.d.ts +8 -0
- package/dist/translations/sl.js +5 -0
- package/dist/translations/sl.umd.js +11 -0
- package/dist/translations/sq.d.ts +8 -0
- package/dist/translations/sq.js +5 -0
- package/dist/translations/sq.umd.js +11 -0
- package/dist/translations/sr-latn.d.ts +8 -0
- package/dist/translations/sr-latn.js +5 -0
- package/dist/translations/sr-latn.umd.js +11 -0
- package/dist/translations/sr.d.ts +8 -0
- package/dist/translations/sr.js +5 -0
- package/dist/translations/sr.umd.js +11 -0
- package/dist/translations/sv.d.ts +8 -0
- package/dist/translations/sv.js +5 -0
- package/dist/translations/sv.umd.js +11 -0
- package/dist/translations/th.d.ts +8 -0
- package/dist/translations/th.js +5 -0
- package/dist/translations/th.umd.js +11 -0
- package/dist/translations/ti.d.ts +8 -0
- package/dist/translations/ti.js +5 -0
- package/dist/translations/ti.umd.js +11 -0
- package/dist/translations/tk.d.ts +8 -0
- package/dist/translations/tk.js +5 -0
- package/dist/translations/tk.umd.js +11 -0
- package/dist/translations/tr.d.ts +8 -0
- package/dist/translations/tr.js +5 -0
- package/dist/translations/tr.umd.js +11 -0
- package/dist/translations/tt.d.ts +8 -0
- package/dist/translations/tt.js +5 -0
- package/dist/translations/tt.umd.js +11 -0
- package/dist/translations/ug.d.ts +8 -0
- package/dist/translations/ug.js +5 -0
- package/dist/translations/ug.umd.js +11 -0
- package/dist/translations/uk.d.ts +8 -0
- package/dist/translations/uk.js +5 -0
- package/dist/translations/uk.umd.js +11 -0
- package/dist/translations/ur.d.ts +8 -0
- package/dist/translations/ur.js +5 -0
- package/dist/translations/ur.umd.js +11 -0
- package/dist/translations/uz.d.ts +8 -0
- package/dist/translations/uz.js +5 -0
- package/dist/translations/uz.umd.js +11 -0
- package/dist/translations/vi.d.ts +8 -0
- package/dist/translations/vi.js +5 -0
- package/dist/translations/vi.umd.js +11 -0
- package/dist/translations/zh-cn.d.ts +8 -0
- package/dist/translations/zh-cn.js +5 -0
- package/dist/translations/zh-cn.umd.js +11 -0
- package/dist/translations/zh.d.ts +8 -0
- package/dist/translations/zh.js +5 -0
- package/dist/translations/zh.umd.js +11 -0
- package/lang/contexts.json +24 -0
- package/lang/translations/af.po +100 -0
- package/lang/translations/ar.po +100 -0
- package/lang/translations/ast.po +100 -0
- package/lang/translations/az.po +100 -0
- package/lang/translations/bg.po +100 -0
- package/lang/translations/bn.po +100 -0
- package/lang/translations/bs.po +100 -0
- package/lang/translations/ca.po +100 -0
- package/lang/translations/cs.po +100 -0
- package/lang/translations/da.po +100 -0
- package/lang/translations/de-ch.po +100 -0
- package/lang/translations/de.po +100 -0
- package/lang/translations/el.po +100 -0
- package/lang/translations/en-au.po +100 -0
- package/lang/translations/en-gb.po +100 -0
- package/lang/translations/en.po +100 -0
- package/lang/translations/eo.po +100 -0
- package/lang/translations/es-co.po +100 -0
- package/lang/translations/es.po +100 -0
- package/lang/translations/et.po +100 -0
- package/lang/translations/eu.po +100 -0
- package/lang/translations/fa.po +100 -0
- package/lang/translations/fi.po +100 -0
- package/lang/translations/fr.po +100 -0
- package/lang/translations/gl.po +100 -0
- package/lang/translations/gu.po +100 -0
- package/lang/translations/he.po +100 -0
- package/lang/translations/hi.po +100 -0
- package/lang/translations/hr.po +100 -0
- package/lang/translations/hu.po +100 -0
- package/lang/translations/hy.po +100 -0
- package/lang/translations/id.po +100 -0
- package/lang/translations/it.po +100 -0
- package/lang/translations/ja.po +100 -0
- package/lang/translations/jv.po +100 -0
- package/lang/translations/kk.po +100 -0
- package/lang/translations/km.po +100 -0
- package/lang/translations/kn.po +100 -0
- package/lang/translations/ko.po +100 -0
- package/lang/translations/ku.po +100 -0
- package/lang/translations/lt.po +100 -0
- package/lang/translations/lv.po +100 -0
- package/lang/translations/ms.po +100 -0
- package/lang/translations/nb.po +100 -0
- package/lang/translations/ne.po +100 -0
- package/lang/translations/nl.po +100 -0
- package/lang/translations/no.po +100 -0
- package/lang/translations/oc.po +100 -0
- package/lang/translations/pl.po +100 -0
- package/lang/translations/pt-br.po +100 -0
- package/lang/translations/pt.po +100 -0
- package/lang/translations/ro.po +100 -0
- package/lang/translations/ru.po +100 -0
- package/lang/translations/si.po +100 -0
- package/lang/translations/sk.po +100 -0
- package/lang/translations/sl.po +100 -0
- package/lang/translations/sq.po +100 -0
- package/lang/translations/sr-latn.po +100 -0
- package/lang/translations/sr.po +100 -0
- package/lang/translations/sv.po +100 -0
- package/lang/translations/th.po +100 -0
- package/lang/translations/ti.po +100 -0
- package/lang/translations/tk.po +100 -0
- package/lang/translations/tr.po +100 -0
- package/lang/translations/tt.po +100 -0
- package/lang/translations/ug.po +100 -0
- package/lang/translations/uk.po +100 -0
- package/lang/translations/ur.po +100 -0
- package/lang/translations/uz.po +100 -0
- package/lang/translations/vi.po +100 -0
- package/lang/translations/zh-cn.po +100 -0
- package/lang/translations/zh.po +100 -0
- package/package.json +58 -5
- package/src/augmentation.d.ts +24 -0
- package/src/augmentation.js +5 -0
- package/src/emoji.d.ts +32 -0
- package/src/emoji.js +38 -0
- package/src/emojicommand.d.ts +24 -0
- package/src/emojicommand.js +33 -0
- package/src/emojiconfig.d.ts +80 -0
- package/src/emojiconfig.js +5 -0
- package/src/emojimention.d.ts +68 -0
- package/src/emojimention.js +193 -0
- package/src/emojipicker.d.ts +97 -0
- package/src/emojipicker.js +255 -0
- package/src/emojirepository.d.ts +139 -0
- package/src/emojirepository.js +267 -0
- package/src/index.d.ts +14 -0
- package/src/index.js +13 -0
- package/src/ui/emojicategoriesview.d.ts +68 -0
- package/src/ui/emojicategoriesview.js +131 -0
- package/src/ui/emojigridview.d.ts +140 -0
- package/src/ui/emojigridview.js +183 -0
- package/src/ui/emojipickerview.d.ts +91 -0
- package/src/ui/emojipickerview.js +172 -0
- package/src/ui/emojisearchview.d.ts +51 -0
- package/src/ui/emojisearchview.js +89 -0
- package/src/ui/emojitoneview.d.ts +46 -0
- package/src/ui/emojitoneview.js +89 -0
- package/theme/emojicategories.css +29 -0
- package/theme/emojigrid.css +55 -0
- package/theme/emojipicker.css +32 -0
- package/theme/emojitone.css +21 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
|
+
*/
|
|
5
|
+
import { Plugin, Command, icons } from '@ckeditor/ckeditor5-core/dist/index.js';
|
|
6
|
+
import { logWarning, FocusTracker, KeystrokeHandler, global, Collection } from '@ckeditor/ckeditor5-utils/dist/index.js';
|
|
7
|
+
import { Typing } from '@ckeditor/ckeditor5-typing/dist/index.js';
|
|
8
|
+
import Fuse from 'fuse.js';
|
|
9
|
+
import { groupBy, escapeRegExp } from 'lodash-es';
|
|
10
|
+
import { View, addKeyboardHandlingForGrid, ButtonView, FocusCycler, SearchTextView, createLabeledInputText, createDropdown, ViewModel, addListToDropdown, SearchInfoView, ContextualBalloon, Dialog, MenuBarMenuListItemButtonView, clickOutsideHandler } from '@ckeditor/ckeditor5-ui/dist/index.js';
|
|
11
|
+
|
|
12
|
+
// An endpoint from which the emoji database will be downloaded during plugin initialization.
|
|
13
|
+
// The `{version}` placeholder is replaced with the value from editor config.
|
|
14
|
+
const EMOJI_DATABASE_URL = 'https://cdn.ckeditor.com/ckeditor5/data/emoji/{version}/en.json';
|
|
15
|
+
const SKIN_TONE_MAP = {
|
|
16
|
+
0: 'default',
|
|
17
|
+
1: 'light',
|
|
18
|
+
2: 'medium-light',
|
|
19
|
+
3: 'medium',
|
|
20
|
+
4: 'medium-dark',
|
|
21
|
+
5: 'dark'
|
|
22
|
+
};
|
|
23
|
+
const BASELINE_EMOJI_WIDTH = 24;
|
|
24
|
+
/**
|
|
25
|
+
* The emoji repository plugin.
|
|
26
|
+
*
|
|
27
|
+
* Loads the emoji database from URL during plugin initialization and provides utility methods to search it.
|
|
28
|
+
*/ class EmojiRepository extends Plugin {
|
|
29
|
+
/**
|
|
30
|
+
* Emoji database.
|
|
31
|
+
*/ _database;
|
|
32
|
+
/**
|
|
33
|
+
* A promise resolved after downloading the emoji database.
|
|
34
|
+
* The promise resolves with `true` when the database is successfully downloaded or `false` otherwise.
|
|
35
|
+
*/ _databasePromise;
|
|
36
|
+
/**
|
|
37
|
+
* An instance of the [Fuse.js](https://www.fusejs.io/) library.
|
|
38
|
+
*/ _fuseSearch;
|
|
39
|
+
/**
|
|
40
|
+
* @inheritDoc
|
|
41
|
+
*/ static get pluginName() {
|
|
42
|
+
return 'EmojiRepository';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* @inheritDoc
|
|
46
|
+
*/ static get isOfficialPlugin() {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* @inheritDoc
|
|
51
|
+
*/ constructor(editor){
|
|
52
|
+
super(editor);
|
|
53
|
+
this.editor.config.define('emoji', {
|
|
54
|
+
version: 16,
|
|
55
|
+
skinTone: 'default'
|
|
56
|
+
});
|
|
57
|
+
this._database = [];
|
|
58
|
+
this._databasePromise = new Promise((resolve)=>{
|
|
59
|
+
this._databasePromiseResolveCallback = resolve;
|
|
60
|
+
});
|
|
61
|
+
this._fuseSearch = null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* @inheritDoc
|
|
65
|
+
*/ async init() {
|
|
66
|
+
const emojiVersion = this.editor.config.get('emoji.version');
|
|
67
|
+
const emojiDatabaseUrl = EMOJI_DATABASE_URL.replace('{version}', `${emojiVersion}`);
|
|
68
|
+
const emojiDatabase = await loadEmojiDatabase(emojiDatabaseUrl);
|
|
69
|
+
// Skip the initialization if the emoji database download has failed.
|
|
70
|
+
// An empty database prevents the initialization of other dependent plugins, such as `EmojiMention` and `EmojiPicker`.
|
|
71
|
+
if (!emojiDatabase.length) {
|
|
72
|
+
return this._databasePromiseResolveCallback(false);
|
|
73
|
+
}
|
|
74
|
+
const container = createEmojiWidthTestingContainer();
|
|
75
|
+
// Store the emoji database after normalizing the raw data.
|
|
76
|
+
this._database = emojiDatabase.filter((item)=>isEmojiCategoryAllowed(item)).filter((item)=>EmojiRepository._isEmojiSupported(item, container)).map((item)=>normalizeEmojiSkinTone(item));
|
|
77
|
+
container.remove();
|
|
78
|
+
// Create instance of the Fuse.js library with configured weighted search keys and disabled fuzzy search.
|
|
79
|
+
this._fuseSearch = new Fuse(this._database, {
|
|
80
|
+
keys: [
|
|
81
|
+
{
|
|
82
|
+
name: 'emoticon',
|
|
83
|
+
weight: 5
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'annotation',
|
|
87
|
+
weight: 3
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'tags',
|
|
91
|
+
weight: 1
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
minMatchCharLength: 2,
|
|
95
|
+
threshold: 0,
|
|
96
|
+
ignoreLocation: true
|
|
97
|
+
});
|
|
98
|
+
return this._databasePromiseResolveCallback(true);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Returns an array of emoji entries that match the search query.
|
|
102
|
+
* If the emoji database is not loaded, the [Fuse.js](https://www.fusejs.io/) instance is not created,
|
|
103
|
+
* hence this method returns an empty array.
|
|
104
|
+
*
|
|
105
|
+
* @param searchQuery A search query to match emoji.
|
|
106
|
+
* @returns An array of emoji entries that match the search query.
|
|
107
|
+
*/ getEmojiByQuery(searchQuery) {
|
|
108
|
+
if (!this._fuseSearch) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const searchQueryTokens = searchQuery.split(/\s/).filter(Boolean);
|
|
112
|
+
// Perform the search only if there is at least two non-white characters next to each other.
|
|
113
|
+
const shouldSearch = searchQueryTokens.some((token)=>token.length >= 2);
|
|
114
|
+
if (!shouldSearch) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
return this._fuseSearch.search({
|
|
118
|
+
'$or': [
|
|
119
|
+
{
|
|
120
|
+
emoticon: searchQuery
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
'$and': searchQueryTokens.map((token)=>({
|
|
124
|
+
annotation: token
|
|
125
|
+
}))
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
'$and': searchQueryTokens.map((token)=>({
|
|
129
|
+
tags: token
|
|
130
|
+
}))
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}).map((result)=>result.item);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Groups all emojis by categories.
|
|
137
|
+
* If the emoji database is not loaded, it returns an empty array.
|
|
138
|
+
*
|
|
139
|
+
* @returns An array of emoji entries grouped by categories.
|
|
140
|
+
*/ getEmojiCategories() {
|
|
141
|
+
if (!this._database.length) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const { t } = this.editor.locale;
|
|
145
|
+
const categories = [
|
|
146
|
+
{
|
|
147
|
+
title: t('Smileys & Expressions'),
|
|
148
|
+
icon: '😀',
|
|
149
|
+
groupId: 0
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
title: t('Gestures & People'),
|
|
153
|
+
icon: '👋',
|
|
154
|
+
groupId: 1
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
title: t('Animals & Nature'),
|
|
158
|
+
icon: '🐻',
|
|
159
|
+
groupId: 3
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
title: t('Food & Drinks'),
|
|
163
|
+
icon: '🍎',
|
|
164
|
+
groupId: 4
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
title: t('Travel & Places'),
|
|
168
|
+
icon: '🚘',
|
|
169
|
+
groupId: 5
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
title: t('Activities'),
|
|
173
|
+
icon: '🏀',
|
|
174
|
+
groupId: 6
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
title: t('Objects'),
|
|
178
|
+
icon: '💡',
|
|
179
|
+
groupId: 7
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
title: t('Symbols'),
|
|
183
|
+
icon: '🟢',
|
|
184
|
+
groupId: 8
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
title: t('Flags'),
|
|
188
|
+
icon: '🏁',
|
|
189
|
+
groupId: 9
|
|
190
|
+
}
|
|
191
|
+
];
|
|
192
|
+
const groups = groupBy(this._database, 'group');
|
|
193
|
+
return categories.map((category)=>{
|
|
194
|
+
return {
|
|
195
|
+
...category,
|
|
196
|
+
items: groups[category.groupId]
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Returns an array of available skin tones.
|
|
202
|
+
*/ getSkinTones() {
|
|
203
|
+
const { t } = this.editor.locale;
|
|
204
|
+
return [
|
|
205
|
+
{
|
|
206
|
+
id: 'default',
|
|
207
|
+
icon: '👋',
|
|
208
|
+
tooltip: t('Default skin tone')
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: 'light',
|
|
212
|
+
icon: '👋🏻',
|
|
213
|
+
tooltip: t('Light skin tone')
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'medium-light',
|
|
217
|
+
icon: '👋🏼',
|
|
218
|
+
tooltip: t('Medium Light skin tone')
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'medium',
|
|
222
|
+
icon: '👋🏽',
|
|
223
|
+
tooltip: t('Medium skin tone')
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: 'medium-dark',
|
|
227
|
+
icon: '👋🏾',
|
|
228
|
+
tooltip: t('Medium Dark skin tone')
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'dark',
|
|
232
|
+
icon: '👋🏿',
|
|
233
|
+
tooltip: t('Dark skin tone')
|
|
234
|
+
}
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Indicates whether the emoji database has been successfully downloaded and the plugin is operational.
|
|
239
|
+
*/ isReady() {
|
|
240
|
+
return this._databasePromise;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* A function used to check if the given emoji is supported in the operating system.
|
|
244
|
+
*
|
|
245
|
+
* Referenced for unit testing purposes.
|
|
246
|
+
*/ static _isEmojiSupported = isEmojiSupported;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Makes the HTTP request to download the emoji database.
|
|
250
|
+
*/ async function loadEmojiDatabase(emojiDatabaseUrl) {
|
|
251
|
+
const result = await fetch(emojiDatabaseUrl).then((response)=>{
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
return response.json();
|
|
256
|
+
}).catch(()=>{
|
|
257
|
+
return [];
|
|
258
|
+
});
|
|
259
|
+
if (!result.length) {
|
|
260
|
+
/**
|
|
261
|
+
* Unable to load the emoji database from CDN.
|
|
262
|
+
*
|
|
263
|
+
* TODO: It could be a problem of CKEditor 5 CDN, but also, Content Security Policy that disallow the request.
|
|
264
|
+
* It would be good to explain what to do in such a case.
|
|
265
|
+
*
|
|
266
|
+
* @error emoji-database-load-failed
|
|
267
|
+
*/ logWarning('emoji-database-load-failed');
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Creates a div for emoji width testing purposes.
|
|
273
|
+
*/ function createEmojiWidthTestingContainer() {
|
|
274
|
+
const container = document.createElement('div');
|
|
275
|
+
container.setAttribute('aria-hidden', 'true');
|
|
276
|
+
container.style.position = 'absolute';
|
|
277
|
+
container.style.left = '-9999px';
|
|
278
|
+
container.style.whiteSpace = 'nowrap';
|
|
279
|
+
container.style.fontSize = BASELINE_EMOJI_WIDTH + 'px';
|
|
280
|
+
document.body.appendChild(container);
|
|
281
|
+
return container;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Returns the width of the provided node.
|
|
285
|
+
*/ function getNodeWidth(container, node) {
|
|
286
|
+
const span = document.createElement('span');
|
|
287
|
+
span.textContent = node;
|
|
288
|
+
container.appendChild(span);
|
|
289
|
+
const nodeWidth = span.offsetWidth;
|
|
290
|
+
container.removeChild(span);
|
|
291
|
+
return nodeWidth;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Checks whether the emoji is supported in the operating system.
|
|
295
|
+
*/ function isEmojiSupported(item, container) {
|
|
296
|
+
const emojiWidth = getNodeWidth(container, item.emoji);
|
|
297
|
+
// On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard
|
|
298
|
+
// against are the ones that are 2x the size, because those are truly broken (person with red hair = person with
|
|
299
|
+
// floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.)
|
|
300
|
+
// So here we set the threshold at 1.8 times the size of the baseline emoji.
|
|
301
|
+
return emojiWidth / 1.8 < BASELINE_EMOJI_WIDTH && emojiWidth >= BASELINE_EMOJI_WIDTH;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Adds default skin tone property to each emoji. If emoji defines other skin tones, they are added as well.
|
|
305
|
+
*/ function normalizeEmojiSkinTone(item) {
|
|
306
|
+
const entry = {
|
|
307
|
+
...item,
|
|
308
|
+
skins: {
|
|
309
|
+
default: item.emoji
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
if (item.skins) {
|
|
313
|
+
item.skins.forEach((skin)=>{
|
|
314
|
+
const skinTone = SKIN_TONE_MAP[skin.tone];
|
|
315
|
+
entry.skins[skinTone] = skin.emoji;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return entry;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Checks whether the emoji belongs to a group that is allowed.
|
|
322
|
+
*/ function isEmojiCategoryAllowed(item) {
|
|
323
|
+
// Category group=2 contains skin tones only, which we do not want to render.
|
|
324
|
+
return item.group !== 2;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const EMOJI_MENTION_MARKER = ':';
|
|
328
|
+
const EMOJI_SHOW_ALL_OPTION_ID = ':__EMOJI_SHOW_ALL:';
|
|
329
|
+
const EMOJI_HINT_OPTION_ID = ':__EMOJI_HINT:';
|
|
330
|
+
/**
|
|
331
|
+
* The emoji mention plugin.
|
|
332
|
+
*
|
|
333
|
+
* Introduces the autocomplete of emojis while typing.
|
|
334
|
+
*/ class EmojiMention extends Plugin {
|
|
335
|
+
/**
|
|
336
|
+
* Defines a number of displayed items in the auto complete dropdown.
|
|
337
|
+
*
|
|
338
|
+
* It includes the "Show all emoji..." option if the `EmojiPicker` plugin is loaded.
|
|
339
|
+
*/ _emojiDropdownLimit;
|
|
340
|
+
/**
|
|
341
|
+
* Defines a skin tone that is set in the emoji config.
|
|
342
|
+
*/ _skinTone;
|
|
343
|
+
/**
|
|
344
|
+
* @inheritDoc
|
|
345
|
+
*/ static get requires() {
|
|
346
|
+
return [
|
|
347
|
+
EmojiRepository,
|
|
348
|
+
Typing,
|
|
349
|
+
'Mention'
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* @inheritDoc
|
|
354
|
+
*/ static get pluginName() {
|
|
355
|
+
return 'EmojiMention';
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* @inheritDoc
|
|
359
|
+
*/ static get isOfficialPlugin() {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* @inheritDoc
|
|
364
|
+
*/ constructor(editor){
|
|
365
|
+
super(editor);
|
|
366
|
+
this.editor.config.define('emoji', {
|
|
367
|
+
dropdownLimit: 6
|
|
368
|
+
});
|
|
369
|
+
this._emojiDropdownLimit = editor.config.get('emoji.dropdownLimit');
|
|
370
|
+
this._skinTone = editor.config.get('emoji.skinTone');
|
|
371
|
+
const mentionFeedsConfigs = editor.config.get('mention.feeds');
|
|
372
|
+
const mergeFieldsPrefix = editor.config.get('mergeFields.prefix');
|
|
373
|
+
const markerAlreadyUsed = mentionFeedsConfigs.some((config)=>config.marker === EMOJI_MENTION_MARKER);
|
|
374
|
+
const isMarkerUsedByMergeFields = mergeFieldsPrefix ? mergeFieldsPrefix[0] === EMOJI_MENTION_MARKER : false;
|
|
375
|
+
if (markerAlreadyUsed || isMarkerUsedByMergeFields) {
|
|
376
|
+
/**
|
|
377
|
+
* The `marker` in the `emoji` config is already used by other plugin configuration.
|
|
378
|
+
*
|
|
379
|
+
* @error emoji-config-marker-already-used
|
|
380
|
+
* @param {string} marker Used marker.
|
|
381
|
+
*/ logWarning('emoji-config-marker-already-used', {
|
|
382
|
+
marker: EMOJI_MENTION_MARKER
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
this._setupMentionConfiguration(mentionFeedsConfigs);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* @inheritDoc
|
|
390
|
+
*/ async init() {
|
|
391
|
+
const editor = this.editor;
|
|
392
|
+
this._emojiPickerPlugin = editor.plugins.has('EmojiPicker') ? editor.plugins.get('EmojiPicker') : null;
|
|
393
|
+
this._emojiRepositoryPlugin = editor.plugins.get('EmojiRepository');
|
|
394
|
+
// Skip overriding the `mention` command listener if the emoji repository is not ready.
|
|
395
|
+
if (!await this._emojiRepositoryPlugin.isReady()) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
editor.once('ready', this._overrideMentionExecuteListener.bind(this));
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Initializes the configuration for emojis in the mention feature.
|
|
402
|
+
*/ _setupMentionConfiguration(mentionFeedsConfigs) {
|
|
403
|
+
const emojiMentionFeedConfig = {
|
|
404
|
+
marker: EMOJI_MENTION_MARKER,
|
|
405
|
+
dropdownLimit: this._emojiDropdownLimit,
|
|
406
|
+
itemRenderer: this._customItemRendererFactory(this.editor.t),
|
|
407
|
+
feed: this._queryEmojiCallbackFactory()
|
|
408
|
+
};
|
|
409
|
+
this.editor.config.set('mention.feeds', [
|
|
410
|
+
...mentionFeedsConfigs,
|
|
411
|
+
emojiMentionFeedConfig
|
|
412
|
+
]);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Returns the `itemRenderer()` callback for mention config.
|
|
416
|
+
*/ _customItemRendererFactory(t) {
|
|
417
|
+
return (item)=>{
|
|
418
|
+
const itemElement = document.createElement('button');
|
|
419
|
+
itemElement.classList.add('ck');
|
|
420
|
+
itemElement.classList.add('ck-button');
|
|
421
|
+
itemElement.classList.add('ck-button_with-text');
|
|
422
|
+
itemElement.id = `mention-list-item-id${item.id.slice(0, -1)}`;
|
|
423
|
+
itemElement.type = 'button';
|
|
424
|
+
itemElement.tabIndex = -1;
|
|
425
|
+
const labelElement = document.createElement('span');
|
|
426
|
+
labelElement.classList.add('ck');
|
|
427
|
+
labelElement.classList.add('ck-button__label');
|
|
428
|
+
itemElement.appendChild(labelElement);
|
|
429
|
+
if (item.id === EMOJI_HINT_OPTION_ID) {
|
|
430
|
+
itemElement.classList.add('ck-list-item-button');
|
|
431
|
+
itemElement.classList.add('ck-disabled');
|
|
432
|
+
labelElement.textContent = t('Keep on typing to see the emoji.');
|
|
433
|
+
} else if (item.id === EMOJI_SHOW_ALL_OPTION_ID) {
|
|
434
|
+
labelElement.textContent = t('Show all emoji...');
|
|
435
|
+
} else {
|
|
436
|
+
labelElement.textContent = `${item.text} ${item.id}`;
|
|
437
|
+
}
|
|
438
|
+
return itemElement;
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Overrides the default mention execute listener to insert an emoji as plain text instead.
|
|
443
|
+
*/ _overrideMentionExecuteListener() {
|
|
444
|
+
const editor = this.editor;
|
|
445
|
+
editor.commands.get('mention').on('execute', (event, data)=>{
|
|
446
|
+
const eventData = data[0];
|
|
447
|
+
// Ignore non-emoji auto-complete actions.
|
|
448
|
+
if (eventData.marker !== EMOJI_MENTION_MARKER) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
// Do not propagate the event.
|
|
452
|
+
event.stop();
|
|
453
|
+
// Do nothing when executing after selecting a hint message.
|
|
454
|
+
if (eventData.mention.id === EMOJI_HINT_OPTION_ID) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// Trigger the picker UI.
|
|
458
|
+
if (eventData.mention.id === EMOJI_SHOW_ALL_OPTION_ID) {
|
|
459
|
+
const text = [
|
|
460
|
+
...eventData.range.getItems()
|
|
461
|
+
].filter((item)=>item.is('$textProxy')).map((item)=>item.data).reduce((result, text)=>result + text, '');
|
|
462
|
+
editor.model.change((writer)=>{
|
|
463
|
+
editor.model.deleteContent(writer.createSelection(eventData.range));
|
|
464
|
+
});
|
|
465
|
+
const emojiPickerPlugin = this._emojiPickerPlugin;
|
|
466
|
+
emojiPickerPlugin.showUI(text.slice(1));
|
|
467
|
+
setTimeout(()=>{
|
|
468
|
+
emojiPickerPlugin.emojiPickerView.focus();
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
editor.execute('insertText', {
|
|
472
|
+
text: eventData.mention.text,
|
|
473
|
+
range: eventData.range
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}, {
|
|
477
|
+
priority: 'high'
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Returns the `feed()` callback for mention config.
|
|
482
|
+
*/ _queryEmojiCallbackFactory() {
|
|
483
|
+
return (searchQuery)=>{
|
|
484
|
+
// Do not show anything when a query starts with a space.
|
|
485
|
+
if (searchQuery.startsWith(' ')) {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
const emojis = this._emojiRepositoryPlugin.getEmojiByQuery(searchQuery).map((emoji)=>{
|
|
489
|
+
let text = emoji.skins[this._skinTone] || emoji.skins.default;
|
|
490
|
+
if (this._emojiPickerPlugin) {
|
|
491
|
+
text = emoji.skins[this._emojiPickerPlugin.skinTone] || emoji.skins.default;
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
id: `:${emoji.annotation}:`,
|
|
495
|
+
text
|
|
496
|
+
};
|
|
497
|
+
});
|
|
498
|
+
if (!this._emojiPickerPlugin) {
|
|
499
|
+
return emojis.slice(0, this._emojiDropdownLimit);
|
|
500
|
+
}
|
|
501
|
+
const actionItem = {
|
|
502
|
+
id: searchQuery.length > 1 ? EMOJI_SHOW_ALL_OPTION_ID : EMOJI_HINT_OPTION_ID
|
|
503
|
+
};
|
|
504
|
+
return [
|
|
505
|
+
...emojis.slice(0, this._emojiDropdownLimit - 1),
|
|
506
|
+
actionItem
|
|
507
|
+
];
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Command that shows the emoji user interface.
|
|
514
|
+
*/ class EmojiCommand extends Command {
|
|
515
|
+
/**
|
|
516
|
+
* Updates the command's {@link #isEnabled} based on the current selection.
|
|
517
|
+
*/ refresh() {
|
|
518
|
+
const editor = this.editor;
|
|
519
|
+
const model = editor.model;
|
|
520
|
+
const schema = model.schema;
|
|
521
|
+
const selection = model.document.selection;
|
|
522
|
+
this.isEnabled = schema.checkChild(selection.getFirstPosition(), '$text');
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Opens emoji user interface for the current document selection.
|
|
526
|
+
*
|
|
527
|
+
* @fires execute
|
|
528
|
+
* @param [searchValue=''] A default query used to filer the grid when opening the UI.
|
|
529
|
+
*/ execute(searchValue = '') {
|
|
530
|
+
const emojiPickerPlugin = this.editor.plugins.get('EmojiPicker');
|
|
531
|
+
emojiPickerPlugin.showUI(searchValue);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* A grid of emoji tiles. It allows browsing emojis and selecting them to be inserted into the content.
|
|
537
|
+
*/ class EmojiGridView extends View {
|
|
538
|
+
/**
|
|
539
|
+
* A collection of the child tile views. Each tile represents a particular emoji.
|
|
540
|
+
*/ tiles;
|
|
541
|
+
/**
|
|
542
|
+
* Tracks information about the DOM focus in the grid.
|
|
543
|
+
*/ focusTracker;
|
|
544
|
+
/**
|
|
545
|
+
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
|
|
546
|
+
*/ keystrokes;
|
|
547
|
+
/**
|
|
548
|
+
* An array containing all emojis grouped by their categories.
|
|
549
|
+
*/ emojiCategories;
|
|
550
|
+
/**
|
|
551
|
+
* A collection of all already created tile views. Each tile represents a particular emoji.
|
|
552
|
+
* The cached tiles collection is used for efficiency purposes to avoid re-creating a particular
|
|
553
|
+
* tile again when the grid view has changed.
|
|
554
|
+
*/ cachedTiles;
|
|
555
|
+
/**
|
|
556
|
+
* A callback used to filter grid items by a specified query.
|
|
557
|
+
*/ _getEmojiByQuery;
|
|
558
|
+
/**
|
|
559
|
+
* @inheritDoc
|
|
560
|
+
*/ constructor(locale, { categoryName, emojiCategories, getEmojiByQuery, skinTone }){
|
|
561
|
+
super(locale);
|
|
562
|
+
this.set('isEmpty', true);
|
|
563
|
+
this.set('categoryName', categoryName);
|
|
564
|
+
this.set('skinTone', skinTone);
|
|
565
|
+
this.tiles = this.createCollection();
|
|
566
|
+
this.cachedTiles = this.createCollection();
|
|
567
|
+
this.focusTracker = new FocusTracker();
|
|
568
|
+
this.keystrokes = new KeystrokeHandler();
|
|
569
|
+
this._getEmojiByQuery = getEmojiByQuery;
|
|
570
|
+
this.emojiCategories = emojiCategories;
|
|
571
|
+
const bind = this.bindTemplate;
|
|
572
|
+
this.setTemplate({
|
|
573
|
+
tag: 'div',
|
|
574
|
+
children: [
|
|
575
|
+
{
|
|
576
|
+
tag: 'div',
|
|
577
|
+
attributes: {
|
|
578
|
+
role: 'grid',
|
|
579
|
+
class: [
|
|
580
|
+
'ck',
|
|
581
|
+
'ck-emoji__grid'
|
|
582
|
+
]
|
|
583
|
+
},
|
|
584
|
+
children: this.tiles
|
|
585
|
+
}
|
|
586
|
+
],
|
|
587
|
+
attributes: {
|
|
588
|
+
role: 'tabpanel',
|
|
589
|
+
class: [
|
|
590
|
+
'ck',
|
|
591
|
+
'ck-emoji__tiles',
|
|
592
|
+
// To avoid issues with focus cycling, ignore a grid when it's empty.
|
|
593
|
+
bind.if('isEmpty', 'ck-hidden', (value)=>value)
|
|
594
|
+
]
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
addKeyboardHandlingForGrid({
|
|
598
|
+
keystrokeHandler: this.keystrokes,
|
|
599
|
+
focusTracker: this.focusTracker,
|
|
600
|
+
gridItems: this.tiles,
|
|
601
|
+
numberOfColumns: ()=>global.window.getComputedStyle(this.element.firstChild) // Responsive `.ck-emoji-grid__tiles`.
|
|
602
|
+
.getPropertyValue('grid-template-columns').split(' ').length,
|
|
603
|
+
uiLanguageDirection: this.locale && this.locale.uiLanguageDirection
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* @inheritDoc
|
|
608
|
+
*/ render() {
|
|
609
|
+
super.render();
|
|
610
|
+
this.keystrokes.listenTo(this.element);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* @inheritDoc
|
|
614
|
+
*/ destroy() {
|
|
615
|
+
super.destroy();
|
|
616
|
+
this.keystrokes.destroy();
|
|
617
|
+
this.focusTracker.destroy();
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Focuses the first focusable in {@link ~EmojiGridView#tiles} if available.
|
|
621
|
+
*/ focus() {
|
|
622
|
+
const firstTile = this.tiles.first;
|
|
623
|
+
if (firstTile) {
|
|
624
|
+
firstTile.focus();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Filters the grid view by the given regular expression.
|
|
629
|
+
*
|
|
630
|
+
* It filters either by the pattern or an emoji category, but never both.
|
|
631
|
+
*
|
|
632
|
+
* @param pattern Expression to search or `null` when filter by category name.
|
|
633
|
+
*/ filter(pattern) {
|
|
634
|
+
const { matchingItems, allItems } = pattern ? this._getItemsByQuery(pattern.source) : this._getItemsByCategory();
|
|
635
|
+
this._updateGrid(matchingItems);
|
|
636
|
+
this.set('isEmpty', matchingItems.length === 0);
|
|
637
|
+
return {
|
|
638
|
+
resultsCount: matchingItems.length,
|
|
639
|
+
totalItemsCount: allItems.length
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Filters emojis to show based on the specified query phrase.
|
|
644
|
+
*
|
|
645
|
+
* @param query A query used to filter the grid.
|
|
646
|
+
*/ _getItemsByQuery(query) {
|
|
647
|
+
return {
|
|
648
|
+
matchingItems: this._getEmojiByQuery(query),
|
|
649
|
+
allItems: this.emojiCategories.flatMap((group)=>group.items)
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Returns emojis that belong to the specified category.
|
|
654
|
+
*/ _getItemsByCategory() {
|
|
655
|
+
const emojiCategory = this.emojiCategories.find((item)=>item.title === this.categoryName);
|
|
656
|
+
const { items } = emojiCategory;
|
|
657
|
+
return {
|
|
658
|
+
matchingItems: items,
|
|
659
|
+
allItems: items
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Updates the grid by removing the existing items and insert the new ones.
|
|
664
|
+
*
|
|
665
|
+
* @param items An array of items to insert.
|
|
666
|
+
*/ _updateGrid(items) {
|
|
667
|
+
// Clean-up.
|
|
668
|
+
[
|
|
669
|
+
...this.tiles
|
|
670
|
+
].forEach((item)=>{
|
|
671
|
+
this.focusTracker.remove(item);
|
|
672
|
+
this.tiles.remove(item);
|
|
673
|
+
});
|
|
674
|
+
items// Create tiles from matching results.
|
|
675
|
+
.map((item)=>{
|
|
676
|
+
const emoji = item.skins[this.skinTone] || item.skins.default;
|
|
677
|
+
return this.cachedTiles.get(emoji) || this._createTile(emoji, item.annotation);
|
|
678
|
+
})// Insert new elements.
|
|
679
|
+
.forEach((item)=>{
|
|
680
|
+
this.tiles.add(item);
|
|
681
|
+
this.focusTracker.add(item);
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Creates a new tile for the grid. Created tile is added to the {@link #cachedTiles} collection for further usage, if needed.
|
|
686
|
+
*
|
|
687
|
+
* @param emoji The emoji itself.
|
|
688
|
+
* @param name The name of the emoji (e.g. "Smiling Face with Smiling Eyes").
|
|
689
|
+
*/ _createTile(emoji, name) {
|
|
690
|
+
const tile = new ButtonView(this.locale);
|
|
691
|
+
tile.viewUid = emoji;
|
|
692
|
+
tile.extendTemplate({
|
|
693
|
+
attributes: {
|
|
694
|
+
class: [
|
|
695
|
+
'ck-emoji__tile'
|
|
696
|
+
]
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
tile.set({
|
|
700
|
+
label: emoji,
|
|
701
|
+
tooltip: name,
|
|
702
|
+
withText: true,
|
|
703
|
+
ariaLabel: name,
|
|
704
|
+
// To improve accessibility, disconnect a button and its label connection so that screen
|
|
705
|
+
// readers can read the `[aria-label]` attribute directly from the more descriptive button.
|
|
706
|
+
ariaLabelledBy: undefined
|
|
707
|
+
});
|
|
708
|
+
tile.on('execute', ()=>{
|
|
709
|
+
this.fire('execute', {
|
|
710
|
+
name,
|
|
711
|
+
emoji
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
this.cachedTiles.add(tile);
|
|
715
|
+
return tile;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* A class representing the navigation part of the emoji UI.
|
|
721
|
+
* It is responsible allowing the user to select a particular emoji category.
|
|
722
|
+
*/ class EmojiCategoriesView extends View {
|
|
723
|
+
/**
|
|
724
|
+
* Tracks information about the DOM focus in the grid.
|
|
725
|
+
*/ focusTracker;
|
|
726
|
+
/**
|
|
727
|
+
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
|
|
728
|
+
*/ keystrokes;
|
|
729
|
+
/**
|
|
730
|
+
* Helps cycling over focusable children in the input view.
|
|
731
|
+
*/ focusCycler;
|
|
732
|
+
/**
|
|
733
|
+
* A collection of the categories buttons.
|
|
734
|
+
*/ buttonViews;
|
|
735
|
+
/**
|
|
736
|
+
* @inheritDoc
|
|
737
|
+
*/ constructor(locale, { emojiCategories, categoryName }){
|
|
738
|
+
super(locale);
|
|
739
|
+
this.buttonViews = this.createCollection(emojiCategories.map((emojiCategory)=>this._createCategoryButton(emojiCategory)));
|
|
740
|
+
this.focusTracker = new FocusTracker();
|
|
741
|
+
this.keystrokes = new KeystrokeHandler();
|
|
742
|
+
this.focusCycler = new FocusCycler({
|
|
743
|
+
focusables: this.buttonViews,
|
|
744
|
+
focusTracker: this.focusTracker,
|
|
745
|
+
keystrokeHandler: this.keystrokes,
|
|
746
|
+
actions: {
|
|
747
|
+
focusPrevious: 'arrowleft',
|
|
748
|
+
focusNext: 'arrowright'
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
this.setTemplate({
|
|
752
|
+
tag: 'div',
|
|
753
|
+
attributes: {
|
|
754
|
+
class: [
|
|
755
|
+
'ck',
|
|
756
|
+
'ck-emoji__categories-list'
|
|
757
|
+
],
|
|
758
|
+
role: 'tablist'
|
|
759
|
+
},
|
|
760
|
+
children: this.buttonViews
|
|
761
|
+
});
|
|
762
|
+
this.on('change:categoryName', (event, name, newValue, oldValue)=>{
|
|
763
|
+
const oldCategoryButton = this.buttonViews.find((button)=>button.tooltip === oldValue);
|
|
764
|
+
if (oldCategoryButton) {
|
|
765
|
+
oldCategoryButton.isOn = false;
|
|
766
|
+
}
|
|
767
|
+
const newCategoryButton = this.buttonViews.find((button)=>button.tooltip === newValue);
|
|
768
|
+
newCategoryButton.isOn = true;
|
|
769
|
+
});
|
|
770
|
+
this.set('categoryName', categoryName);
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* @inheritDoc
|
|
774
|
+
*/ render() {
|
|
775
|
+
super.render();
|
|
776
|
+
this.buttonViews.forEach((buttonView)=>{
|
|
777
|
+
this.focusTracker.add(buttonView);
|
|
778
|
+
});
|
|
779
|
+
this.keystrokes.listenTo(this.element);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* @inheritDoc
|
|
783
|
+
*/ destroy() {
|
|
784
|
+
super.destroy();
|
|
785
|
+
this.focusTracker.destroy();
|
|
786
|
+
this.keystrokes.destroy();
|
|
787
|
+
this.buttonViews.destroy();
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* @inheritDoc
|
|
791
|
+
*/ focus() {
|
|
792
|
+
this.buttonViews.first.focus();
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Marks all categories buttons as enabled (clickable).
|
|
796
|
+
*/ enableCategories() {
|
|
797
|
+
this.buttonViews.forEach((buttonView)=>{
|
|
798
|
+
buttonView.isEnabled = true;
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Marks all categories buttons as disabled (non-clickable).
|
|
803
|
+
*/ disableCategories() {
|
|
804
|
+
this.buttonViews.forEach((buttonView)=>{
|
|
805
|
+
buttonView.set({
|
|
806
|
+
class: '',
|
|
807
|
+
isEnabled: false,
|
|
808
|
+
isOn: false
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Creates a button representing a category item.
|
|
814
|
+
*/ _createCategoryButton(emojiCategory) {
|
|
815
|
+
const buttonView = new ButtonView();
|
|
816
|
+
const bind = buttonView.bindTemplate;
|
|
817
|
+
// A `[role="tab"]` element requires also the `[aria-selected]` attribute with its state.
|
|
818
|
+
buttonView.extendTemplate({
|
|
819
|
+
attributes: {
|
|
820
|
+
'aria-selected': bind.to('isOn', (value)=>value.toString()),
|
|
821
|
+
class: [
|
|
822
|
+
'ck-emoji__category-item'
|
|
823
|
+
]
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
buttonView.set({
|
|
827
|
+
ariaLabel: emojiCategory.title,
|
|
828
|
+
label: emojiCategory.icon,
|
|
829
|
+
role: 'tab',
|
|
830
|
+
tooltip: emojiCategory.title,
|
|
831
|
+
withText: true,
|
|
832
|
+
// To improve accessibility, disconnect a button and its label connection so that screen
|
|
833
|
+
// readers can read the `[aria-label]` attribute directly from the more descriptive button.
|
|
834
|
+
ariaLabelledBy: undefined
|
|
835
|
+
});
|
|
836
|
+
buttonView.on('execute', ()=>{
|
|
837
|
+
this.categoryName = emojiCategory.title;
|
|
838
|
+
});
|
|
839
|
+
buttonView.on('change:isEnabled', ()=>{
|
|
840
|
+
if (buttonView.isEnabled && buttonView.tooltip === this.categoryName) {
|
|
841
|
+
buttonView.isOn = true;
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
return buttonView;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* A view responsible for providing an input element that allows filtering emoji by the provided query.
|
|
850
|
+
*/ class EmojiSearchView extends View {
|
|
851
|
+
/**
|
|
852
|
+
* The find in text input view that stores the searched string.
|
|
853
|
+
*/ inputView;
|
|
854
|
+
/**
|
|
855
|
+
* An instance of the `EmojiGridView`.
|
|
856
|
+
*/ gridView;
|
|
857
|
+
/**
|
|
858
|
+
* @inheritDoc
|
|
859
|
+
*/ constructor(locale, { gridView, resultsView }){
|
|
860
|
+
super(locale);
|
|
861
|
+
this.gridView = gridView;
|
|
862
|
+
const t = locale.t;
|
|
863
|
+
this.inputView = new SearchTextView(this.locale, {
|
|
864
|
+
queryView: {
|
|
865
|
+
label: t('Find an emoji (min. 2 characters)'),
|
|
866
|
+
creator: createLabeledInputText
|
|
867
|
+
},
|
|
868
|
+
filteredView: this.gridView,
|
|
869
|
+
infoView: {
|
|
870
|
+
instance: resultsView
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
this.setTemplate({
|
|
874
|
+
tag: 'div',
|
|
875
|
+
attributes: {
|
|
876
|
+
class: [
|
|
877
|
+
'ck',
|
|
878
|
+
'ck-search'
|
|
879
|
+
],
|
|
880
|
+
tabindex: '-1'
|
|
881
|
+
},
|
|
882
|
+
children: [
|
|
883
|
+
this.inputView.queryView
|
|
884
|
+
]
|
|
885
|
+
});
|
|
886
|
+
// Pass through the `search` event to handle it by a parent view.
|
|
887
|
+
this.inputView.delegate('search').to(this);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* @inheritDoc
|
|
891
|
+
*/ destroy() {
|
|
892
|
+
super.destroy();
|
|
893
|
+
this.inputView.destroy();
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Searches the {@link #gridView} for the given query.
|
|
897
|
+
*
|
|
898
|
+
* @param query The search query string.
|
|
899
|
+
*/ search(query) {
|
|
900
|
+
const regExp = query ? new RegExp(escapeRegExp(query), 'ig') : null;
|
|
901
|
+
const filteringResults = this.gridView.filter(regExp);
|
|
902
|
+
this.inputView.fire('search', {
|
|
903
|
+
query,
|
|
904
|
+
...filteringResults
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Allows defining the default value in the search text field.
|
|
909
|
+
*
|
|
910
|
+
* @param value The new value.
|
|
911
|
+
*/ setInputValue(value) {
|
|
912
|
+
if (!value) {
|
|
913
|
+
this.inputView.queryView.fieldView.reset();
|
|
914
|
+
} else {
|
|
915
|
+
this.inputView.queryView.fieldView.value = value;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Returns an input provided by a user in the search text field.
|
|
920
|
+
*/ getInputValue() {
|
|
921
|
+
return this.inputView.queryView.fieldView.element.value;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* @inheritDoc
|
|
925
|
+
*/ focus() {
|
|
926
|
+
this.inputView.focus();
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* A view responsible for selecting a skin tone for an emoji.
|
|
932
|
+
*/ class EmojiToneView extends View {
|
|
933
|
+
/**
|
|
934
|
+
* A dropdown element for selecting an active skin tone.
|
|
935
|
+
*/ dropdownView;
|
|
936
|
+
/**
|
|
937
|
+
* An array of available skin tones.
|
|
938
|
+
*/ _skinTones;
|
|
939
|
+
/**
|
|
940
|
+
* @inheritDoc
|
|
941
|
+
*/ constructor(locale, { skinTone, skinTones }){
|
|
942
|
+
super(locale);
|
|
943
|
+
this.set('skinTone', skinTone);
|
|
944
|
+
this._skinTones = skinTones;
|
|
945
|
+
const t = locale.t;
|
|
946
|
+
const accessibleLabel = t('Select skin tone');
|
|
947
|
+
const dropdownView = createDropdown(locale);
|
|
948
|
+
const itemDefinitions = new Collection();
|
|
949
|
+
for (const { id, icon, tooltip } of this._skinTones){
|
|
950
|
+
const def = {
|
|
951
|
+
type: 'button',
|
|
952
|
+
model: new ViewModel({
|
|
953
|
+
value: id,
|
|
954
|
+
label: icon,
|
|
955
|
+
ariaLabel: tooltip,
|
|
956
|
+
tooltip,
|
|
957
|
+
tooltipPosition: 'e',
|
|
958
|
+
role: 'menuitemradio',
|
|
959
|
+
withText: true,
|
|
960
|
+
// To improve accessibility, disconnect a button and its label connection so that screen
|
|
961
|
+
// readers can read the `[aria-label]` attribute directly from the more descriptive button.
|
|
962
|
+
ariaLabelledBy: undefined
|
|
963
|
+
})
|
|
964
|
+
};
|
|
965
|
+
def.model.bind('isOn').to(this, 'skinTone', (value)=>value === id);
|
|
966
|
+
itemDefinitions.add(def);
|
|
967
|
+
}
|
|
968
|
+
addListToDropdown(dropdownView, itemDefinitions, {
|
|
969
|
+
ariaLabel: accessibleLabel,
|
|
970
|
+
role: 'menu'
|
|
971
|
+
});
|
|
972
|
+
dropdownView.buttonView.set({
|
|
973
|
+
label: this._getSkinTone().icon,
|
|
974
|
+
ariaLabel: accessibleLabel,
|
|
975
|
+
ariaLabelledBy: undefined,
|
|
976
|
+
isOn: false,
|
|
977
|
+
withText: true,
|
|
978
|
+
tooltip: accessibleLabel
|
|
979
|
+
});
|
|
980
|
+
this.dropdownView = dropdownView;
|
|
981
|
+
// Execute command when an item from the dropdown is selected.
|
|
982
|
+
this.listenTo(dropdownView, 'execute', (evt)=>{
|
|
983
|
+
this.skinTone = evt.source.value;
|
|
984
|
+
});
|
|
985
|
+
dropdownView.buttonView.bind('label').to(this, 'skinTone', ()=>{
|
|
986
|
+
return this._getSkinTone().icon;
|
|
987
|
+
});
|
|
988
|
+
dropdownView.buttonView.bind('ariaLabel').to(this, 'skinTone', ()=>{
|
|
989
|
+
// Render a current state, but also what the dropdown does.
|
|
990
|
+
return `${this._getSkinTone().tooltip}, ${accessibleLabel}`;
|
|
991
|
+
});
|
|
992
|
+
this.setTemplate({
|
|
993
|
+
tag: 'div',
|
|
994
|
+
attributes: {
|
|
995
|
+
class: [
|
|
996
|
+
'ck',
|
|
997
|
+
'ck-emoji__skin-tone'
|
|
998
|
+
]
|
|
999
|
+
},
|
|
1000
|
+
children: [
|
|
1001
|
+
dropdownView
|
|
1002
|
+
]
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* @inheritDoc
|
|
1007
|
+
*/ focus() {
|
|
1008
|
+
this.dropdownView.buttonView.focus();
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Helper method for receiving an object describing the active skin tone.
|
|
1012
|
+
*/ _getSkinTone() {
|
|
1013
|
+
return this._skinTones.find((tone)=>tone.id === this.skinTone);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* A view that glues pieces of the emoji panel together.
|
|
1019
|
+
*/ class EmojiPickerView extends View {
|
|
1020
|
+
/**
|
|
1021
|
+
* A collection of the focusable children of the view.
|
|
1022
|
+
*/ items;
|
|
1023
|
+
/**
|
|
1024
|
+
* Tracks information about the DOM focus in the view.
|
|
1025
|
+
*/ focusTracker;
|
|
1026
|
+
/**
|
|
1027
|
+
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
|
|
1028
|
+
*/ keystrokes;
|
|
1029
|
+
/**
|
|
1030
|
+
* Helps cycling over focusable {@link #items} in the view.
|
|
1031
|
+
*/ focusCycler;
|
|
1032
|
+
/**
|
|
1033
|
+
* An instance of the `EmojiSearchView`.
|
|
1034
|
+
*/ searchView;
|
|
1035
|
+
/**
|
|
1036
|
+
* An instance of the `EmojiToneView`.
|
|
1037
|
+
*/ toneView;
|
|
1038
|
+
/**
|
|
1039
|
+
* An instance of the `EmojiCategoriesView`.
|
|
1040
|
+
*/ categoriesView;
|
|
1041
|
+
/**
|
|
1042
|
+
* An instance of the `EmojiGridView`.
|
|
1043
|
+
*/ gridView;
|
|
1044
|
+
/**
|
|
1045
|
+
* An instance of the `EmojiGridView`.
|
|
1046
|
+
*/ infoView;
|
|
1047
|
+
/**
|
|
1048
|
+
* @inheritDoc
|
|
1049
|
+
*/ constructor(locale, { emojiCategories, getEmojiByQuery, skinTone, skinTones }){
|
|
1050
|
+
super(locale);
|
|
1051
|
+
const categoryName = emojiCategories[0].title;
|
|
1052
|
+
this.gridView = new EmojiGridView(locale, {
|
|
1053
|
+
categoryName,
|
|
1054
|
+
emojiCategories,
|
|
1055
|
+
getEmojiByQuery,
|
|
1056
|
+
skinTone
|
|
1057
|
+
});
|
|
1058
|
+
this.infoView = new SearchInfoView();
|
|
1059
|
+
this.searchView = new EmojiSearchView(locale, {
|
|
1060
|
+
gridView: this.gridView,
|
|
1061
|
+
resultsView: this.infoView
|
|
1062
|
+
});
|
|
1063
|
+
this.categoriesView = new EmojiCategoriesView(locale, {
|
|
1064
|
+
emojiCategories,
|
|
1065
|
+
categoryName
|
|
1066
|
+
});
|
|
1067
|
+
this.toneView = new EmojiToneView(locale, {
|
|
1068
|
+
skinTone,
|
|
1069
|
+
skinTones
|
|
1070
|
+
});
|
|
1071
|
+
this.items = this.createCollection([
|
|
1072
|
+
this.searchView,
|
|
1073
|
+
this.toneView,
|
|
1074
|
+
this.categoriesView,
|
|
1075
|
+
this.gridView,
|
|
1076
|
+
this.infoView
|
|
1077
|
+
]);
|
|
1078
|
+
this.focusTracker = new FocusTracker();
|
|
1079
|
+
this.keystrokes = new KeystrokeHandler();
|
|
1080
|
+
this.focusCycler = new FocusCycler({
|
|
1081
|
+
focusables: this.items,
|
|
1082
|
+
focusTracker: this.focusTracker,
|
|
1083
|
+
keystrokeHandler: this.keystrokes,
|
|
1084
|
+
actions: {
|
|
1085
|
+
focusPrevious: 'shift + tab',
|
|
1086
|
+
focusNext: 'tab'
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
this.setTemplate({
|
|
1090
|
+
tag: 'div',
|
|
1091
|
+
children: [
|
|
1092
|
+
{
|
|
1093
|
+
tag: 'div',
|
|
1094
|
+
children: [
|
|
1095
|
+
this.searchView,
|
|
1096
|
+
this.toneView
|
|
1097
|
+
],
|
|
1098
|
+
attributes: {
|
|
1099
|
+
class: [
|
|
1100
|
+
'ck',
|
|
1101
|
+
'ck-emoji__search'
|
|
1102
|
+
]
|
|
1103
|
+
}
|
|
1104
|
+
},
|
|
1105
|
+
this.categoriesView,
|
|
1106
|
+
this.gridView,
|
|
1107
|
+
{
|
|
1108
|
+
tag: 'div',
|
|
1109
|
+
children: [
|
|
1110
|
+
this.infoView
|
|
1111
|
+
],
|
|
1112
|
+
attributes: {
|
|
1113
|
+
class: [
|
|
1114
|
+
'ck',
|
|
1115
|
+
'ck-search__results'
|
|
1116
|
+
]
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
],
|
|
1120
|
+
attributes: {
|
|
1121
|
+
tabindex: '-1',
|
|
1122
|
+
class: [
|
|
1123
|
+
'ck',
|
|
1124
|
+
'ck-emoji',
|
|
1125
|
+
'ck-search'
|
|
1126
|
+
]
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
this._setupEventListeners();
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* @inheritDoc
|
|
1133
|
+
*/ render() {
|
|
1134
|
+
super.render();
|
|
1135
|
+
this.focusTracker.add(this.searchView.element);
|
|
1136
|
+
this.focusTracker.add(this.toneView.element);
|
|
1137
|
+
this.focusTracker.add(this.categoriesView.element);
|
|
1138
|
+
this.focusTracker.add(this.gridView.element);
|
|
1139
|
+
this.focusTracker.add(this.infoView.element);
|
|
1140
|
+
// Start listening for the keystrokes coming from #element.
|
|
1141
|
+
this.keystrokes.listenTo(this.element);
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* @inheritDoc
|
|
1145
|
+
*/ destroy() {
|
|
1146
|
+
super.destroy();
|
|
1147
|
+
this.focusTracker.destroy();
|
|
1148
|
+
this.keystrokes.destroy();
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Focuses the search input.
|
|
1152
|
+
*/ focus() {
|
|
1153
|
+
this.searchView.focus();
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Initializes interactions between sub-views.
|
|
1157
|
+
*/ _setupEventListeners() {
|
|
1158
|
+
const t = this.locale.t;
|
|
1159
|
+
// Disable the category switcher when filtering by a query.
|
|
1160
|
+
this.searchView.on('search', (evt, data)=>{
|
|
1161
|
+
if (data.query) {
|
|
1162
|
+
this.categoriesView.disableCategories();
|
|
1163
|
+
} else {
|
|
1164
|
+
this.categoriesView.enableCategories();
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
// Show a user-friendly message depending on the search query.
|
|
1168
|
+
this.searchView.on('search', (evt, data)=>{
|
|
1169
|
+
if (data.query.length === 1) {
|
|
1170
|
+
this.infoView.set({
|
|
1171
|
+
primaryText: t('Keep on typing to see the emoji.'),
|
|
1172
|
+
secondaryText: t('The query must contain at least two characters.'),
|
|
1173
|
+
isVisible: true
|
|
1174
|
+
});
|
|
1175
|
+
} else if (!data.resultsCount) {
|
|
1176
|
+
this.infoView.set({
|
|
1177
|
+
primaryText: t('No emojis were found matching "%0".', data.query),
|
|
1178
|
+
secondaryText: t('Please try a different phrase or check the spelling.'),
|
|
1179
|
+
isVisible: true
|
|
1180
|
+
});
|
|
1181
|
+
} else {
|
|
1182
|
+
this.infoView.set({
|
|
1183
|
+
isVisible: false
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
// Emit an update event to react to balloon dimensions changes.
|
|
1188
|
+
this.searchView.on('search', ()=>{
|
|
1189
|
+
this.fire('update');
|
|
1190
|
+
});
|
|
1191
|
+
// Update the grid of emojis when the selected category is changed.
|
|
1192
|
+
this.categoriesView.on('change:categoryName', (ev, args, categoryName)=>{
|
|
1193
|
+
this.gridView.categoryName = categoryName;
|
|
1194
|
+
this.searchView.search('');
|
|
1195
|
+
});
|
|
1196
|
+
// Update the grid of emojis when the selected skin tone is changed.
|
|
1197
|
+
// In such a case, the displayed emoji should use an updated skin tone value.
|
|
1198
|
+
this.toneView.on('change:skinTone', (evt, propertyName, newValue)=>{
|
|
1199
|
+
this.gridView.skinTone = newValue;
|
|
1200
|
+
this.searchView.search(this.searchView.getInputValue());
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const VISUAL_SELECTION_MARKER_NAME = 'emoji-picker';
|
|
1206
|
+
/**
|
|
1207
|
+
* The emoji picker plugin.
|
|
1208
|
+
*
|
|
1209
|
+
* Introduces the `'emoji'` dropdown.
|
|
1210
|
+
*/ class EmojiPicker extends Plugin {
|
|
1211
|
+
/**
|
|
1212
|
+
* @inheritDoc
|
|
1213
|
+
*/ static get requires() {
|
|
1214
|
+
return [
|
|
1215
|
+
EmojiRepository,
|
|
1216
|
+
ContextualBalloon,
|
|
1217
|
+
Dialog,
|
|
1218
|
+
Typing
|
|
1219
|
+
];
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* @inheritDoc
|
|
1223
|
+
*/ static get pluginName() {
|
|
1224
|
+
return 'EmojiPicker';
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* @inheritDoc
|
|
1228
|
+
*/ static get isOfficialPlugin() {
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* @inheritDoc
|
|
1233
|
+
*/ async init() {
|
|
1234
|
+
const editor = this.editor;
|
|
1235
|
+
this._balloonPlugin = editor.plugins.get('ContextualBalloon');
|
|
1236
|
+
this._emojiRepositoryPlugin = editor.plugins.get('EmojiRepository');
|
|
1237
|
+
// Skip registering a button in the toolbar and list item in the menu bar if the emoji repository is not ready.
|
|
1238
|
+
if (!await this._emojiRepositoryPlugin.isReady()) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const command = new EmojiCommand(editor);
|
|
1242
|
+
editor.commands.add('emoji', command);
|
|
1243
|
+
editor.ui.componentFactory.add('emoji', ()=>{
|
|
1244
|
+
const button = this._createButton(ButtonView, command);
|
|
1245
|
+
button.set({
|
|
1246
|
+
tooltip: true
|
|
1247
|
+
});
|
|
1248
|
+
return button;
|
|
1249
|
+
});
|
|
1250
|
+
editor.ui.componentFactory.add('menuBar:emoji', ()=>{
|
|
1251
|
+
return this._createButton(MenuBarMenuListItemButtonView, command);
|
|
1252
|
+
});
|
|
1253
|
+
this._setupConversion();
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* @inheritDoc
|
|
1257
|
+
*/ destroy() {
|
|
1258
|
+
super.destroy();
|
|
1259
|
+
if (this.emojiPickerView) {
|
|
1260
|
+
this.emojiPickerView.destroy();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Represents an active skin tone. Its value depends on the emoji UI plugin.
|
|
1265
|
+
*
|
|
1266
|
+
* Before opening the UI for the first time, the returned value is read from the editor configuration.
|
|
1267
|
+
* Otherwise, it reflects the user's intention.
|
|
1268
|
+
*/ get skinTone() {
|
|
1269
|
+
if (!this.emojiPickerView) {
|
|
1270
|
+
return this.editor.config.get('emoji.skinTone');
|
|
1271
|
+
}
|
|
1272
|
+
return this.emojiPickerView.gridView.skinTone;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Displays the balloon with the emoji picker.
|
|
1276
|
+
*
|
|
1277
|
+
* @param [searchValue=''] A default query used to filer the grid when opening the UI.
|
|
1278
|
+
*/ showUI(searchValue = '') {
|
|
1279
|
+
// Show visual selection on a text when the contextual balloon is displayed.
|
|
1280
|
+
// See #17654.
|
|
1281
|
+
this._showFakeVisualSelection();
|
|
1282
|
+
if (!this.emojiPickerView) {
|
|
1283
|
+
this.emojiPickerView = this._createEmojiPickerView();
|
|
1284
|
+
}
|
|
1285
|
+
if (searchValue) {
|
|
1286
|
+
this.emojiPickerView.searchView.setInputValue(searchValue);
|
|
1287
|
+
}
|
|
1288
|
+
this.emojiPickerView.searchView.search(searchValue);
|
|
1289
|
+
if (!this._balloonPlugin.hasView(this.emojiPickerView)) {
|
|
1290
|
+
this._balloonPlugin.add({
|
|
1291
|
+
view: this.emojiPickerView,
|
|
1292
|
+
position: this._getBalloonPositionData()
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
this.emojiPickerView.focus();
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Creates a button for toolbar and menu bar that will show the emoji dialog.
|
|
1299
|
+
*/ _createButton(ViewClass, command) {
|
|
1300
|
+
const buttonView = new ViewClass(this.editor.locale);
|
|
1301
|
+
const t = this.editor.locale.t;
|
|
1302
|
+
buttonView.bind('isEnabled').to(command, 'isEnabled');
|
|
1303
|
+
buttonView.set({
|
|
1304
|
+
label: t('Emoji'),
|
|
1305
|
+
icon: icons.emoji,
|
|
1306
|
+
isToggleable: true
|
|
1307
|
+
});
|
|
1308
|
+
buttonView.on('execute', ()=>{
|
|
1309
|
+
this.showUI();
|
|
1310
|
+
});
|
|
1311
|
+
return buttonView;
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Creates an instance of the `EmojiPickerView` class that represents an emoji balloon.
|
|
1315
|
+
*/ _createEmojiPickerView() {
|
|
1316
|
+
const emojiPickerView = new EmojiPickerView(this.editor.locale, {
|
|
1317
|
+
emojiCategories: this._emojiRepositoryPlugin.getEmojiCategories(),
|
|
1318
|
+
skinTone: this.editor.config.get('emoji.skinTone'),
|
|
1319
|
+
skinTones: this._emojiRepositoryPlugin.getSkinTones(),
|
|
1320
|
+
getEmojiByQuery: (query)=>{
|
|
1321
|
+
return this._emojiRepositoryPlugin.getEmojiByQuery(query);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
// Insert an emoji on a tile click.
|
|
1325
|
+
this.listenTo(emojiPickerView.gridView, 'execute', (evt, data)=>{
|
|
1326
|
+
const editor = this.editor;
|
|
1327
|
+
const textToInsert = data.emoji;
|
|
1328
|
+
this._hideUI();
|
|
1329
|
+
editor.execute('insertText', {
|
|
1330
|
+
text: textToInsert
|
|
1331
|
+
});
|
|
1332
|
+
});
|
|
1333
|
+
// Update the balloon position when layout is changed.
|
|
1334
|
+
this.listenTo(emojiPickerView, 'update', ()=>{
|
|
1335
|
+
if (this._balloonPlugin.visibleView === emojiPickerView) {
|
|
1336
|
+
this._balloonPlugin.updatePosition();
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
// Close the panel on `Esc` key press when the **actions have focus**.
|
|
1340
|
+
emojiPickerView.keystrokes.set('Esc', (data, cancel)=>{
|
|
1341
|
+
this._hideUI();
|
|
1342
|
+
cancel();
|
|
1343
|
+
});
|
|
1344
|
+
// Close the dialog when clicking outside of it.
|
|
1345
|
+
clickOutsideHandler({
|
|
1346
|
+
emitter: emojiPickerView,
|
|
1347
|
+
contextElements: [
|
|
1348
|
+
this._balloonPlugin.view.element
|
|
1349
|
+
],
|
|
1350
|
+
callback: ()=>this._hideUI(),
|
|
1351
|
+
activator: ()=>this._balloonPlugin.visibleView === emojiPickerView
|
|
1352
|
+
});
|
|
1353
|
+
return emojiPickerView;
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Hides the balloon with the emoji picker.
|
|
1357
|
+
*/ _hideUI() {
|
|
1358
|
+
this._balloonPlugin.remove(this.emojiPickerView);
|
|
1359
|
+
this.emojiPickerView.searchView.setInputValue('');
|
|
1360
|
+
this.editor.editing.view.focus();
|
|
1361
|
+
this._hideFakeVisualSelection();
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Registers converters.
|
|
1365
|
+
*/ _setupConversion() {
|
|
1366
|
+
const editor = this.editor;
|
|
1367
|
+
// Renders a fake visual selection marker on an expanded selection.
|
|
1368
|
+
editor.conversion.for('editingDowncast').markerToHighlight({
|
|
1369
|
+
model: VISUAL_SELECTION_MARKER_NAME,
|
|
1370
|
+
view: {
|
|
1371
|
+
classes: [
|
|
1372
|
+
'ck-fake-emoji-selection'
|
|
1373
|
+
]
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
// Renders a fake visual selection marker on a collapsed selection.
|
|
1377
|
+
editor.conversion.for('editingDowncast').markerToElement({
|
|
1378
|
+
model: VISUAL_SELECTION_MARKER_NAME,
|
|
1379
|
+
view: (data, { writer })=>{
|
|
1380
|
+
if (!data.markerRange.isCollapsed) {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
const markerElement = writer.createUIElement('span');
|
|
1384
|
+
writer.addClass([
|
|
1385
|
+
'ck-fake-emoji-selection',
|
|
1386
|
+
'ck-fake-emoji-selection_collapsed'
|
|
1387
|
+
], markerElement);
|
|
1388
|
+
return markerElement;
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Returns positioning options for the {@link #_balloonPlugin}. They control the way the balloon is attached
|
|
1394
|
+
* to the target element or selection.
|
|
1395
|
+
*/ _getBalloonPositionData() {
|
|
1396
|
+
const view = this.editor.editing.view;
|
|
1397
|
+
const viewDocument = view.document;
|
|
1398
|
+
// Set a target position by converting view selection range to DOM.
|
|
1399
|
+
const target = ()=>view.domConverter.viewRangeToDom(viewDocument.selection.getFirstRange());
|
|
1400
|
+
return {
|
|
1401
|
+
target
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Displays a fake visual selection when the contextual balloon is displayed.
|
|
1406
|
+
*
|
|
1407
|
+
* This adds an 'emoji-picker' marker into the document that is rendered as a highlight on selected text fragment.
|
|
1408
|
+
*/ _showFakeVisualSelection() {
|
|
1409
|
+
const model = this.editor.model;
|
|
1410
|
+
model.change((writer)=>{
|
|
1411
|
+
const range = model.document.selection.getFirstRange();
|
|
1412
|
+
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
1413
|
+
writer.updateMarker(VISUAL_SELECTION_MARKER_NAME, {
|
|
1414
|
+
range
|
|
1415
|
+
});
|
|
1416
|
+
} else {
|
|
1417
|
+
if (range.start.isAtEnd) {
|
|
1418
|
+
const startPosition = range.start.getLastMatchingPosition(({ item })=>!model.schema.isContent(item), {
|
|
1419
|
+
boundaries: range
|
|
1420
|
+
});
|
|
1421
|
+
writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
|
1422
|
+
usingOperation: false,
|
|
1423
|
+
affectsData: false,
|
|
1424
|
+
range: writer.createRange(startPosition, range.end)
|
|
1425
|
+
});
|
|
1426
|
+
} else {
|
|
1427
|
+
writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
|
1428
|
+
usingOperation: false,
|
|
1429
|
+
affectsData: false,
|
|
1430
|
+
range
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Hides the fake visual selection.
|
|
1438
|
+
*/ _hideFakeVisualSelection() {
|
|
1439
|
+
const model = this.editor.model;
|
|
1440
|
+
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
1441
|
+
model.change((writer)=>{
|
|
1442
|
+
writer.removeMarker(VISUAL_SELECTION_MARKER_NAME);
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* The emoji plugin.
|
|
1450
|
+
*
|
|
1451
|
+
* This is a "glue" plugin which loads the following plugins:
|
|
1452
|
+
*
|
|
1453
|
+
* * {@link module:emoji/emojimention~EmojiMention},
|
|
1454
|
+
* * {@link module:emoji/emojipicker~EmojiPicker},
|
|
1455
|
+
*/ class Emoji extends Plugin {
|
|
1456
|
+
/**
|
|
1457
|
+
* @inheritDoc
|
|
1458
|
+
*/ static get requires() {
|
|
1459
|
+
return [
|
|
1460
|
+
EmojiMention,
|
|
1461
|
+
EmojiPicker
|
|
1462
|
+
];
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* @inheritDoc
|
|
1466
|
+
*/ static get pluginName() {
|
|
1467
|
+
return 'Emoji';
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* @inheritDoc
|
|
1471
|
+
*/ static get isOfficialPlugin() {
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
export { Emoji, EmojiCommand, EmojiMention, EmojiPicker, EmojiRepository };
|
|
1477
|
+
//# sourceMappingURL=index.js.map
|