// Package imports. import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { defineMessages, FormattedMessage, } from 'react-intl'; import Textarea from 'react-textarea-autosize'; // Components. import EmojiPicker from 'flavours/glitch/features/emoji_picker'; import ComposerTextareaIcons from './icons'; import ComposerTextareaSuggestions from './suggestions'; // Utils. import { isRtl } from 'flavours/glitch/util/rtl'; import { assignHandlers, hiddenComponent, } from 'flavours/glitch/util/react_helpers'; // Messages. const messages = defineMessages({ placeholder: { defaultMessage: 'What is on your mind?', id: 'compose_form.placeholder', }, }); // Handlers. const handlers = { // When blurring the textarea, suggestions are hidden. handleBlur () { this.setState({ suggestionsHidden: true }); }, // When the contents of the textarea change, we have to pull up new // autosuggest suggestions if applicable, and also change the value // of the textarea in our store. handleChange ({ target: { selectionStart, value, }, }) { const { onChange, onSuggestionsFetchRequested, onSuggestionsClearRequested, } = this.props; const { lastToken } = this.state; // This gets the token at the caret location, if it begins with an // `@` (mentions) or `:` (shortcodes). const left = value.slice(0, selectionStart).search(/[^\s\u200B]+$/); const right = value.slice(selectionStart).search(/[\s\u200B]/); const token = function () { switch (true) { case left < 0 || !/[@:]/.test(value[left]): return null; case right < 0: return value.slice(left); default: return value.slice(left, right + selectionStart).trim().toLowerCase(); } }(); // We only request suggestions for tokens which are at least 3 // characters long. if (onSuggestionsFetchRequested && token && token.length >= 3) { if (lastToken !== token) { this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart: left, }); onSuggestionsFetchRequested(token); } } else { this.setState({ lastToken: null }); if (onSuggestionsClearRequested) { onSuggestionsClearRequested(); } } // Updates the value of the textarea. if (onChange) { onChange(value); } }, // Handles a click on an autosuggestion. handleClickSuggestion (index) { const { textarea } = this; const { onSuggestionSelected, suggestions, } = this.props; const { lastToken, tokenStart, } = this.state; onSuggestionSelected(tokenStart, lastToken, suggestions.get(index)); textarea.focus(); }, // Handles a keypress. If the autosuggestions are visible, we need // to allow keypresses to navigate and sleect them. handleKeyDown (e) { const { disabled, onSubmit, onSuggestionSelected, suggestions, } = this.props; const { lastToken, suggestionsHidden, selectedSuggestion, tokenStart, } = this.state; // Keypresses do nothing if the composer is disabled. if (disabled) { e.preventDefault(); return; } // We submit the status on control/meta + enter. if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { onSubmit(); } // Switches over the pressed key. switch(e.key) { // On arrow down, we pick the next suggestion. case 'ArrowDown': if (suggestions && suggestions.size > 0 && !suggestionsHidden) { e.preventDefault(); this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); } return; // On arrow up, we pick the previous suggestion. case 'ArrowUp': if (suggestions && suggestions.size > 0 && !suggestionsHidden) { e.preventDefault(); this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); } return; // On enter or tab, we select the suggestion. case 'Enter': case 'Tab': if (onSuggestionSelected && lastToken !== null && suggestions && suggestions.size > 0 && !suggestionsHidden) { e.preventDefault(); e.stopPropagation(); onSuggestionSelected(tokenStart, lastToken, suggestions.get(selectedSuggestion)); } return; } }, // When the escape key is released, we either close the suggestions // window or focus the UI. handleKeyUp ({ key }) { const { suggestionsHidden } = this.state; if (key === 'Escape') { if (!suggestionsHidden) { this.setState({ suggestionsHidden: true }); } else { document.querySelector('.ui').parentElement.focus(); } } }, // Handles the pasting of images into the composer. handlePaste (e) { const { onPaste } = this.props; let d; if (onPaste && (d = e.clipboardData) && (d = d.files).length === 1) { onPaste(d); e.preventDefault(); } }, // Saves a reference to the textarea. handleRefTextarea (textarea) { this.textarea = textarea; }, }; // The component. export default class ComposerTextarea extends React.Component { // Constructor. constructor (props) { super(props); assignHandlers(this, handlers); this.state = { suggestionsHidden: false, selectedSuggestion: 0, lastToken: null, tokenStart: 0, }; // Instance variables. this.textarea = null; } // When we receive new suggestions, we unhide the suggestions window // if we didn't have any suggestions before. componentWillReceiveProps (nextProps) { const { suggestions } = this.props; const { suggestionsHidden } = this.state; if (nextProps.suggestions && nextProps.suggestions !== suggestions && nextProps.suggestions.size > 0 && suggestionsHidden) { this.setState({ suggestionsHidden: false }); } } // Rendering. render () { const { handleBlur, handleChange, handleClickSuggestion, handleKeyDown, handleKeyUp, handlePaste, handleRefTextarea, } = this.handlers; const { advancedOptions, autoFocus, disabled, intl, onPickEmoji, suggestions, value, } = this.props; const { selectedSuggestion, suggestionsHidden, } = this.state; // The result. return (