Good enough emoji picker
This commit is contained in:
parent
328aca48ab
commit
35ddcb27f0
1 changed files with 95 additions and 86 deletions
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue