|
@@ -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 (
|
|
|
- <>
|
|
|
|
|
- <Popover
|
|
|
|
|
- open={open}
|
|
|
|
|
- elevation={3}
|
|
|
|
|
- onClose={props.onClose}
|
|
|
|
|
- anchorEl={props.anchorEl}
|
|
|
|
|
- anchorOrigin={{
|
|
|
|
|
- vertical: 'bottom',
|
|
|
|
|
- horizontal: 'left',
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- <Box sx={{ padding: 2, paddingRight: 0, width: "370px", maxHeight: "300px" }}>
|
|
|
|
|
- <TextField
|
|
|
|
|
- inputRef={searchRef}
|
|
|
|
|
- margin="dense"
|
|
|
|
|
- size="small"
|
|
|
|
|
- placeholder="Search emoji"
|
|
|
|
|
- value={search}
|
|
|
|
|
- onChange={ev => setSearch(ev.target.value)}
|
|
|
|
|
- type="text"
|
|
|
|
|
- variant="standard"
|
|
|
|
|
- fullWidth
|
|
|
|
|
- sx={{ marginTop: 0, paddingRight: 2 }}
|
|
|
|
|
- InputProps={{
|
|
|
|
|
- endAdornment:
|
|
|
|
|
- <InputAdornment position="end" sx={{ display: (search) ? '' : 'none' }}>
|
|
|
|
|
- <IconButton size="small" onClick={handleSearchClear} edge="end"><Close/></IconButton>
|
|
|
|
|
- </InputAdornment>
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- <Box sx={{ display: "flex", flexWrap: "wrap", paddingRight: 0, marginTop: 1 }}>
|
|
|
|
|
- {Object.keys(emojisByCategory).map(category =>
|
|
|
|
|
- <Category
|
|
|
|
|
- key={category}
|
|
|
|
|
- title={category}
|
|
|
|
|
- emojis={emojisByCategory[category]}
|
|
|
|
|
- search={search.toLowerCase()}
|
|
|
|
|
- onPick={props.onEmojiPick}
|
|
|
|
|
|
|
+ <Popper
|
|
|
|
|
+ open={open}
|
|
|
|
|
+ anchorEl={props.anchorEl}
|
|
|
|
|
+ placement="bottom-start"
|
|
|
|
|
+ sx={{ zIndex: 10005 }}
|
|
|
|
|
+ transition
|
|
|
|
|
+ >
|
|
|
|
|
+ {({ TransitionProps }) => (
|
|
|
|
|
+ <ClickAwayListener onClickAway={props.onClose}>
|
|
|
|
|
+ <Fade {...TransitionProps} timeout={350}>
|
|
|
|
|
+ <Box sx={{
|
|
|
|
|
+ boxShadow: 3,
|
|
|
|
|
+ padding: 2,
|
|
|
|
|
+ paddingRight: 0,
|
|
|
|
|
+ paddingBottom: 1,
|
|
|
|
|
+ width: "380px",
|
|
|
|
|
+ maxHeight: "300px",
|
|
|
|
|
+ backgroundColor: 'background.paper',
|
|
|
|
|
+ overflowY: "scroll"
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <TextField
|
|
|
|
|
+ inputRef={searchRef}
|
|
|
|
|
+ margin="dense"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ placeholder="Search emoji"
|
|
|
|
|
+ value={search}
|
|
|
|
|
+ onChange={ev => setSearch(ev.target.value)}
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ variant="standard"
|
|
|
|
|
+ fullWidth
|
|
|
|
|
+ sx={{ marginTop: 0, marginBottom: "12px", paddingRight: 2 }}
|
|
|
|
|
+ InputProps={{
|
|
|
|
|
+ endAdornment:
|
|
|
|
|
+ <InputAdornment position="end" sx={{ display: (search) ? '' : 'none' }}>
|
|
|
|
|
+ <IconButton size="small" onClick={handleSearchClear} edge="end"><Close/></IconButton>
|
|
|
|
|
+ </InputAdornment>
|
|
|
|
|
+ }}
|
|
|
/>
|
|
/>
|
|
|
- )}
|
|
|
|
|
- </Box>
|
|
|
|
|
- </Box>
|
|
|
|
|
- </Popover>
|
|
|
|
|
- </>
|
|
|
|
|
|
|
+ <Box sx={{ display: "flex", flexWrap: "wrap", paddingRight: 0, marginTop: 1 }}>
|
|
|
|
|
+ {Object.keys(emojisByCategory).map(category =>
|
|
|
|
|
+ <Category
|
|
|
|
|
+ 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 = search === ""
|
|
|
|
|
- || emoji.description.toLowerCase().indexOf(search) !== -1
|
|
|
|
|
- || matchInArray(emoji.aliases, search)
|
|
|
|
|
- || matchInArray(emoji.tags, search);
|
|
|
|
|
- if (!matches) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const matches = emojiMatches(emoji, props.search);
|
|
|
return (
|
|
return (
|
|
|
- <div
|
|
|
|
|
|
|
+ <EmojiDiv
|
|
|
onClick={props.onClick}
|
|
onClick={props.onClick}
|
|
|
title={`${emoji.description} (${emoji.aliases[0]})`}
|
|
title={`${emoji.description} (${emoji.aliases[0]})`}
|
|
|
- style={{
|
|
|
|
|
- fontSize: "30px",
|
|
|
|
|
- width: "30px",
|
|
|
|
|
- height: "30px",
|
|
|
|
|
- marginTop: "8px",
|
|
|
|
|
- marginBottom: "8px",
|
|
|
|
|
- marginRight: "8px",
|
|
|
|
|
- lineHeight: "30px",
|
|
|
|
|
- cursor: "pointer"
|
|
|
|
|
- }}
|
|
|
|
|
|
|
+ style={{ display: (matches) ? '' : 'none' }}
|
|
|
>
|
|
>
|
|
|
{props.emoji.emoji}
|
|
{props.emoji.emoji}
|
|
|
- </div>
|
|
|
|
|
|
|
+ </EmojiDiv>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const matchInArray = (arr, search) => {
|
|
|
|
|
- if (!arr || !search) {
|
|
|
|
|
- return false;
|
|
|
|
|
|
|
+const EmojiDiv = styled("div")({
|
|
|
|
|
+ fontSize: "30px",
|
|
|
|
|
+ width: "30px",
|
|
|
|
|
+ height: "30px",
|
|
|
|
|
+ marginTop: "8px",
|
|
|
|
|
+ marginBottom: "8px",
|
|
|
|
|
+ marginRight: "8px",
|
|
|
|
|
+ lineHeight: "30px",
|
|
|
|
|
+ cursor: "pointer",
|
|
|
|
|
+ opacity: 0.85,
|
|
|
|
|
+ "&:hover": {
|
|
|
|
|
+ opacity: 1
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const emojiMatches = (emoji, words) => {
|
|
|
|
|
+ if (words.length === 0) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (const word of words) {
|
|
|
|
|
+ if (emoji.searchBase.indexOf(word) === -1) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- return arr.filter(s => s.indexOf(search) !== -1).length > 0;
|
|
|
|
|
|
|
+ return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export default EmojiPicker;
|
|
export default EmojiPicker;
|