* #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
295 строки
9.7 KiB
JavaScript
295 строки
9.7 KiB
JavaScript
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
|
// See License.txt for license information.
|
|
|
|
import $ from 'jquery';
|
|
import AtMentionProvider from './suggestion/at_mention_provider.jsx';
|
|
import ChannelMentionProvider from './suggestion/channel_mention_provider.jsx';
|
|
import CommandProvider from './suggestion/command_provider.jsx';
|
|
import EmoticonProvider from './suggestion/emoticon_provider.jsx';
|
|
import SuggestionList from './suggestion/suggestion_list.jsx';
|
|
import SuggestionBox from './suggestion/suggestion_box.jsx';
|
|
import ErrorStore from 'stores/error_store.jsx';
|
|
|
|
import * as TextFormatting from 'utils/text_formatting.jsx';
|
|
import * as Utils from 'utils/utils.jsx';
|
|
import Constants from 'utils/constants.jsx';
|
|
|
|
import {FormattedMessage} from 'react-intl';
|
|
|
|
const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
|
|
|
|
import React from 'react';
|
|
|
|
export default class Textbox extends React.Component {
|
|
static propTypes = {
|
|
id: React.PropTypes.string.isRequired,
|
|
channelId: React.PropTypes.string,
|
|
value: React.PropTypes.string.isRequired,
|
|
onChange: React.PropTypes.func.isRequired,
|
|
onKeyPress: React.PropTypes.func.isRequired,
|
|
createMessage: React.PropTypes.string.isRequired,
|
|
onKeyDown: React.PropTypes.func,
|
|
onBlur: React.PropTypes.func,
|
|
supportsCommands: React.PropTypes.bool.isRequired,
|
|
handlePostError: React.PropTypes.func,
|
|
suggestionListStyle: React.PropTypes.string,
|
|
emojiEnabled: React.PropTypes.bool
|
|
};
|
|
|
|
static defaultProps = {
|
|
supportsCommands: true
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.focus = this.focus.bind(this);
|
|
this.recalculateSize = this.recalculateSize.bind(this);
|
|
this.onReceivedError = this.onReceivedError.bind(this);
|
|
this.handleKeyPress = this.handleKeyPress.bind(this);
|
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
this.handleBlur = this.handleBlur.bind(this);
|
|
this.handleHeightChange = this.handleHeightChange.bind(this);
|
|
this.showPreview = this.showPreview.bind(this);
|
|
this.handleChange = this.handleChange.bind(this);
|
|
|
|
this.state = {
|
|
connection: ''
|
|
};
|
|
|
|
this.suggestionProviders = [
|
|
new AtMentionProvider(this.props.channelId),
|
|
new ChannelMentionProvider(),
|
|
new EmoticonProvider()
|
|
];
|
|
if (props.supportsCommands) {
|
|
this.suggestionProviders.push(new CommandProvider());
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
ErrorStore.addChangeListener(this.onReceivedError);
|
|
}
|
|
|
|
componentWillMount() {
|
|
this.checkMessageLength(this.props.value);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
ErrorStore.removeChangeListener(this.onReceivedError);
|
|
}
|
|
|
|
onReceivedError() {
|
|
const errorCount = ErrorStore.getConnectionErrorCount();
|
|
|
|
if (errorCount > 1) {
|
|
this.setState({connection: 'bad-connection'});
|
|
} else {
|
|
this.setState({connection: ''});
|
|
}
|
|
}
|
|
|
|
handleChange(e) {
|
|
this.checkMessageLength(e.target.value);
|
|
this.props.onChange(e);
|
|
}
|
|
|
|
checkMessageLength(message) {
|
|
if (this.props.handlePostError) {
|
|
if (message.length > Constants.CHARACTER_LIMIT) {
|
|
const errorMessage = (
|
|
<FormattedMessage
|
|
id='create_post.error_message'
|
|
defaultMessage='Your message is too long. Character count: {length}/{limit}'
|
|
values={{
|
|
length: message.length,
|
|
limit: Constants.CHARACTER_LIMIT
|
|
}}
|
|
/>);
|
|
this.props.handlePostError(errorMessage);
|
|
} else {
|
|
this.props.handlePostError(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
handleKeyPress(e) {
|
|
this.props.onKeyPress(e);
|
|
}
|
|
|
|
handleKeyDown(e) {
|
|
if (this.props.onKeyDown) {
|
|
this.props.onKeyDown(e);
|
|
}
|
|
}
|
|
|
|
handleBlur(e) {
|
|
if (this.props.onBlur) {
|
|
this.props.onBlur(e);
|
|
}
|
|
}
|
|
|
|
handleHeightChange(height, maxHeight) {
|
|
const wrapper = $(this.refs.wrapper);
|
|
|
|
// Move over attachment icon to compensate for the scrollbar
|
|
if (height > maxHeight) {
|
|
wrapper.closest('.post-create').addClass('scroll');
|
|
} else {
|
|
wrapper.closest('.post-create').removeClass('scroll');
|
|
}
|
|
}
|
|
|
|
focus() {
|
|
const textbox = this.refs.message.getTextbox();
|
|
|
|
textbox.focus();
|
|
Utils.placeCaretAtEnd(textbox);
|
|
}
|
|
|
|
recalculateSize() {
|
|
this.refs.message.recalculateSize();
|
|
}
|
|
|
|
showPreview(e) {
|
|
e.preventDefault();
|
|
e.target.blur();
|
|
this.setState({preview: !this.state.preview});
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
if (nextProps.channelId !== this.props.channelId) {
|
|
// Update channel id for AtMentionProvider.
|
|
const providers = this.suggestionProviders;
|
|
for (let i = 0; i < providers.length; i++) {
|
|
if (providers[i] instanceof AtMentionProvider) {
|
|
providers[i] = new AtMentionProvider(nextProps.channelId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const hasText = this.props.value && this.props.value.length > 0;
|
|
|
|
let previewLink = null;
|
|
if (Utils.isFeatureEnabled(PreReleaseFeatures.MARKDOWN_PREVIEW)) {
|
|
previewLink = (
|
|
<a
|
|
onClick={this.showPreview}
|
|
className='textbox-preview-link'
|
|
>
|
|
{this.state.preview ? (
|
|
<FormattedMessage
|
|
id='textbox.edit'
|
|
defaultMessage='Edit message'
|
|
/>
|
|
) : (
|
|
<FormattedMessage
|
|
id='textbox.preview'
|
|
defaultMessage='Preview'
|
|
/>
|
|
)}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
const helpText = (
|
|
<div
|
|
style={{visibility: hasText ? 'visible' : 'hidden', opacity: hasText ? '0.45' : '0'}}
|
|
className='help__format-text'
|
|
>
|
|
<b>
|
|
<FormattedMessage
|
|
id='textbox.bold'
|
|
defaultMessage='**bold**'
|
|
/>
|
|
</b>
|
|
<i>
|
|
<FormattedMessage
|
|
id='textbox.italic'
|
|
defaultMessage='_italic_'
|
|
/>
|
|
</i>
|
|
<span>
|
|
{'~~'}
|
|
<strike>
|
|
<FormattedMessage
|
|
id='textbox.strike'
|
|
defaultMessage='strike'
|
|
/>
|
|
</strike>
|
|
{'~~ '}
|
|
</span>
|
|
<span>
|
|
<FormattedMessage
|
|
id='textbox.inlinecode'
|
|
defaultMessage='`inline code`'
|
|
/>
|
|
</span>
|
|
<span>
|
|
<FormattedMessage
|
|
id='textbox.preformatted'
|
|
defaultMessage='```preformatted```'
|
|
/>
|
|
</span>
|
|
<span>
|
|
<FormattedMessage
|
|
id='textbox.quote'
|
|
defaultMessage='>quote'
|
|
/>
|
|
</span>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div
|
|
ref='wrapper'
|
|
className='textarea-wrapper'
|
|
>
|
|
<SuggestionBox
|
|
id={this.props.id}
|
|
ref='message'
|
|
className={`form-control custom-textarea${this.props.emojiEnabled ? '-emoji' : ''} ${this.state.connection}`}
|
|
type='textarea'
|
|
spellCheck='true'
|
|
placeholder={this.props.createMessage}
|
|
onChange={this.handleChange}
|
|
onKeyPress={this.handleKeyPress}
|
|
onKeyDown={this.handleKeyDown}
|
|
onBlur={this.handleBlur}
|
|
onHeightChange={this.handleHeightChange}
|
|
style={{visibility: this.state.preview ? 'hidden' : 'visible'}}
|
|
listComponent={SuggestionList}
|
|
listStyle={this.props.suggestionListStyle}
|
|
providers={this.suggestionProviders}
|
|
channelId={this.props.channelId}
|
|
value={this.props.value}
|
|
renderDividers={true}
|
|
/>
|
|
<div
|
|
ref='preview'
|
|
className='form-control custom-textarea textbox-preview-area'
|
|
style={{display: this.state.preview ? 'block' : 'none'}}
|
|
dangerouslySetInnerHTML={{__html: this.state.preview ? TextFormatting.formatText(this.props.value) : ''}}
|
|
/>
|
|
<div className='help__text'>
|
|
{helpText}
|
|
{previewLink}
|
|
<a
|
|
target='_blank'
|
|
rel='noopener noreferrer'
|
|
href='/help/messaging'
|
|
className='textbox-help-link'
|
|
>
|
|
<FormattedMessage
|
|
id='textbox.help'
|
|
defaultMessage='Help'
|
|
/>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|