Added an autocomplete dropdown to the search bar
Этот коммит содержится в:
139
web/react/components/search_autocomplete.jsx
Обычный файл
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 Constants = require('../utils/constants.jsx');
|
||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
var Popover = ReactBootstrap.Popover;
|
var Popover = ReactBootstrap.Popover;
|
||||||
|
var SearchAutocomplete = require('./search_autocomplete.jsx');
|
||||||
|
|
||||||
export default class SearchBar extends React.Component {
|
export default class SearchBar extends React.Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -21,6 +22,7 @@ export default class SearchBar extends React.Component {
|
|||||||
this.handleUserBlur = this.handleUserBlur.bind(this);
|
this.handleUserBlur = this.handleUserBlur.bind(this);
|
||||||
this.performSearch = this.performSearch.bind(this);
|
this.performSearch = this.performSearch.bind(this);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.completeWord = this.completeWord.bind(this);
|
||||||
|
|
||||||
const state = this.getSearchTermStateFromStores();
|
const state = this.getSearchTermStateFromStores();
|
||||||
state.focused = false;
|
state.focused = false;
|
||||||
@@ -79,6 +81,8 @@ export default class SearchBar extends React.Component {
|
|||||||
PostStore.storeSearchTerm(term);
|
PostStore.storeSearchTerm(term);
|
||||||
PostStore.emitSearchTermChange(false);
|
PostStore.emitSearchTermChange(false);
|
||||||
this.setState({searchTerm: term});
|
this.setState({searchTerm: term});
|
||||||
|
|
||||||
|
this.refs.autocomplete.handleInputChange(e.target, term);
|
||||||
}
|
}
|
||||||
handleMouseInput(e) {
|
handleMouseInput(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -120,6 +124,24 @@ export default class SearchBar extends React.Component {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.performSearch(this.state.searchTerm.trim());
|
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() {
|
render() {
|
||||||
var isSearching = null;
|
var isSearching = null;
|
||||||
if (this.state.isSearching) {
|
if (this.state.isSearching) {
|
||||||
@@ -149,6 +171,7 @@ export default class SearchBar extends React.Component {
|
|||||||
role='form'
|
role='form'
|
||||||
className='search__form relative-div'
|
className='search__form relative-div'
|
||||||
onSubmit={this.handleSubmit}
|
onSubmit={this.handleSubmit}
|
||||||
|
style={{"overflow": "visible"}}
|
||||||
>
|
>
|
||||||
<span className='glyphicon glyphicon-search sidebar__search-icon' />
|
<span className='glyphicon glyphicon-search sidebar__search-icon' />
|
||||||
<input
|
<input
|
||||||
@@ -163,6 +186,10 @@ export default class SearchBar extends React.Component {
|
|||||||
onMouseUp={this.handleMouseInput}
|
onMouseUp={this.handleMouseInput}
|
||||||
/>
|
/>
|
||||||
{isSearching}
|
{isSearching}
|
||||||
|
<SearchAutocomplete
|
||||||
|
ref='autocomplete'
|
||||||
|
completeWord={this.completeWord}
|
||||||
|
/>
|
||||||
<Popover
|
<Popover
|
||||||
placement='bottom'
|
placement='bottom'
|
||||||
className={helpClass}
|
className={helpClass}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class UserStoreClass extends EventEmitter {
|
|||||||
this.getProfilesUsernameMap = this.getProfilesUsernameMap.bind(this);
|
this.getProfilesUsernameMap = this.getProfilesUsernameMap.bind(this);
|
||||||
this.getProfiles = this.getProfiles.bind(this);
|
this.getProfiles = this.getProfiles.bind(this);
|
||||||
this.getActiveOnlyProfiles = this.getActiveOnlyProfiles.bind(this);
|
this.getActiveOnlyProfiles = this.getActiveOnlyProfiles.bind(this);
|
||||||
|
this.getActiveOnlyProfileList = this.getActiveOnlyProfileList.bind(this);
|
||||||
this.saveProfile = this.saveProfile.bind(this);
|
this.saveProfile = this.saveProfile.bind(this);
|
||||||
this.setSessions = this.setSessions.bind(this);
|
this.setSessions = this.setSessions.bind(this);
|
||||||
this.getSessions = this.getSessions.bind(this);
|
this.getSessions = this.getSessions.bind(this);
|
||||||
@@ -215,6 +216,17 @@ class UserStoreClass extends EventEmitter {
|
|||||||
return active;
|
return active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getActiveOnlyProfileList() {
|
||||||
|
const profileMap = this.getActiveOnlyProfiles();
|
||||||
|
const profiles = [];
|
||||||
|
|
||||||
|
for (const id in profileMap) {
|
||||||
|
profiles.push(profileMap[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
saveProfile(profile) {
|
saveProfile(profile) {
|
||||||
var ps = this.getProfiles();
|
var ps = this.getProfiles();
|
||||||
ps[profile.id] = profile;
|
ps[profile.id] = profile;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user