submodule: Move picker element to submodule

This commit is contained in:
Eragon 2023-06-12 15:12:14 +02:00
parent b892d9ae68
commit 5a6d000be6
Signed by: Eragon
GPG Key ID: 087126EBFC725006
24 changed files with 5 additions and 6511 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "submodules/emoji-picker-element"]
path = submodules/emoji-picker-element
url = https://gitea.planet-casio.com/devs/emoji-picker-element.git

View File

@ -0,0 +1 @@
./

File diff suppressed because one or more lines are too long

View File

@ -1,972 +0,0 @@
function assertNonEmptyString (str) {
if (typeof str !== 'string' || !str) {
throw new Error('expected a non-empty string, got: ' + str)
}
}
function assertNumber (number) {
if (typeof number !== 'number') {
throw new Error('expected a number, got: ' + number)
}
}
const DB_VERSION_CURRENT = 1;
const DB_VERSION_INITIAL = 1;
const STORE_EMOJI = 'emoji';
const STORE_KEYVALUE = 'keyvalue';
const STORE_FAVORITES = 'favorites';
const FIELD_TOKENS = 'tokens';
const INDEX_TOKENS = 'tokens';
const FIELD_UNICODE = 'unicode';
const INDEX_COUNT = 'count';
const FIELD_GROUP = 'group';
const FIELD_ORDER = 'order';
const INDEX_GROUP_AND_ORDER = 'group-order';
const KEY_ETAG = 'eTag';
const KEY_URL = 'url';
const KEY_PREFERRED_SKINTONE = 'skinTone';
const MODE_READONLY = 'readonly';
const MODE_READWRITE = 'readwrite';
const INDEX_SKIN_UNICODE = 'skinUnicodes';
const FIELD_SKIN_UNICODE = 'skinUnicodes';
const DEFAULT_DATA_SOURCE = 'https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json';
const DEFAULT_LOCALE = 'en';
// like lodash's uniqBy but much smaller
function uniqBy (arr, func) {
const set = new Set();
const res = [];
for (const item of arr) {
const key = func(item);
if (!set.has(key)) {
set.add(key);
res.push(item);
}
}
return res
}
function uniqEmoji (emojis) {
return uniqBy(emojis, _ => _.unicode)
}
function initialMigration (db) {
function createObjectStore (name, keyPath, indexes) {
const store = keyPath
? db.createObjectStore(name, { keyPath })
: db.createObjectStore(name);
if (indexes) {
for (const [indexName, [keyPath, multiEntry]] of Object.entries(indexes)) {
store.createIndex(indexName, keyPath, { multiEntry });
}
}
return store
}
createObjectStore(STORE_KEYVALUE);
createObjectStore(STORE_EMOJI, /* keyPath */ FIELD_UNICODE, {
[INDEX_TOKENS]: [FIELD_TOKENS, /* multiEntry */ true],
[INDEX_GROUP_AND_ORDER]: [[FIELD_GROUP, FIELD_ORDER]],
[INDEX_SKIN_UNICODE]: [FIELD_SKIN_UNICODE, /* multiEntry */ true]
});
createObjectStore(STORE_FAVORITES, undefined, {
[INDEX_COUNT]: ['']
});
}
const openIndexedDBRequests = {};
const databaseCache = {};
const onCloseListeners = {};
function handleOpenOrDeleteReq (resolve, reject, req) {
// These things are almost impossible to test with fakeIndexedDB sadly
/* istanbul ignore next */
req.onerror = () => reject(req.error);
/* istanbul ignore next */
req.onblocked = () => reject(new Error('IDB blocked'));
req.onsuccess = () => resolve(req.result);
}
async function createDatabase (dbName) {
const db = await new Promise((resolve, reject) => {
const req = indexedDB.open(dbName, DB_VERSION_CURRENT);
openIndexedDBRequests[dbName] = req;
req.onupgradeneeded = e => {
// Technically there is only one version, so we don't need this `if` check
// But if an old version of the JS is in another browser tab
// and it gets upgraded in the future and we have a new DB version, well...
// better safe than sorry.
/* istanbul ignore else */
if (e.oldVersion < DB_VERSION_INITIAL) {
initialMigration(req.result);
}
};
handleOpenOrDeleteReq(resolve, reject, req);
});
// Handle abnormal closes, e.g. "delete database" in chrome dev tools.
// No need for removeEventListener, because once the DB can no longer
// fire "close" events, it will auto-GC.
// Unfortunately cannot test in fakeIndexedDB: https://github.com/dumbmatter/fakeIndexedDB/issues/50
/* istanbul ignore next */
db.onclose = () => closeDatabase(dbName);
return db
}
function openDatabase (dbName) {
if (!databaseCache[dbName]) {
databaseCache[dbName] = createDatabase(dbName);
}
return databaseCache[dbName]
}
function dbPromise (db, storeName, readOnlyOrReadWrite, cb) {
return new Promise((resolve, reject) => {
// Use relaxed durability because neither the emoji data nor the favorites/preferred skin tone
// are really irreplaceable data. IndexedDB is just a cache in this case.
const txn = db.transaction(storeName, readOnlyOrReadWrite, { durability: 'relaxed' });
const store = typeof storeName === 'string'
? txn.objectStore(storeName)
: storeName.map(name => txn.objectStore(name));
let res;
cb(store, txn, (result) => {
res = result;
});
txn.oncomplete = () => resolve(res);
/* istanbul ignore next */
txn.onerror = () => reject(txn.error);
})
}
function closeDatabase (dbName) {
// close any open requests
const req = openIndexedDBRequests[dbName];
const db = req && req.result;
if (db) {
db.close();
const listeners = onCloseListeners[dbName];
/* istanbul ignore else */
if (listeners) {
for (const listener of listeners) {
listener();
}
}
}
delete openIndexedDBRequests[dbName];
delete databaseCache[dbName];
delete onCloseListeners[dbName];
}
function deleteDatabase (dbName) {
return new Promise((resolve, reject) => {
// close any open requests
closeDatabase(dbName);
const req = indexedDB.deleteDatabase(dbName);
handleOpenOrDeleteReq(resolve, reject, req);
})
}
// The "close" event occurs during an abnormal shutdown, e.g. a user clearing their browser data.
// However, it doesn't occur with the normal "close" event, so we handle that separately.
// https://www.w3.org/TR/IndexedDB/#close-a-database-connection
function addOnCloseListener (dbName, listener) {
let listeners = onCloseListeners[dbName];
if (!listeners) {
listeners = onCloseListeners[dbName] = [];
}
listeners.push(listener);
}
// list of emoticons that don't match a simple \W+ regex
// extracted using:
// require('emoji-picker-element-data/en/emojibase/data.json').map(_ => _.emoticon).filter(Boolean).filter(_ => !/^\W+$/.test(_))
const irregularEmoticons = new Set([
':D', 'XD', ":'D", 'O:)',
':X', ':P', ';P', 'XP',
':L', ':Z', ':j', '8D',
'XO', '8)', ':B', ':O',
':S', ":'o", 'Dx', 'X(',
'D:', ':C', '>0)', ':3',
'</3', '<3', '\\M/', ':E',
'8#'
]);
function extractTokens (str) {
return str
.split(/[\s_]+/)
.map(word => {
if (!word.match(/\w/) || irregularEmoticons.has(word)) {
// for pure emoticons like :) or :-), just leave them as-is
return word.toLowerCase()
}
return word
.replace(/[)(:,]/g, '')
.replace(//g, "'")
.toLowerCase()
}).filter(Boolean)
}
const MIN_SEARCH_TEXT_LENGTH = 2;
// This is an extra step in addition to extractTokens(). The difference here is that we expect
// the input to have already been run through extractTokens(). This is useful for cases like
// emoticons, where we don't want to do any tokenization (because it makes no sense to split up
// ">:)" by the colon) but we do want to lowercase it to have consistent search results, so that
// the user can type ':P' or ':p' and still get the same result.
function normalizeTokens (str) {
return str
.filter(Boolean)
.map(_ => _.toLowerCase())
.filter(_ => _.length >= MIN_SEARCH_TEXT_LENGTH)
}
// Transform emoji data for storage in IDB
function transformEmojiData (emojiData) {
const res = emojiData.map(({ annotation, emoticon, group, order, shortcodes, skins, tags, emoji, version }) => {
const tokens = [...new Set(
normalizeTokens([
...(shortcodes || []).map(extractTokens).flat(),
...tags.map(extractTokens).flat(),
...extractTokens(annotation),
emoticon
])
)].sort();
const res = {
annotation,
group,
order,
tags,
tokens,
unicode: emoji,
version
};
if (emoticon) {
res.emoticon = emoticon;
}
if (shortcodes) {
res.shortcodes = shortcodes;
}
if (skins) {
res.skinTones = [];
res.skinUnicodes = [];
res.skinVersions = [];
for (const { tone, emoji, version } of skins) {
res.skinTones.push(tone);
res.skinUnicodes.push(emoji);
res.skinVersions.push(version);
}
}
return res
});
return res
}
// helper functions that help compress the code better
function callStore (store, method, key, cb) {
store[method](key).onsuccess = e => (cb && cb(e.target.result));
}
function getIDB (store, key, cb) {
callStore(store, 'get', key, cb);
}
function getAllIDB (store, key, cb) {
callStore(store, 'getAll', key, cb);
}
function commit (txn) {
/* istanbul ignore else */
if (txn.commit) {
txn.commit();
}
}
// like lodash's minBy
function minBy (array, func) {
let minItem = array[0];
for (let i = 1; i < array.length; i++) {
const item = array[i];
if (func(minItem) > func(item)) {
minItem = item;
}
}
return minItem
}
// return an array of results representing all items that are found in each one of the arrays
function findCommonMembers (arrays, uniqByFunc) {
const shortestArray = minBy(arrays, _ => _.length);
const results = [];
for (const item of shortestArray) {
// if this item is included in every array in the intermediate results, add it to the final results
if (!arrays.some(array => array.findIndex(_ => uniqByFunc(_) === uniqByFunc(item)) === -1)) {
results.push(item);
}
}
return results
}
async function isEmpty (db) {
return !(await get(db, STORE_KEYVALUE, KEY_URL))
}
async function hasData (db, url, eTag) {
const [oldETag, oldUrl] = await Promise.all([KEY_ETAG, KEY_URL]
.map(key => get(db, STORE_KEYVALUE, key)));
return (oldETag === eTag && oldUrl === url)
}
async function doFullDatabaseScanForSingleResult (db, predicate) {
// This batching algorithm is just a perf improvement over a basic
// cursor. The BATCH_SIZE is an estimate of what would give the best
// perf for doing a full DB scan (worst case).
//
// Mini-benchmark for determining the best batch size:
//
// PERF=1 yarn build:rollup && yarn test:adhoc
//
// (async () => {
// performance.mark('start')
// await $('emoji-picker').database.getEmojiByShortcode('doesnotexist')
// performance.measure('total', 'start')
// console.log(performance.getEntriesByName('total').slice(-1)[0].duration)
// })()
const BATCH_SIZE = 50; // Typically around 150ms for 6x slowdown in Chrome for above benchmark
return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {
let lastKey;
const processNextBatch = () => {
emojiStore.getAll(lastKey && IDBKeyRange.lowerBound(lastKey, true), BATCH_SIZE).onsuccess = e => {
const results = e.target.result;
for (const result of results) {
lastKey = result.unicode;
if (predicate(result)) {
return cb(result)
}
}
if (results.length < BATCH_SIZE) {
return cb()
}
processNextBatch();
};
};
processNextBatch();
})
}
async function loadData (db, emojiData, url, eTag) {
try {
const transformedData = transformEmojiData(emojiData);
await dbPromise(db, [STORE_EMOJI, STORE_KEYVALUE], MODE_READWRITE, ([emojiStore, metaStore], txn) => {
let oldETag;
let oldUrl;
let todo = 0;
function checkFetched () {
if (++todo === 2) { // 2 requests made
onFetched();
}
}
function onFetched () {
if (oldETag === eTag && oldUrl === url) {
// check again within the transaction to guard against concurrency, e.g. multiple browser tabs
return
}
// delete old data
emojiStore.clear();
// insert new data
for (const data of transformedData) {
emojiStore.put(data);
}
metaStore.put(eTag, KEY_ETAG);
metaStore.put(url, KEY_URL);
commit(txn);
}
getIDB(metaStore, KEY_ETAG, result => {
oldETag = result;
checkFetched();
});
getIDB(metaStore, KEY_URL, result => {
oldUrl = result;
checkFetched();
});
});
} finally {
}
}
async function getEmojiByGroup (db, group) {
return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {
const range = IDBKeyRange.bound([group, 0], [group + 1, 0], false, true);
getAllIDB(emojiStore.index(INDEX_GROUP_AND_ORDER), range, cb);
})
}
async function getEmojiBySearchQuery (db, query) {
const tokens = normalizeTokens(extractTokens(query));
if (!tokens.length) {
return []
}
return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {
// get all results that contain all tokens (i.e. an AND query)
const intermediateResults = [];
const checkDone = () => {
if (intermediateResults.length === tokens.length) {
onDone();
}
};
const onDone = () => {
const results = findCommonMembers(intermediateResults, _ => _.unicode);
cb(results.sort((a, b) => a.order < b.order ? -1 : 1));
};
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
const range = i === tokens.length - 1
? IDBKeyRange.bound(token, token + '\uffff', false, true) // treat last token as a prefix search
: IDBKeyRange.only(token); // treat all other tokens as an exact match
getAllIDB(emojiStore.index(INDEX_TOKENS), range, result => {
intermediateResults.push(result);
checkDone();
});
}
})
}
// This could have been implemented as an IDB index on shortcodes, but it seemed wasteful to do that
// when we can already query by tokens and this will give us what we're looking for 99.9% of the time
async function getEmojiByShortcode (db, shortcode) {
const emojis = await getEmojiBySearchQuery(db, shortcode);
// In very rare cases (e.g. the shortcode "v" as in "v for victory"), we cannot search because
// there are no usable tokens (too short in this case). In that case, we have to do an inefficient
// full-database scan, which I believe is an acceptable tradeoff for not having to have an extra
// index on shortcodes.
if (!emojis.length) {
const predicate = _ => ((_.shortcodes || []).includes(shortcode.toLowerCase()));
return (await doFullDatabaseScanForSingleResult(db, predicate)) || null
}
return emojis.filter(_ => {
const lowerShortcodes = (_.shortcodes || []).map(_ => _.toLowerCase());
return lowerShortcodes.includes(shortcode.toLowerCase())
})[0] || null
}
async function getEmojiByUnicode (db, unicode) {
return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => (
getIDB(emojiStore, unicode, result => {
if (result) {
return cb(result)
}
getIDB(emojiStore.index(INDEX_SKIN_UNICODE), unicode, result => cb(result || null));
})
))
}
function get (db, storeName, key) {
return dbPromise(db, storeName, MODE_READONLY, (store, txn, cb) => (
getIDB(store, key, cb)
))
}
function set (db, storeName, key, value) {
return dbPromise(db, storeName, MODE_READWRITE, (store, txn) => {
store.put(value, key);
commit(txn);
})
}
function incrementFavoriteEmojiCount (db, unicode) {
return dbPromise(db, STORE_FAVORITES, MODE_READWRITE, (store, txn) => (
getIDB(store, unicode, result => {
store.put((result || 0) + 1, unicode);
commit(txn);
})
))
}
function getTopFavoriteEmoji (db, customEmojiIndex, limit) {
if (limit === 0) {
return []
}
return dbPromise(db, [STORE_FAVORITES, STORE_EMOJI], MODE_READONLY, ([favoritesStore, emojiStore], txn, cb) => {
const results = [];
favoritesStore.index(INDEX_COUNT).openCursor(undefined, 'prev').onsuccess = e => {
const cursor = e.target.result;
if (!cursor) { // no more results
return cb(results)
}
function addResult (result) {
results.push(result);
if (results.length === limit) {
return cb(results) // done, reached the limit
}
cursor.continue();
}
const unicodeOrName = cursor.primaryKey;
const custom = customEmojiIndex.byName(unicodeOrName);
if (custom) {
return addResult(custom)
}
// This could be done in parallel (i.e. make the cursor and the get()s parallelized),
// but my testing suggests it's not actually faster.
getIDB(emojiStore, unicodeOrName, emoji => {
if (emoji) {
return addResult(emoji)
}
// emoji not found somehow, ignore (may happen if custom emoji change)
cursor.continue();
});
};
})
}
// trie data structure for prefix searches
// loosely based on https://github.com/nolanlawson/substring-trie
const CODA_MARKER = ''; // marks the end of the string
function trie (arr, itemToTokens) {
const map = new Map();
for (const item of arr) {
const tokens = itemToTokens(item);
for (const token of tokens) {
let currentMap = map;
for (let i = 0; i < token.length; i++) {
const char = token.charAt(i);
let nextMap = currentMap.get(char);
if (!nextMap) {
nextMap = new Map();
currentMap.set(char, nextMap);
}
currentMap = nextMap;
}
let valuesAtCoda = currentMap.get(CODA_MARKER);
if (!valuesAtCoda) {
valuesAtCoda = [];
currentMap.set(CODA_MARKER, valuesAtCoda);
}
valuesAtCoda.push(item);
}
}
const search = (query, exact) => {
let currentMap = map;
for (let i = 0; i < query.length; i++) {
const char = query.charAt(i);
const nextMap = currentMap.get(char);
if (nextMap) {
currentMap = nextMap;
} else {
return []
}
}
if (exact) {
const results = currentMap.get(CODA_MARKER);
return results || []
}
const results = [];
// traverse
const queue = [currentMap];
while (queue.length) {
const currentMap = queue.shift();
const entriesSortedByKey = [...currentMap.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1);
for (const [key, value] of entriesSortedByKey) {
if (key === CODA_MARKER) { // CODA_MARKER always comes first; it's the empty string
results.push(...value);
} else {
queue.push(value);
}
}
}
return results
};
return search
}
const requiredKeys$1 = [
'name',
'url'
];
function assertCustomEmojis (customEmojis) {
const isArray = customEmojis && Array.isArray(customEmojis);
const firstItemIsFaulty = isArray &&
customEmojis.length &&
(!customEmojis[0] || requiredKeys$1.some(key => !(key in customEmojis[0])));
if (!isArray || firstItemIsFaulty) {
throw new Error('Custom emojis are in the wrong format')
}
}
function customEmojiIndex (customEmojis) {
assertCustomEmojis(customEmojis);
const sortByName = (a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
//
// all()
//
const all = customEmojis.sort(sortByName);
//
// search()
//
const emojiToTokens = emoji => (
[...new Set((emoji.shortcodes || []).map(shortcode => extractTokens(shortcode)).flat())]
);
const searchTrie = trie(customEmojis, emojiToTokens);
const searchByExactMatch = _ => searchTrie(_, true);
const searchByPrefix = _ => searchTrie(_, false);
// Search by query for custom emoji. Similar to how we do this in IDB, the last token
// is treated as a prefix search, but every other one is treated as an exact match.
// Then we AND the results together
const search = query => {
const tokens = extractTokens(query);
const intermediateResults = tokens.map((token, i) => (
(i < tokens.length - 1 ? searchByExactMatch : searchByPrefix)(token)
));
return findCommonMembers(intermediateResults, _ => _.name).sort(sortByName)
};
//
// byShortcode, byName
//
const shortcodeToEmoji = new Map();
const nameToEmoji = new Map();
for (const customEmoji of customEmojis) {
nameToEmoji.set(customEmoji.name.toLowerCase(), customEmoji);
for (const shortcode of (customEmoji.shortcodes || [])) {
shortcodeToEmoji.set(shortcode.toLowerCase(), customEmoji);
}
}
const byShortcode = shortcode => shortcodeToEmoji.get(shortcode.toLowerCase());
const byName = name => nameToEmoji.get(name.toLowerCase());
return {
all,
search,
byShortcode,
byName
}
}
// remove some internal implementation details, i.e. the "tokens" array on the emoji object
// essentially, convert the emoji from the version stored in IDB to the version used in-memory
function cleanEmoji (emoji) {
if (!emoji) {
return emoji
}
delete emoji.tokens;
if (emoji.skinTones) {
const len = emoji.skinTones.length;
emoji.skins = Array(len);
for (let i = 0; i < len; i++) {
emoji.skins[i] = {
tone: emoji.skinTones[i],
unicode: emoji.skinUnicodes[i],
version: emoji.skinVersions[i]
};
}
delete emoji.skinTones;
delete emoji.skinUnicodes;
delete emoji.skinVersions;
}
return emoji
}
function warnETag (eTag) {
if (!eTag) {
console.warn('emoji-picker-element is more efficient if the dataSource server exposes an ETag header.');
}
}
const requiredKeys = [
'annotation',
'emoji',
'group',
'order',
'tags',
'version'
];
function assertEmojiData (emojiData) {
if (!emojiData ||
!Array.isArray(emojiData) ||
!emojiData[0] ||
(typeof emojiData[0] !== 'object') ||
requiredKeys.some(key => (!(key in emojiData[0])))) {
throw new Error('Emoji data is in the wrong format')
}
}
function assertStatus (response, dataSource) {
if (Math.floor(response.status / 100) !== 2) {
throw new Error('Failed to fetch: ' + dataSource + ': ' + response.status)
}
}
async function getETag (dataSource) {
const response = await fetch(dataSource, { method: 'HEAD' });
assertStatus(response, dataSource);
const eTag = response.headers.get('etag');
warnETag(eTag);
return eTag
}
async function getETagAndData (dataSource) {
const response = await fetch(dataSource);
assertStatus(response, dataSource);
const eTag = response.headers.get('etag');
warnETag(eTag);
const emojiData = await response.json();
assertEmojiData(emojiData);
return [eTag, emojiData]
}
// TODO: including these in blob-util.ts causes typedoc to generate docs for them,
/**
* Convert an `ArrayBuffer` to a binary string.
*
* Example:
*
* ```js
* var myString = blobUtil.arrayBufferToBinaryString(arrayBuff)
* ```
*
* @param buffer - array buffer
* @returns binary string
*/
function arrayBufferToBinaryString(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
var length = bytes.byteLength;
var i = -1;
while (++i < length) {
binary += String.fromCharCode(bytes[i]);
}
return binary;
}
/**
* Convert a binary string to an `ArrayBuffer`.
*
* ```js
* var myBuffer = blobUtil.binaryStringToArrayBuffer(binaryString)
* ```
*
* @param binary - binary string
* @returns array buffer
*/
function binaryStringToArrayBuffer(binary) {
var length = binary.length;
var buf = new ArrayBuffer(length);
var arr = new Uint8Array(buf);
var i = -1;
while (++i < length) {
arr[i] = binary.charCodeAt(i);
}
return buf;
}
// generate a checksum based on the stringified JSON
async function jsonChecksum (object) {
const inString = JSON.stringify(object);
const inBuffer = binaryStringToArrayBuffer(inString);
// this does not need to be cryptographically secure, SHA-1 is fine
const outBuffer = await crypto.subtle.digest('SHA-1', inBuffer);
const outBinString = arrayBufferToBinaryString(outBuffer);
const res = btoa(outBinString);
return res
}
async function checkForUpdates (db, dataSource) {
// just do a simple HEAD request first to see if the eTags match
let emojiData;
let eTag = await getETag(dataSource);
if (!eTag) { // work around lack of ETag/Access-Control-Expose-Headers
const eTagAndData = await getETagAndData(dataSource);
eTag = eTagAndData[0];
emojiData = eTagAndData[1];
if (!eTag) {
eTag = await jsonChecksum(emojiData);
}
}
if (await hasData(db, dataSource, eTag)) ; else {
if (!emojiData) {
const eTagAndData = await getETagAndData(dataSource);
emojiData = eTagAndData[1];
}
await loadData(db, emojiData, dataSource, eTag);
}
}
async function loadDataForFirstTime (db, dataSource) {
let [eTag, emojiData] = await getETagAndData(dataSource);
if (!eTag) {
// Handle lack of support for ETag or Access-Control-Expose-Headers
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers#Browser_compatibility
eTag = await jsonChecksum(emojiData);
}
await loadData(db, emojiData, dataSource, eTag);
}
class Database {
constructor ({ dataSource = DEFAULT_DATA_SOURCE, locale = DEFAULT_LOCALE, customEmoji = [] } = {}) {
this.dataSource = dataSource;
this.locale = locale;
this._dbName = `emoji-picker-element-${this.locale}`;
this._db = undefined;
this._lazyUpdate = undefined;
this._custom = customEmojiIndex(customEmoji);
this._clear = this._clear.bind(this);
this._ready = this._init();
}
async _init () {
const db = this._db = await openDatabase(this._dbName);
addOnCloseListener(this._dbName, this._clear);
const dataSource = this.dataSource;
const empty = await isEmpty(db);
if (empty) {
await loadDataForFirstTime(db, dataSource);
} else { // offline-first - do an update asynchronously
this._lazyUpdate = checkForUpdates(db, dataSource);
}
}
async ready () {
const checkReady = async () => {
if (!this._ready) {
this._ready = this._init();
}
return this._ready
};
await checkReady();
// There's a possibility of a race condition where the element gets added, removed, and then added again
// with a particular timing, which would set the _db to undefined.
// We *could* do a while loop here, but that seems excessive and could lead to an infinite loop.
if (!this._db) {
await checkReady();
}
}
async getEmojiByGroup (group) {
assertNumber(group);
await this.ready();
return uniqEmoji(await getEmojiByGroup(this._db, group)).map(cleanEmoji)
}
async getEmojiBySearchQuery (query) {
assertNonEmptyString(query);
await this.ready();
const customs = this._custom.search(query);
const natives = uniqEmoji(await getEmojiBySearchQuery(this._db, query)).map(cleanEmoji);
return [
...customs,
...natives
]
}
async getEmojiByShortcode (shortcode) {
assertNonEmptyString(shortcode);
await this.ready();
const custom = this._custom.byShortcode(shortcode);
if (custom) {
return custom
}
return cleanEmoji(await getEmojiByShortcode(this._db, shortcode))
}
async getEmojiByUnicodeOrName (unicodeOrName) {
assertNonEmptyString(unicodeOrName);
await this.ready();
const custom = this._custom.byName(unicodeOrName);
if (custom) {
return custom
}
return cleanEmoji(await getEmojiByUnicode(this._db, unicodeOrName))
}
async getPreferredSkinTone () {
await this.ready();
return (await get(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE)) || 0
}
async setPreferredSkinTone (skinTone) {
assertNumber(skinTone);
await this.ready();
return set(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE, skinTone)
}
async incrementFavoriteEmojiCount (unicodeOrName) {
assertNonEmptyString(unicodeOrName);
await this.ready();
return incrementFavoriteEmojiCount(this._db, unicodeOrName)
}
async getTopFavoriteEmoji (limit) {
assertNumber(limit);
await this.ready();
return (await getTopFavoriteEmoji(this._db, this._custom, limit)).map(cleanEmoji)
}
set customEmoji (customEmojis) {
this._custom = customEmojiIndex(customEmojis);
}
get customEmoji () {
return this._custom.all
}
async _shutdown () {
await this.ready(); // reopen if we've already been closed/deleted
try {
await this._lazyUpdate; // allow any lazy updates to process before closing/deleting
} catch (err) { /* ignore network errors (offline-first) */ }
}
// clear references to IDB, e.g. during a close event
_clear () {
// We don't need to call removeEventListener or remove the manual "close" listeners.
// The memory leak tests prove this is unnecessary. It's because:
// 1) IDBDatabases that can no longer fire "close" automatically have listeners GCed
// 2) we clear the manual close listeners in databaseLifecycle.js.
this._db = this._ready = this._lazyUpdate = undefined;
}
async close () {
await this._shutdown();
await closeDatabase(this._dbName);
}
async delete () {
await this._shutdown();
await deleteDatabase(this._dbName);
}
}
export { Database as default };

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'الفئات',
emojiUnsupportedMessage: 'متصفحك لا يدعم رموز المشاعر الملونة.',
favoritesLabel: 'المفضلة',
loadingMessage: 'جارٍ التحميل…',
networkErrorMessage: 'تعذر تحميل رمز مشاعر.',
regionLabel: 'منتقي رموز المشاعر',
searchDescription: 'عندما تكون نتائج البحث متاحة، اضغط السهم لأعلى أو لأسفل للتحديد واضغط enter للاختيار.',
searchLabel: 'بحث',
searchResultsLabel: 'نتائج البحث',
skinToneDescription: 'عند توسيع النتائج، اضغط السهم لأعلى أو لأسفل للتحديد واضغط enter للاختيار.',
skinToneLabel: 'اختر درجة لون البشرة (حاليًا {skinTone})',
skinTonesLabel: 'درجات لون البشرة',
skinTones: [
'افتراضي',
'فاتح',
'فاتح متوسط',
'متوسط',
'داكن متوسط',
'داكن'
],
categories: {
custom: 'مخصص',
'smileys-emotion': 'الوجوه الضاحكة ورموز المشاعر',
'people-body': 'الأشخاص والجسد',
'animals-nature': 'الحيوانات والطبيعة',
'food-drink': 'الطعام والشراب',
'travel-places': 'السفر والأماكن',
activities: 'الأنشطة',
objects: 'الأشياء',
symbols: 'الرموز',
flags: 'الأعلام'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Kategorien',
emojiUnsupportedMessage: 'Dein Browser unterstützt keine farbigen Emojis.',
favoritesLabel: 'Favoriten',
loadingMessage: 'Wird geladen…',
networkErrorMessage: 'Konnte Emoji nicht laden.',
regionLabel: 'Emoji auswählen',
searchDescription: 'Wenn Suchergebnisse verfügbar sind, wähle sie mit Pfeil rauf und runter, dann Eingabetaste, aus.',
searchLabel: 'Suchen',
searchResultsLabel: 'Suchergebnisse',
skinToneDescription: 'Wenn angezeigt, nutze Pfeiltasten rauf und runter zum Auswählen, Eingabe zum Akzeptieren.',
skinToneLabel: 'Wähle einen Hautton (aktuell {skinTone})',
skinTonesLabel: 'Hauttöne',
skinTones: [
'Standard',
'Hell',
'Mittel-hell',
'Mittel',
'Mittel-dunkel',
'Dunkel'
],
categories: {
custom: 'Benutzerdefiniert',
'smileys-emotion': 'Smileys und Emoticons',
'people-body': 'Menschen und Körper',
'animals-nature': 'Tiere und Natur',
'food-drink': 'Essen und Trinken',
'travel-places': 'Reisen und Orte',
activities: 'Aktivitäten',
objects: 'Objekte',
symbols: 'Symbole',
flags: 'Flaggen'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Categories',
emojiUnsupportedMessage: 'Your browser does not support color emoji.',
favoritesLabel: 'Favorites',
loadingMessage: 'Loading…',
networkErrorMessage: 'Could not load emoji.',
regionLabel: 'Emoji picker',
searchDescription: 'When search results are available, press up or down to select and enter to choose.',
searchLabel: 'Search',
searchResultsLabel: 'Search results',
skinToneDescription: 'When expanded, press up or down to select and enter to choose.',
skinToneLabel: 'Choose a skin tone (currently {skinTone})',
skinTonesLabel: 'Skin tones',
skinTones: [
'Default',
'Light',
'Medium-Light',
'Medium',
'Medium-Dark',
'Dark'
],
categories: {
custom: 'Custom',
'smileys-emotion': 'Smileys and emoticons',
'people-body': 'People and body',
'animals-nature': 'Animals and nature',
'food-drink': 'Food and drink',
'travel-places': 'Travel and places',
activities: 'Activities',
objects: 'Objects',
symbols: 'Symbols',
flags: 'Flags'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Categorías',
emojiUnsupportedMessage: 'El navegador no admite emojis de color.',
favoritesLabel: 'Favoritos',
loadingMessage: 'Cargando…',
networkErrorMessage: 'No se pudo cargar el emoji.',
regionLabel: 'Selector de emojis',
searchDescription: 'Cuando estén disponibles los resultados, pulsa la tecla hacia arriba o hacia abajo para seleccionar y la tecla intro para elegir.',
searchLabel: 'Buscar',
searchResultsLabel: 'Resultados de búsqueda',
skinToneDescription: 'Cuando se abran las opciones, pulsa la tecla hacia arriba o hacia abajo para seleccionar y la tecla intro para elegir.',
skinToneLabel: 'Elige un tono de piel ({skinTone} es el actual)',
skinTonesLabel: 'Tonos de piel',
skinTones: [
'Predeterminado',
'Claro',
'Claro medio',
'Medio',
'Oscuro medio',
'Oscuro'
],
categories: {
custom: 'Personalizado',
'smileys-emotion': 'Emojis y emoticones',
'people-body': 'Personas y partes del cuerpo',
'animals-nature': 'Animales y naturaleza',
'food-drink': 'Comida y bebida',
'travel-places': 'Viajes y lugares',
activities: 'Actividades',
objects: 'Objetos',
symbols: 'Símbolos',
flags: 'Banderas'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Catégories',
emojiUnsupportedMessage: 'Votre navigateur ne soutient pas les emojis en couleur.',
favoritesLabel: 'Favoris',
loadingMessage: 'Chargement en cours…',
networkErrorMessage: 'Impossible de charger les emojis.',
regionLabel: 'Choisir un emoji',
searchDescription: 'Quand les résultats sont disponisbles, appuyez la fleche vers le haut ou le bas et la touche entrée pour choisir.',
searchLabel: 'Rechercher',
searchResultsLabel: 'Résultats',
skinToneDescription: 'Quand disponible, appuyez la fleche vers le haut ou le bas et la touch entrée pour choisir.',
skinToneLabel: 'Choisir une couleur de peau (actuellement {skinTone})',
skinTonesLabel: 'Couleurs de peau',
skinTones: [
'Défaut',
'Clair',
'Moyennement clair',
'Moyen',
'Moyennement sombre',
'Sombre'
],
categories: {
custom: 'Customisé',
'smileys-emotion': 'Les smileyes et les émoticônes',
'people-body': 'Les gens et le corps',
'animals-nature': 'Les animaux et la nature',
'food-drink': 'La nourriture et les boissons',
'travel-places': 'Les voyages et les endroits',
activities: 'Les activités',
objects: 'Les objets',
symbols: 'Les symbols',
flags: 'Les drapeaux'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'श्रेणियाँ',
emojiUnsupportedMessage: 'आपका ब्राउज़र कलर इमोजी का समर्थन नहीं करता।',
favoritesLabel: 'पसंदीदा',
loadingMessage: 'लोड हो रहा है...',
networkErrorMessage: 'इमोजी लोड नहीं हो सके।',
regionLabel: 'इमोजी चुननेवाला',
searchDescription: 'जब खोज परिणाम उपलब्ध हों तो चयन करने के लिए ऊपर या नीचे दबाएं और चुनने के लिए एंटर दबाएं।',
searchLabel: 'खोज',
searchResultsLabel: 'खोज के परिणाम',
skinToneDescription: 'जब विस्तृत किया जाता है तो चयन करने के लिए ऊपर या नीचे दबाएं और चुनने के लिए एंटर दबाएं।',
skinToneLabel: 'त्वचा का रंग चुनें (वर्तमान में {skinTone})',
skinTonesLabel: 'त्वचा के रंग',
skinTones: [
'डिफॉल्ट',
'हल्का',
'मध्यम हल्का',
'मध्यम',
'मध्यम गहरा',
'गहरा'
],
categories: {
custom: 'कस्टम',
'smileys-emotion': 'स्माइली और इमोटिकॉन्स',
'people-body': 'लोग और शरीर',
'animals-nature': 'पशु और प्रकृति',
'food-drink': 'खाद्य और पेय',
'travel-places': 'यात्रा और स्थान',
activities: 'गतिविधियां',
objects: 'वस्तुएं',
symbols: 'प्रतीक',
flags: 'झंडे'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Kategori',
emojiUnsupportedMessage: 'Browser Anda tidak mendukung emoji warna.',
favoritesLabel: 'Favorit',
loadingMessage: 'Memuat...',
networkErrorMessage: 'Tidak dapat memuat emoji.',
regionLabel: 'Pemilih emoji',
searchDescription: 'Ketika hasil pencarian tersedia, tekan atas atau bawah untuk menyeleksi dan enter untuk memilih.',
searchLabel: 'Cari',
searchResultsLabel: 'Hasil Pencarian',
skinToneDescription: 'Saat diperluas tekan atas atau bawah untuk menyeleksi dan enter untuk memilih.',
skinToneLabel: 'Pilih warna skin (saat ini {skinTone})',
skinTonesLabel: 'Warna skin',
skinTones: [
'Default',
'Light',
'Medium light',
'Medium',
'Medium dark',
'Dark'
],
categories: {
custom: 'Kustom',
'smileys-emotion': 'Smiley dan emoticon',
'people-body': 'Orang dan bagian tubuh',
'animals-nature': 'Hewan dan tumbuhan',
'food-drink': 'Makanan dan minuman',
'travel-places': 'Rekreasi dan tempat',
activities: 'Aktivitas',
objects: 'Objek',
symbols: 'Simbol',
flags: 'Bendera'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Categorie',
emojiUnsupportedMessage: 'Il tuo browser non supporta le emoji colorate.',
favoritesLabel: 'Preferiti',
loadingMessage: 'Caricamento...',
networkErrorMessage: 'Impossibile caricare le emoji.',
regionLabel: 'Selezione emoji',
searchDescription: 'Quando i risultati della ricerca sono disponibili, premi su o giù per selezionare e invio per scegliere.',
searchLabel: 'Cerca',
searchResultsLabel: 'Risultati di ricerca',
skinToneDescription: 'Quando espanso, premi su o giù per selezionare e invio per scegliere.',
skinToneLabel: 'Scegli una tonalità della pelle (corrente {skinTone})',
skinTonesLabel: 'Tonalità della pelle',
skinTones: [
'Predefinita',
'Chiara',
'Medio-Chiara',
'Media',
'Medio-Scura',
'Scura'
],
categories: {
custom: 'Personalizzata',
'smileys-emotion': 'Faccine ed emozioni',
'people-body': 'Persone e corpi',
'animals-nature': 'Animali e natura',
'food-drink': 'Cibi e bevande',
'travel-places': 'Viaggi e luoghi',
activities: 'Attività',
objects: 'Oggetti',
symbols: 'Simboli',
flags: 'Bandiere'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Kategori',
emojiUnsupportedMessage: 'Penyemak imbas anda tidak menyokong emoji warna.',
favoritesLabel: 'Kegemaran',
loadingMessage: 'Memuat…',
networkErrorMessage: 'Tidak dapat memuatkan emoji.',
regionLabel: 'Pemilih emoji',
searchDescription: 'Apabila hasil carian tersedia, tekan atas atau bawah untuk memilih dan tekan masukkan untuk memilih.',
searchLabel: 'Cari',
searchResultsLabel: 'Hasil carian',
skinToneDescription: 'Apabila dikembangkan, tekan atas atau bawah untuk memilih dan tekan masukkan untuk memilih.',
skinToneLabel: 'Pilih warna kulit (pada masa ini {skinTone})',
skinTonesLabel: 'Warna kulit',
skinTones: [
'Lalai',
'Cerah',
'Kuning langsat',
'Sederhana cerah',
'Sawo matang',
'Gelap'
],
categories: {
custom: 'Tersuai',
'smileys-emotion': 'Smiley dan emotikon',
'people-body': 'Orang dan badan',
'animals-nature': 'Haiwan dan alam semula jadi',
'food-drink': 'Makanan dan minuman',
'travel-places': 'Perjalanan dan tempat',
activities: 'Aktiviti',
objects: 'Objek',
symbols: 'Simbol',
flags: 'Bendera'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Categorieën',
emojiUnsupportedMessage: 'Uw browser ondersteunt geen kleurenemoji.',
favoritesLabel: 'Favorieten',
loadingMessage: 'Bezig met laden…',
networkErrorMessage: 'Kan emoji niet laden.',
regionLabel: 'Emoji-kiezer',
searchDescription: 'Als er zoekresultaten beschikbaar zijn, drukt u op omhoog of omlaag om te selecteren en op enter om te kiezen.',
searchLabel: 'Zoeken',
searchResultsLabel: 'Zoekresultaten',
skinToneDescription: 'Wanneer uitgevouwen, druk omhoog of omlaag om te selecteren en enter om te kiezen.',
skinToneLabel: 'Kies een huidskleur (momenteel {skinTone})',
skinTonesLabel: 'Huidskleuren',
skinTones: [
'Standaard',
'Licht',
'Medium-Licht',
'Medium',
'Middeldonker',
'Donker'
],
categories: {
custom: 'Aangepast',
'smileys-emotion': 'Smileys en emoticons',
'people-body': 'Mensen en lichaam',
'animals-nature': 'Dieren en natuur',
'food-drink': 'Eten en drinken',
'travel-places': 'Reizen en plaatsen',
activities: 'Activiteiten',
objects: 'Voorwerpen',
symbols: 'Symbolen',
flags: 'Vlaggen'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Kategorie',
emojiUnsupportedMessage: 'Twoja przeglądarka nie wspiera kolorowych emotikon.',
favoritesLabel: 'Ulubione',
loadingMessage: 'Ładuję…',
networkErrorMessage: 'Nie można załadować emoji.',
regionLabel: 'Selektor emoji',
searchDescription: 'Kiedy wyniki wyszukiwania będą dostępne, wciśnij góra lub dół aby wybrać oraz enter aby zatwierdzić wybór.',
searchLabel: 'Wyszukaj',
searchResultsLabel: 'Wyniki wyszukiwania',
skinToneDescription: 'Po rozwinięciu wciśnij góra lub dół aby wybrać oraz enter aby zatwierdzić wybór.',
skinToneLabel: 'Wybierz odcień skóry (aktualnie {skinTone})',
skinTonesLabel: 'Odcienie skóry',
skinTones: [
'Domyślna',
'Jasna',
'Średnio-jasna',
'Średnia',
'Średnio-ciemna',
'Ciemna'
],
categories: {
custom: 'Własne',
'smileys-emotion': 'Uśmiechy',
'people-body': 'Ludzie',
'animals-nature': 'Zwierzęta i natura',
'food-drink': 'Żywność i napoje',
'travel-places': 'Podróże i miejsca',
activities: 'Aktywności',
objects: 'Obiekty',
symbols: 'Symbole',
flags: 'Flagi'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Categorias',
emojiUnsupportedMessage: 'Seu navegador não suporta emojis coloridos.',
favoritesLabel: 'Favoritos',
loadingMessage: 'Carregando…',
networkErrorMessage: 'Não foi possível carregar o emoji.',
regionLabel: 'Seletor de emoji',
searchDescription: 'Quando os resultados da pesquisa estiverem disponíveis, pressione para cima ou para baixo para selecionar e “enter” para escolher.',
searchLabel: 'Procurar',
searchResultsLabel: 'Resultados da pesquisa',
skinToneDescription: 'Quando expandido, pressione para cima ou para baixo para selecionar e “enter” para escolher.',
skinToneLabel: 'Escolha um tom de pele (atualmente {skinTone})',
skinTonesLabel: 'Tons de pele',
skinTones: [
'Padrão',
'Claro',
'Claro médio',
'Médio',
'Escuro médio',
'Escuro'
],
categories: {
custom: 'Personalizar',
'smileys-emotion': 'Carinhas e emoticons',
'people-body': 'Pessoas e corpo',
'animals-nature': 'Animais e natureza',
'food-drink': 'Alimentos e bebidas',
'travel-places': 'Viagem e lugares',
activities: 'Atividades',
objects: 'Objetos',
symbols: 'Símbolos',
flags: 'Bandeiras'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Categorias',
emojiUnsupportedMessage: 'O seu browser não suporta emojis.',
favoritesLabel: 'Favoritos',
loadingMessage: 'A Carregar…',
networkErrorMessage: 'Não foi possível carregar o emoji.',
regionLabel: 'Emoji picker',
searchDescription: 'Quando os resultados da pesquisa estiverem disponíveis, pressione para cima ou para baixo para selecionar e digite para escolher.',
searchLabel: 'Procurar',
searchResultsLabel: 'Resultados da procura',
skinToneDescription: 'Quando expandido, pressione para cima ou para baixo para selecionar e digite para escolher.',
skinToneLabel: 'Escolha um tom de pele (atual {skinTone})',
skinTonesLabel: 'Tons de pele',
skinTones: [
'Pré-definido',
'Claro',
'Médio-Claro',
'Médio',
'Médio-Escuro',
'Escuro'
],
categories: {
custom: 'Personalizados',
'smileys-emotion': 'Smileys e emoticons',
'people-body': 'Pessoas e corpo',
'animals-nature': 'Animais e natureza',
'food-drink': 'Comida e bebida',
'travel-places': 'Viagens e locais',
activities: 'Atividades',
objects: 'Objetos',
symbols: 'Símbolos',
flags: 'Bandeiras'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Категории',
emojiUnsupportedMessage: 'Ваш браузер не поддерживает цветные эмодзи.',
favoritesLabel: 'Избранное',
loadingMessage: 'Загрузка…',
networkErrorMessage: 'Не удалось загрузить эмодзи. Попробуйте перезагрузить страницу.',
regionLabel: 'Выберите эмодзи',
searchDescription: 'Когда результаты поиска станут доступны, выберите их с помощью стрелок вверх и вниз, затем нажмите для подтверждения.',
searchLabel: 'Искать',
searchResultsLabel: 'Результаты поиска',
skinToneDescription: 'При отображении используйте клавиши со стрелками вверх и вниз для выбора, нажмите для подтверждения.',
skinToneLabel: 'Выберите оттенок кожи (текущий {skinTone})',
skinTonesLabel: 'Оттенки кожи',
skinTones: [
'Стандартный',
'Светлый',
'Средне-светлый',
'Средний',
'Средне-темный',
'Темный'
],
categories: {
custom: 'Пользовательский',
'smileys-emotion': 'Смайлики и Эмотиконы',
'people-body': 'Люди и Тела',
'animals-nature': 'Животные и Природа',
'food-drink': 'Еда и Напитки',
'travel-places': 'Путешествия и Места',
activities: 'Виды деятельности',
objects: 'Объекты',
symbols: 'Символы',
flags: 'Флаги'
}
}

View File

@ -1,34 +0,0 @@
export default {
categoriesLabel: 'Kategoriler',
emojiUnsupportedMessage: 'Tarayıcınız renkli emojiyi desteklemiyor.',
favoritesLabel: 'Favoriler',
loadingMessage: 'Yükleniyor…',
networkErrorMessage: 'Emoji yüklenemedi.',
regionLabel: 'Emoji seçici',
searchDescription: 'Arama sonuçları mevcut olduğunda seçmek için yukarı veya aşağı basın ve seçmek için girin.',
searchLabel: 'Arama',
searchResultsLabel: 'Arama sonuçları',
skinToneDescription: 'Genişletildiğinde seçmek için yukarı veya aşağı basın ve seçmek için girin.',
skinToneLabel: 'Cilt tonu seçin (şu anda {skinTone})',
skinTonesLabel: 'Cilt tonları',
skinTones: [
'Varsayılan',
'Işık',
'Orta ışık',
'Orta',
'Orta koyu',
'Karanlık'
],
categories: {
custom: 'Gelenek',
'smileys-emotion': 'Suratlar ve ifadeler',
'people-body': 'İnsanlar ve vücut',
'animals-nature': 'Hayvanlar ve doğa',
'food-drink': 'Yiyecek ve içecek',
'travel-places': 'Seyahat ve yerler',
activities: 'Aktiviteler',
objects: 'Nesneler',
symbols: 'Semboller',
flags: 'Bayraklar'
}
}

View File

@ -1,27 +0,0 @@
export default {
categoriesLabel: '类别',
emojiUnsupportedMessage: '您的浏览器不支持彩色表情符号。',
favoritesLabel: '收藏夹',
loadingMessage: '正在加载…',
networkErrorMessage: '无法加载表情符号。',
regionLabel: '表情符号选择器',
searchDescription: '当搜索结果可用时,按向上或向下选择并输入选择。',
searchLabel: '搜索',
searchResultsLabel: '搜索结果',
skinToneDescription: '展开时,按向上或向下键进行选择,按回车键进行选择。',
skinToneLabel: '选择肤色(当前为 {skinTone}',
skinTonesLabel: '肤色',
skinTones: ['默认', '明亮', '微亮', '中等', '微暗', '暗'],
categories: {
custom: '自定义',
'smileys-emotion': '笑脸和表情',
'people-body': '人物和身体',
'animals-nature': '动物与自然',
'food-drink': '食品饮料',
'travel-places': '旅行和地方',
activities: '活动',
objects: '物体',
symbols: '符号',
flags: '旗帜'
}
}

View File

@ -1,3 +0,0 @@
import Picker from './picker.js'
import Database from './database.js'
export { Picker, Database }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
Subproject commit 1b7ae3d2e3c609a4424f90249d8fea7b879d52e5