summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
authorhmhealey <harrisonmhealey@gmail.com>2015-10-20 16:50:55 -0400
committerhmhealey <harrisonmhealey@gmail.com>2015-10-23 13:05:37 -0400
commit4a1f6ad2a972bb0f30414db3dc1899d88a01d29b (patch)
treee6d9daec4cc39974c7ca5be9d65ad20218d4d8cf /web
parenta7852a4810b26436cd9ab952d013d610d9d8ec6b (diff)
downloadchat-4a1f6ad2a972bb0f30414db3dc1899d88a01d29b.tar.gz
chat-4a1f6ad2a972bb0f30414db3dc1899d88a01d29b.tar.bz2
chat-4a1f6ad2a972bb0f30414db3dc1899d88a01d29b.zip
Added an autocomplete dropdown to the search bar
Diffstat (limited to 'web')
-rw-r--r--web/react/components/search_autocomplete.jsx139
-rw-r--r--web/react/components/search_bar.jsx27
-rw-r--r--web/react/stores/user_store.jsx12
3 files changed, 178 insertions, 0 deletions
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
new file mode 100644
index 000000000..284b475c1
--- /dev/null
+++ b/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
+};
diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx
index 7540b41a4..509ca94e9 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -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}
diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx
index 575e6d51e..e3e1944ce 100644
--- a/web/react/stores/user_store.jsx
+++ b/web/react/stores/user_store.jsx
@@ -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;