The code powering m.abunchtell.com https://m.abunchtell.com
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.
 
 
 
 

214 lines
6.0 KiB

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