The code powering m.abunchtell.com https://m.abunchtell.com
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

240 linhas
7.0 KiB

  1. import React from 'react';
  2. import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
  3. import AutosuggestEmoji from './autosuggest_emoji';
  4. import AutosuggestHashtag from './autosuggest_hashtag';
  5. import ImmutablePropTypes from 'react-immutable-proptypes';
  6. import PropTypes from 'prop-types';
  7. import { isRtl } from '../rtl';
  8. import ImmutablePureComponent from 'react-immutable-pure-component';
  9. import Textarea from 'react-textarea-autosize';
  10. import classNames from 'classnames';
  11. const textAtCursorMatchesToken = (str, caretPosition) => {
  12. let word;
  13. let left = str.slice(0, caretPosition).search(/\S+$/);
  14. let right = str.slice(caretPosition).search(/\s/);
  15. if (right < 0) {
  16. word = str.slice(left);
  17. } else {
  18. word = str.slice(left, right + caretPosition);
  19. }
  20. if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
  21. return [null, null];
  22. }
  23. word = word.trim().toLowerCase();
  24. if (word.length > 0) {
  25. return [left + 1, word];
  26. } else {
  27. return [null, null];
  28. }
  29. };
  30. export default class AutosuggestTextarea extends ImmutablePureComponent {
  31. static propTypes = {
  32. value: PropTypes.string,
  33. suggestions: ImmutablePropTypes.list,
  34. disabled: PropTypes.bool,
  35. placeholder: PropTypes.string,
  36. onSuggestionSelected: PropTypes.func.isRequired,
  37. onSuggestionsClearRequested: PropTypes.func.isRequired,
  38. onSuggestionsFetchRequested: PropTypes.func.isRequired,
  39. onChange: PropTypes.func.isRequired,
  40. onKeyUp: PropTypes.func,
  41. onKeyDown: PropTypes.func,
  42. onPaste: PropTypes.func.isRequired,
  43. autoFocus: PropTypes.bool,
  44. };
  45. static defaultProps = {
  46. autoFocus: true,
  47. };
  48. state = {
  49. suggestionsHidden: true,
  50. focused: false,
  51. selectedSuggestion: 0,
  52. lastToken: null,
  53. tokenStart: 0,
  54. };
  55. onChange = (e) => {
  56. const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
  57. if (token !== null && this.state.lastToken !== token) {
  58. this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
  59. this.props.onSuggestionsFetchRequested(token);
  60. } else if (token === null) {
  61. this.setState({ lastToken: null });
  62. this.props.onSuggestionsClearRequested();
  63. }
  64. this.props.onChange(e);
  65. }
  66. onKeyDown = (e) => {
  67. const { suggestions, disabled } = this.props;
  68. const { selectedSuggestion, suggestionsHidden } = this.state;
  69. if (disabled) {
  70. e.preventDefault();
  71. return;
  72. }
  73. if (e.which === 229 || e.isComposing) {
  74. // Ignore key events during text composition
  75. // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
  76. return;
  77. }
  78. switch(e.key) {
  79. case 'Escape':
  80. if (suggestions.size === 0 || suggestionsHidden) {
  81. document.querySelector('.ui').parentElement.focus();
  82. } else {
  83. e.preventDefault();
  84. this.setState({ suggestionsHidden: true });
  85. }
  86. break;
  87. case 'ArrowDown':
  88. if (suggestions.size > 0 && !suggestionsHidden) {
  89. e.preventDefault();
  90. this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
  91. }
  92. break;
  93. case 'ArrowUp':
  94. if (suggestions.size > 0 && !suggestionsHidden) {
  95. e.preventDefault();
  96. this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
  97. }
  98. break;
  99. case 'Enter':
  100. case 'Tab':
  101. // Select suggestion
  102. if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
  103. e.preventDefault();
  104. e.stopPropagation();
  105. this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
  106. }
  107. break;
  108. }
  109. if (e.defaultPrevented || !this.props.onKeyDown) {
  110. return;
  111. }
  112. this.props.onKeyDown(e);
  113. }
  114. onBlur = () => {
  115. this.setState({ suggestionsHidden: true, focused: false });
  116. }
  117. onFocus = (e) => {
  118. this.setState({ focused: true });
  119. if (this.props.onFocus) {
  120. this.props.onFocus(e);
  121. }
  122. }
  123. onSuggestionClick = (e) => {
  124. const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
  125. e.preventDefault();
  126. this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
  127. this.textarea.focus();
  128. }
  129. componentWillReceiveProps (nextProps) {
  130. if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
  131. this.setState({ suggestionsHidden: false });
  132. }
  133. }
  134. setTextarea = (c) => {
  135. this.textarea = c;
  136. }
  137. onPaste = (e) => {
  138. if (e.clipboardData && e.clipboardData.files.length === 1) {
  139. this.props.onPaste(e.clipboardData.files);
  140. e.preventDefault();
  141. }
  142. }
  143. renderSuggestion = (suggestion, i) => {
  144. const { selectedSuggestion } = this.state;
  145. let inner, key;
  146. if (suggestion.type === 'emoji') {
  147. inner = <AutosuggestEmoji emoji={suggestion} />;
  148. key = suggestion.id;
  149. } else if (suggestion.type === 'hashtag') {
  150. inner = <AutosuggestHashtag tag={suggestion} />;
  151. key = suggestion.name;
  152. } else if (suggestion.type === 'account') {
  153. inner = <AutosuggestAccountContainer id={suggestion.id} />;
  154. key = suggestion.id;
  155. }
  156. return (
  157. <div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
  158. {inner}
  159. </div>
  160. );
  161. }
  162. render () {
  163. const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
  164. const { suggestionsHidden } = this.state;
  165. const style = { direction: 'ltr' };
  166. if (isRtl(value)) {
  167. style.direction = 'rtl';
  168. }
  169. return [
  170. <div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
  171. <div className='autosuggest-textarea'>
  172. <label>
  173. <span style={{ display: 'none' }}>{placeholder}</span>
  174. <Textarea
  175. inputRef={this.setTextarea}
  176. className='autosuggest-textarea__textarea'
  177. disabled={disabled}
  178. placeholder={placeholder}
  179. autoFocus={autoFocus}
  180. value={value}
  181. onChange={this.onChange}
  182. onKeyDown={this.onKeyDown}
  183. onKeyUp={onKeyUp}
  184. onFocus={this.onFocus}
  185. onBlur={this.onBlur}
  186. onPaste={this.onPaste}
  187. style={style}
  188. aria-autocomplete='list'
  189. />
  190. </label>
  191. </div>
  192. {children}
  193. </div>,
  194. <div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
  195. <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
  196. {suggestions.map(this.renderSuggestion)}
  197. </div>
  198. </div>,
  199. ];
  200. }
  201. }