|
|
- // 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 (
- <div className='composer--textarea'>
- <label>
- <span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span>
- <ComposerTextareaIcons
- advancedOptions={advancedOptions}
- intl={intl}
- />
- <Textarea
- aria-autocomplete='list'
- autoFocus={autoFocus}
- className='textarea'
- disabled={disabled}
- inputRef={handleRefTextarea}
- onBlur={handleBlur}
- onChange={handleChange}
- onKeyDown={handleKeyDown}
- onKeyUp={handleKeyUp}
- onPaste={handlePaste}
- placeholder={intl.formatMessage(messages.placeholder)}
- value={value}
- style={{ direction: isRtl(value) ? 'rtl' : 'ltr' }}
- />
- </label>
- <EmojiPicker onPickEmoji={onPickEmoji} />
- <ComposerTextareaSuggestions
- hidden={suggestionsHidden}
- onSuggestionClick={handleClickSuggestion}
- suggestions={suggestions}
- value={selectedSuggestion}
- />
- </div>
- );
- }
-
- }
-
- // Props.
- ComposerTextarea.propTypes = {
- advancedOptions: ImmutablePropTypes.map,
- autoFocus: PropTypes.bool,
- disabled: PropTypes.bool,
- intl: PropTypes.object.isRequired,
- onChange: PropTypes.func,
- onPaste: PropTypes.func,
- onPickEmoji: PropTypes.func,
- onSubmit: PropTypes.func,
- onSuggestionsClearRequested: PropTypes.func,
- onSuggestionsFetchRequested: PropTypes.func,
- onSuggestionSelected: PropTypes.func,
- suggestions: ImmutablePropTypes.list,
- value: PropTypes.string,
- };
-
- // Default props.
- ComposerTextarea.defaultProps = { autoFocus: true };
|