WIPgit status <Compose> Refactor; <Composer> ed.
This commit is contained in:
		@@ -0,0 +1,297 @@
 | 
			
		||||
//  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 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.
 | 
			
		||||
  blur () {
 | 
			
		||||
    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.
 | 
			
		||||
  change ({
 | 
			
		||||
    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.
 | 
			
		||||
  clickSuggestion (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.
 | 
			
		||||
  keyDown (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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //  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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //  We submit the status on control/meta + enter.
 | 
			
		||||
    if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
 | 
			
		||||
      onSubmit();
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  //  When the escape key is released, we either close the suggestions
 | 
			
		||||
  //  window or focus the UI.
 | 
			
		||||
  keyUp ({ 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.
 | 
			
		||||
  paste (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.
 | 
			
		||||
  refTextarea (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 {
 | 
			
		||||
      blur,
 | 
			
		||||
      change,
 | 
			
		||||
      clickSuggestion,
 | 
			
		||||
      keyDown,
 | 
			
		||||
      keyUp,
 | 
			
		||||
      paste,
 | 
			
		||||
      refTextarea,
 | 
			
		||||
    } = this.handlers;
 | 
			
		||||
    const {
 | 
			
		||||
      autoFocus,
 | 
			
		||||
      disabled,
 | 
			
		||||
      intl,
 | 
			
		||||
      onPickEmoji,
 | 
			
		||||
      suggestions,
 | 
			
		||||
      value,
 | 
			
		||||
    } = this.props;
 | 
			
		||||
    const {
 | 
			
		||||
      selectedSuggestion,
 | 
			
		||||
      suggestionsHidden,
 | 
			
		||||
    } = this.state;
 | 
			
		||||
 | 
			
		||||
    //  The result.
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='autosuggest-textarea'>
 | 
			
		||||
        <label>
 | 
			
		||||
          <span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span>
 | 
			
		||||
          <Textarea
 | 
			
		||||
            aria-autocomplete='list'
 | 
			
		||||
            autoFocus={autoFocus}
 | 
			
		||||
            disabled={disabled}
 | 
			
		||||
            inputRef={refTextarea}
 | 
			
		||||
            onBlur={blur}
 | 
			
		||||
            onChange={change}
 | 
			
		||||
            onKeyDown={keyDown}
 | 
			
		||||
            onKeyUp={keyUp}
 | 
			
		||||
            onPaste={paste}
 | 
			
		||||
            placeholder={intl.formatMessage(messages.placeholder)}
 | 
			
		||||
            value={value}
 | 
			
		||||
            style={{ direction: isRtl(value) ? 'rtl' : 'ltr' }}
 | 
			
		||||
          />
 | 
			
		||||
        </label>
 | 
			
		||||
        <EmojiPicker onPickEmoji={onPickEmoji} />
 | 
			
		||||
        <ComposerTextareaSuggestions
 | 
			
		||||
          hidden={suggestionsHidden}
 | 
			
		||||
          onSuggestionClick={clickSuggestion}
 | 
			
		||||
          suggestions={suggestions}
 | 
			
		||||
          value={selectedSuggestion}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//  Props.
 | 
			
		||||
ComposerTextarea.propTypes = {
 | 
			
		||||
  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 };
 | 
			
		||||
		Reference in New Issue
	
	Block a user