diff options
Diffstat (limited to 'web/react')
-rw-r--r-- | web/react/components/search_autocomplete.jsx | 139 | ||||
-rw-r--r-- | web/react/components/search_bar.jsx | 27 | ||||
-rw-r--r-- | web/react/stores/user_store.jsx | 12 |
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; |