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';
12 const textAtCursorMatchesToken
= (str
, caretPosition
) => {
15 let left
= str
.slice(0, caretPosition
).search(/\S+$/);
16 let right
= str
.slice(caretPosition
).search(/\s/);
19 word
= str
.slice(left
);
21 word
= str
.slice(left
, right
+ caretPosition
);
24 if (!word
|| word
.trim().length
< 3 || ['@', ':', '#'].indexOf(word
[0]) === -1) {
28 word
= word
.trim().toLowerCase();
30 if (word
.length
> 0) {
31 return [left
+ 1, word
];
37 export default class AutosuggestTextarea
extends ImmutablePureComponent
{
40 value: PropTypes
.string
,
41 suggestions: ImmutablePropTypes
.list
,
42 disabled: PropTypes
.bool
,
43 placeholder: PropTypes
.string
,
44 onSuggestionSelected: PropTypes
.func
.isRequired
,
45 onSuggestionsClearRequested: PropTypes
.func
.isRequired
,
46 onSuggestionsFetchRequested: 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: true,
61 selectedSuggestion: 0,
67 const [ tokenStart
, token
] = textAtCursorMatchesToken(e
.target
.value
, e
.target
.selectionStart
);
69 if (token
!== null && this.state
.lastToken
!== token
) {
70 this.setState({ lastToken: token
, selectedSuggestion: 0, tokenStart
});
71 this.props
.onSuggestionsFetchRequested(token
);
72 } else if (token
=== null) {
73 this.setState({ lastToken: null });
74 this.props
.onSuggestionsClearRequested();
77 this.props
.onChange(e
);
81 const { suggestions
, disabled
} = this.props
;
82 const { selectedSuggestion
, suggestionsHidden
} = this.state
;
89 if (e
.which
=== 229 || e
.isComposing
) {
90 // Ignore key events during text composition
91 // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
97 if (suggestions
.size
=== 0 || suggestionsHidden
) {
98 document
.querySelector('.ui').parentElement
.focus();
101 this.setState({ suggestionsHidden: true });
106 if (suggestions
.size
> 0 && !suggestionsHidden
) {
108 this.setState({ selectedSuggestion: Math
.min(selectedSuggestion
+ 1, suggestions
.size
- 1) });
113 if (suggestions
.size
> 0 && !suggestionsHidden
) {
115 this.setState({ selectedSuggestion: Math
.max(selectedSuggestion
- 1, 0) });
122 if (this.state
.lastToken
!== null && suggestions
.size
> 0 && !suggestionsHidden
) {
125 this.props
.onSuggestionSelected(this.state
.tokenStart
, this.state
.lastToken
, suggestions
.get(selectedSuggestion
));
131 if (e
.defaultPrevented
|| !this.props
.onKeyDown
) {
135 this.props
.onKeyDown(e
);
139 this.setState({ suggestionsHidden: true, focused: false });
143 this.setState({ focused: true });
144 if (this.props
.onFocus
) {
145 this.props
.onFocus(e
);
149 onSuggestionClick
= (e
) => {
150 const suggestion
= this.props
.suggestions
.get(e
.currentTarget
.getAttribute('data-index'));
152 this.props
.onSuggestionSelected(this.state
.tokenStart
, this.state
.lastToken
, suggestion
);
153 this.textarea
.focus();
156 componentWillReceiveProps (nextProps
) {
157 if (nextProps
.suggestions
!== this.props
.suggestions
&& nextProps
.suggestions
.size
> 0 && this.state
.suggestionsHidden
&& this.state
.focused
) {
158 this.setState({ suggestionsHidden: false });
162 setTextarea
= (c
) => {
167 if (e
.clipboardData
&& e
.clipboardData
.files
.length
=== 1) {
168 this.props
.onPaste(e
.clipboardData
.files
);
173 renderSuggestion
= (suggestion
, i
) => {
174 const { selectedSuggestion
} = this.state
;
177 if (typeof suggestion
=== 'object' && suggestion
.shortcode
) {
178 inner
= <AutosuggestEmoji emoji
={suggestion
} />;
180 } else if (typeof suggestion
=== 'object' && suggestion
.name
) {
181 inner
= <AutosuggestHashtag tag
={suggestion
} />;
182 key
= suggestion
.name
;
184 inner
= <AutosuggestAccountContainer id
={suggestion
} />;
189 <div role
='button' tabIndex
='0' key
={key
} data
-index
={i
} className
={classNames('autosuggest-textarea__suggestions__item', { selected: i
=== selectedSuggestion
})} onMouseDown
={this.onSuggestionClick
}>
196 const { value
, suggestions
, disabled
, placeholder
, onKeyUp
, autoFocus
, children
} = this.props
;
197 const { suggestionsHidden
} = this.state
;
198 const style
= { direction: 'ltr' };
201 style
.direction
= 'rtl';
205 <div className
='compose-form__autosuggest-wrapper' key
='autosuggest-wrapper'>
206 <div className
='autosuggest-textarea'>
208 <span style
={{ display: 'none' }}>{placeholder
}</span
>
211 inputRef
={this.setTextarea
}
212 className
='autosuggest-textarea__textarea'
214 placeholder
={placeholder
}
215 autoFocus
={autoFocus
}
217 onChange
={this.onChange
}
218 onKeyDown
={this.onKeyDown
}
220 onFocus
={this.onFocus
}
222 onPaste
={this.onPaste
}
224 aria
-autocomplete
='list'
231 <div className
='autosuggest-textarea__suggestions-wrapper' key
='suggestions-wrapper'>
232 <div className
={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
233 {suggestions
.map(this.renderSuggestion
)}