You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
5.9 KiB

  1. import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
  2. import ImmutablePropTypes from 'react-immutable-proptypes';
  3. import PropTypes from 'prop-types';
  4. import { isRtl } from '../rtl';
  5. const textAtCursorMatchesToken = (str, caretPosition) => {
  6. let word;
  7. let left = str.slice(0, caretPosition).search(/\S+$/);
  8. let right = str.slice(caretPosition).search(/\s/);
  9. if (right < 0) {
  10. word = str.slice(left);
  11. } else {
  12. word = str.slice(left, right + caretPosition);
  13. }
  14. if (!word || word.trim().length < 2 || word[0] !== '@') {
  15. return [null, null];
  16. }
  17. word = word.trim().toLowerCase().slice(1);
  18. if (word.length > 0) {
  19. return [left + 1, word];
  20. } else {
  21. return [null, null];
  22. }
  23. };
  24. class AutosuggestTextarea extends React.Component {
  25. constructor (props, context) {
  26. super(props, context);
  27. this.state = {
  28. suggestionsHidden: false,
  29. selectedSuggestion: 0,
  30. lastToken: null,
  31. tokenStart: 0
  32. };
  33. this.onChange = this.onChange.bind(this);
  34. this.onKeyDown = this.onKeyDown.bind(this);
  35. this.onBlur = this.onBlur.bind(this);
  36. this.onSuggestionClick = this.onSuggestionClick.bind(this);
  37. this.setTextarea = this.setTextarea.bind(this);
  38. this.onPaste = this.onPaste.bind(this);
  39. }
  40. onChange (e) {
  41. const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
  42. if (token !== null && this.state.lastToken !== token) {
  43. this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
  44. this.props.onSuggestionsFetchRequested(token);
  45. } else if (token === null) {
  46. this.setState({ lastToken: null });
  47. this.props.onSuggestionsClearRequested();
  48. }
  49. // auto-resize textarea
  50. e.target.style.height = 'auto';
  51. e.target.style.height = `${e.target.scrollHeight}px`;
  52. this.props.onChange(e);
  53. }
  54. onKeyDown (e) {
  55. const { suggestions, disabled } = this.props;
  56. const { selectedSuggestion, suggestionsHidden } = this.state;
  57. if (disabled) {
  58. e.preventDefault();
  59. return;
  60. }
  61. switch(e.key) {
  62. case 'Escape':
  63. if (!suggestionsHidden) {
  64. e.preventDefault();
  65. this.setState({ suggestionsHidden: true });
  66. }
  67. break;
  68. case 'ArrowDown':
  69. if (suggestions.size > 0 && !suggestionsHidden) {
  70. e.preventDefault();
  71. this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
  72. }
  73. break;
  74. case 'ArrowUp':
  75. if (suggestions.size > 0 && !suggestionsHidden) {
  76. e.preventDefault();
  77. this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
  78. }
  79. break;
  80. case 'Enter':
  81. case 'Tab':
  82. // Select suggestion
  83. if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
  84. e.preventDefault();
  85. e.stopPropagation();
  86. this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
  87. }
  88. break;
  89. }
  90. if (e.defaultPrevented || !this.props.onKeyDown) {
  91. return;
  92. }
  93. this.props.onKeyDown(e);
  94. }
  95. onBlur () {
  96. // If we hide the suggestions immediately, then this will prevent the
  97. // onClick for the suggestions themselves from firing.
  98. // Setting a short window for that to take place before hiding the
  99. // suggestions ensures that can't happen.
  100. setTimeout(() => {
  101. this.setState({ suggestionsHidden: true });
  102. }, 100);
  103. }
  104. onSuggestionClick (suggestion, e) {
  105. e.preventDefault();
  106. this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
  107. this.textarea.focus();
  108. }
  109. componentWillReceiveProps (nextProps) {
  110. if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
  111. this.setState({ suggestionsHidden: false });
  112. }
  113. }
  114. setTextarea (c) {
  115. this.textarea = c;
  116. }
  117. onPaste (e) {
  118. if (e.clipboardData && e.clipboardData.files.length === 1) {
  119. this.props.onPaste(e.clipboardData.files)
  120. e.preventDefault();
  121. }
  122. }
  123. render () {
  124. const { value, suggestions, disabled, placeholder, onKeyUp } = this.props;
  125. const { suggestionsHidden, selectedSuggestion } = this.state;
  126. const className = 'autosuggest-textarea__textarea';
  127. const style = { direction: 'ltr' };
  128. if (isRtl(value)) {
  129. style.direction = 'rtl';
  130. }
  131. return (
  132. <div className='autosuggest-textarea'>
  133. <textarea
  134. ref={this.setTextarea}
  135. className={className}
  136. disabled={disabled}
  137. placeholder={placeholder}
  138. autoFocus={true}
  139. value={value}
  140. onChange={this.onChange}
  141. onKeyDown={this.onKeyDown}
  142. onKeyUp={onKeyUp}
  143. onBlur={this.onBlur}
  144. onPaste={this.onPaste}
  145. style={style}
  146. />
  147. <div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
  148. {suggestions.map((suggestion, i) => (
  149. <div
  150. role='button'
  151. tabIndex='0'
  152. key={suggestion}
  153. className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
  154. onClick={this.onSuggestionClick.bind(this, suggestion)}>
  155. <AutosuggestAccountContainer id={suggestion} />
  156. </div>
  157. ))}
  158. </div>
  159. </div>
  160. );
  161. }
  162. };
  163. AutosuggestTextarea.propTypes = {
  164. value: PropTypes.string,
  165. suggestions: ImmutablePropTypes.list,
  166. disabled: PropTypes.bool,
  167. placeholder: PropTypes.string,
  168. onSuggestionSelected: PropTypes.func.isRequired,
  169. onSuggestionsClearRequested: PropTypes.func.isRequired,
  170. onSuggestionsFetchRequested: PropTypes.func.isRequired,
  171. onChange: PropTypes.func.isRequired,
  172. onKeyUp: PropTypes.func,
  173. onKeyDown: PropTypes.func,
  174. onPaste: PropTypes.func.isRequired,
  175. };
  176. export default AutosuggestTextarea;