// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import React from 'react'; import $ from 'jquery'; import * as Emoji from 'utils/emoji.jsx'; import EmojiStore from 'stores/emoji_store.jsx'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; import ReactOutsideEvent from 'react-outside-event'; import {FormattedMessage} from 'react-intl'; import EmojiPickerCategory from './components/emoji_picker_category.jsx'; import EmojiPickerItem from './components/emoji_picker_item.jsx'; import EmojiPickerPreview from './components/emoji_picker_preview.jsx'; // This should include all the categories available in Emoji.CategoryNames const CATEGORIES = [ 'recent', 'people', 'nature', 'food', 'activity', 'travel', 'objects', 'symbols', 'flags', 'custom' ]; class EmojiPicker extends React.Component { static propTypes = { customEmojis: React.PropTypes.object, onEmojiClick: React.PropTypes.func.isRequired, pickerLocation: React.PropTypes.string.isRequired, emojiOffset: React.PropTypes.number, outsideClick: React.PropTypes.func } constructor(props) { super(props); // All props are primitives or treated as immutable this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); this.handleCategoryClick = this.handleCategoryClick.bind(this); this.handleFilterChange = this.handleFilterChange.bind(this); this.handleItemOver = this.handleItemOver.bind(this); this.handleItemOut = this.handleItemOut.bind(this); this.handleItemClick = this.handleItemClick.bind(this); this.handleScroll = this.handleScroll.bind(this); this.handleItemUnmount = this.handleItemUnmount.bind(this); this.renderCategory = this.renderCategory.bind(this); this.onOutsideEvent = this.onOutsideEvent.bind(this); this.state = { category: 'recent', filter: '', selected: null }; } componentDidMount() { this.searchInput.focus(); } onOutsideEvent = (event) => { // Handle the event. if (this.props.outsideClick) { this.props.outsideClick(event); } } handleCategoryClick(category) { const items = this.refs.items; if (category === CATEGORIES[0]) { // First category includes the search box so just scroll to the top items.scrollTop = 0; } else { const cat = this.refs[category]; items.scrollTop = cat.offsetTop; } } handleFilterChange(e) { this.setState({filter: e.target.value}); } handleItemOver(emoji) { clearTimeout(this.timeouthandler); this.setState({selected: emoji}); } handleItemOut() { this.timeouthandler = setTimeout(() => this.setState({selected: null}), 500); } handleItemUnmount(emoji) { //Prevent emoji preview from showing emoji which is not present anymore (due to filter) if (this.state.selected === emoji) { this.setState({selected: null}); } } handleItemClick(emoji) { this.props.onEmojiClick(emoji); } handleScroll() { const items = $(ReactDOM.findDOMNode(this.refs.items)); const contentTop = items.scrollTop(); const contentTopPadding = parseInt(items.css('padding-top'), 10); const scrollPct = (contentTop / (items[0].scrollHeight - items[0].clientHeight)) * 100.0; if (scrollPct > 99.0) { this.setState({category: 'custom'}); return; } for (const category of CATEGORIES) { const header = $(ReactDOM.findDOMNode(this.refs[category])); const headerBottomMargin = parseInt(header.css('margin-bottom'), 10) + parseInt(header.css('padding-bottom'), 10); const headerBottom = header[0].offsetTop + header.height() + headerBottomMargin; // If category is the first one visible, highlight it in the bar at the top if (headerBottom - contentTopPadding >= contentTop) { if (this.state.category !== category) { this.setState({category: String(category)}); } break; } } } renderCategory(category, filter) { const items = []; let indices = []; let recentEmojis = []; if (category === 'recent') { recentEmojis = EmojiStore.getRecentEmojis(); indices = [...Array(recentEmojis.length).keys()]; // reverse indices so most recently added is first indices.reverse(); } else { indices = Emoji.EmojiIndicesByCategory.get(category) || []; } for (const index of indices) { let emoji = {}; if (category === 'recent') { emoji = recentEmojis[index]; } else { emoji = Emoji.Emojis[index]; } if (filter) { let matches = false; for (const alias of emoji.aliases || [...emoji.name]) { if (alias.indexOf(filter) !== -1) { matches = true; break; } } if (!matches) { continue; } } items.push( ); } if (category === 'custom') { const customEmojis = EmojiStore.getCustomEmojiMap().values(); for (const emoji of customEmojis) { if (filter && emoji.name.indexOf(filter) === -1) { continue; } items.push( ); } } // Only render the header if there's any visible items let header = null; if (items.length > 0) { header = (
); } return (
{header}
{items}
); } renderPreview(selected) { if (selected) { let name; let aliases; let previewImage; if (selected.name) { // This is a custom emoji that matches the model on the server name = selected.name; aliases = [selected.name]; previewImage = (); } else { // This is a system emoji which only has a list of aliases name = selected.aliases[0]; aliases = selected.aliases; previewImage = (); } return (
{previewImage} {name} {aliases.map((alias) => ':' + alias + ':').join(' ')}
); } return ( ); } render() { const items = []; for (const category of CATEGORIES) { if (category === 'custom') { items.push(this.renderCategory('custom', this.state.filter, this.props.customEmojis)); } else { items.push(this.renderCategory(category, this.state.filter)); } } let cssclass = 'emoji-picker '; if (this.props.pickerLocation === 'top') { cssclass += 'emoji-picker-top'; } else if (this.props.pickerLocation === 'bottom') { cssclass += 'emoji-picker-bottom'; } else if (this.props.pickerLocation === 'react') { cssclass = 'emoji-picker-react'; } else if (this.props.pickerLocation === 'react-rhs-comment') { cssclass = 'emoji-picker-react-rhs-comment'; } const pickerStyle = this.props.emojiOffset ? {top: this.props.emojiOffset} : {}; return (
} onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'recent'} /> } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'people'} /> } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'nature'} /> } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'food'} /> } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'activity'} /> } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'travel'} /> } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'objects'} /> } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'symbols'} /> } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'flags'} /> } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'custom'} />
{ this.searchInput = input; }} className='emoji-picker__search' type='text' onChange={this.handleFilterChange} placeholder={Utils.localizeMessage('emoji_picker.search', 'search')} />
{items}
); } } // disabling eslint check for outslide click handler // eslint-disable-next-line new-cap export default ReactOutsideEvent(EmojiPicker, ['click']);