闭社主体 forked from https://github.com/tootsuite/mastodon
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.

200 lines
5.5 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. import Textarea from 'react-textarea-autosize';
  8. const textAtCursorMatchesToken = (str, caretPosition) => {
  9. let word;
  10. let left = str.slice(0, caretPosition).search(/\S+$/);
  11. let right = str.slice(caretPosition).search(/\s/);
  12. if (right < 0) {
  13. word = str.slice(left);
  14. } else {
  15. word = str.slice(left, right + caretPosition);
  16. }
  17. if (!word || word.trim().length < 2 || word[0] !== '@') {
  18. return [null, null];
  19. }
  20. word = word.trim().toLowerCase().slice(1);
  21. if (word.length > 0) {
  22. return [left + 1, word];
  23. } else {
  24. return [null, null];
  25. }
  26. };
  27. class AutosuggestTextarea extends ImmutablePureComponent {
  28. static propTypes = {
  29. value: PropTypes.string,
  30. suggestions: ImmutablePropTypes.list,
  31. disabled: PropTypes.bool,
  32. placeholder: PropTypes.string,
  33. onSuggestionSelected: PropTypes.func.isRequired,
  34. onSuggestionsClearRequested: PropTypes.func.isRequired,
  35. onSuggestionsFetchRequested: PropTypes.func.isRequired,
  36. onChange: PropTypes.func.isRequired,
  37. onKeyUp: PropTypes.func,
  38. onKeyDown: PropTypes.func,
  39. onPaste: PropTypes.func.isRequired,
  40. autoFocus: PropTypes.bool,
  41. };
  42. static defaultProps = {
  43. autoFocus: true,
  44. };
  45. state = {
  46. suggestionsHidden: false,
  47. selectedSuggestion: 0,
  48. lastToken: null,
  49. tokenStart: 0,
  50. };
  51. onChange = (e) => {
  52. const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
  53. if (token !== null && this.state.lastToken !== token) {
  54. this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
  55. this.props.onSuggestionsFetchRequested(token);
  56. } else if (token === null) {
  57. this.setState({ lastToken: null });
  58. this.props.onSuggestionsClearRequested();
  59. }
  60. this.props.onChange(e);
  61. }
  62. onKeyDown = (e) => {
  63. const { suggestions, disabled } = this.props;
  64. const { selectedSuggestion, suggestionsHidden } = this.state;
  65. if (disabled) {
  66. e.preventDefault();
  67. return;
  68. }
  69. switch(e.key) {
  70. case 'Escape':
  71. if (!suggestionsHidden) {
  72. e.preventDefault();
  73. this.setState({ suggestionsHidden: true });
  74. }
  75. break;
  76. case 'ArrowDown':
  77. if (suggestions.size > 0 && !suggestionsHidden) {
  78. e.preventDefault();
  79. this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
  80. }
  81. break;
  82. case 'ArrowUp':
  83. if (suggestions.size > 0 && !suggestionsHidden) {
  84. e.preventDefault();
  85. this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
  86. }
  87. break;
  88. case 'Enter':
  89. case 'Tab':
  90. // Select suggestion
  91. if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
  92. e.preventDefault();
  93. e.stopPropagation();
  94. this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
  95. }
  96. break;
  97. }
  98. if (e.defaultPrevented || !this.props.onKeyDown) {
  99. return;
  100. }
  101. this.props.onKeyDown(e);
  102. }
  103. onBlur = () => {
  104. this.setState({ suggestionsHidden: true });
  105. }
  106. onSuggestionClick = (e) => {
  107. const suggestion = Number(e.currentTarget.getAttribute('data-index'));
  108. e.preventDefault();
  109. this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
  110. this.textarea.focus();
  111. }
  112. componentWillReceiveProps (nextProps) {
  113. if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
  114. this.setState({ suggestionsHidden: false });
  115. }
  116. }
  117. setTextarea = (c) => {
  118. this.textarea = c;
  119. }
  120. onPaste = (e) => {
  121. if (e.clipboardData && e.clipboardData.files.length === 1) {
  122. this.props.onPaste(e.clipboardData.files);
  123. e.preventDefault();
  124. }
  125. }
  126. render () {
  127. const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props;
  128. const { suggestionsHidden, selectedSuggestion } = this.state;
  129. const style = { direction: 'ltr' };
  130. if (isRtl(value)) {
  131. style.direction = 'rtl';
  132. }
  133. return (
  134. <div className='autosuggest-textarea'>
  135. <Textarea
  136. inputRef={this.setTextarea}
  137. className='autosuggest-textarea__textarea'
  138. disabled={disabled}
  139. placeholder={placeholder}
  140. autoFocus={autoFocus}
  141. value={value}
  142. onChange={this.onChange}
  143. onKeyDown={this.onKeyDown}
  144. onKeyUp={onKeyUp}
  145. onBlur={this.onBlur}
  146. onPaste={this.onPaste}
  147. style={style}
  148. />
  149. <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
  150. {suggestions.map((suggestion, i) => (
  151. <div
  152. role='button'
  153. tabIndex='0'
  154. key={suggestion}
  155. data-index={suggestion}
  156. className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
  157. onMouseDown={this.onSuggestionClick}
  158. >
  159. <AutosuggestAccountContainer id={suggestion} />
  160. </div>
  161. ))}
  162. </div>
  163. </div>
  164. );
  165. }
  166. }
  167. export default AutosuggestTextarea;