EmojiPicker.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import * as React from 'react';
  2. import {useRef, useState} from 'react';
  3. import Typography from '@mui/material/Typography';
  4. import {rawEmojis} from '../app/emojis';
  5. import Box from "@mui/material/Box";
  6. import TextField from "@mui/material/TextField";
  7. import {ClickAwayListener, Fade, InputAdornment, styled} from "@mui/material";
  8. import IconButton from "@mui/material/IconButton";
  9. import {Close} from "@mui/icons-material";
  10. import Popper from "@mui/material/Popper";
  11. import {splitNoEmpty} from "../app/utils";
  12. import {useTranslation} from "react-i18next";
  13. // Create emoji list by category and create a search base (string with all search words)
  14. //
  15. // This also filters emojis that are not supported by Desktop Chrome.
  16. // This is a hack, but on Ubuntu 18.04, with Chrome 99, only Emoji <= 11 are supported.
  17. const emojisByCategory = {};
  18. const isDesktopChrome = /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent);
  19. const maxSupportedVersionForDesktopChrome = 11;
  20. rawEmojis.forEach(emoji => {
  21. if (!emojisByCategory[emoji.category]) {
  22. emojisByCategory[emoji.category] = [];
  23. }
  24. try {
  25. const unicodeVersion = parseFloat(emoji.unicode_version);
  26. const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome;
  27. if (supportedEmoji) {
  28. const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`;
  29. const emojiWithSearchBase = { ...emoji, searchBase: searchBase };
  30. emojisByCategory[emoji.category].push(emojiWithSearchBase);
  31. }
  32. } catch (e) {
  33. // Nothing. Ignore.
  34. }
  35. });
  36. const EmojiPicker = (props) => {
  37. const { t } = useTranslation();
  38. const open = Boolean(props.anchorEl);
  39. const [search, setSearch] = useState("");
  40. const searchRef = useRef(null);
  41. const searchFields = splitNoEmpty(search.toLowerCase(), " ");
  42. const handleSearchClear = () => {
  43. setSearch("");
  44. searchRef.current?.focus();
  45. };
  46. return (
  47. <Popper
  48. open={open}
  49. anchorEl={props.anchorEl}
  50. placement="bottom-start"
  51. sx={{ zIndex: 10005 }}
  52. transition
  53. >
  54. {({ TransitionProps }) => (
  55. <ClickAwayListener onClickAway={props.onClose}>
  56. <Fade {...TransitionProps} timeout={350}>
  57. <Box sx={{
  58. boxShadow: 3,
  59. padding: 2,
  60. paddingRight: 0,
  61. paddingBottom: 1,
  62. width: "380px",
  63. maxHeight: "300px",
  64. backgroundColor: 'background.paper',
  65. overflowY: "scroll"
  66. }}>
  67. <TextField
  68. inputRef={searchRef}
  69. margin="dense"
  70. size="small"
  71. placeholder={t("emoji_picker_search_placeholder")}
  72. value={search}
  73. onChange={ev => setSearch(ev.target.value)}
  74. type="text"
  75. variant="standard"
  76. fullWidth
  77. sx={{ marginTop: 0, marginBottom: "12px", paddingRight: 2 }}
  78. InputProps={{
  79. endAdornment:
  80. <InputAdornment position="end" sx={{ display: (search) ? '' : 'none' }}>
  81. <IconButton size="small" onClick={handleSearchClear} edge="end"><Close/></IconButton>
  82. </InputAdornment>
  83. }}
  84. />
  85. <Box sx={{ display: "flex", flexWrap: "wrap", paddingRight: 0, marginTop: 1 }}>
  86. {Object.keys(emojisByCategory).map(category =>
  87. <Category
  88. key={category}
  89. title={category}
  90. emojis={emojisByCategory[category]}
  91. search={searchFields}
  92. onPick={props.onEmojiPick}
  93. />
  94. )}
  95. </Box>
  96. </Box>
  97. </Fade>
  98. </ClickAwayListener>
  99. )}
  100. </Popper>
  101. );
  102. };
  103. const Category = (props) => {
  104. const showTitle = props.search.length === 0;
  105. return (
  106. <>
  107. {showTitle &&
  108. <Typography variant="body1" sx={{ width: "100%", marginBottom: 1 }}>
  109. {props.title}
  110. </Typography>
  111. }
  112. {props.emojis.map(emoji =>
  113. <Emoji
  114. key={emoji.aliases[0]}
  115. emoji={emoji}
  116. search={props.search}
  117. onClick={() => props.onPick(emoji.aliases[0])}
  118. />
  119. )}
  120. </>
  121. );
  122. };
  123. const Emoji = (props) => {
  124. const emoji = props.emoji;
  125. const matches = emojiMatches(emoji, props.search);
  126. return (
  127. <EmojiDiv
  128. onClick={props.onClick}
  129. title={`${emoji.description} (${emoji.aliases[0]})`}
  130. style={{ display: (matches) ? '' : 'none' }}
  131. >
  132. {props.emoji.emoji}
  133. </EmojiDiv>
  134. );
  135. };
  136. const EmojiDiv = styled("div")({
  137. fontSize: "30px",
  138. width: "30px",
  139. height: "30px",
  140. marginTop: "8px",
  141. marginBottom: "8px",
  142. marginRight: "8px",
  143. lineHeight: "30px",
  144. cursor: "pointer",
  145. opacity: 0.85,
  146. "&:hover": {
  147. opacity: 1
  148. }
  149. });
  150. const emojiMatches = (emoji, words) => {
  151. if (words.length === 0) {
  152. return true;
  153. }
  154. for (const word of words) {
  155. if (emoji.searchBase.indexOf(word) === -1) {
  156. return false;
  157. }
  158. }
  159. return true;
  160. }
  161. export default EmojiPicker;