Fuzzy-Searchable Emoji Picker
This commit is contained in:
parent
2f47a3570b
commit
0e9d549cf3
48 changed files with 1697 additions and 280 deletions
192
ts/components/emoji/lib.ts
Normal file
192
ts/components/emoji/lib.ts
Normal file
|
@ -0,0 +1,192 @@
|
|||
// @ts-ignore: untyped json
|
||||
import untypedData from 'emoji-datasource';
|
||||
import {
|
||||
compact,
|
||||
flatMap,
|
||||
groupBy,
|
||||
isNumber,
|
||||
keyBy,
|
||||
map,
|
||||
mapValues,
|
||||
sortBy,
|
||||
} from 'lodash';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
export type ValuesOf<T extends Array<any>> = T[number];
|
||||
|
||||
export const skinTones = ['1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF'];
|
||||
|
||||
export type SkinToneKey = '1F3FB' | '1F3FC' | '1F3FD' | '1F3FE' | '1F3FF';
|
||||
|
||||
export type EmojiData = {
|
||||
name: string;
|
||||
unified: string;
|
||||
non_qualified: string | null;
|
||||
docomo: string | null;
|
||||
au: string | null;
|
||||
softbank: string | null;
|
||||
google: string | null;
|
||||
image: string;
|
||||
sheet_x: number;
|
||||
sheet_y: number;
|
||||
short_name: string;
|
||||
short_names: Array<string>;
|
||||
text: string | null;
|
||||
texts: Array<string> | null;
|
||||
category: string;
|
||||
sort_order: number;
|
||||
added_in: string;
|
||||
has_img_apple: boolean;
|
||||
has_img_google: boolean;
|
||||
has_img_twitter: boolean;
|
||||
has_img_emojione: boolean;
|
||||
has_img_facebook: boolean;
|
||||
has_img_messenger: boolean;
|
||||
skin_variations?: {
|
||||
[key: string]: {
|
||||
unified: string;
|
||||
non_qualified: null;
|
||||
image: string;
|
||||
sheet_x: number;
|
||||
sheet_y: number;
|
||||
added_in: string;
|
||||
has_img_apple: boolean;
|
||||
has_img_google: boolean;
|
||||
has_img_twitter: boolean;
|
||||
has_img_emojione: boolean;
|
||||
has_img_facebook: boolean;
|
||||
has_img_messenger: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const data: Array<EmojiData> = untypedData;
|
||||
|
||||
export const dataByShortName = keyBy(data, 'short_name');
|
||||
|
||||
data.forEach(emoji => {
|
||||
const { short_names } = emoji;
|
||||
if (short_names) {
|
||||
short_names.forEach(name => {
|
||||
dataByShortName[name] = emoji;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const dataByCategory = mapValues(
|
||||
groupBy(data, ({ category }) => {
|
||||
if (category === 'Activities') {
|
||||
return 'activity';
|
||||
}
|
||||
|
||||
if (category === 'Animals & Nature') {
|
||||
return 'animal';
|
||||
}
|
||||
|
||||
if (category === 'Flags') {
|
||||
return 'flag';
|
||||
}
|
||||
|
||||
if (category === 'Food & Drink') {
|
||||
return 'food';
|
||||
}
|
||||
|
||||
if (category === 'Objects') {
|
||||
return 'object';
|
||||
}
|
||||
|
||||
if (category === 'Travel & Places') {
|
||||
return 'travel';
|
||||
}
|
||||
|
||||
if (category === 'Smileys & People') {
|
||||
return 'emoji';
|
||||
}
|
||||
|
||||
if (category === 'Symbols') {
|
||||
return 'symbol';
|
||||
}
|
||||
|
||||
return 'misc';
|
||||
}),
|
||||
arr => sortBy(arr, 'sort_order')
|
||||
);
|
||||
|
||||
export function getSheetCoordinates(
|
||||
shortName: keyof typeof dataByShortName,
|
||||
skinTone?: SkinToneKey | number
|
||||
): [number, number] {
|
||||
const base = dataByShortName[shortName];
|
||||
|
||||
if (skinTone && base.skin_variations) {
|
||||
const variation = isNumber(skinTone) ? skinTones[skinTone - 1] : skinTone;
|
||||
const { sheet_x, sheet_y } = base.skin_variations[variation];
|
||||
|
||||
return [sheet_x, sheet_y];
|
||||
}
|
||||
|
||||
return [base.sheet_x, base.sheet_y];
|
||||
}
|
||||
|
||||
const fuse = new Fuse(data, {
|
||||
shouldSort: true,
|
||||
threshold: 0.3,
|
||||
location: 0,
|
||||
distance: 5,
|
||||
maxPatternLength: 20,
|
||||
minMatchCharLength: 1,
|
||||
keys: ['name', 'short_name', 'short_names'],
|
||||
});
|
||||
|
||||
export function search(query: string) {
|
||||
return fuse.search(query);
|
||||
}
|
||||
|
||||
const shortNames = new Set([
|
||||
...map(data, 'short_name'),
|
||||
...compact<string>(flatMap(data, 'short_names')),
|
||||
]);
|
||||
|
||||
export function isShortName(name: string) {
|
||||
return shortNames.has(name);
|
||||
}
|
||||
|
||||
export function unifiedToEmoji(unified: string) {
|
||||
return unified
|
||||
.split('-')
|
||||
.map(c => String.fromCodePoint(parseInt(c, 16)))
|
||||
.join('');
|
||||
}
|
||||
|
||||
export function convertShortName(shortName: string, skinTone: number = 0) {
|
||||
const base = dataByShortName[shortName];
|
||||
|
||||
if (!base) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (skinTone && base.skin_variations) {
|
||||
const toneKey = skinTones[0];
|
||||
const variation = base.skin_variations[toneKey];
|
||||
if (variation) {
|
||||
return unifiedToEmoji(variation.unified);
|
||||
}
|
||||
}
|
||||
|
||||
return unifiedToEmoji(base.unified);
|
||||
}
|
||||
|
||||
export function replaceColons(str: string) {
|
||||
return str.replace(/:[a-z0-9-_+]+:(?::skin-tone-[1-4]:)?/gi, m => {
|
||||
const [shortName = '', skinTone = '0'] = m
|
||||
.replace('skin-tone-', '')
|
||||
.split(':')
|
||||
.filter(Boolean);
|
||||
|
||||
if (shortName) {
|
||||
return convertShortName(shortName, parseInt(skinTone, 10));
|
||||
}
|
||||
|
||||
return m;
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue