You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

351 lines
10 KiB

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import { defineMessages, injectIntl } from 'react-intl';
  4. import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
  5. import { Overlay } from 'react-overlays';
  6. import classNames from 'classnames';
  7. import ImmutablePropTypes from 'react-immutable-proptypes';
  8. import { buildCustomEmojis } from '../../../emoji';
  9. const messages = defineMessages({
  10. emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
  11. emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
  12. emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' },
  13. custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
  14. recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
  15. search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
  16. people: { id: 'emoji_button.people', defaultMessage: 'People' },
  17. nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
  18. food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
  19. activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
  20. travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
  21. objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
  22. symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
  23. flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
  24. });
  25. const assetHost = process.env.CDN_HOST || '';
  26. let EmojiPicker, Emoji; // load asynchronously
  27. const backgroundImageFn = () => `${assetHost}/emoji/sheet.png`;
  28. class ModifierPickerMenu extends React.PureComponent {
  29. static propTypes = {
  30. active: PropTypes.bool,
  31. onSelect: PropTypes.func.isRequired,
  32. onClose: PropTypes.func.isRequired,
  33. };
  34. handleClick = (e) => {
  35. const modifier = [].slice.call(e.currentTarget.parentNode.children).indexOf(e.target) + 1;
  36. this.props.onSelect(modifier);
  37. }
  38. componentWillReceiveProps (nextProps) {
  39. if (nextProps.active) {
  40. this.attachListeners();
  41. } else {
  42. this.removeListeners();
  43. }
  44. }
  45. componentWillUnmount () {
  46. this.removeListeners();
  47. }
  48. handleDocumentClick = e => {
  49. if (this.node && !this.node.contains(e.target)) {
  50. this.props.onClose();
  51. }
  52. }
  53. attachListeners () {
  54. document.addEventListener('click', this.handleDocumentClick, false);
  55. document.addEventListener('touchend', this.handleDocumentClick, false);
  56. }
  57. removeListeners () {
  58. document.removeEventListener('click', this.handleDocumentClick, false);
  59. document.removeEventListener('touchend', this.handleDocumentClick, false);
  60. }
  61. setRef = c => {
  62. this.node = c;
  63. }
  64. render () {
  65. const { active } = this.props;
  66. return (
  67. <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
  68. <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
  69. <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
  70. <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
  71. <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
  72. <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
  73. <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
  74. </div>
  75. );
  76. }
  77. }
  78. class ModifierPicker extends React.PureComponent {
  79. static propTypes = {
  80. active: PropTypes.bool,
  81. modifier: PropTypes.number,
  82. onChange: PropTypes.func,
  83. onClose: PropTypes.func,
  84. onOpen: PropTypes.func,
  85. };
  86. handleClick = () => {
  87. if (this.props.active) {
  88. this.props.onClose();
  89. } else {
  90. this.props.onOpen();
  91. }
  92. }
  93. handleSelect = modifier => {
  94. this.props.onChange(modifier);
  95. this.props.onClose();
  96. }
  97. render () {
  98. const { active, modifier } = this.props;
  99. return (
  100. <div className='emoji-picker-dropdown__modifiers'>
  101. <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
  102. <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
  103. </div>
  104. );
  105. }
  106. }
  107. @injectIntl
  108. class EmojiPickerMenu extends React.PureComponent {
  109. static propTypes = {
  110. custom_emojis: ImmutablePropTypes.list,
  111. loading: PropTypes.bool,
  112. onClose: PropTypes.func.isRequired,
  113. onPick: PropTypes.func.isRequired,
  114. style: PropTypes.object,
  115. placement: PropTypes.string,
  116. arrowOffsetLeft: PropTypes.string,
  117. arrowOffsetTop: PropTypes.string,
  118. intl: PropTypes.object.isRequired,
  119. };
  120. static defaultProps = {
  121. style: {},
  122. loading: true,
  123. placement: 'bottom',
  124. };
  125. state = {
  126. modifierOpen: false,
  127. modifier: 1,
  128. };
  129. handleDocumentClick = e => {
  130. if (this.node && !this.node.contains(e.target)) {
  131. this.props.onClose();
  132. }
  133. }
  134. componentDidMount () {
  135. document.addEventListener('click', this.handleDocumentClick, false);
  136. document.addEventListener('touchend', this.handleDocumentClick, false);
  137. }
  138. componentWillUnmount () {
  139. document.removeEventListener('click', this.handleDocumentClick, false);
  140. document.removeEventListener('touchend', this.handleDocumentClick, false);
  141. }
  142. setRef = c => {
  143. this.node = c;
  144. }
  145. getI18n = () => {
  146. const { intl } = this.props;
  147. return {
  148. search: intl.formatMessage(messages.emoji_search),
  149. notfound: intl.formatMessage(messages.emoji_not_found),
  150. categories: {
  151. search: intl.formatMessage(messages.search_results),
  152. recent: intl.formatMessage(messages.recent),
  153. people: intl.formatMessage(messages.people),
  154. nature: intl.formatMessage(messages.nature),
  155. foods: intl.formatMessage(messages.food),
  156. activity: intl.formatMessage(messages.activity),
  157. places: intl.formatMessage(messages.travel),
  158. objects: intl.formatMessage(messages.objects),
  159. symbols: intl.formatMessage(messages.symbols),
  160. flags: intl.formatMessage(messages.flags),
  161. custom: intl.formatMessage(messages.custom),
  162. },
  163. };
  164. }
  165. handleClick = emoji => {
  166. if (!emoji.native) {
  167. emoji.native = emoji.colons;
  168. }
  169. this.props.onClose();
  170. this.props.onPick(emoji);
  171. }
  172. handleModifierOpen = () => {
  173. this.setState({ modifierOpen: true });
  174. }
  175. handleModifierClose = () => {
  176. this.setState({ modifierOpen: false });
  177. }
  178. handleModifierChange = modifier => {
  179. if (modifier !== this.state.modifier) {
  180. this.setState({ modifier });
  181. }
  182. }
  183. render () {
  184. const { loading, style, intl } = this.props;
  185. if (loading) {
  186. return <div style={{ width: 299 }} />;
  187. }
  188. const title = intl.formatMessage(messages.emoji);
  189. const { modifierOpen, modifier } = this.state;
  190. return (
  191. <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
  192. <EmojiPicker
  193. custom={buildCustomEmojis(this.props.custom_emojis)}
  194. perLine={8}
  195. emojiSize={22}
  196. sheetSize={32}
  197. color=''
  198. emoji=''
  199. set='twitter'
  200. title={title}
  201. i18n={this.getI18n()}
  202. onClick={this.handleClick}
  203. skin={modifier}
  204. backgroundImageFn={backgroundImageFn}
  205. />
  206. <ModifierPicker
  207. active={modifierOpen}
  208. modifier={modifier}
  209. onOpen={this.handleModifierOpen}
  210. onClose={this.handleModifierClose}
  211. onChange={this.handleModifierChange}
  212. />
  213. </div>
  214. );
  215. }
  216. }
  217. @injectIntl
  218. export default class EmojiPickerDropdown extends React.PureComponent {
  219. static propTypes = {
  220. custom_emojis: ImmutablePropTypes.list,
  221. intl: PropTypes.object.isRequired,
  222. onPickEmoji: PropTypes.func.isRequired,
  223. };
  224. state = {
  225. active: false,
  226. loading: false,
  227. };
  228. setRef = (c) => {
  229. this.dropdown = c;
  230. }
  231. onShowDropdown = () => {
  232. this.setState({ active: true });
  233. if (!EmojiPicker) {
  234. this.setState({ loading: true });
  235. EmojiPickerAsync().then(EmojiMart => {
  236. EmojiPicker = EmojiMart.Picker;
  237. Emoji = EmojiMart.Emoji;
  238. this.setState({ loading: false });
  239. }).catch(() => {
  240. this.setState({ loading: false });
  241. });
  242. }
  243. }
  244. onHideDropdown = () => {
  245. this.setState({ active: false });
  246. }
  247. onToggle = (e) => {
  248. if (!this.state.loading && (!e.key || e.key === 'Enter')) {
  249. if (this.state.active) {
  250. this.onHideDropdown();
  251. } else {
  252. this.onShowDropdown();
  253. }
  254. }
  255. }
  256. handleKeyDown = e => {
  257. if (e.key === 'Escape') {
  258. this.onHideDropdown();
  259. }
  260. }
  261. setTargetRef = c => {
  262. this.target = c;
  263. }
  264. findTarget = () => {
  265. return this.target;
  266. }
  267. render () {
  268. const { intl, onPickEmoji } = this.props;
  269. const title = intl.formatMessage(messages.emoji);
  270. const { active, loading } = this.state;
  271. return (
  272. <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
  273. <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
  274. <img
  275. className={classNames('emojione', { 'pulse-loading': active && loading })}
  276. alt='🙂'
  277. src={`${assetHost}/emoji/1f602.svg`}
  278. />
  279. </div>
  280. <Overlay show={active} placement='bottom' target={this.findTarget}>
  281. <EmojiPickerMenu
  282. custom_emojis={this.props.custom_emojis}
  283. loading={loading}
  284. onClose={this.onHideDropdown}
  285. onPick={onPickEmoji}
  286. />
  287. </Overlay>
  288. </div>
  289. );
  290. }
  291. }