1 import React
from 'react';
2 import AutosuggestAccountContainer
from '../features/compose/containers/autosuggest_account_container';
3 import AutosuggestShortcode
from '../features/compose/components/autosuggest_shortcode';
4 import ImmutablePropTypes
from 'react-immutable-proptypes';
5 import PropTypes
from 'prop-types';
6 import { isRtl
} from '../rtl';
7 import ImmutablePureComponent
from 'react-immutable-pure-component';
8 import Textarea
from 'react-textarea-autosize';
10 const textAtCursorMatchesToken
= (str
, caretPosition
) => {
13 let left
= str
.slice(0, caretPosition
).search(/\S+$/);
14 let right
= str
.slice(caretPosition
).search(/\s/);
17 word
= str
.slice(left
);
19 word
= str
.slice(left
, right
+ caretPosition
);
22 if (!word
|| word
.trim().length
< 2 || ['@', ':', '#'].indexOf(word
[0]) === -1) {
26 word
= word
.trim().toLowerCase();
27 // was: .slice(1); - we leave the leading char there, handler can decide what to do based on it
29 if (word
.length
> 0) {
30 return [left
+ 1, word
];
36 export default class AutosuggestTextarea
extends ImmutablePureComponent
{
39 value: PropTypes
.string
,
40 suggestions: ImmutablePropTypes
.list
,
41 disabled: PropTypes
.bool
,
42 placeholder: PropTypes
.string
,
43 onSuggestionSelected: PropTypes
.func
.isRequired
,
44 onSuggestionsClearRequested: PropTypes
.func
.isRequired
,
45 onSuggestionsFetchRequested: PropTypes
.func
.isRequired
,
46 onLocalSuggestionsFetchRequested: PropTypes
.func
.isRequired
,
47 onChange: PropTypes
.func
.isRequired
,
48 onKeyUp: PropTypes
.func
,
49 onKeyDown: PropTypes
.func
,
50 onPaste: PropTypes
.func
.isRequired
,
51 autoFocus: PropTypes
.bool
,
54 static defaultProps
= {
59 suggestionsHidden: false,
60 selectedSuggestion: 0,
66 const [ tokenStart
, token
] = textAtCursorMatchesToken(e
.target
.value
, e
.target
.selectionStart
);
68 if (token
!== null && this.state
.lastToken
!== token
) {
69 this.setState({ lastToken: token
, selectedSuggestion: 0, tokenStart
});
70 if (token
[0] === ':') {
71 // faster debounce for shortcodes.
72 // hashtags have long debounce because they're fetched from server.
73 this.props
.onLocalSuggestionsFetchRequested(token
);
75 this.props
.onSuggestionsFetchRequested(token
);
77 } else if (token
=== null) {
78 this.setState({ lastToken: null });
79 this.props
.onSuggestionsClearRequested();
82 this.props
.onChange(e
);
86 const { suggestions
, disabled
} = this.props
;
87 const { selectedSuggestion
, suggestionsHidden
} = this.state
;
96 if (!suggestionsHidden
) {
98 this.setState({ suggestionsHidden: true });
103 if (suggestions
.size
> 0 && !suggestionsHidden
) {
105 this.setState({ selectedSuggestion: Math
.min(selectedSuggestion
+ 1, suggestions
.size
- 1) });
110 if (suggestions
.size
> 0 && !suggestionsHidden
) {
112 this.setState({ selectedSuggestion: Math
.max(selectedSuggestion
- 1, 0) });
119 if (this.state
.lastToken
!== null && suggestions
.size
> 0 && !suggestionsHidden
) {
122 this.props
.onSuggestionSelected(this.state
.tokenStart
, this.state
.lastToken
, suggestions
.get(selectedSuggestion
));
128 if (e
.defaultPrevented
|| !this.props
.onKeyDown
) {
132 this.props
.onKeyDown(e
);
136 this.setState({ suggestionsHidden: true });
139 onSuggestionClick
= (e
) => {
140 // leave suggestion string unchanged if it's a hash / shortcode suggestion. convert account number to int.
141 const suggestionStr
= e
.currentTarget
.getAttribute('data-index');
142 const suggestion
= [':', '#'].indexOf(suggestionStr
[0]) !== -1 ? suggestionStr : Number(suggestionStr
);
144 this.props
.onSuggestionSelected(this.state
.tokenStart
, this.state
.lastToken
, suggestion
);
145 this.textarea
.focus();
148 componentWillReceiveProps (nextProps
) {
149 if (nextProps
.suggestions
!== this.props
.suggestions
&& nextProps
.suggestions
.size
> 0 && this.state
.suggestionsHidden
) {
150 this.setState({ suggestionsHidden: false });
154 setTextarea
= (c
) => {
159 if (e
.clipboardData
&& e
.clipboardData
.files
.length
=== 1) {
160 this.props
.onPaste(e
.clipboardData
.files
);
166 const { value
, suggestions
, disabled
, placeholder
, onKeyUp
, autoFocus
} = this.props
;
167 const { suggestionsHidden
, selectedSuggestion
} = this.state
;
168 const style
= { direction: 'ltr' };
171 style
.direction
= 'rtl';
174 let makeItem
= (suggestion
) => {
175 if (suggestion
[0] === ':') return <AutosuggestShortcode shortcode
={suggestion
} />;
176 if (suggestion
[0] === '#') return suggestion
; // hashtag
178 // else - accounts are always returned as IDs with no prefix
179 return <AutosuggestAccountContainer id
={suggestion
} />;
183 <div className
='autosuggest-textarea'>
185 <span style
={{ display: 'none' }}>{placeholder
}</span
>
187 inputRef
={this.setTextarea
}
188 className
='autosuggest-textarea__textarea'
190 placeholder
={placeholder
}
191 autoFocus
={autoFocus
}
193 onChange
={this.onChange
}
194 onKeyDown
={this.onKeyDown
}
197 onPaste
={this.onPaste
}
202 <div className
={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
203 {suggestions
.map((suggestion
, i
) => (
208 data
-index
={suggestion
}
209 className
={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
210 onMouseDown
={this.onSuggestionClick
}
212 {makeItem(suggestion
)}