// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import React from 'react'; import PropTypes from 'prop-types'; import * as Emoji from 'utils/emoji.jsx'; import EmojiStore from 'stores/emoji_store.jsx'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import * as Utils from 'utils/utils.jsx'; 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' ]; export default class EmojiPicker extends React.Component { static propTypes = { style: PropTypes.object, placement: PropTypes.oneOf(['top', 'bottom', 'left']), customEmojis: PropTypes.object, onEmojiClick: PropTypes.func.isRequired } 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.state = { category: 'recent', filter: '', selected: null }; } componentDidMount() { // Delay taking focus because this briefly renders offscreen when using an Overlay // so focusing it immediately on mount can cause weird scrolling requestAnimationFrame(() => { this.searchInput.focus(); }); } 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 = this.refs.items; const contentTop = items.scrollTop; const itemsPaddingTop = getComputedStyle(items).paddingTop; const contentTopPadding = parseInt(itemsPaddingTop, 10); const scrollPct = (contentTop / (items.scrollHeight - items.clientHeight)) * 100.0; if (scrollPct > 99.0) { this.setState({category: 'custom'}); return; } for (const category of CATEGORIES) { const header = this.refs[category]; const headerStyle = getComputedStyle(header); const headerBottomMargin = parseInt(headerStyle.marginBottom, 10); const headerBottomPadding = parseInt(headerStyle.paddingBottom, 10); const headerBottomSpace = headerBottomMargin + headerBottomPadding; const headerBottom = header.offsetTop + header.offsetHeight + headerBottomSpace; // 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 pickerStyle; if (this.props.style && !(this.props.style.left === 0 || this.props.style.top === 0)) { if (this.props.placement === 'top' || this.props.placement === 'bottom') { // Only take the top/bottom position passed by React Bootstrap since we want to be right-aligned pickerStyle = { top: this.props.style.top, bottom: this.props.style.bottom, right: 1 }; } else { pickerStyle = this.props.style; } } 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}
); } }