Added an autocomplete dropdown to the search bar

Этот коммит содержится в:
hmhealey
2015-10-20 16:50:55 -04:00
родитель a7852a4810
Коммит 4a1f6ad2a9
3 изменённых файлов: 178 добавлений и 0 удалений

139
web/react/components/search_autocomplete.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,139 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
const ChannelStore = require('../stores/channel_store.jsx');
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
};
export default class SearchAutocomplete extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleDocumentClick = this.handleDocumentClick.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.state = {
show: false,
mode: '',
filter: ''
};
}
componentDidMount() {
$(document).on('click', this.handleDocumentClick);
}
componentWillUnmount() {
$(document).off('click', this.handleDocumentClick);
}
handleClick(value) {
this.props.completeWord(this.state.filter, value);
this.setState({
show: false,
mode: '',
filter: ''
});
}
handleDocumentClick(e) {
const container = $(ReactDOM.findDOMNode(this.refs.container));
if (!(container.is(e.target) || container.has(e.target).length > 0)) {
this.setState({
show: false
});
}
}
handleInputChange(textbox, text) {
const caret = Utils.getCaretPosition(textbox);
const preText = text.substring(0, caret);
let mode = '';
let filter = '';
for (const pattern in patterns) {
const result = patterns[pattern].exec(preText);
if (result) {
mode = pattern;
filter = result[1];
break;
}
}
this.setState({
mode,
filter,
show: mode || filter
});
}
render() {
if (!this.state.show) {
return null;
}
let suggestions = [];
if (this.state.mode === 'channels') {
let channels = ChannelStore.getAll();
if (this.state.filter) {
channels = channels.filter((channel) => channel.name.startsWith(this.state.filter));
}
suggestions = channels.map((channel) => {
return (
<div
key={channel.id}
onClick={this.handleClick.bind(this, channel.name)}
>
{channel.name}
</div>
);
});
} else if (this.state.mode === 'users') {
let users = UserStore.getActiveOnlyProfileList();
if (this.state.filter) {
users = users.filter((user) => user.username.startsWith(this.state.filter));
}
suggestions = users.map((user) => {
return (
<div
key={user.id}
onClick={this.handleClick.bind(this, user.username)}
>
{user.username}
</div>
);
});
}
if (suggestions.length === 0) {
return null;
}
return (
<div
ref='container'
style={{overflow: 'visible', position: 'absolute', zIndex: '100', background: 'yellow'}}
>
{suggestions}
</div>
);
}
}
SearchAutocomplete.propTypes = {
completeWord: React.PropTypes.func.isRequired
};

Просмотреть файл

@@ -9,6 +9,7 @@ var utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var Popover = ReactBootstrap.Popover;
var SearchAutocomplete = require('./search_autocomplete.jsx');
export default class SearchBar extends React.Component {
constructor() {
@@ -21,6 +22,7 @@ export default class SearchBar extends React.Component {
this.handleUserBlur = this.handleUserBlur.bind(this);
this.performSearch = this.performSearch.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.completeWord = this.completeWord.bind(this);
const state = this.getSearchTermStateFromStores();
state.focused = false;
@@ -79,6 +81,8 @@ export default class SearchBar extends React.Component {
PostStore.storeSearchTerm(term);
PostStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
this.refs.autocomplete.handleInputChange(e.target, term);
}
handleMouseInput(e) {
e.preventDefault();
@@ -120,6 +124,24 @@ export default class SearchBar extends React.Component {
e.preventDefault();
this.performSearch(this.state.searchTerm.trim());
}
completeWord(partialWord, word) {
const textbox = ReactDOM.findDOMNode(this.refs.search);
let text = textbox.value;
const caret = utils.getCaretPosition(textbox);
const preText = text.substring(0, caret - partialWord.length);
const postText = text.substring(caret);
text = preText + word + postText;
textbox.value = text;
utils.setCaretPosition(textbox, preText.length + word.length);
PostStore.storeSearchTerm(text);
PostStore.emitSearchTermChange(false);
this.setState({searchTerm: text});
}
render() {
var isSearching = null;
if (this.state.isSearching) {
@@ -149,6 +171,7 @@ export default class SearchBar extends React.Component {
role='form'
className='search__form relative-div'
onSubmit={this.handleSubmit}
style={{"overflow": "visible"}}
>
<span className='glyphicon glyphicon-search sidebar__search-icon' />
<input
@@ -163,6 +186,10 @@ export default class SearchBar extends React.Component {
onMouseUp={this.handleMouseInput}
/>
{isSearching}
<SearchAutocomplete
ref='autocomplete'
completeWord={this.completeWord}
/>
<Popover
placement='bottom'
className={helpClass}

Просмотреть файл

@@ -48,6 +48,7 @@ class UserStoreClass extends EventEmitter {
this.getProfilesUsernameMap = this.getProfilesUsernameMap.bind(this);
this.getProfiles = this.getProfiles.bind(this);
this.getActiveOnlyProfiles = this.getActiveOnlyProfiles.bind(this);
this.getActiveOnlyProfileList = this.getActiveOnlyProfileList.bind(this);
this.saveProfile = this.saveProfile.bind(this);
this.setSessions = this.setSessions.bind(this);
this.getSessions = this.getSessions.bind(this);
@@ -215,6 +216,17 @@ class UserStoreClass extends EventEmitter {
return active;
}
getActiveOnlyProfileList() {
const profileMap = this.getActiveOnlyProfiles();
const profiles = [];
for (const id in profileMap) {
profiles.push(profileMap[id]);
}
return profiles;
}
saveProfile(profile) {
var ps = this.getProfiles();
ps[profile.id] = profile;