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…
	
	Add table
		Add a link
		
	
		Reference in a new issue