@ohos-ports/emoji-picker-element 1.29.1-beta.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.
Files changed (60) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +1025 -0
  3. package/custom-elements.json +257 -0
  4. package/database.d.ts +108 -0
  5. package/database.js +1004 -0
  6. package/database.js.map +1 -0
  7. package/i18n/ar.d.ts +3 -0
  8. package/i18n/ar.js +34 -0
  9. package/i18n/de.d.ts +3 -0
  10. package/i18n/de.js +34 -0
  11. package/i18n/en.d.ts +3 -0
  12. package/i18n/en.js +34 -0
  13. package/i18n/es.d.ts +3 -0
  14. package/i18n/es.js +34 -0
  15. package/i18n/fa.d.ts +3 -0
  16. package/i18n/fa.js +34 -0
  17. package/i18n/fr.d.ts +3 -0
  18. package/i18n/fr.js +34 -0
  19. package/i18n/hi.d.ts +3 -0
  20. package/i18n/hi.js +34 -0
  21. package/i18n/hu.d.ts +3 -0
  22. package/i18n/hu.js +34 -0
  23. package/i18n/id.d.ts +3 -0
  24. package/i18n/id.js +34 -0
  25. package/i18n/it.d.ts +3 -0
  26. package/i18n/it.js +34 -0
  27. package/i18n/ja.d.ts +3 -0
  28. package/i18n/ja.js +34 -0
  29. package/i18n/ms_MY.d.ts +3 -0
  30. package/i18n/ms_MY.js +34 -0
  31. package/i18n/nl.d.ts +3 -0
  32. package/i18n/nl.js +34 -0
  33. package/i18n/pl.d.ts +3 -0
  34. package/i18n/pl.js +34 -0
  35. package/i18n/pt_BR.d.ts +3 -0
  36. package/i18n/pt_BR.js +34 -0
  37. package/i18n/pt_PT.d.ts +3 -0
  38. package/i18n/pt_PT.js +34 -0
  39. package/i18n/ru_RU.d.ts +3 -0
  40. package/i18n/ru_RU.js +34 -0
  41. package/i18n/tr.d.ts +3 -0
  42. package/i18n/tr.js +34 -0
  43. package/i18n/vi.d.ts +3 -0
  44. package/i18n/vi.js +34 -0
  45. package/i18n/zh_CN.d.ts +3 -0
  46. package/i18n/zh_CN.js +27 -0
  47. package/index.d.ts +7 -0
  48. package/index.js +6 -0
  49. package/package.json +196 -0
  50. package/picker.d.ts +30 -0
  51. package/picker.js +1836 -0
  52. package/picker.js.map +1 -0
  53. package/polyfill.js +62 -0
  54. package/shared.d.ts +99 -0
  55. package/svelte.js +4 -0
  56. package/trimEmojiData.cjs +54 -0
  57. package/trimEmojiData.cjs.map +1 -0
  58. package/trimEmojiData.d.ts +8 -0
  59. package/trimEmojiData.js +52 -0
  60. package/trimEmojiData.js.map +1 -0
package/database.js ADDED
@@ -0,0 +1,1004 @@
1
+ function assertNonEmptyString (str) {
2
+ if (typeof str !== 'string' || !str) {
3
+ throw new Error('expected a non-empty string, got: ' + str)
4
+ }
5
+ }
6
+
7
+ function assertNumber (number) {
8
+ if (typeof number !== 'number') {
9
+ throw new Error('expected a number, got: ' + number)
10
+ }
11
+ }
12
+
13
+ const DB_VERSION_CURRENT = 1;
14
+ const DB_VERSION_INITIAL = 1;
15
+ const STORE_EMOJI = 'emoji';
16
+ const STORE_KEYVALUE = 'keyvalue';
17
+ const STORE_FAVORITES = 'favorites';
18
+ const FIELD_TOKENS = 'tokens';
19
+ const INDEX_TOKENS = 'tokens';
20
+ const FIELD_UNICODE = 'unicode';
21
+ const INDEX_COUNT = 'count';
22
+ const FIELD_GROUP = 'group';
23
+ const FIELD_ORDER = 'order';
24
+ const INDEX_GROUP_AND_ORDER = 'group-order';
25
+ const KEY_ETAG = 'eTag';
26
+ const KEY_URL = 'url';
27
+ const KEY_PREFERRED_SKINTONE = 'skinTone';
28
+ const MODE_READONLY = 'readonly';
29
+ const MODE_READWRITE = 'readwrite';
30
+ const INDEX_SKIN_UNICODE = 'skinUnicodes';
31
+ const FIELD_SKIN_UNICODE = 'skinUnicodes';
32
+
33
+ const DEFAULT_DATA_SOURCE = 'https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json';
34
+ const DEFAULT_LOCALE = 'en';
35
+
36
+ // like lodash's uniqBy but much smaller
37
+ function uniqBy (arr, func) {
38
+ const set = new Set();
39
+ const res = [];
40
+ for (const item of arr) {
41
+ const key = func(item);
42
+ if (!set.has(key)) {
43
+ set.add(key);
44
+ res.push(item);
45
+ }
46
+ }
47
+ return res
48
+ }
49
+
50
+ function uniqEmoji (emojis) {
51
+ return uniqBy(emojis, _ => _.unicode)
52
+ }
53
+
54
+ function initialMigration (db) {
55
+ function createObjectStore (name, keyPath, indexes) {
56
+ const store = keyPath
57
+ ? db.createObjectStore(name, { keyPath })
58
+ : db.createObjectStore(name);
59
+ if (indexes) {
60
+ for (const [indexName, [keyPath, multiEntry]] of Object.entries(indexes)) {
61
+ store.createIndex(indexName, keyPath, { multiEntry });
62
+ }
63
+ }
64
+ return store
65
+ }
66
+
67
+ createObjectStore(STORE_KEYVALUE);
68
+ createObjectStore(STORE_EMOJI, /* keyPath */ FIELD_UNICODE, {
69
+ [INDEX_TOKENS]: [FIELD_TOKENS, /* multiEntry */ true],
70
+ [INDEX_GROUP_AND_ORDER]: [[FIELD_GROUP, FIELD_ORDER]],
71
+ [INDEX_SKIN_UNICODE]: [FIELD_SKIN_UNICODE, /* multiEntry */ true]
72
+ });
73
+ createObjectStore(STORE_FAVORITES, undefined, {
74
+ [INDEX_COUNT]: ['']
75
+ });
76
+ }
77
+
78
+ const openIndexedDBRequests = {};
79
+ const databaseCache = {};
80
+ const onCloseListeners = {};
81
+
82
+ function handleOpenOrDeleteReq (resolve, reject, req) {
83
+ // These things are almost impossible to test with fakeIndexedDB sadly
84
+ /* istanbul ignore next */
85
+ req.onerror = () => reject(req.error);
86
+ /* istanbul ignore next */
87
+ req.onblocked = () => reject(new Error('IDB blocked'));
88
+ req.onsuccess = () => resolve(req.result);
89
+ }
90
+
91
+ async function createDatabase (dbName) {
92
+ const db = await new Promise((resolve, reject) => {
93
+ const req = indexedDB.open(dbName, DB_VERSION_CURRENT);
94
+ openIndexedDBRequests[dbName] = req;
95
+ req.onupgradeneeded = e => {
96
+ // Technically there is only one version, so we don't need this `if` check
97
+ // But if an old version of the JS is in another browser tab
98
+ // and it gets upgraded in the future and we have a new DB version, well...
99
+ // better safe than sorry.
100
+ /* istanbul ignore else */
101
+ if (e.oldVersion < DB_VERSION_INITIAL) {
102
+ initialMigration(req.result);
103
+ }
104
+ };
105
+ handleOpenOrDeleteReq(resolve, reject, req);
106
+ });
107
+ // Handle abnormal closes, e.g. "delete database" in chrome dev tools.
108
+ // No need for removeEventListener, because once the DB can no longer
109
+ // fire "close" events, it will auto-GC.
110
+ db.onclose = () => closeDatabase(dbName);
111
+ return db
112
+ }
113
+
114
+ function openDatabase (dbName) {
115
+ if (!databaseCache[dbName]) {
116
+ databaseCache[dbName] = createDatabase(dbName);
117
+ }
118
+ return databaseCache[dbName]
119
+ }
120
+
121
+ function dbPromise (db, storeName, readOnlyOrReadWrite, cb) {
122
+ return new Promise((resolve, reject) => {
123
+ // Use relaxed durability because neither the emoji data nor the favorites/preferred skin tone
124
+ // are really irreplaceable data. IndexedDB is just a cache in this case.
125
+ const txn = db.transaction(storeName, readOnlyOrReadWrite, { durability: 'relaxed' });
126
+ const store = typeof storeName === 'string'
127
+ ? txn.objectStore(storeName)
128
+ : storeName.map(name => txn.objectStore(name));
129
+ let res;
130
+ cb(store, txn, (result) => {
131
+ res = result;
132
+ });
133
+
134
+ txn.oncomplete = () => resolve(res);
135
+ /* istanbul ignore next */
136
+ txn.onerror = () => reject(txn.error);
137
+ })
138
+ }
139
+
140
+ function closeDatabase (dbName) {
141
+ // close any open requests
142
+ const req = openIndexedDBRequests[dbName];
143
+ const db = req && req.result;
144
+ if (db) {
145
+ db.close();
146
+ const listeners = onCloseListeners[dbName];
147
+ /* istanbul ignore else */
148
+ if (listeners) {
149
+ for (const listener of listeners) {
150
+ listener();
151
+ }
152
+ }
153
+ }
154
+ delete openIndexedDBRequests[dbName];
155
+ delete databaseCache[dbName];
156
+ delete onCloseListeners[dbName];
157
+ }
158
+
159
+ function deleteDatabase (dbName) {
160
+ return new Promise((resolve, reject) => {
161
+ // close any open requests
162
+ closeDatabase(dbName);
163
+ const req = indexedDB.deleteDatabase(dbName);
164
+ handleOpenOrDeleteReq(resolve, reject, req);
165
+ })
166
+ }
167
+
168
+ // The "close" event occurs during an abnormal shutdown, e.g. a user clearing their browser data.
169
+ // However, it doesn't occur with the normal "close" event, so we handle that separately.
170
+ // https://www.w3.org/TR/IndexedDB/#close-a-database-connection
171
+ function addOnCloseListener (dbName, listener) {
172
+ let listeners = onCloseListeners[dbName];
173
+ if (!listeners) {
174
+ listeners = onCloseListeners[dbName] = [];
175
+ }
176
+ listeners.push(listener);
177
+ }
178
+
179
+ // list of emoticons that don't match a simple \W+ regex
180
+ // extracted using:
181
+ // require('emoji-picker-element-data/en/emojibase/data.json').map(_ => _.emoticon).filter(Boolean).filter(_ => !/^\W+$/.test(_))
182
+ const irregularEmoticons = new Set([
183
+ ':D', 'XD', ":'D", 'O:)',
184
+ ':X', ':P', ';P', 'XP',
185
+ ':L', ':Z', ':j', '8D',
186
+ 'XO', '8)', ':B', ':O',
187
+ ':S', ":'o", 'Dx', 'X(',
188
+ 'D:', ':C', '>0)', ':3',
189
+ '</3', '<3', '\\M/', ':E',
190
+ '8#'
191
+ ]);
192
+
193
+ function extractTokens (str) {
194
+ return str
195
+ .split(/[\s_]+/)
196
+ .map(word => {
197
+ if (!word.match(/\w/) || irregularEmoticons.has(word)) {
198
+ // for pure emoticons like :) or :-), just leave them as-is
199
+ return word.toLowerCase()
200
+ }
201
+
202
+ return word
203
+ .replace(/[)(:,]/g, '')
204
+ .replace(/’/g, "'")
205
+ .toLowerCase()
206
+ }).filter(Boolean)
207
+ }
208
+
209
+ const MIN_SEARCH_TEXT_LENGTH = 2;
210
+
211
+ // This is an extra step in addition to extractTokens(). The difference here is that we expect
212
+ // the input to have already been run through extractTokens(). This is useful for cases like
213
+ // emoticons, where we don't want to do any tokenization (because it makes no sense to split up
214
+ // ">:)" by the colon) but we do want to lowercase it to have consistent search results, so that
215
+ // the user can type ':P' or ':p' and still get the same result.
216
+ function normalizeTokens (str) {
217
+ return str
218
+ .filter(Boolean)
219
+ .map(_ => _.toLowerCase())
220
+ .filter(_ => _.length >= MIN_SEARCH_TEXT_LENGTH)
221
+ }
222
+
223
+ // Transform emoji data for storage in IDB
224
+ function transformEmojiData (emojiData) {
225
+ const res = emojiData.map(({ annotation, emoticon, group, order, shortcodes, skins, tags, emoji, version }) => {
226
+ const tokens = [...new Set(
227
+ normalizeTokens([
228
+ ...(shortcodes || []).map(extractTokens).flat(),
229
+ ...(tags || []).map(extractTokens).flat(),
230
+ ...extractTokens(annotation),
231
+ emoticon
232
+ ])
233
+ )].sort();
234
+ const res = {
235
+ annotation,
236
+ group,
237
+ order,
238
+ tags,
239
+ tokens,
240
+ unicode: emoji,
241
+ version
242
+ };
243
+ if (emoticon) {
244
+ res.emoticon = emoticon;
245
+ }
246
+ if (shortcodes) {
247
+ res.shortcodes = shortcodes;
248
+ }
249
+ if (skins) {
250
+ res.skinTones = [];
251
+ res.skinUnicodes = [];
252
+ res.skinVersions = [];
253
+ for (const { tone, emoji, version } of skins) {
254
+ res.skinTones.push(tone);
255
+ res.skinUnicodes.push(emoji);
256
+ res.skinVersions.push(version);
257
+ }
258
+ }
259
+ return res
260
+ });
261
+ return res
262
+ }
263
+
264
+ // helper functions that help compress the code better
265
+
266
+ function callStore (store, method, key, cb) {
267
+ store[method](key).onsuccess = e => (cb && cb(e.target.result));
268
+ }
269
+
270
+ function getIDB (store, key, cb) {
271
+ callStore(store, 'get', key, cb);
272
+ }
273
+
274
+ function getAllIDB (store, key, cb) {
275
+ callStore(store, 'getAll', key, cb);
276
+ }
277
+
278
+ function commit (txn) {
279
+ /* istanbul ignore else */
280
+ if (txn.commit) {
281
+ txn.commit();
282
+ }
283
+ }
284
+
285
+ // like lodash's minBy
286
+ function minBy (array, func) {
287
+ let minItem = array[0];
288
+ for (let i = 1; i < array.length; i++) {
289
+ const item = array[i];
290
+ if (func(minItem) > func(item)) {
291
+ minItem = item;
292
+ }
293
+ }
294
+ return minItem
295
+ }
296
+
297
+ // return an array of results representing all items that are found in each one of the arrays
298
+ //
299
+
300
+ function findCommonMembers (arrays, uniqByFunc) {
301
+ const shortestArray = minBy(arrays, _ => _.length);
302
+ const results = [];
303
+ for (const item of shortestArray) {
304
+ // if this item is included in every array in the intermediate results, add it to the final results
305
+ if (!arrays.some(array => array.findIndex(_ => uniqByFunc(_) === uniqByFunc(item)) === -1)) {
306
+ results.push(item);
307
+ }
308
+ }
309
+ return results
310
+ }
311
+
312
+ async function isEmpty (db) {
313
+ return !(await get(db, STORE_KEYVALUE, KEY_URL))
314
+ }
315
+
316
+ async function hasData (db, url, eTag) {
317
+ const [oldETag, oldUrl] = await Promise.all([KEY_ETAG, KEY_URL]
318
+ .map(key => get(db, STORE_KEYVALUE, key)));
319
+ return (oldETag === eTag && oldUrl === url)
320
+ }
321
+
322
+ async function doFullDatabaseScanForSingleResult (db, predicate) {
323
+ // This batching algorithm is just a perf improvement over a basic
324
+ // cursor. The BATCH_SIZE is an estimate of what would give the best
325
+ // perf for doing a full DB scan (worst case).
326
+ //
327
+ // Mini-benchmark for determining the best batch size:
328
+ //
329
+ // PERF=1 pnpm build:rollup && pnpm test:adhoc
330
+ //
331
+ // (async () => {
332
+ // performance.mark('start')
333
+ // await $('emoji-picker').database.getEmojiByShortcode('doesnotexist')
334
+ // performance.measure('total', 'start')
335
+ // console.log(performance.getEntriesByName('total').slice(-1)[0].duration)
336
+ // })()
337
+ const BATCH_SIZE = 50; // Typically around 150ms for 6x slowdown in Chrome for above benchmark
338
+ return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {
339
+ let lastKey;
340
+
341
+ const processNextBatch = () => {
342
+ emojiStore.getAll(lastKey && IDBKeyRange.lowerBound(lastKey, true), BATCH_SIZE).onsuccess = e => {
343
+ const results = e.target.result;
344
+ for (const result of results) {
345
+ lastKey = result.unicode;
346
+ if (predicate(result)) {
347
+ return cb(result)
348
+ }
349
+ }
350
+ if (results.length < BATCH_SIZE) {
351
+ return cb()
352
+ }
353
+ processNextBatch();
354
+ };
355
+ };
356
+ processNextBatch();
357
+ })
358
+ }
359
+
360
+ async function loadData (db, emojiData, url, eTag) {
361
+ try {
362
+ const transformedData = transformEmojiData(emojiData);
363
+ await dbPromise(db, [STORE_EMOJI, STORE_KEYVALUE], MODE_READWRITE, ([emojiStore, metaStore], txn) => {
364
+ let oldETag;
365
+ let oldUrl;
366
+ let todo = 0;
367
+
368
+ function checkFetched () {
369
+ if (++todo === 2) { // 2 requests made
370
+ onFetched();
371
+ }
372
+ }
373
+
374
+ function onFetched () {
375
+ if (oldETag === eTag && oldUrl === url) {
376
+ // check again within the transaction to guard against concurrency, e.g. multiple browser tabs
377
+ return
378
+ }
379
+ // delete old data
380
+ emojiStore.clear();
381
+ // insert new data
382
+ for (const data of transformedData) {
383
+ emojiStore.put(data);
384
+ }
385
+ metaStore.put(eTag, KEY_ETAG);
386
+ metaStore.put(url, KEY_URL);
387
+ commit(txn);
388
+ }
389
+
390
+ getIDB(metaStore, KEY_ETAG, result => {
391
+ oldETag = result;
392
+ checkFetched();
393
+ });
394
+
395
+ getIDB(metaStore, KEY_URL, result => {
396
+ oldUrl = result;
397
+ checkFetched();
398
+ });
399
+ });
400
+ } finally {
401
+ }
402
+ }
403
+
404
+ async function getEmojiByGroup (db, group) {
405
+ return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {
406
+ const range = IDBKeyRange.bound([group, 0], [group + 1, 0], false, true);
407
+ getAllIDB(emojiStore.index(INDEX_GROUP_AND_ORDER), range, cb);
408
+ })
409
+ }
410
+
411
+ async function getEmojiBySearchQuery (db, query) {
412
+ const tokens = normalizeTokens(extractTokens(query));
413
+
414
+ if (!tokens.length) {
415
+ return []
416
+ }
417
+
418
+ return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {
419
+ // get all results that contain all tokens (i.e. an AND query)
420
+ const intermediateResults = [];
421
+
422
+ const checkDone = () => {
423
+ if (intermediateResults.length === tokens.length) {
424
+ onDone();
425
+ }
426
+ };
427
+
428
+ const onDone = () => {
429
+ const results = findCommonMembers(intermediateResults, _ => _.unicode);
430
+ cb(results.sort((a, b) => a.order < b.order ? -1 : 1));
431
+ };
432
+
433
+ for (let i = 0; i < tokens.length; i++) {
434
+ const token = tokens[i];
435
+ const range = i === tokens.length - 1
436
+ ? IDBKeyRange.bound(token, token + '\uffff', false, true) // treat last token as a prefix search
437
+ : IDBKeyRange.only(token); // treat all other tokens as an exact match
438
+ getAllIDB(emojiStore.index(INDEX_TOKENS), range, result => {
439
+ intermediateResults.push(result);
440
+ checkDone();
441
+ });
442
+ }
443
+ })
444
+ }
445
+
446
+ // This could have been implemented as an IDB index on shortcodes, but it seemed wasteful to do that
447
+ // when we can already query by tokens and this will give us what we're looking for 99.9% of the time
448
+ async function getEmojiByShortcode (db, shortcode) {
449
+ const emojis = await getEmojiBySearchQuery(db, shortcode);
450
+
451
+ // In very rare cases (e.g. the shortcode "v" as in "v for victory"), we cannot search because
452
+ // there are no usable tokens (too short in this case). In that case, we have to do an inefficient
453
+ // full-database scan, which I believe is an acceptable tradeoff for not having to have an extra
454
+ // index on shortcodes.
455
+
456
+ if (!emojis.length) {
457
+ const predicate = _ => ((_.shortcodes || []).includes(shortcode.toLowerCase()));
458
+ return (await doFullDatabaseScanForSingleResult(db, predicate)) || null
459
+ }
460
+
461
+ return emojis.filter(_ => {
462
+ const lowerShortcodes = (_.shortcodes || []).map(_ => _.toLowerCase());
463
+ return lowerShortcodes.includes(shortcode.toLowerCase())
464
+ })[0] || null
465
+ }
466
+
467
+ async function getEmojiByUnicode (db, unicode) {
468
+ return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => (
469
+ getIDB(emojiStore, unicode, result => {
470
+ if (result) {
471
+ return cb(result)
472
+ }
473
+ getIDB(emojiStore.index(INDEX_SKIN_UNICODE), unicode, result => cb(result || null));
474
+ })
475
+ ))
476
+ }
477
+
478
+ function get (db, storeName, key) {
479
+ return dbPromise(db, storeName, MODE_READONLY, (store, txn, cb) => (
480
+ getIDB(store, key, cb)
481
+ ))
482
+ }
483
+
484
+ function set (db, storeName, key, value) {
485
+ return dbPromise(db, storeName, MODE_READWRITE, (store, txn) => {
486
+ store.put(value, key);
487
+ commit(txn);
488
+ })
489
+ }
490
+
491
+ function incrementFavoriteEmojiCount (db, unicode) {
492
+ return dbPromise(db, STORE_FAVORITES, MODE_READWRITE, (store, txn) => (
493
+ getIDB(store, unicode, result => {
494
+ store.put((result || 0) + 1, unicode);
495
+ commit(txn);
496
+ })
497
+ ))
498
+ }
499
+
500
+ function getTopFavoriteEmoji (db, customEmojiIndex, limit) {
501
+ if (limit === 0) {
502
+ return []
503
+ }
504
+ return dbPromise(db, [STORE_FAVORITES, STORE_EMOJI], MODE_READONLY, ([favoritesStore, emojiStore], txn, cb) => {
505
+ const results = [];
506
+ favoritesStore.index(INDEX_COUNT).openCursor(undefined, 'prev').onsuccess = e => {
507
+ const cursor = e.target.result;
508
+ if (!cursor) { // no more results
509
+ return cb(results)
510
+ }
511
+
512
+ function addResult (result) {
513
+ results.push(result);
514
+ if (results.length === limit) {
515
+ return cb(results) // done, reached the limit
516
+ }
517
+ cursor.continue();
518
+ }
519
+
520
+ const unicodeOrName = cursor.primaryKey;
521
+ const custom = customEmojiIndex.byName(unicodeOrName);
522
+ if (custom) {
523
+ return addResult(custom)
524
+ }
525
+ // This could be done in parallel (i.e. make the cursor and the get()s parallelized),
526
+ // but my testing suggests it's not actually faster.
527
+ getIDB(emojiStore, unicodeOrName, emoji => {
528
+ if (emoji) {
529
+ return addResult(emoji)
530
+ }
531
+ // emoji not found somehow, ignore (may happen if custom emoji change)
532
+ cursor.continue();
533
+ });
534
+ };
535
+ })
536
+ }
537
+
538
+ // trie data structure for prefix searches
539
+ // loosely based on https://github.com/nolanlawson/substring-trie
540
+
541
+ const CODA_MARKER = ''; // marks the end of the string
542
+
543
+ function trie (arr, itemToTokens) {
544
+ const map = new Map();
545
+ for (const item of arr) {
546
+ const tokens = itemToTokens(item);
547
+ for (const token of tokens) {
548
+ let currentMap = map;
549
+ for (let i = 0; i < token.length; i++) {
550
+ const char = token.charAt(i);
551
+ let nextMap = currentMap.get(char);
552
+ if (!nextMap) {
553
+ nextMap = new Map();
554
+ currentMap.set(char, nextMap);
555
+ }
556
+ currentMap = nextMap;
557
+ }
558
+ let valuesAtCoda = currentMap.get(CODA_MARKER);
559
+ if (!valuesAtCoda) {
560
+ valuesAtCoda = [];
561
+ currentMap.set(CODA_MARKER, valuesAtCoda);
562
+ }
563
+ valuesAtCoda.push(item);
564
+ }
565
+ }
566
+
567
+ const search = (query, exact) => {
568
+ let currentMap = map;
569
+ for (let i = 0; i < query.length; i++) {
570
+ const char = query.charAt(i);
571
+ const nextMap = currentMap.get(char);
572
+ if (nextMap) {
573
+ currentMap = nextMap;
574
+ } else {
575
+ return []
576
+ }
577
+ }
578
+
579
+ if (exact) {
580
+ const results = currentMap.get(CODA_MARKER);
581
+ return results || []
582
+ }
583
+
584
+ const results = [];
585
+ // traverse
586
+ const queue = [currentMap];
587
+ while (queue.length) {
588
+ const currentMap = queue.shift();
589
+ const entriesSortedByKey = [...currentMap.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1);
590
+ for (const [key, value] of entriesSortedByKey) {
591
+ if (key === CODA_MARKER) { // CODA_MARKER always comes first; it's the empty string
592
+ results.push(...value);
593
+ } else {
594
+ queue.push(value);
595
+ }
596
+ }
597
+ }
598
+ return results
599
+ };
600
+
601
+ return search
602
+ }
603
+
604
+ const requiredKeys$1 = [
605
+ 'name',
606
+ 'url'
607
+ ];
608
+
609
+ function assertCustomEmojis (customEmojis) {
610
+ const isArray = customEmojis && Array.isArray(customEmojis);
611
+ const firstItemIsFaulty = isArray &&
612
+ customEmojis.length &&
613
+ (!customEmojis[0] || requiredKeys$1.some(key => !(key in customEmojis[0])));
614
+ if (!isArray || firstItemIsFaulty) {
615
+ throw new Error('Custom emojis are in the wrong format')
616
+ }
617
+ }
618
+
619
+ function customEmojiIndex (customEmojis) {
620
+ assertCustomEmojis(customEmojis);
621
+
622
+ const sortByName = (a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
623
+
624
+ //
625
+ // all()
626
+ //
627
+ const all = customEmojis.sort(sortByName);
628
+
629
+ //
630
+ // search()
631
+ //
632
+ const emojiToTokens = emoji => {
633
+ const set = new Set();
634
+ if (emoji.shortcodes) {
635
+ for (const shortcode of emoji.shortcodes) {
636
+ for (const token of extractTokens(shortcode)) {
637
+ set.add(token);
638
+ }
639
+ }
640
+ }
641
+ return set
642
+ };
643
+ const searchTrie = trie(customEmojis, emojiToTokens);
644
+ const searchByExactMatch = _ => searchTrie(_, true);
645
+ const searchByPrefix = _ => searchTrie(_, false);
646
+
647
+ // Search by query for custom emoji. Similar to how we do this in IDB, the last token
648
+ // is treated as a prefix search, but every other one is treated as an exact match.
649
+ // Then we AND the results together
650
+ const search = query => {
651
+ const tokens = extractTokens(query);
652
+ const intermediateResults = tokens.map((token, i) => (
653
+ (i < tokens.length - 1 ? searchByExactMatch : searchByPrefix)(token)
654
+ ));
655
+ return findCommonMembers(intermediateResults, _ => _.name).sort(sortByName)
656
+ };
657
+
658
+ //
659
+ // byShortcode, byName
660
+ //
661
+ const shortcodeToEmoji = new Map();
662
+ const nameToEmoji = new Map();
663
+ for (const customEmoji of customEmojis) {
664
+ nameToEmoji.set(customEmoji.name.toLowerCase(), customEmoji);
665
+ for (const shortcode of (customEmoji.shortcodes || [])) {
666
+ shortcodeToEmoji.set(shortcode.toLowerCase(), customEmoji);
667
+ }
668
+ }
669
+
670
+ const byShortcode = shortcode => shortcodeToEmoji.get(shortcode.toLowerCase());
671
+ const byName = name => nameToEmoji.get(name.toLowerCase());
672
+
673
+ return {
674
+ all,
675
+ search,
676
+ byShortcode,
677
+ byName
678
+ }
679
+ }
680
+
681
+ const isFirefoxContentScript = typeof wrappedJSObject !== 'undefined';
682
+
683
+ // remove some internal implementation details, i.e. the "tokens" array on the emoji object
684
+ // essentially, convert the emoji from the version stored in IDB to the version used in-memory
685
+ function cleanEmoji (emoji) {
686
+ if (!emoji) {
687
+ return emoji
688
+ }
689
+ // if inside a Firefox content script, need to clone the emoji object to prevent Firefox from complaining about
690
+ // cross-origin object. See: https://github.com/nolanlawson/emoji-picker-element/issues/356
691
+ /* istanbul ignore if */
692
+ if (isFirefoxContentScript) {
693
+ emoji = structuredClone(emoji);
694
+ }
695
+ delete emoji.tokens;
696
+ if (emoji.skinTones) {
697
+ const len = emoji.skinTones.length;
698
+ emoji.skins = Array(len);
699
+ for (let i = 0; i < len; i++) {
700
+ emoji.skins[i] = {
701
+ tone: emoji.skinTones[i],
702
+ unicode: emoji.skinUnicodes[i],
703
+ version: emoji.skinVersions[i]
704
+ };
705
+ }
706
+ delete emoji.skinTones;
707
+ delete emoji.skinUnicodes;
708
+ delete emoji.skinVersions;
709
+ }
710
+ return emoji
711
+ }
712
+
713
+ function warnETag (eTag) {
714
+ if (!eTag) {
715
+ console.warn('emoji-picker-element is more efficient if the dataSource server exposes an ETag header.');
716
+ }
717
+ }
718
+
719
+ const requiredKeys = [
720
+ 'annotation',
721
+ 'emoji',
722
+ 'group',
723
+ 'order',
724
+ 'version'
725
+ ];
726
+
727
+ function assertEmojiData (emojiData) {
728
+ if (!emojiData ||
729
+ !Array.isArray(emojiData) ||
730
+ !emojiData[0] ||
731
+ (typeof emojiData[0] !== 'object') ||
732
+ requiredKeys.some(key => (!(key in emojiData[0])))) {
733
+ throw new Error('Emoji data is in the wrong format')
734
+ }
735
+ }
736
+
737
+ function assertStatus (response, dataSource) {
738
+ if (Math.floor(response.status / 100) !== 2) {
739
+ throw new Error('Failed to fetch: ' + dataSource + ': ' + response.status)
740
+ }
741
+ }
742
+
743
+ async function getETag (dataSource) {
744
+ const response = await fetch(dataSource, { method: 'HEAD' });
745
+ assertStatus(response, dataSource);
746
+ const eTag = response.headers.get('etag');
747
+ warnETag(eTag);
748
+ return eTag
749
+ }
750
+
751
+ async function getETagAndData (dataSource) {
752
+ const response = await fetch(dataSource);
753
+ assertStatus(response, dataSource);
754
+ const eTag = response.headers.get('etag');
755
+ warnETag(eTag);
756
+ const emojiData = await response.json();
757
+ assertEmojiData(emojiData);
758
+ return [eTag, emojiData]
759
+ }
760
+
761
+ // TODO: including these in blob-util.ts causes typedoc to generate docs for them,
762
+ // even with --excludePrivate ¯\_(ツ)_/¯
763
+ /** @private */
764
+ /**
765
+ * Convert an `ArrayBuffer` to a binary string.
766
+ *
767
+ * Example:
768
+ *
769
+ * ```js
770
+ * var myString = blobUtil.arrayBufferToBinaryString(arrayBuff)
771
+ * ```
772
+ *
773
+ * @param buffer - array buffer
774
+ * @returns binary string
775
+ */
776
+ function arrayBufferToBinaryString(buffer) {
777
+ var binary = '';
778
+ var bytes = new Uint8Array(buffer);
779
+ var length = bytes.byteLength;
780
+ var i = -1;
781
+ while (++i < length) {
782
+ binary += String.fromCharCode(bytes[i]);
783
+ }
784
+ return binary;
785
+ }
786
+ /**
787
+ * Convert a binary string to an `ArrayBuffer`.
788
+ *
789
+ * ```js
790
+ * var myBuffer = blobUtil.binaryStringToArrayBuffer(binaryString)
791
+ * ```
792
+ *
793
+ * @param binary - binary string
794
+ * @returns array buffer
795
+ */
796
+ function binaryStringToArrayBuffer(binary) {
797
+ var length = binary.length;
798
+ var buf = new ArrayBuffer(length);
799
+ var arr = new Uint8Array(buf);
800
+ var i = -1;
801
+ while (++i < length) {
802
+ arr[i] = binary.charCodeAt(i);
803
+ }
804
+ return buf;
805
+ }
806
+
807
+ // generate a checksum based on the stringified JSON
808
+ async function jsonChecksum (object) {
809
+ const inString = JSON.stringify(object);
810
+ let inBuffer = binaryStringToArrayBuffer(inString);
811
+
812
+ // this does not need to be cryptographically secure, SHA-1 is fine
813
+ const outBuffer = await crypto.subtle.digest('SHA-1', inBuffer);
814
+ const outBinString = arrayBufferToBinaryString(outBuffer);
815
+ const res = btoa(outBinString);
816
+ return res
817
+ }
818
+
819
+ async function doCheckForUpdates (db, dataSource) {
820
+ // just do a simple HEAD request first to see if the eTags match
821
+ let emojiData;
822
+ let eTag = await getETag(dataSource);
823
+ if (!eTag) { // work around lack of ETag/Access-Control-Expose-Headers
824
+ const eTagAndData = await getETagAndData(dataSource);
825
+ eTag = eTagAndData[0];
826
+ emojiData = eTagAndData[1];
827
+ if (!eTag) {
828
+ eTag = await jsonChecksum(emojiData);
829
+ }
830
+ }
831
+ if (await hasData(db, dataSource, eTag)) ; else {
832
+ if (!emojiData) {
833
+ const eTagAndData = await getETagAndData(dataSource);
834
+ emojiData = eTagAndData[1];
835
+ }
836
+ await loadData(db, emojiData, dataSource, eTag);
837
+ }
838
+ }
839
+
840
+ async function loadDataForFirstTime (db, dataSource) {
841
+ let [eTag, emojiData] = await getETagAndData(dataSource);
842
+ if (!eTag) {
843
+ // Handle lack of support for ETag or Access-Control-Expose-Headers
844
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers#Browser_compatibility
845
+ eTag = await jsonChecksum(emojiData);
846
+ }
847
+
848
+ await loadData(db, emojiData, dataSource, eTag);
849
+ }
850
+
851
+ async function checkForUpdates (db, dataSource) {
852
+ try {
853
+ await doCheckForUpdates(db, dataSource);
854
+ } catch (err) {
855
+ // Checking for updates is not a critical operation, and it can fail if e.g. the picker is quickly removed and
856
+ // re-added to the DOM. In those cases, we may get an IndexedDB InvalidStateError because we are attempting to close
857
+ // the database connection, possibly while another request is inflight. So there's effectively no way to prevent
858
+ // InvalidStateErrors unless we were to carefully sequence our IndexedDB operations. Much more simply, we can just
859
+ // ignore IndexedDB InvalidStateErrors here and give users one less useless error message in their console.
860
+ if (err.name !== 'InvalidStateError') {
861
+ throw err
862
+ }
863
+ }
864
+ }
865
+
866
+ class Database {
867
+ constructor ({ dataSource = DEFAULT_DATA_SOURCE, locale = DEFAULT_LOCALE, customEmoji = [] } = {}) {
868
+ this.dataSource = dataSource;
869
+ this.locale = locale;
870
+ this._dbName = `emoji-picker-element-${this.locale}`;
871
+ this._db = undefined;
872
+ this._lazyUpdate = undefined;
873
+ this._custom = customEmojiIndex(customEmoji);
874
+
875
+ this._clear = this._clear.bind(this);
876
+ this._ready = this._init();
877
+ }
878
+
879
+ async _init () {
880
+ const db = this._db = await openDatabase(this._dbName);
881
+
882
+ addOnCloseListener(this._dbName, this._clear);
883
+ const dataSource = this.dataSource;
884
+ const empty = await isEmpty(db);
885
+
886
+ if (empty) {
887
+ await loadDataForFirstTime(db, dataSource);
888
+ } else { // offline-first - do an update asynchronously
889
+ this._lazyUpdate = checkForUpdates(db, dataSource);
890
+ }
891
+ }
892
+
893
+ async ready () {
894
+ const checkReady = async () => {
895
+ if (!this._ready) {
896
+ this._ready = this._init();
897
+ }
898
+ return this._ready
899
+ };
900
+ await checkReady();
901
+ // There's a possibility of a race condition where the element gets added, removed, and then added again
902
+ // with a particular timing, which would set the _db to undefined.
903
+ // We *could* do a while loop here, but that seems excessive and could lead to an infinite loop.
904
+ if (!this._db) {
905
+ await checkReady();
906
+ }
907
+ }
908
+
909
+ async getEmojiByGroup (group) {
910
+ assertNumber(group);
911
+ await this.ready();
912
+ return uniqEmoji(await getEmojiByGroup(this._db, group)).map(cleanEmoji)
913
+ }
914
+
915
+ async getEmojiBySearchQuery (query) {
916
+ assertNonEmptyString(query);
917
+ await this.ready();
918
+ const customs = this._custom.search(query);
919
+ const natives = uniqEmoji(await getEmojiBySearchQuery(this._db, query)).map(cleanEmoji);
920
+ return [
921
+ ...customs,
922
+ ...natives
923
+ ]
924
+ }
925
+
926
+ async getEmojiByShortcode (shortcode) {
927
+ assertNonEmptyString(shortcode);
928
+ await this.ready();
929
+ const custom = this._custom.byShortcode(shortcode);
930
+ if (custom) {
931
+ return custom
932
+ }
933
+ return cleanEmoji(await getEmojiByShortcode(this._db, shortcode))
934
+ }
935
+
936
+ async getEmojiByUnicodeOrName (unicodeOrName) {
937
+ assertNonEmptyString(unicodeOrName);
938
+ await this.ready();
939
+ const custom = this._custom.byName(unicodeOrName);
940
+ if (custom) {
941
+ return custom
942
+ }
943
+ return cleanEmoji(await getEmojiByUnicode(this._db, unicodeOrName))
944
+ }
945
+
946
+ async getPreferredSkinTone () {
947
+ await this.ready();
948
+ return (await get(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE)) || 0
949
+ }
950
+
951
+ async setPreferredSkinTone (skinTone) {
952
+ assertNumber(skinTone);
953
+ await this.ready();
954
+ return set(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE, skinTone)
955
+ }
956
+
957
+ async incrementFavoriteEmojiCount (unicodeOrName) {
958
+ assertNonEmptyString(unicodeOrName);
959
+ await this.ready();
960
+ return incrementFavoriteEmojiCount(this._db, unicodeOrName)
961
+ }
962
+
963
+ async getTopFavoriteEmoji (limit) {
964
+ assertNumber(limit);
965
+ await this.ready();
966
+ return (await getTopFavoriteEmoji(this._db, this._custom, limit)).map(cleanEmoji)
967
+ }
968
+
969
+ set customEmoji (customEmojis) {
970
+ this._custom = customEmojiIndex(customEmojis);
971
+ }
972
+
973
+ get customEmoji () {
974
+ return this._custom.all
975
+ }
976
+
977
+ async _shutdown () {
978
+ await this.ready(); // reopen if we've already been closed/deleted
979
+ try {
980
+ await this._lazyUpdate; // allow any lazy updates to process before closing/deleting
981
+ } catch (err) { /* ignore network errors (offline-first) */ }
982
+ }
983
+
984
+ // clear references to IDB, e.g. during a close event
985
+ _clear () {
986
+ // We don't need to call removeEventListener or remove the manual "close" listeners.
987
+ // The memory leak tests prove this is unnecessary. It's because:
988
+ // 1) IDBDatabases that can no longer fire "close" automatically have listeners GCed
989
+ // 2) we clear the manual close listeners in databaseLifecycle.js.
990
+ this._db = this._ready = this._lazyUpdate = undefined;
991
+ }
992
+
993
+ async close () {
994
+ await this._shutdown();
995
+ await closeDatabase(this._dbName);
996
+ }
997
+
998
+ async delete () {
999
+ await this._shutdown();
1000
+ await deleteDatabase(this._dbName);
1001
+ }
1002
+ }
1003
+
1004
+ export { Database as default };