diff options
Diffstat (limited to 'app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js')
-rw-r--r-- | app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js | 334 |
1 files changed, 258 insertions, 76 deletions
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index 9d05b7a34..621cc21ce 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -1,12 +1,19 @@ import React from 'react'; -import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; -import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; +import { Picker, Emoji } from 'emoji-mart'; +import { Overlay } from 'react-overlays'; +import classNames from 'classnames'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import detectPassiveEvents from 'detect-passive-events'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' }, + emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' }, + custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' }, + recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' }, + search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' }, people: { id: 'emoji_button.people', defaultMessage: 'People' }, nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' }, food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' }, @@ -17,48 +24,250 @@ const messages = defineMessages({ flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' }, }); -const settings = { - imageType: 'png', - sprites: false, - imagePathPNG: '/emoji/', -}; +const assetHost = process.env.CDN_HOST || ''; +const backgroundImageFn = () => `${assetHost}/emoji/sheet.png`; +const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; -let EmojiPicker; // load asynchronously +class ModifierPickerMenu extends React.PureComponent { + + static propTypes = { + active: PropTypes.bool, + onSelect: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + }; + + handleClick = (e) => { + const modifier = [].slice.call(e.currentTarget.parentNode.children).indexOf(e.target) + 1; + this.props.onSelect(modifier); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.active) { + this.attachListeners(); + } else { + this.removeListeners(); + } + } + + componentWillUnmount () { + this.removeListeners(); + } + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + } + + attachListeners () { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + removeListeners () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + } + + render () { + const { active } = this.props; + + return ( + <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button> + </div> + ); + } + +} + +class ModifierPicker extends React.PureComponent { + + static propTypes = { + active: PropTypes.bool, + modifier: PropTypes.number, + onChange: PropTypes.func, + onClose: PropTypes.func, + onOpen: PropTypes.func, + }; + + handleClick = () => { + if (this.props.active) { + this.props.onClose(); + } else { + this.props.onOpen(); + } + } + + handleSelect = modifier => { + this.props.onChange(modifier); + this.props.onClose(); + } + + render () { + const { active, modifier } = this.props; + + return ( + <div className='emoji-picker-dropdown__modifiers'> + <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} /> + <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} /> + </div> + ); + } + +} + +@injectIntl +class EmojiPickerMenu extends React.PureComponent { + + static propTypes = { + custom_emojis: ImmutablePropTypes.list, + onClose: PropTypes.func.isRequired, + onPick: PropTypes.func.isRequired, + style: PropTypes.object, + placement: PropTypes.string, + arrowOffsetLeft: PropTypes.string, + arrowOffsetTop: PropTypes.string, + intl: PropTypes.object.isRequired, + }; + + static defaultProps = { + style: {}, + placement: 'bottom', + }; + + state = { + modifierOpen: false, + modifier: 1, + }; + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + } + + componentDidMount () { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + componentWillUnmount () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + } + + getI18n = () => { + const { intl } = this.props; + + return { + search: intl.formatMessage(messages.emoji_search), + notfound: intl.formatMessage(messages.emoji_not_found), + categories: { + search: intl.formatMessage(messages.search_results), + recent: intl.formatMessage(messages.recent), + people: intl.formatMessage(messages.people), + nature: intl.formatMessage(messages.nature), + foods: intl.formatMessage(messages.food), + activity: intl.formatMessage(messages.activity), + places: intl.formatMessage(messages.travel), + objects: intl.formatMessage(messages.objects), + symbols: intl.formatMessage(messages.symbols), + flags: intl.formatMessage(messages.flags), + custom: intl.formatMessage(messages.custom), + }, + }; + } + + handleClick = emoji => { + if (!emoji.native) { + emoji.native = emoji.colons; + } + + this.props.onClose(); + this.props.onPick(emoji); + } + + handleModifierOpen = () => { + this.setState({ modifierOpen: true }); + } + + handleModifierClose = () => { + this.setState({ modifierOpen: false }); + } + + handleModifierChange = modifier => { + if (modifier !== this.state.modifier) { + this.setState({ modifier }); + } + } + + render () { + const { style, intl } = this.props; + const title = intl.formatMessage(messages.emoji); + const { modifierOpen, modifier } = this.state; + + return ( + <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}> + <Picker + perLine={8} + emojiSize={22} + sheetSize={32} + color='' + emoji='' + set='twitter' + title={title} + i18n={this.getI18n()} + onClick={this.handleClick} + skin={modifier} + backgroundImageFn={backgroundImageFn} + /> + + <ModifierPicker + active={modifierOpen} + modifier={modifier} + onOpen={this.handleModifierOpen} + onClose={this.handleModifierClose} + onChange={this.handleModifierChange} + /> + </div> + ); + } + +} @injectIntl export default class EmojiPickerDropdown extends React.PureComponent { static propTypes = { + custom_emojis: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, onPickEmoji: PropTypes.func.isRequired, }; state = { active: false, - loading: false, }; setRef = (c) => { this.dropdown = c; } - handleChange = (data) => { - this.dropdown.hide(); - this.props.onPickEmoji(data); - } - onShowDropdown = () => { this.setState({ active: true }); - if (!EmojiPicker) { - this.setState({ loading: true }); - EmojiPickerAsync().then(TheEmojiPicker => { - EmojiPicker = TheEmojiPicker.default; - this.setState({ loading: false }); - }).catch(() => { - // TODO: show the user an error? - this.setState({ loading: false }); - }); - } } onHideDropdown = () => { @@ -66,7 +275,7 @@ export default class EmojiPickerDropdown extends React.PureComponent { } onToggle = (e) => { - if (!this.state.loading && (!e.key || e.key === 'Enter')) { + if (!e.key || e.key === 'Enter') { if (this.state.active) { this.onHideDropdown(); } else { @@ -75,70 +284,43 @@ export default class EmojiPickerDropdown extends React.PureComponent { } } - onEmojiPickerKeyDown = (e) => { + handleKeyDown = e => { if (e.key === 'Escape') { this.onHideDropdown(); } } - render () { - const { intl } = this.props; + setTargetRef = c => { + this.target = c; + } - const categories = { - people: { - title: intl.formatMessage(messages.people), - emoji: 'smile', - }, - nature: { - title: intl.formatMessage(messages.nature), - emoji: 'hamster', - }, - food: { - title: intl.formatMessage(messages.food), - emoji: 'pizza', - }, - activity: { - title: intl.formatMessage(messages.activity), - emoji: 'soccer', - }, - travel: { - title: intl.formatMessage(messages.travel), - emoji: 'earth_americas', - }, - objects: { - title: intl.formatMessage(messages.objects), - emoji: 'bulb', - }, - symbols: { - title: intl.formatMessage(messages.symbols), - emoji: 'clock9', - }, - flags: { - title: intl.formatMessage(messages.flags), - emoji: 'flag_gb', - }, - }; + findTarget = () => { + return this.target; + } - const { active, loading } = this.state; + render () { + const { intl, onPickEmoji } = this.props; const title = intl.formatMessage(messages.emoji); + const { active } = this.state; return ( - <Dropdown ref={this.setRef} className='emoji-picker__dropdown' active={active && !loading} onShow={this.onShowDropdown} onHide={this.onHideDropdown}> - <DropdownTrigger className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onKeyDown={this.onToggle} tabIndex={0} > + <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}> + <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}> <img - className={`emojione ${active && loading ? 'pulse-loading' : ''}`} + className='emojione' alt='🙂' - src='/emoji/1f602.svg' + src={`${assetHost}/emoji/1f602.svg`} + /> + </div> + + <Overlay show={active} placement='bottom' target={this.findTarget}> + <EmojiPickerMenu + custom_emojis={this.props.custom_emojis} + onClose={this.onHideDropdown} + onPick={onPickEmoji} /> - </DropdownTrigger> - - <DropdownContent className='dropdown__left'> - { - this.state.active && !this.state.loading && - (<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} onKeyDown={this.onEmojiPickerKeyDown} categories={categories} search />) - } - </DropdownContent> - </Dropdown> + </Overlay> + </div> ); } |