Good enough emoji picker

This commit is contained in:
Philipp Heckel 2022-04-05 19:40:34 -04:00
parent 328aca48ab
commit 35ddcb27f0

View file

@ -1,15 +1,20 @@
import * as React from 'react'; import * as React from 'react';
import {useRef, useState} from 'react'; import {useRef, useState} from 'react';
import Popover from '@mui/material/Popover';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import {rawEmojis} from '../app/emojis'; import {rawEmojis} from '../app/emojis';
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import {InputAdornment} from "@mui/material"; import {ClickAwayListener, Fade, InputAdornment, styled} from "@mui/material";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import {Close} from "@mui/icons-material"; import {Close} from "@mui/icons-material";
import Popper from "@mui/material/Popper";
import {splitNoEmpty} from "../app/utils";
// Create emoji list by category and create a search base (string with all search words)
//
// This also filters emojis that are not supported by Desktop Chrome.
// This is a hack, but on Ubuntu 18.04, with Chrome 99, only Emoji <= 11 are supported.
// Create emoji list by category; filter emojis that are not supported by Desktop Chrome
const emojisByCategory = {}; const emojisByCategory = {};
const isDesktopChrome = /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent); const isDesktopChrome = /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent);
const maxSupportedVersionForDesktopChrome = 11; const maxSupportedVersionForDesktopChrome = 11;
@ -21,7 +26,9 @@ rawEmojis.forEach(emoji => {
const unicodeVersion = parseFloat(emoji.unicode_version); const unicodeVersion = parseFloat(emoji.unicode_version);
const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome;
if (supportedEmoji) { if (supportedEmoji) {
emojisByCategory[emoji.category].push(emoji); const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`;
const emojiWithSearchBase = { ...emoji, searchBase: searchBase };
emojisByCategory[emoji.category].push(emojiWithSearchBase);
} }
} catch (e) { } catch (e) {
// Nothing. Ignore. // Nothing. Ignore.
@ -32,79 +39,77 @@ const EmojiPicker = (props) => {
const open = Boolean(props.anchorEl); const open = Boolean(props.anchorEl);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const searchRef = useRef(null); const searchRef = useRef(null);
const searchFields = splitNoEmpty(search.toLowerCase(), " ");
/*
FIXME Search is inefficient, somehow make it faster
useEffect(() => {
const matching = rawEmojis.filter(e => {
const searchLower = search.toLowerCase();
return e.description.toLowerCase().indexOf(searchLower) !== -1
|| matchInArray(e.aliases, searchLower)
|| matchInArray(e.tags, searchLower);
});
console.log("matching", matching.length);
}, [search]);
*/
const handleSearchClear = () => { const handleSearchClear = () => {
setSearch(""); setSearch("");
searchRef.current?.focus(); searchRef.current?.focus();
}; };
return ( return (
<> <Popper
<Popover open={open}
open={open} anchorEl={props.anchorEl}
elevation={3} placement="bottom-start"
onClose={props.onClose} sx={{ zIndex: 10005 }}
anchorEl={props.anchorEl} transition
anchorOrigin={{ >
vertical: 'bottom', {({ TransitionProps }) => (
horizontal: 'left', <ClickAwayListener onClickAway={props.onClose}>
}} <Fade {...TransitionProps} timeout={350}>
> <Box sx={{
<Box sx={{ padding: 2, paddingRight: 0, width: "370px", maxHeight: "300px" }}> boxShadow: 3,
<TextField padding: 2,
inputRef={searchRef} paddingRight: 0,
margin="dense" paddingBottom: 1,
size="small" width: "380px",
placeholder="Search emoji" maxHeight: "300px",
value={search} backgroundColor: 'background.paper',
onChange={ev => setSearch(ev.target.value)} overflowY: "scroll"
type="text" }}>
variant="standard" <TextField
fullWidth inputRef={searchRef}
sx={{ marginTop: 0, paddingRight: 2 }} margin="dense"
InputProps={{ size="small"
endAdornment: placeholder="Search emoji"
<InputAdornment position="end" sx={{ display: (search) ? '' : 'none' }}> value={search}
<IconButton size="small" onClick={handleSearchClear} edge="end"><Close/></IconButton> onChange={ev => setSearch(ev.target.value)}
</InputAdornment> type="text"
}} variant="standard"
/> fullWidth
<Box sx={{ display: "flex", flexWrap: "wrap", paddingRight: 0, marginTop: 1 }}> sx={{ marginTop: 0, marginBottom: "12px", paddingRight: 2 }}
{Object.keys(emojisByCategory).map(category => InputProps={{
<Category endAdornment:
key={category} <InputAdornment position="end" sx={{ display: (search) ? '' : 'none' }}>
title={category} <IconButton size="small" onClick={handleSearchClear} edge="end"><Close/></IconButton>
emojis={emojisByCategory[category]} </InputAdornment>
search={search.toLowerCase()} }}
onPick={props.onEmojiPick}
/> />
)} <Box sx={{ display: "flex", flexWrap: "wrap", paddingRight: 0, marginTop: 1 }}>
</Box> {Object.keys(emojisByCategory).map(category =>
</Box> <Category
</Popover> key={category}
</> title={category}
emojis={emojisByCategory[category]}
search={searchFields}
onPick={props.onEmojiPick}
/>
)}
</Box>
</Box>
</Fade>
</ClickAwayListener>
)}
</Popper>
); );
}; };
const Category = (props) => { const Category = (props) => {
const showTitle = !props.search; const showTitle = props.search.length === 0;
return ( return (
<> <>
{showTitle && {showTitle &&
<Typography variant="body1" sx={{ width: "100%", marginTop: 1, marginBottom: 1 }}> <Typography variant="body1" sx={{ width: "100%", marginBottom: 1 }}>
{props.title} {props.title}
</Typography> </Typography>
} }
@ -122,39 +127,43 @@ const Category = (props) => {
const Emoji = (props) => { const Emoji = (props) => {
const emoji = props.emoji; const emoji = props.emoji;
const search = props.search; const matches = emojiMatches(emoji, props.search);
const matches = search === ""
|| emoji.description.toLowerCase().indexOf(search) !== -1
|| matchInArray(emoji.aliases, search)
|| matchInArray(emoji.tags, search);
if (!matches) {
return null;
}
return ( return (
<div <EmojiDiv
onClick={props.onClick} onClick={props.onClick}
title={`${emoji.description} (${emoji.aliases[0]})`} title={`${emoji.description} (${emoji.aliases[0]})`}
style={{ style={{ display: (matches) ? '' : 'none' }}
fontSize: "30px",
width: "30px",
height: "30px",
marginTop: "8px",
marginBottom: "8px",
marginRight: "8px",
lineHeight: "30px",
cursor: "pointer"
}}
> >
{props.emoji.emoji} {props.emoji.emoji}
</div> </EmojiDiv>
); );
}; };
const matchInArray = (arr, search) => { const EmojiDiv = styled("div")({
if (!arr || !search) { fontSize: "30px",
return false; width: "30px",
height: "30px",
marginTop: "8px",
marginBottom: "8px",
marginRight: "8px",
lineHeight: "30px",
cursor: "pointer",
opacity: 0.85,
"&:hover": {
opacity: 1
} }
return arr.filter(s => s.indexOf(search) !== -1).length > 0; });
const emojiMatches = (emoji, words) => {
if (words.length === 0) {
return true;
}
for (const word of words) {
if (emoji.searchBase.indexOf(word) === -1) {
return false;
}
}
return true;
} }
export default EmojiPicker; export default EmojiPicker;