Added keyboard selection to search autocomplete
Этот коммит содержится в:
@@ -2,13 +2,14 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
const ChannelStore = require('../stores/channel_store.jsx');
|
||||
const KeyCodes = require('../utils/constants.jsx').KeyCodes;
|
||||
const UserStore = require('../stores/user_store.jsx');
|
||||
const Utils = require('../utils/utils.jsx');
|
||||
|
||||
const patterns = {
|
||||
channels: /\b(?:in|channel):\s*(\S*)$/i,
|
||||
users: /\bfrom:\s*(\S*)$/i
|
||||
};
|
||||
const patterns = new Map([
|
||||
['channels', /\b(?:in|channel):\s*(\S*)$/i],
|
||||
['users', /\bfrom:\s*(\S*)$/i]
|
||||
]);
|
||||
|
||||
export default class SearchAutocomplete extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -17,11 +18,17 @@ export default class SearchAutocomplete extends React.Component {
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleDocumentClick = this.handleDocumentClick.bind(this);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
|
||||
this.completeWord = this.completeWord.bind(this);
|
||||
this.updateSuggestions = this.updateSuggestions.bind(this);
|
||||
|
||||
this.state = {
|
||||
show: false,
|
||||
mode: '',
|
||||
filter: ''
|
||||
filter: '',
|
||||
selection: 0,
|
||||
suggestions: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,13 +41,7 @@ export default class SearchAutocomplete extends React.Component {
|
||||
}
|
||||
|
||||
handleClick(value) {
|
||||
this.props.completeWord(this.state.filter, value);
|
||||
|
||||
this.setState({
|
||||
show: false,
|
||||
mode: '',
|
||||
filter: ''
|
||||
});
|
||||
this.completeWord(value);
|
||||
}
|
||||
|
||||
handleDocumentClick(e) {
|
||||
@@ -59,16 +60,20 @@ export default class SearchAutocomplete extends React.Component {
|
||||
|
||||
let mode = '';
|
||||
let filter = '';
|
||||
for (const pattern in patterns) {
|
||||
const result = patterns[pattern].exec(preText);
|
||||
for (const [modeForPattern, pattern] of patterns) {
|
||||
const result = pattern.exec(preText);
|
||||
|
||||
if (result) {
|
||||
mode = pattern;
|
||||
mode = modeForPattern;
|
||||
filter = result[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode !== this.state.mode || filter !== this.state.filter) {
|
||||
this.updateSuggestions(mode, filter);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
mode,
|
||||
filter,
|
||||
@@ -76,48 +81,147 @@ export default class SearchAutocomplete extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (!this.state.show || this.state.suggestions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.which === KeyCodes.UP || e.which === KeyCodes.DOWN) {
|
||||
e.preventDefault();
|
||||
|
||||
let selection = this.state.selection;
|
||||
|
||||
if (e.which === KeyCodes.UP) {
|
||||
selection -= 1;
|
||||
} else {
|
||||
selection += 1;
|
||||
}
|
||||
|
||||
if (selection >= 0 && selection < this.state.suggestions.length) {
|
||||
this.setState({
|
||||
selection
|
||||
});
|
||||
}
|
||||
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) {
|
||||
e.preventDefault();
|
||||
|
||||
this.completeSelectedWord();
|
||||
}
|
||||
}
|
||||
|
||||
completeSelectedWord() {
|
||||
if (this.state.mode === 'channels') {
|
||||
this.completeWord(this.state.suggestions[this.state.selection].name);
|
||||
} else if (this.state.mode === 'users') {
|
||||
this.completeWord(this.state.suggestions[this.state.selection].username);
|
||||
}
|
||||
}
|
||||
|
||||
completeWord(value) {
|
||||
// add a space so that anything else typed doesn't interfere with the search flag
|
||||
this.props.completeWord(this.state.filter, value + ' ');
|
||||
|
||||
this.setState({
|
||||
show: false,
|
||||
mode: '',
|
||||
filter: '',
|
||||
selection: 0
|
||||
});
|
||||
}
|
||||
|
||||
updateSuggestions(mode, filter) {
|
||||
let suggestions = [];
|
||||
|
||||
if (mode === 'channels') {
|
||||
let channels = ChannelStore.getAll();
|
||||
|
||||
if (filter) {
|
||||
channels = channels.filter((channel) => channel.name.startsWith(filter));
|
||||
}
|
||||
|
||||
channels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
suggestions = channels;
|
||||
} else if (mode === 'users') {
|
||||
let users = UserStore.getActiveOnlyProfileList();
|
||||
|
||||
if (filter) {
|
||||
users = users.filter((user) => user.username.startsWith(filter));
|
||||
}
|
||||
|
||||
users.sort((a, b) => a.username.localeCompare(b.username));
|
||||
|
||||
suggestions = users;
|
||||
}
|
||||
|
||||
let selection = this.state.selection;
|
||||
|
||||
// keep the same user/channel selected if it's still visible as a suggestion
|
||||
if (selection > 0 && this.state.suggestions.length > 0) {
|
||||
// we can't just use indexOf to find if the selection is still in the list since they are different javascript objects
|
||||
const currentSelectionId = this.state.suggestions[selection].id;
|
||||
let found = false;
|
||||
|
||||
for (let i = 0; i < suggestions.length; i++) {
|
||||
if (suggestions[i].id === currentSelectionId) {
|
||||
selection = i;
|
||||
found = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
selection = 0;
|
||||
}
|
||||
} else {
|
||||
selection = 0;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
suggestions,
|
||||
selection
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.show) {
|
||||
if (!this.state.show || this.state.suggestions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let suggestions = [];
|
||||
|
||||
if (this.state.mode === 'channels') {
|
||||
let channels = ChannelStore.getAll();
|
||||
suggestions = this.state.suggestions.map((channel, index) => {
|
||||
let className = 'search-autocomplete__channel';
|
||||
if (this.state.selection === index) {
|
||||
className += ' selected';
|
||||
}
|
||||
|
||||
if (this.state.filter) {
|
||||
channels = channels.filter((channel) => channel.name.startsWith(this.state.filter));
|
||||
}
|
||||
|
||||
channels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
suggestions = channels.map((channel) => {
|
||||
return (
|
||||
<div
|
||||
key={channel.id}
|
||||
key={channel.name}
|
||||
ref={channel.name}
|
||||
onClick={this.handleClick.bind(this, channel.name)}
|
||||
className='search-autocomplete__channel'
|
||||
className={className}
|
||||
>
|
||||
{channel.name}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
} else if (this.state.mode === 'users') {
|
||||
let users = UserStore.getActiveOnlyProfileList();
|
||||
suggestions = this.state.suggestions.map((user, index) => {
|
||||
let className = 'search-autocomplete__user';
|
||||
if (this.state.selection === index) {
|
||||
className += ' selected';
|
||||
}
|
||||
|
||||
if (this.state.filter) {
|
||||
users = users.filter((user) => user.username.startsWith(this.state.filter));
|
||||
}
|
||||
|
||||
users.sort((a, b) => a.username.localeCompare(b.username));
|
||||
|
||||
suggestions = users.map((user) => {
|
||||
return (
|
||||
<div
|
||||
key={user.id}
|
||||
key={user.username}
|
||||
ref={user.username}
|
||||
onClick={this.handleClick.bind(this, user.username)}
|
||||
className='search-autocomplete__user'
|
||||
className={className}
|
||||
>
|
||||
<img
|
||||
className='profile-img'
|
||||
@@ -129,10 +233,6 @@ export default class SearchAutocomplete extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref='container'
|
||||
|
||||
@@ -17,6 +17,7 @@ export default class SearchBar extends React.Component {
|
||||
this.mounted = false;
|
||||
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleUserInput = this.handleUserInput.bind(this);
|
||||
this.handleUserFocus = this.handleUserFocus.bind(this);
|
||||
this.handleUserBlur = this.handleUserBlur.bind(this);
|
||||
@@ -76,6 +77,11 @@ export default class SearchBar extends React.Component {
|
||||
results: null
|
||||
});
|
||||
}
|
||||
handleKeyDown(e) {
|
||||
if (this.refs.autocomplete) {
|
||||
this.refs.autocomplete.handleKeyDown(e);
|
||||
}
|
||||
}
|
||||
handleUserInput(e) {
|
||||
var term = e.target.value;
|
||||
PostStore.storeSearchTerm(term);
|
||||
@@ -101,7 +107,7 @@ export default class SearchBar extends React.Component {
|
||||
this.setState({isSearching: true});
|
||||
client.search(
|
||||
terms,
|
||||
function success(data) {
|
||||
(data) => {
|
||||
this.setState({isSearching: false});
|
||||
if (utils.isMobile()) {
|
||||
ReactDOM.findDOMNode(this.refs.search).value = '';
|
||||
@@ -112,11 +118,11 @@ export default class SearchBar extends React.Component {
|
||||
results: data,
|
||||
is_mention_search: isMentionSearch
|
||||
});
|
||||
}.bind(this),
|
||||
function error(err) {
|
||||
},
|
||||
(err) => {
|
||||
this.setState({isSearching: false});
|
||||
AsyncClient.dispatchError(err, 'search');
|
||||
}.bind(this)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -165,13 +171,13 @@ export default class SearchBar extends React.Component {
|
||||
className='search__clear'
|
||||
onClick={this.clearFocus}
|
||||
>
|
||||
Cancel
|
||||
{'Cancel'}
|
||||
</span>
|
||||
<form
|
||||
role='form'
|
||||
className='search__form relative-div'
|
||||
onSubmit={this.handleSubmit}
|
||||
style={{"overflow": "visible"}}
|
||||
style={{overflow: 'visible'}}
|
||||
>
|
||||
<span className='glyphicon glyphicon-search sidebar__search-icon' />
|
||||
<input
|
||||
@@ -183,6 +189,7 @@ export default class SearchBar extends React.Component {
|
||||
onFocus={this.handleUserFocus}
|
||||
onBlur={this.handleUserBlur}
|
||||
onChange={this.handleUserInput}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onMouseUp={this.handleMouseInput}
|
||||
/>
|
||||
{isSearching}
|
||||
|
||||
@@ -221,7 +221,9 @@ class UserStoreClass extends EventEmitter {
|
||||
const profiles = [];
|
||||
|
||||
for (const id in profileMap) {
|
||||
profiles.push(profileMap[id]);
|
||||
if (profileMap.hasOwnProperty(id)) {
|
||||
profiles.push(profileMap[id]);
|
||||
}
|
||||
}
|
||||
|
||||
return profiles;
|
||||
|
||||
@@ -311,6 +311,7 @@ module.exports = {
|
||||
RIGHT: 39,
|
||||
BACKSPACE: 8,
|
||||
ENTER: 13,
|
||||
ESCAPE: 27
|
||||
ESCAPE: 27,
|
||||
SPACE: 32
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,11 +124,17 @@
|
||||
}
|
||||
|
||||
.search-autocomplete__channel {
|
||||
cursor: pointer;
|
||||
height: 36px;
|
||||
padding: 0px 6px;
|
||||
|
||||
&.selected {
|
||||
background-color:rgba(51, 51, 51, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.search-autocomplete__user {
|
||||
cursor: pointer;
|
||||
height: 36px;
|
||||
padding: 0px;
|
||||
|
||||
@@ -138,4 +144,8 @@
|
||||
width: 32px;
|
||||
@include border-radius(16px);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color:rgba(51, 51, 51, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user