From 28ad645153b206ba84ddc4935280eaed94bb0138 Mon Sep 17 00:00:00 2001 From: bonespiked Date: Fri, 24 Mar 2017 09:09:51 -0400 Subject: Ticket 4665 - Emoji Picker (#5157) * #4665 Added EmojiPicker Work primarily by @broernr-de and @harrison on pre-release.mattermost.com * Final fixes to handle custom emojis from internal review and single merge error * ESLint fixes * CSS changes and other code to support emoji picker in reply window * Fix for file upload and emoji picker icon positions on post and comment. RHS emoji picker appearing see-through at this time. * Fix for two ESLint issues. * covered most of feedback: RHS emoji picker looks correct color-wise RHS emoji picker dynamically positions against height of thread size (post + reply messages) escape closes emoji window search box focused on open ESLint fixes against other files oversized emoji preview fixes * Adding in 'outside click' eventing to dismiss the emoji window * Changing some formatting to fix mismatch between my local eslant rules and jenkins. * adding alternative import method due to downstream testing errors * yet another attempt to retain functionality and pass tests - skipping import of browser store * fix for feedback items 5 and 7: * move search to float on top with stylistic changes * whitespace in the header (+1 squashed commit) Squashed commits: [6a26d32] changes that address items 1, 2, 6, 8, and 9 of latest feedback * Fix for attachment preview location on mobile * Fix for latest rounds of feedback * fixing eslint issue * making emojipicker sprite based, fixing alignments * Fix for emoji quality, fixing some behavior (hover background and cursor settings) undoing config changes * Preview feature for emojis * Adjustments to config file, and changing layout/design of attachment and emoji icon. * manual revert from master branch for config.json * reverting paperclip and fixing alignments. Additionally fixing inadvertent display of picker on mobile. * CSS changes to try to fix the hover behavior - currently working for emoji picker (when enabled), but hover for attachment isn't working * Made suggested changes by jwilander except for jQuery removal * Adding hover for both icons * removal of some usages of jQuery * Fix for two layout issues on IE11/Edge * UI improvements for emoji picker * Fix for many minor display issues * fix for additional appearance items * fix to two minor UI items * A little extra padding for IE11 * fix for IE11 scroll issue, and removing align attribute on img tag which was throwing js error * fixes some display issues on firefox * fix for uneven sides of emojis * fix for eslint issues that I didn't introduce * fix for missing bottom edge of RHS emojipicker. also fixing text overlapping icons on text area (including RHS) * Update "emoji selector" to "emoji picker" * changes for code review - removal of ..getDOMNode - use sprite imagery for emoji preview - remove lastBlurAt from state as it wasn't used * fixes for: - fake custom emoji preview in picker - RHS scrollbar on preview * fix for minor alignment of preview emoji --- webapp/components/emoji_picker/emoji_picker.jsx | 417 ++++++++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 webapp/components/emoji_picker/emoji_picker.jsx (limited to 'webapp/components/emoji_picker/emoji_picker.jsx') diff --git a/webapp/components/emoji_picker/emoji_picker.jsx b/webapp/components/emoji_picker/emoji_picker.jsx new file mode 100644 index 000000000..e12974054 --- /dev/null +++ b/webapp/components/emoji_picker/emoji_picker.jsx @@ -0,0 +1,417 @@ +// Copyright (c) 2016 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, + topOrBottom: 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. + 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)); + } + } + const cssclass = this.props.topOrBottom === 'top' ? 'emoji-picker' : 'emoji-picker-bottom'; + 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']); -- cgit v1.2.3-1-g7c22