Merge pull request #1579 from hmhealey/plt1298
PLT-1298 Converted Command and @Mention autocomplete to new suggestion components
Этот коммит содержится в:
@@ -1,99 +0,0 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import * as client from '../utils/client.jsx';
|
|
||||||
|
|
||||||
export default class CommandList extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
|
||||||
this.addFirstCommand = this.addFirstCommand.bind(this);
|
|
||||||
this.isEmpty = this.isEmpty.bind(this);
|
|
||||||
this.getSuggestedCommands = this.getSuggestedCommands.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
suggestions: [],
|
|
||||||
cmd: ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick(i) {
|
|
||||||
this.props.addCommand(this.state.suggestions[i].suggestion);
|
|
||||||
this.setState({suggestions: [], cmd: ''});
|
|
||||||
}
|
|
||||||
|
|
||||||
addFirstCommand() {
|
|
||||||
if (this.state.suggestions.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleClick(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty() {
|
|
||||||
return this.state.suggestions.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSuggestedCommands(cmd) {
|
|
||||||
if (!cmd || cmd.charAt(0) !== '/') {
|
|
||||||
this.setState({suggestions: [], cmd: ''});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
client.executeCommand(
|
|
||||||
this.props.channelId,
|
|
||||||
cmd,
|
|
||||||
true,
|
|
||||||
function success(data) {
|
|
||||||
if (data.suggestions.length === 1 && data.suggestions[0].suggestion === cmd) {
|
|
||||||
data.suggestions = [];
|
|
||||||
}
|
|
||||||
this.setState({suggestions: data.suggestions, cmd: cmd});
|
|
||||||
}.bind(this),
|
|
||||||
function fail() {
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.suggestions.length === 0) {
|
|
||||||
return (<div/>);
|
|
||||||
}
|
|
||||||
|
|
||||||
var suggestions = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < this.state.suggestions.length; i++) {
|
|
||||||
if (this.state.suggestions[i].suggestion !== this.state.cmd) {
|
|
||||||
suggestions.push(
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className='command-name'
|
|
||||||
onClick={this.handleClick.bind(this, i)}
|
|
||||||
>
|
|
||||||
<div className='command__title'><strong>{this.state.suggestions[i].suggestion}</strong></div>
|
|
||||||
<div className='command__desc'>{this.state.suggestions[i].description}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref='mentionlist'
|
|
||||||
className='command-box'
|
|
||||||
style={{height: (suggestions.length * 56) + 2}}
|
|
||||||
>
|
|
||||||
{suggestions}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandList.defaultProps = {
|
|
||||||
channelId: null
|
|
||||||
};
|
|
||||||
|
|
||||||
CommandList.propTypes = {
|
|
||||||
addCommand: React.PropTypes.func,
|
|
||||||
channelId: React.PropTypes.string
|
|
||||||
};
|
|
||||||
@@ -335,6 +335,7 @@ export default class CreateComment extends React.Component {
|
|||||||
messageText={this.state.messageText}
|
messageText={this.state.messageText}
|
||||||
createMessage='Add a comment...'
|
createMessage='Add a comment...'
|
||||||
initialText=''
|
initialText=''
|
||||||
|
supportsCommands={false}
|
||||||
id='reply_textbox'
|
id='reply_textbox'
|
||||||
ref='textbox'
|
ref='textbox'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export default class EditPostModal extends React.Component {
|
|||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
messageText={this.state.editText}
|
messageText={this.state.editText}
|
||||||
createMessage='Edit the post...'
|
createMessage='Edit the post...'
|
||||||
|
supportsCommands={false}
|
||||||
id='edit_textbox'
|
id='edit_textbox'
|
||||||
ref='editbox'
|
ref='editbox'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
import UserStore from '../stores/user_store.jsx';
|
|
||||||
import * as Utils from '../utils/utils.jsx';
|
|
||||||
|
|
||||||
export default class Mention extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
|
||||||
|
|
||||||
this.state = null;
|
|
||||||
}
|
|
||||||
handleClick() {
|
|
||||||
this.props.handleClick(this.props.username);
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
var icon;
|
|
||||||
var timestamp = UserStore.getCurrentUser().update_at;
|
|
||||||
if (this.props.id === 'allmention' || this.props.id === 'channelmention') {
|
|
||||||
icon = <span><i className='mention-img fa fa-users fa-2x'></i></span>;
|
|
||||||
} else if (this.props.id == null) {
|
|
||||||
icon = <span><i className='mention-img fa fa-users fa-2x'></i></span>;
|
|
||||||
} else {
|
|
||||||
icon = (
|
|
||||||
<span>
|
|
||||||
<img
|
|
||||||
className='mention-img'
|
|
||||||
src={'/api/v1/users/' + this.props.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={'mentions-name ' + this.props.isFocused}
|
|
||||||
id={this.props.id + '_mentions'}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
onMouseEnter={this.props.handleMouseEnter}
|
|
||||||
>
|
|
||||||
<div className='pull-left'>{icon}</div>
|
|
||||||
<div className='pull-left mention-align'><span>@{this.props.username}</span><span className='mention-fullname'>{this.props.secondary_text}</span></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Mention.defaultProps = {
|
|
||||||
username: '',
|
|
||||||
id: '',
|
|
||||||
isFocused: '',
|
|
||||||
secondary_text: ''
|
|
||||||
};
|
|
||||||
Mention.propTypes = {
|
|
||||||
handleClick: React.PropTypes.func.isRequired,
|
|
||||||
handleMouseEnter: React.PropTypes.func.isRequired,
|
|
||||||
username: React.PropTypes.string,
|
|
||||||
id: React.PropTypes.string,
|
|
||||||
isFocused: React.PropTypes.string,
|
|
||||||
secondary_text: React.PropTypes.string
|
|
||||||
};
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import UserStore from '../stores/user_store.jsx';
|
|
||||||
import SearchStore from '../stores/search_store.jsx';
|
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
|
||||||
import Mention from './mention.jsx';
|
|
||||||
|
|
||||||
import Constants from '../utils/constants.jsx';
|
|
||||||
import * as Utils from '../utils/utils.jsx';
|
|
||||||
var ActionTypes = Constants.ActionTypes;
|
|
||||||
|
|
||||||
var MAX_HEIGHT_LIST = 292;
|
|
||||||
var MAX_ITEMS_IN_LIST = 25;
|
|
||||||
var ITEM_HEIGHT = 36;
|
|
||||||
|
|
||||||
export default class MentionList extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.onListenerChange = this.onListenerChange.bind(this);
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
|
||||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
|
||||||
this.getSelection = this.getSelection.bind(this);
|
|
||||||
this.addCurrentMention = this.addCurrentMention.bind(this);
|
|
||||||
this.addFirstMention = this.addFirstMention.bind(this);
|
|
||||||
this.isEmpty = this.isEmpty.bind(this);
|
|
||||||
this.scrollToMention = this.scrollToMention.bind(this);
|
|
||||||
this.onScroll = this.onScroll.bind(this);
|
|
||||||
this.onMentionListKey = this.onMentionListKey.bind(this);
|
|
||||||
this.onClick = this.onClick.bind(this);
|
|
||||||
|
|
||||||
this.state = {excludeUsers: [], mentionText: '-1', selectedMention: 0, selectedUsername: ''};
|
|
||||||
}
|
|
||||||
onScroll() {
|
|
||||||
if ($('.mentions--top').length) {
|
|
||||||
$('#reply_mention_tab .mentions--top').css({bottom: $(window).height() - $('.post-right__scroll #reply_textbox').offset().top});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onMentionListKey(e) {
|
|
||||||
if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 13 || e.which === 9)) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.addCurrentMention();
|
|
||||||
} else if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 38 || e.which === 40)) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (e.which === 38) {
|
|
||||||
if (this.getSelection(this.state.selectedMention - 1)) {
|
|
||||||
this.setState({selectedMention: this.state.selectedMention - 1, selectedUsername: this.refs['mention' + (this.state.selectedMention - 1)].props.username});
|
|
||||||
}
|
|
||||||
} else if (e.which === 40) {
|
|
||||||
if (this.getSelection(this.state.selectedMention + 1)) {
|
|
||||||
this.setState({selectedMention: this.state.selectedMention + 1, selectedUsername: this.refs['mention' + (this.state.selectedMention + 1)].props.username});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scrollToMention(e.which);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onClick(e) {
|
|
||||||
if (!($('#' + this.props.id).is(e.target) || $('#' + this.props.id).has(e.target).length ||
|
|
||||||
('mentionlist' in this.refs && $(ReactDOM.findDOMNode(this.refs.mentionlist)).has(e.target).length))) {
|
|
||||||
this.setState({mentionText: '-1'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
componentDidMount() {
|
|
||||||
SearchStore.addMentionDataChangeListener(this.onListenerChange);
|
|
||||||
|
|
||||||
$('.post-right__scroll').scroll(this.onScroll);
|
|
||||||
|
|
||||||
$('body').on('keydown.mentionlist', '#' + this.props.id, this.onMentionListKey);
|
|
||||||
$(document).click(this.onClick);
|
|
||||||
}
|
|
||||||
componentWillUnmount() {
|
|
||||||
SearchStore.removeMentionDataChangeListener(this.onListenerChange);
|
|
||||||
$('body').off('keydown.mentionlist', '#' + this.props.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This component is poorly designed, nessesitating some state modification
|
|
||||||
* in the componentDidUpdate function. This is generally discouraged as it
|
|
||||||
* is a performance issue and breaks with good react design. This component
|
|
||||||
* should be redesigned.
|
|
||||||
*/
|
|
||||||
componentDidUpdate() {
|
|
||||||
if (this.state.mentionText !== '-1') {
|
|
||||||
if (this.state.selectedUsername !== '' && (!this.getSelection(this.state.selectedMention) || this.state.selectedUsername !== this.refs['mention' + this.state.selectedMention].props.username)) {
|
|
||||||
var tempSelectedMention = -1;
|
|
||||||
var foundMatch = false;
|
|
||||||
while (tempSelectedMention < this.state.selectedMention && this.getSelection(++tempSelectedMention)) {
|
|
||||||
if (this.state.selectedUsername === this.refs['mention' + tempSelectedMention].props.username) {
|
|
||||||
this.setState({selectedMention: tempSelectedMention}); //eslint-disable-line react/no-did-update-set-state
|
|
||||||
foundMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.getSelection(0) && !foundMatch) {
|
|
||||||
this.setState({selectedMention: 0, selectedUsername: this.refs.mention0.props.username}); //eslint-disable-line react/no-did-update-set-state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (this.state.selectedMention !== 0) {
|
|
||||||
this.setState({selectedMention: 0, selectedUsername: ''}); //eslint-disable-line react/no-did-update-set-state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onListenerChange(id, mentionText) {
|
|
||||||
if (id !== this.props.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newState = this.state;
|
|
||||||
if (mentionText != null) {
|
|
||||||
newState.mentionText = mentionText;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
handleClick(name) {
|
|
||||||
AppDispatcher.handleViewAction({
|
|
||||||
type: ActionTypes.RECIEVED_ADD_MENTION,
|
|
||||||
id: this.props.id,
|
|
||||||
username: name
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({mentionText: '-1'});
|
|
||||||
}
|
|
||||||
handleMouseEnter(listId) {
|
|
||||||
this.setState({selectedMention: listId, selectedUsername: this.refs['mention' + listId].props.username});
|
|
||||||
}
|
|
||||||
getSelection(listId) {
|
|
||||||
if (!this.refs['mention' + listId]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
addCurrentMention() {
|
|
||||||
if (this.getSelection(this.state.selectedMention)) {
|
|
||||||
this.refs['mention' + this.state.selectedMention].handleClick();
|
|
||||||
} else {
|
|
||||||
this.addFirstMention();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addFirstMention() {
|
|
||||||
if (!this.refs.mention0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.refs.mention0.handleClick();
|
|
||||||
}
|
|
||||||
isEmpty() {
|
|
||||||
return (!this.refs.mention0);
|
|
||||||
}
|
|
||||||
scrollToMention(keyPressed) {
|
|
||||||
var direction;
|
|
||||||
if (keyPressed === 38) {
|
|
||||||
direction = 'up';
|
|
||||||
} else {
|
|
||||||
direction = 'down';
|
|
||||||
}
|
|
||||||
var scrollAmount = 0;
|
|
||||||
|
|
||||||
if (direction === 'up') {
|
|
||||||
scrollAmount = '-=' + ($('#' + this.refs['mention' + this.state.selectedMention].props.id + '_mentions').innerHeight() - 5);
|
|
||||||
} else if (direction === 'down') {
|
|
||||||
scrollAmount = '+=' + ($('#' + this.refs['mention' + this.state.selectedMention].props.id + '_mentions').innerHeight() - 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#mentionsbox').animate({
|
|
||||||
scrollTop: scrollAmount
|
|
||||||
}, 75);
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
var mentionText = this.state.mentionText;
|
|
||||||
if (mentionText === '-1') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var profiles = UserStore.getActiveOnlyProfiles();
|
|
||||||
var users = [];
|
|
||||||
for (let id in profiles) {
|
|
||||||
if (profiles[id]) {
|
|
||||||
users.push(profiles[id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// var all = {};
|
|
||||||
// all.username = 'all';
|
|
||||||
// all.nickname = '';
|
|
||||||
// all.secondary_text = 'Notifies everyone in the team';
|
|
||||||
// all.id = 'allmention';
|
|
||||||
// users.push(all);
|
|
||||||
|
|
||||||
var channel = {};
|
|
||||||
channel.username = 'channel';
|
|
||||||
channel.nickname = '';
|
|
||||||
channel.secondary_text = 'Notifies everyone in the channel';
|
|
||||||
channel.id = 'channelmention';
|
|
||||||
users.push(channel);
|
|
||||||
|
|
||||||
users.sort(function sortByUsername(a, b) {
|
|
||||||
if (a.username < b.username) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.username > b.username) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
var mentions = [];
|
|
||||||
var index = 0;
|
|
||||||
|
|
||||||
for (var i = 0; i < users.length && index < MAX_ITEMS_IN_LIST; i++) {
|
|
||||||
if ((users[i].first_name && users[i].first_name.lastIndexOf(mentionText, 0) === 0) ||
|
|
||||||
(users[i].last_name && users[i].last_name.lastIndexOf(mentionText, 0) === 0) ||
|
|
||||||
users[i].username.lastIndexOf(mentionText, 0) === 0) {
|
|
||||||
let isFocused = '';
|
|
||||||
if (this.state.selectedMention === index) {
|
|
||||||
isFocused = 'mentions-focus';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!users[i].secondary_text) {
|
|
||||||
users[i].secondary_text = Utils.getFullName(users[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
mentions[index] = (
|
|
||||||
<Mention
|
|
||||||
key={'mention_key_' + index}
|
|
||||||
ref={'mention' + index}
|
|
||||||
username={users[i].username}
|
|
||||||
secondary_text={users[i].secondary_text}
|
|
||||||
id={users[i].id}
|
|
||||||
listId={index}
|
|
||||||
isFocused={isFocused}
|
|
||||||
handleMouseEnter={this.handleMouseEnter.bind(this, index)}
|
|
||||||
handleClick={this.handleClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var numMentions = mentions.length;
|
|
||||||
|
|
||||||
if (numMentions < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var $mentionTab = $('#' + this.props.id);
|
|
||||||
var maxHeight = Math.min(MAX_HEIGHT_LIST, $mentionTab.offset().top - 10);
|
|
||||||
var style = {
|
|
||||||
height: Math.min(maxHeight, (numMentions * ITEM_HEIGHT) + 4),
|
|
||||||
width: $mentionTab.parent().width(),
|
|
||||||
bottom: $(window).height() - $mentionTab.offset().top,
|
|
||||||
left: $mentionTab.offset().left
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='mentions--top'
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref='mentionlist'
|
|
||||||
className='mentions-box'
|
|
||||||
id='mentionsbox'
|
|
||||||
>
|
|
||||||
{mentions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MentionList.propTypes = {
|
|
||||||
id: React.PropTypes.string
|
|
||||||
};
|
|
||||||
@@ -5,9 +5,10 @@ import * as client from '../utils/client.jsx';
|
|||||||
import * as AsyncClient from '../utils/async_client.jsx';
|
import * as AsyncClient from '../utils/async_client.jsx';
|
||||||
import SearchStore from '../stores/search_store.jsx';
|
import SearchStore from '../stores/search_store.jsx';
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||||
import SuggestionBox from '../components/suggestion_box.jsx';
|
import SuggestionBox from './suggestion/suggestion_box.jsx';
|
||||||
import SearchChannelProvider from '../components/search_channel_provider.jsx';
|
import SearchChannelProvider from './suggestion/search_channel_provider.jsx';
|
||||||
import SearchUserProvider from '../components/search_user_provider.jsx';
|
import SearchSuggestionList from './suggestion/search_suggestion_list.jsx';
|
||||||
|
import SearchUserProvider from './suggestion/search_user_provider.jsx';
|
||||||
import * as utils from '../utils/utils.jsx';
|
import * as utils from '../utils/utils.jsx';
|
||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
@@ -164,6 +165,7 @@ export default class SearchBar extends React.Component {
|
|||||||
onFocus={this.handleUserFocus}
|
onFocus={this.handleUserFocus}
|
||||||
onBlur={this.handleUserBlur}
|
onBlur={this.handleUserBlur}
|
||||||
onUserInput={this.handleUserInput}
|
onUserInput={this.handleUserInput}
|
||||||
|
listComponent={SearchSuggestionList}
|
||||||
providers={this.suggestionProviders}
|
providers={this.suggestionProviders}
|
||||||
/>
|
/>
|
||||||
{isSearching}
|
{isSearching}
|
||||||
|
|||||||
100
web/react/components/suggestion/at_mention_provider.jsx
Обычный файл
100
web/react/components/suggestion/at_mention_provider.jsx
Обычный файл
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import SuggestionStore from '../../stores/suggestion_store.jsx';
|
||||||
|
import UserStore from '../../stores/user_store.jsx';
|
||||||
|
import * as Utils from '../../utils/utils.jsx';
|
||||||
|
|
||||||
|
class AtMentionSuggestion extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {item, isSelection, onClick} = this.props;
|
||||||
|
|
||||||
|
let username;
|
||||||
|
let description;
|
||||||
|
let icon;
|
||||||
|
if (item.username === 'all') {
|
||||||
|
username = 'all';
|
||||||
|
description = 'Notifies everyone in the team';
|
||||||
|
icon = <i className='mention-img fa fa-users fa-2x' />;
|
||||||
|
} else if (item.username === 'channel') {
|
||||||
|
username = 'channel';
|
||||||
|
description = 'Notifies everyone in the channel';
|
||||||
|
icon = <i className='mention-img fa fa-users fa-2x' />;
|
||||||
|
} else {
|
||||||
|
username = item.username;
|
||||||
|
description = Utils.getFullName(item);
|
||||||
|
icon = (
|
||||||
|
<img
|
||||||
|
className='mention-img'
|
||||||
|
src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at + '&' + Utils.getSessionIndex()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let className = 'mentions-name';
|
||||||
|
if (isSelection) {
|
||||||
|
className += ' suggestion--selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className='pull-left'>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className='pull-left mention-align'>
|
||||||
|
<span>
|
||||||
|
{'@' + username}
|
||||||
|
</span>
|
||||||
|
<span className='mention-fullname'>
|
||||||
|
{description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AtMentionSuggestion.propTypes = {
|
||||||
|
item: React.PropTypes.object.isRequired,
|
||||||
|
isSelection: React.PropTypes.bool,
|
||||||
|
onClick: React.PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class AtMentionProvider {
|
||||||
|
handlePretextChanged(suggestionId, pretext) {
|
||||||
|
const captured = (/@([a-z0-9\-\._]*)$/i).exec(pretext);
|
||||||
|
if (captured) {
|
||||||
|
const usernamePrefix = captured[1];
|
||||||
|
|
||||||
|
const users = UserStore.getProfiles();
|
||||||
|
let filtered = [];
|
||||||
|
|
||||||
|
for (const id of Object.keys(users)) {
|
||||||
|
const user = users[id];
|
||||||
|
|
||||||
|
if (user.username.startsWith(usernamePrefix)) {
|
||||||
|
filtered.push(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add dummy users to represent the @all and @channel special mentions
|
||||||
|
if ('all'.startsWith(usernamePrefix)) {
|
||||||
|
filtered.push({username: 'all'});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('channel'.startsWith(usernamePrefix)) {
|
||||||
|
filtered.push({username: 'channel'});
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = filtered.sort((a, b) => a.username.localeCompare(b.username));
|
||||||
|
|
||||||
|
const mentions = filtered.map((user) => '@' + user.username);
|
||||||
|
|
||||||
|
SuggestionStore.setMatchedPretext(suggestionId, captured[0]);
|
||||||
|
SuggestionStore.addSuggestions(suggestionId, mentions, filtered, AtMentionSuggestion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
web/react/components/suggestion/command_provider.jsx
Обычный файл
47
web/react/components/suggestion/command_provider.jsx
Обычный файл
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import * as AsyncClient from '../../utils/async_client.jsx';
|
||||||
|
import SuggestionStore from '../../stores/suggestion_store.jsx';
|
||||||
|
|
||||||
|
class CommandSuggestion extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {item, isSelection, onClick} = this.props;
|
||||||
|
|
||||||
|
let className = 'command-name';
|
||||||
|
if (isSelection) {
|
||||||
|
className += ' suggestion--selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className='command__title'>
|
||||||
|
<string>{item.suggestion}</string>
|
||||||
|
</div>
|
||||||
|
<div className='command__desc'>
|
||||||
|
{item.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandSuggestion.propTypes = {
|
||||||
|
item: React.PropTypes.object.isRequired,
|
||||||
|
isSelection: React.PropTypes.bool,
|
||||||
|
onClick: React.PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class CommandProvider {
|
||||||
|
handlePretextChanged(suggestionId, pretext) {
|
||||||
|
if (pretext.startsWith('/')) {
|
||||||
|
SuggestionStore.setMatchedPretext(suggestionId, pretext);
|
||||||
|
SuggestionStore.setCompleteOnSpace(suggestionId, false);
|
||||||
|
|
||||||
|
AsyncClient.getSuggestedCommands(pretext, suggestionId, CommandSuggestion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import ChannelStore from '../stores/channel_store.jsx';
|
import ChannelStore from '../../stores/channel_store.jsx';
|
||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../../utils/constants.jsx';
|
||||||
import SuggestionStore from '../stores/suggestion_store.jsx';
|
import SuggestionStore from '../../stores/suggestion_store.jsx';
|
||||||
|
|
||||||
class SearchChannelSuggestion extends React.Component {
|
class SearchChannelSuggestion extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
86
web/react/components/suggestion/search_suggestion_list.jsx
Обычный файл
86
web/react/components/suggestion/search_suggestion_list.jsx
Обычный файл
@@ -0,0 +1,86 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import Constants from '../../utils/constants.jsx';
|
||||||
|
import SuggestionList from './suggestion_list.jsx';
|
||||||
|
import * as Utils from '../../utils/utils.jsx';
|
||||||
|
|
||||||
|
export default class SearchSuggestionList extends SuggestionList {
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (this.state.items.length > 0 && prevState.items.length === 0) {
|
||||||
|
this.getContent().perfectScrollbar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getContent() {
|
||||||
|
return $(ReactDOM.findDOMNode(this.refs.popover)).find('.popover-content');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChannelDivider(type) {
|
||||||
|
let text;
|
||||||
|
if (type === Constants.OPEN_CHANNEL) {
|
||||||
|
text = 'Public ' + Utils.getChannelTerm(type) + 's';
|
||||||
|
} else {
|
||||||
|
text = 'Private ' + Utils.getChannelTerm(type) + 's';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={type + '-divider'}
|
||||||
|
className='search-autocomplete__divider'
|
||||||
|
>
|
||||||
|
<span>{text}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.items.length === 0 || !this.props.show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
for (let i = 0; i < this.state.items.length; i++) {
|
||||||
|
const item = this.state.items[i];
|
||||||
|
const term = this.state.terms[i];
|
||||||
|
const isSelection = term === this.state.selection;
|
||||||
|
|
||||||
|
// ReactComponent names need to be upper case when used in JSX
|
||||||
|
const Component = this.state.components[i];
|
||||||
|
|
||||||
|
// temporary hack to add dividers between public and private channels in the search suggestion list
|
||||||
|
if (i === 0 || item.type !== this.state.items[i - 1].type) {
|
||||||
|
if (item.type === Constants.OPEN_CHANNEL) {
|
||||||
|
items.push(this.renderChannelDivider(Constants.OPEN_CHANNEL));
|
||||||
|
} else if (item.type === Constants.PRIVATE_CHANNEL) {
|
||||||
|
items.push(this.renderChannelDivider(Constants.PRIVATE_CHANNEL));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
<Component
|
||||||
|
key={term}
|
||||||
|
ref={term}
|
||||||
|
item={item}
|
||||||
|
isSelection={isSelection}
|
||||||
|
onClick={this.handleItemClick.bind(this, term)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactBootstrap.Popover
|
||||||
|
ref='popover'
|
||||||
|
id='search-autocomplete__popover'
|
||||||
|
className='search-help-popover autocomplete visible'
|
||||||
|
placement='bottom'
|
||||||
|
>
|
||||||
|
{items}
|
||||||
|
</ReactBootstrap.Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchSuggestionList.propTypes = {
|
||||||
|
...SuggestionList.propTypes
|
||||||
|
};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import SuggestionStore from '../stores/suggestion_store.jsx';
|
import SuggestionStore from '../../stores/suggestion_store.jsx';
|
||||||
import UserStore from '../stores/user_store.jsx';
|
import UserStore from '../../stores/user_store.jsx';
|
||||||
|
|
||||||
class SearchUserSuggestion extends React.Component {
|
class SearchUserSuggestion extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import Constants from '../../utils/constants.jsx';
|
||||||
import Constants from '../utils/constants.jsx';
|
import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
|
||||||
import SuggestionList from './suggestion_list.jsx';
|
import SuggestionStore from '../../stores/suggestion_store.jsx';
|
||||||
import SuggestionStore from '../stores/suggestion_store.jsx';
|
import * as Utils from '../../utils/utils.jsx';
|
||||||
import * as Utils from '../utils/utils.jsx';
|
|
||||||
|
|
||||||
const ActionTypes = Constants.ActionTypes;
|
|
||||||
const KeyCodes = Constants.KeyCodes;
|
const KeyCodes = Constants.KeyCodes;
|
||||||
|
|
||||||
export default class SuggestionBox extends React.Component {
|
export default class SuggestionBox extends React.Component {
|
||||||
@@ -45,6 +43,11 @@ export default class SuggestionBox extends React.Component {
|
|||||||
$(document).off('click', this.handleDocumentClick);
|
$(document).off('click', this.handleDocumentClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTextbox() {
|
||||||
|
// this is to support old code that looks at the input/textarea DOM nodes
|
||||||
|
return ReactDOM.findDOMNode(this.refs.textbox);
|
||||||
|
}
|
||||||
|
|
||||||
handleDocumentClick(e) {
|
handleDocumentClick(e) {
|
||||||
if (!this.state.focused) {
|
if (!this.state.focused) {
|
||||||
return;
|
return;
|
||||||
@@ -75,11 +78,7 @@ export default class SuggestionBox extends React.Component {
|
|||||||
const caret = Utils.getCaretPosition(textbox);
|
const caret = Utils.getCaretPosition(textbox);
|
||||||
const pretext = textbox.value.substring(0, caret);
|
const pretext = textbox.value.substring(0, caret);
|
||||||
|
|
||||||
AppDispatcher.handleViewAction({
|
EventHelpers.emitSuggestionPretextChanged(this.suggestionId, pretext);
|
||||||
type: ActionTypes.SUGGESTION_PRETEXT_CHANGED,
|
|
||||||
id: this.suggestionId,
|
|
||||||
pretext
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.props.onUserInput) {
|
if (this.props.onUserInput) {
|
||||||
this.props.onUserInput(textbox.value);
|
this.props.onUserInput(textbox.value);
|
||||||
@@ -109,24 +108,19 @@ export default class SuggestionBox extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown(e) {
|
handleKeyDown(e) {
|
||||||
if (e.which === KeyCodes.UP) {
|
if (SuggestionStore.hasSuggestions(this.suggestionId)) {
|
||||||
AppDispatcher.handleViewAction({
|
if (e.which === KeyCodes.UP) {
|
||||||
type: ActionTypes.SUGGESTION_SELECT_PREVIOUS,
|
EventHelpers.emitSelectPreviousSuggestion(this.suggestionId);
|
||||||
id: this.suggestionId
|
e.preventDefault();
|
||||||
});
|
} else if (e.which === KeyCodes.DOWN) {
|
||||||
e.preventDefault();
|
EventHelpers.emitSelectNextSuggestion(this.suggestionId);
|
||||||
} else if (e.which === KeyCodes.DOWN) {
|
e.preventDefault();
|
||||||
AppDispatcher.handleViewAction({
|
} else if (e.which === KeyCodes.ENTER || (e.which === KeyCodes.SPACE && SuggestionStore.shouldCompleteOnSpace(this.suggestionId))) {
|
||||||
type: ActionTypes.SUGGESTION_SELECT_NEXT,
|
EventHelpers.emitCompleteWordSuggestion(this.suggestionId);
|
||||||
id: this.suggestionId
|
e.preventDefault();
|
||||||
});
|
} else if (this.props.onKeyDown) {
|
||||||
e.preventDefault();
|
this.props.onKeyDown(e);
|
||||||
} else if ((e.which === KeyCodes.SPACE || e.which === KeyCodes.ENTER) && SuggestionStore.hasSuggestions(this.suggestionId)) {
|
}
|
||||||
AppDispatcher.handleViewAction({
|
|
||||||
type: ActionTypes.SUGGESTION_COMPLETE_WORD,
|
|
||||||
id: this.suggestionId
|
|
||||||
});
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (this.props.onKeyDown) {
|
} else if (this.props.onKeyDown) {
|
||||||
this.props.onKeyDown(e);
|
this.props.onKeyDown(e);
|
||||||
}
|
}
|
||||||
@@ -145,20 +139,45 @@ export default class SuggestionBox extends React.Component {
|
|||||||
onKeyDown: this.handleKeyDown
|
onKeyDown: this.handleKeyDown
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
let textbox = null;
|
||||||
<div>
|
if (this.props.type === 'input') {
|
||||||
|
textbox = (
|
||||||
<input
|
<input
|
||||||
ref='textbox'
|
ref='textbox'
|
||||||
type='text'
|
type='text'
|
||||||
{...newProps}
|
{...newProps}
|
||||||
/>
|
/>
|
||||||
<SuggestionList suggestionId={this.suggestionId} />
|
);
|
||||||
|
} else if (this.props.type === 'textarea') {
|
||||||
|
textbox = (
|
||||||
|
<textarea
|
||||||
|
ref='textbox'
|
||||||
|
{...newProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SuggestionListComponent = this.props.listComponent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{textbox}
|
||||||
|
<SuggestionListComponent
|
||||||
|
suggestionId={this.suggestionId}
|
||||||
|
show={this.state.focused}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SuggestionBox.defaultProps = {
|
||||||
|
type: 'input'
|
||||||
|
};
|
||||||
|
|
||||||
SuggestionBox.propTypes = {
|
SuggestionBox.propTypes = {
|
||||||
|
listComponent: React.PropTypes.func.isRequired,
|
||||||
|
type: React.PropTypes.oneOf(['input', 'textarea']).isRequired,
|
||||||
value: React.PropTypes.string.isRequired,
|
value: React.PropTypes.string.isRequired,
|
||||||
onUserInput: React.PropTypes.func,
|
onUserInput: React.PropTypes.func,
|
||||||
providers: React.PropTypes.arrayOf(React.PropTypes.object),
|
providers: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
|
||||||
import Constants from '../utils/constants.jsx';
|
import SuggestionStore from '../../stores/suggestion_store.jsx';
|
||||||
import SuggestionStore from '../stores/suggestion_store.jsx';
|
|
||||||
import * as Utils from '../utils/utils.jsx';
|
|
||||||
|
|
||||||
export default class SuggestionList extends React.Component {
|
export default class SuggestionList extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.getContent = this.getContent.bind(this);
|
||||||
|
|
||||||
this.handleItemClick = this.handleItemClick.bind(this);
|
this.handleItemClick = this.handleItemClick.bind(this);
|
||||||
this.handleSuggestionsChanged = this.handleSuggestionsChanged.bind(this);
|
this.handleSuggestionsChanged = this.handleSuggestionsChanged.bind(this);
|
||||||
|
|
||||||
@@ -27,23 +27,16 @@ export default class SuggestionList extends React.Component {
|
|||||||
SuggestionStore.addSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
|
SuggestionStore.addSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
if (this.state.items.length > 0 && prevState.items.length === 0) {
|
|
||||||
const content = $(ReactDOM.findDOMNode(this.refs.popover)).find('.popover-content');
|
|
||||||
content.perfectScrollbar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
SuggestionStore.removeSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
|
SuggestionStore.removeSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getContent() {
|
||||||
|
return $(ReactDOM.findDOMNode(this.refs.content));
|
||||||
|
}
|
||||||
|
|
||||||
handleItemClick(term, e) {
|
handleItemClick(term, e) {
|
||||||
AppDispatcher.handleViewAction({
|
EventHelpers.emitCompleteWordSuggestion(this.props.suggestionId, term);
|
||||||
type: Constants.ActionTypes.SUGGESTION_COMPLETE_WORD,
|
|
||||||
id: this.props.suggestionId,
|
|
||||||
term
|
|
||||||
});
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -64,7 +57,7 @@ export default class SuggestionList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollToItem(term) {
|
scrollToItem(term) {
|
||||||
const content = $(ReactDOM.findDOMNode(this.refs.popover)).find('.popover-content');
|
const content = this.getContent();
|
||||||
const visibleContentHeight = content[0].clientHeight;
|
const visibleContentHeight = content[0].clientHeight;
|
||||||
const actualContentHeight = content[0].scrollHeight;
|
const actualContentHeight = content[0].scrollHeight;
|
||||||
|
|
||||||
@@ -75,7 +68,8 @@ export default class SuggestionList extends React.Component {
|
|||||||
|
|
||||||
const item = $(ReactDOM.findDOMNode(this.refs[term]));
|
const item = $(ReactDOM.findDOMNode(this.refs[term]));
|
||||||
const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10);
|
const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10);
|
||||||
const itemBottom = item[0].offsetTop + item.height() + parseInt(item.css('margin-bottom'), 10);
|
const itemBottomMargin = parseInt(item.css('margin-bottom'), 10) + parseInt(item.css('padding-bottom'), 10);
|
||||||
|
const itemBottom = item[0].offsetTop + item.height() + itemBottomMargin;
|
||||||
|
|
||||||
if (itemTop - contentTopPadding < contentTop) {
|
if (itemTop - contentTopPadding < contentTop) {
|
||||||
// the item is off the top of the visible space
|
// the item is off the top of the visible space
|
||||||
@@ -87,26 +81,8 @@ export default class SuggestionList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderChannelDivider(type) {
|
|
||||||
let text;
|
|
||||||
if (type === Constants.OPEN_CHANNEL) {
|
|
||||||
text = 'Public ' + Utils.getChannelTerm(type) + 's';
|
|
||||||
} else {
|
|
||||||
text = 'Private ' + Utils.getChannelTerm(type) + 's';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={type + '-divider'}
|
|
||||||
className='search-autocomplete__divider'
|
|
||||||
>
|
|
||||||
<span>{text}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.items.length === 0) {
|
if (this.state.items.length === 0 || !this.props.show) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,15 +95,6 @@ export default class SuggestionList extends React.Component {
|
|||||||
// ReactComponent names need to be upper case when used in JSX
|
// ReactComponent names need to be upper case when used in JSX
|
||||||
const Component = this.state.components[i];
|
const Component = this.state.components[i];
|
||||||
|
|
||||||
// temporary hack to add dividers between public and private channels in the search suggestion list
|
|
||||||
if (i === 0 || item.type !== this.state.items[i - 1].type) {
|
|
||||||
if (item.type === Constants.OPEN_CHANNEL) {
|
|
||||||
items.push(this.renderChannelDivider(Constants.OPEN_CHANNEL));
|
|
||||||
} else if (item.type === Constants.PRIVATE_CHANNEL) {
|
|
||||||
items.push(this.renderChannelDivider(Constants.PRIVATE_CHANNEL));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
<Component
|
<Component
|
||||||
key={term}
|
key={term}
|
||||||
@@ -140,18 +107,19 @@ export default class SuggestionList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactBootstrap.Popover
|
<div className='suggestion-list suggestion-list--top'>
|
||||||
ref='popover'
|
<div
|
||||||
id='search-autocomplete__popover'
|
ref='content'
|
||||||
className='search-help-popover autocomplete visible'
|
className='suggestion-content suggestion-content--top'
|
||||||
placement='bottom'
|
>
|
||||||
>
|
{items}
|
||||||
{items}
|
</div>
|
||||||
</ReactBootstrap.Popover>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SuggestionList.propTypes = {
|
SuggestionList.propTypes = {
|
||||||
suggestionId: React.PropTypes.string.isRequired
|
suggestionId: React.PropTypes.string.isRequired,
|
||||||
};
|
show: React.PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import AtMentionProvider from './suggestion/at_mention_provider.jsx';
|
||||||
import SearchStore from '../stores/search_store.jsx';
|
import CommandProvider from './suggestion/command_provider.jsx';
|
||||||
import CommandList from './command_list.jsx';
|
import SuggestionList from './suggestion/suggestion_list.jsx';
|
||||||
|
import SuggestionBox from './suggestion/suggestion_box.jsx';
|
||||||
import ErrorStore from '../stores/error_store.jsx';
|
import ErrorStore from '../stores/error_store.jsx';
|
||||||
|
|
||||||
import * as TextFormatting from '../utils/text_formatting.jsx';
|
import * as TextFormatting from '../utils/text_formatting.jsx';
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
const ActionTypes = Constants.ActionTypes;
|
|
||||||
const KeyCodes = Constants.KeyCodes;
|
|
||||||
const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
|
const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
|
||||||
|
|
||||||
export default class Textbox extends React.Component {
|
export default class Textbox extends React.Component {
|
||||||
@@ -18,32 +17,22 @@ export default class Textbox extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.getStateFromStores = this.getStateFromStores.bind(this);
|
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||||
this.onListenerChange = this.onListenerChange.bind(this);
|
|
||||||
this.onRecievedError = this.onRecievedError.bind(this);
|
this.onRecievedError = this.onRecievedError.bind(this);
|
||||||
this.updateMentionTab = this.updateMentionTab.bind(this);
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
|
||||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
this.handleBackspace = this.handleBackspace.bind(this);
|
|
||||||
this.checkForNewMention = this.checkForNewMention.bind(this);
|
|
||||||
this.addMention = this.addMention.bind(this);
|
|
||||||
this.addCommand = this.addCommand.bind(this);
|
|
||||||
this.resize = this.resize.bind(this);
|
this.resize = this.resize.bind(this);
|
||||||
this.handleFocus = this.handleFocus.bind(this);
|
this.handleFocus = this.handleFocus.bind(this);
|
||||||
this.handleBlur = this.handleBlur.bind(this);
|
this.handleBlur = this.handleBlur.bind(this);
|
||||||
this.handlePaste = this.handlePaste.bind(this);
|
|
||||||
this.showPreview = this.showPreview.bind(this);
|
this.showPreview = this.showPreview.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
mentionText: '-1',
|
|
||||||
mentions: [],
|
|
||||||
connection: ''
|
connection: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
this.caret = -1;
|
this.suggestionProviders = [new AtMentionProvider()];
|
||||||
this.addedMention = false;
|
if (props.supportsCommands) {
|
||||||
this.doProcessMentions = false;
|
this.suggestionProviders.push(new CommandProvider());
|
||||||
this.mentions = [];
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStateFromStores() {
|
getStateFromStores() {
|
||||||
@@ -57,24 +46,15 @@ export default class Textbox extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
SearchStore.addAddMentionListener(this.onListenerChange);
|
|
||||||
ErrorStore.addChangeListener(this.onRecievedError);
|
ErrorStore.addChangeListener(this.onRecievedError);
|
||||||
|
|
||||||
this.resize();
|
this.resize();
|
||||||
this.updateMentionTab(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
SearchStore.removeAddMentionListener(this.onListenerChange);
|
|
||||||
ErrorStore.removeChangeListener(this.onRecievedError);
|
ErrorStore.removeChangeListener(this.onRecievedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
onListenerChange(id, username) {
|
|
||||||
if (id === this.props.id) {
|
|
||||||
this.addMention(username);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onRecievedError() {
|
onRecievedError() {
|
||||||
const errorState = ErrorStore.getLastError();
|
const errorState = ErrorStore.getLastError();
|
||||||
|
|
||||||
@@ -86,158 +66,21 @@ export default class Textbox extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
if (this.caret >= 0) {
|
|
||||||
Utils.setCaretPosition(ReactDOM.findDOMNode(this.refs.message), this.caret);
|
|
||||||
this.caret = -1;
|
|
||||||
}
|
|
||||||
if (this.doProcessMentions) {
|
|
||||||
this.updateMentionTab(null);
|
|
||||||
this.doProcessMentions = false;
|
|
||||||
}
|
|
||||||
this.resize();
|
this.resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (!this.addedMention) {
|
|
||||||
this.checkForNewMention(nextProps.messageText);
|
|
||||||
}
|
|
||||||
const text = ReactDOM.findDOMNode(this.refs.message).value;
|
|
||||||
if (nextProps.channelId !== this.props.channelId || nextProps.messageText !== text) {
|
|
||||||
this.doProcessMentions = true;
|
|
||||||
}
|
|
||||||
this.addedMention = false;
|
|
||||||
this.refs.commands.getSuggestedCommands(nextProps.messageText);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMentionTab(mentionText) {
|
|
||||||
// using setTimeout so dispatch isn't called during an in progress dispatch
|
|
||||||
setTimeout(() => {
|
|
||||||
AppDispatcher.handleViewAction({
|
|
||||||
type: ActionTypes.RECIEVED_MENTION_DATA,
|
|
||||||
id: this.props.id,
|
|
||||||
mention_text: mentionText
|
|
||||||
});
|
|
||||||
}, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange() {
|
|
||||||
const text = ReactDOM.findDOMNode(this.refs.message).value;
|
|
||||||
this.props.onUserInput(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyPress(e) {
|
handleKeyPress(e) {
|
||||||
const text = ReactDOM.findDOMNode(this.refs.message).value;
|
|
||||||
|
|
||||||
if (!this.refs.commands.isEmpty() && text.indexOf('/') === 0 && e.which === 13) {
|
|
||||||
this.refs.commands.addFirstCommand();
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.doProcessMentions) {
|
|
||||||
const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
|
|
||||||
const preText = text.substring(0, caret);
|
|
||||||
const lastSpace = preText.lastIndexOf(' ');
|
|
||||||
const lastAt = preText.lastIndexOf('@');
|
|
||||||
|
|
||||||
if (caret > lastAt && lastSpace < lastAt) {
|
|
||||||
this.doProcessMentions = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onKeyPress(e);
|
this.props.onKeyPress(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown(e) {
|
handleKeyDown(e) {
|
||||||
if (Utils.getSelectedText(ReactDOM.findDOMNode(this.refs.message)) !== '') {
|
if (this.props.onKeyDown) {
|
||||||
this.doProcessMentions = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.keyCode === KeyCodes.BACKSPACE) {
|
|
||||||
this.handleBackspace(e);
|
|
||||||
} else if (this.props.onKeyDown) {
|
|
||||||
this.props.onKeyDown(e);
|
this.props.onKeyDown(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBackspace() {
|
|
||||||
const text = ReactDOM.findDOMNode(this.refs.message).value;
|
|
||||||
if (text.indexOf('/') === 0) {
|
|
||||||
this.refs.commands.getSuggestedCommands(text.substring(0, text.length - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.doProcessMentions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
|
|
||||||
const preText = text.substring(0, caret);
|
|
||||||
const lastSpace = preText.lastIndexOf(' ');
|
|
||||||
const lastAt = preText.lastIndexOf('@');
|
|
||||||
|
|
||||||
if (caret > lastAt && (lastSpace > lastAt || lastSpace === -1)) {
|
|
||||||
this.doProcessMentions = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkForNewMention(text) {
|
|
||||||
const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
|
|
||||||
|
|
||||||
const preText = text.substring(0, caret);
|
|
||||||
|
|
||||||
const atIndex = preText.lastIndexOf('@');
|
|
||||||
|
|
||||||
// The @ character not typed, so nothing to do.
|
|
||||||
if (atIndex === -1) {
|
|
||||||
this.updateMentionTab('-1');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastCharSpace = preText.lastIndexOf(String.fromCharCode(160));
|
|
||||||
const lastSpace = preText.lastIndexOf(' ');
|
|
||||||
|
|
||||||
// If there is a space after the last @, nothing to do.
|
|
||||||
if (lastSpace > atIndex || lastCharSpace > atIndex) {
|
|
||||||
this.updateMentionTab('-1');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the name typed so far.
|
|
||||||
const name = preText.substring(atIndex + 1, preText.length).toLowerCase();
|
|
||||||
this.updateMentionTab(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
addMention(name) {
|
|
||||||
const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
|
|
||||||
|
|
||||||
const text = this.props.messageText;
|
|
||||||
|
|
||||||
const preText = text.substring(0, caret);
|
|
||||||
|
|
||||||
const atIndex = preText.lastIndexOf('@');
|
|
||||||
|
|
||||||
// The @ character not typed, so nothing to do.
|
|
||||||
if (atIndex === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefix = text.substring(0, atIndex);
|
|
||||||
const suffix = text.substring(caret, text.length);
|
|
||||||
this.caret = prefix.length + name.length + 2;
|
|
||||||
this.addedMention = true;
|
|
||||||
this.doProcessMentions = true;
|
|
||||||
|
|
||||||
this.props.onUserInput(`${prefix}@${name} ${suffix}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
addCommand(cmd) {
|
|
||||||
const elm = ReactDOM.findDOMNode(this.refs.message);
|
|
||||||
elm.value = cmd;
|
|
||||||
this.handleChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
resize() {
|
resize() {
|
||||||
const e = ReactDOM.findDOMNode(this.refs.message);
|
const e = this.refs.message.getTextbox();
|
||||||
const w = ReactDOM.findDOMNode(this.refs.wrapper);
|
const w = ReactDOM.findDOMNode(this.refs.wrapper);
|
||||||
|
|
||||||
const prevHeight = $(e).height();
|
const prevHeight = $(e).height();
|
||||||
@@ -272,23 +115,19 @@ export default class Textbox extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFocus() {
|
handleFocus() {
|
||||||
const elm = ReactDOM.findDOMNode(this.refs.message);
|
const elm = this.refs.message.getTextbox();
|
||||||
if (elm.title === elm.value) {
|
if (elm.title === elm.value) {
|
||||||
elm.value = '';
|
elm.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBlur() {
|
handleBlur() {
|
||||||
const elm = ReactDOM.findDOMNode(this.refs.message);
|
const elm = this.refs.message.getTextbox();
|
||||||
if (elm.value === '') {
|
if (elm.value === '') {
|
||||||
elm.value = elm.title;
|
elm.value = elm.title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePaste() {
|
|
||||||
this.doProcessMentions = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
showPreview(e) {
|
showPreview(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.target.blur();
|
e.target.blur();
|
||||||
@@ -323,15 +162,11 @@ export default class Textbox extends React.Component {
|
|||||||
ref='wrapper'
|
ref='wrapper'
|
||||||
className='textarea-wrapper'
|
className='textarea-wrapper'
|
||||||
>
|
>
|
||||||
<CommandList
|
<SuggestionBox
|
||||||
ref='commands'
|
|
||||||
addCommand={this.addCommand}
|
|
||||||
channelId={this.props.channelId}
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
id={this.props.id}
|
id={this.props.id}
|
||||||
ref='message'
|
ref='message'
|
||||||
className={`form-control custom-textarea ${this.state.connection}`}
|
className={`form-control custom-textarea ${this.state.connection}`}
|
||||||
|
type='textarea'
|
||||||
spellCheck='true'
|
spellCheck='true'
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
autoCorrect='off'
|
autoCorrect='off'
|
||||||
@@ -339,14 +174,15 @@ export default class Textbox extends React.Component {
|
|||||||
maxLength={Constants.MAX_POST_LEN}
|
maxLength={Constants.MAX_POST_LEN}
|
||||||
placeholder={this.props.createMessage}
|
placeholder={this.props.createMessage}
|
||||||
value={this.props.messageText}
|
value={this.props.messageText}
|
||||||
onInput={this.handleChange}
|
onUserInput={this.props.onUserInput}
|
||||||
onChange={this.handleChange}
|
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
onFocus={this.handleFocus}
|
onFocus={this.handleFocus}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onPaste={this.handlePaste}
|
onPaste={this.handlePaste}
|
||||||
style={{visibility: this.state.preview ? 'hidden' : 'visible'}}
|
style={{visibility: this.state.preview ? 'hidden' : 'visible'}}
|
||||||
|
listComponent={SuggestionList}
|
||||||
|
providers={this.suggestionProviders}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref='preview'
|
ref='preview'
|
||||||
@@ -367,6 +203,10 @@ export default class Textbox extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Textbox.defaultProps = {
|
||||||
|
supportsCommands: true
|
||||||
|
};
|
||||||
|
|
||||||
Textbox.propTypes = {
|
Textbox.propTypes = {
|
||||||
id: React.PropTypes.string.isRequired,
|
id: React.PropTypes.string.isRequired,
|
||||||
channelId: React.PropTypes.string,
|
channelId: React.PropTypes.string,
|
||||||
@@ -375,5 +215,6 @@ Textbox.propTypes = {
|
|||||||
onKeyPress: React.PropTypes.func.isRequired,
|
onKeyPress: React.PropTypes.func.isRequired,
|
||||||
onHeightChange: React.PropTypes.func,
|
onHeightChange: React.PropTypes.func,
|
||||||
createMessage: React.PropTypes.string.isRequired,
|
createMessage: React.PropTypes.string.isRequired,
|
||||||
onKeyDown: React.PropTypes.func
|
onKeyDown: React.PropTypes.func,
|
||||||
|
supportsCommands: React.PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,3 +111,33 @@ export function showRegisterAppModal() {
|
|||||||
value: true
|
value: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function emitSuggestionPretextChanged(suggestionId, pretext) {
|
||||||
|
AppDispatcher.handleViewAction({
|
||||||
|
type: ActionTypes.SUGGESTION_PRETEXT_CHANGED,
|
||||||
|
id: suggestionId,
|
||||||
|
pretext
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitSelectNextSuggestion(suggestionId) {
|
||||||
|
AppDispatcher.handleViewAction({
|
||||||
|
type: ActionTypes.SUGGESTION_SELECT_NEXT,
|
||||||
|
id: suggestionId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitSelectPreviousSuggestion(suggestionId) {
|
||||||
|
AppDispatcher.handleViewAction({
|
||||||
|
type: ActionTypes.SUGGESTION_SELECT_PREVIOUS,
|
||||||
|
id: suggestionId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitCompleteWordSuggestion(suggestionId, term = '') {
|
||||||
|
AppDispatcher.handleViewAction({
|
||||||
|
type: Constants.ActionTypes.SUGGESTION_COMPLETE_WORD,
|
||||||
|
id: suggestionId,
|
||||||
|
term
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import ChannelLoader from '../components/channel_loader.jsx';
|
|||||||
import ErrorBar from '../components/error_bar.jsx';
|
import ErrorBar from '../components/error_bar.jsx';
|
||||||
import ErrorStore from '../stores/error_store.jsx';
|
import ErrorStore from '../stores/error_store.jsx';
|
||||||
|
|
||||||
import MentionList from '../components/mention_list.jsx';
|
|
||||||
import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx';
|
import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx';
|
||||||
import RenameChannelModal from '../components/rename_channel_modal.jsx';
|
import RenameChannelModal from '../components/rename_channel_modal.jsx';
|
||||||
import EditPostModal from '../components/edit_post_modal.jsx';
|
import EditPostModal from '../components/edit_post_modal.jsx';
|
||||||
@@ -47,21 +46,6 @@ function setupChannelPage(props, team, channel) {
|
|||||||
document.getElementById('channel_view')
|
document.getElementById('channel_view')
|
||||||
);
|
);
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<MentionList id='post_textbox' />,
|
|
||||||
document.getElementById('post_mention_tab')
|
|
||||||
);
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<MentionList id='reply_textbox' />,
|
|
||||||
document.getElementById('reply_mention_tab')
|
|
||||||
);
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<MentionList id='edit_textbox' />,
|
|
||||||
document.getElementById('edit_mention_tab')
|
|
||||||
);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Modals
|
// Modals
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ var ActionTypes = Constants.ActionTypes;
|
|||||||
var CHANGE_EVENT = 'change';
|
var CHANGE_EVENT = 'change';
|
||||||
var SEARCH_CHANGE_EVENT = 'search_change';
|
var SEARCH_CHANGE_EVENT = 'search_change';
|
||||||
var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
|
var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
|
||||||
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
|
|
||||||
var ADD_MENTION_EVENT = 'add_mention';
|
|
||||||
var SHOW_SEARCH_EVENT = 'show_search';
|
var SHOW_SEARCH_EVENT = 'show_search';
|
||||||
|
|
||||||
class SearchStoreClass extends EventEmitter {
|
class SearchStoreClass extends EventEmitter {
|
||||||
@@ -32,10 +30,6 @@ class SearchStoreClass extends EventEmitter {
|
|||||||
this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this);
|
this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this);
|
||||||
this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this);
|
this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this);
|
||||||
|
|
||||||
this.emitMentionDataChange = this.emitMentionDataChange.bind(this);
|
|
||||||
this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this);
|
|
||||||
this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.bind(this);
|
|
||||||
|
|
||||||
this.emitShowSearch = this.emitShowSearch.bind(this);
|
this.emitShowSearch = this.emitShowSearch.bind(this);
|
||||||
this.addShowSearchListener = this.addShowSearchListener.bind(this);
|
this.addShowSearchListener = this.addShowSearchListener.bind(this);
|
||||||
this.removeShowSearchListener = this.removeShowSearchListener.bind(this);
|
this.removeShowSearchListener = this.removeShowSearchListener.bind(this);
|
||||||
@@ -113,30 +107,6 @@ class SearchStoreClass extends EventEmitter {
|
|||||||
return BrowserStore.getItem('search_term');
|
return BrowserStore.getItem('search_term');
|
||||||
}
|
}
|
||||||
|
|
||||||
emitMentionDataChange(id, mentionText) {
|
|
||||||
this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText);
|
|
||||||
}
|
|
||||||
|
|
||||||
addMentionDataChangeListener(callback) {
|
|
||||||
this.on(MENTION_DATA_CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeMentionDataChangeListener(callback) {
|
|
||||||
this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitAddMention(id, username) {
|
|
||||||
this.emit(ADD_MENTION_EVENT, id, username);
|
|
||||||
}
|
|
||||||
|
|
||||||
addAddMentionListener(callback) {
|
|
||||||
this.on(ADD_MENTION_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAddMentionListener(callback) {
|
|
||||||
this.removeListener(ADD_MENTION_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
storeSearchResults(results, isMentionSearch) {
|
storeSearchResults(results, isMentionSearch) {
|
||||||
BrowserStore.setItem('search_results', results);
|
BrowserStore.setItem('search_results', results);
|
||||||
BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
|
BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
|
||||||
@@ -157,12 +127,6 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => {
|
|||||||
SearchStore.storeSearchTerm(action.term);
|
SearchStore.storeSearchTerm(action.term);
|
||||||
SearchStore.emitSearchTermChange(action.do_search, action.is_mention_search);
|
SearchStore.emitSearchTermChange(action.do_search, action.is_mention_search);
|
||||||
break;
|
break;
|
||||||
case ActionTypes.RECIEVED_MENTION_DATA:
|
|
||||||
SearchStore.emitMentionDataChange(action.id, action.mention_text);
|
|
||||||
break;
|
|
||||||
case ActionTypes.RECIEVED_ADD_MENTION:
|
|
||||||
SearchStore.emitAddMention(action.id, action.username);
|
|
||||||
break;
|
|
||||||
case ActionTypes.SHOW_SEARCH:
|
case ActionTypes.SHOW_SEARCH:
|
||||||
SearchStore.emitShowSearch();
|
SearchStore.emitShowSearch();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class SuggestionStore extends EventEmitter {
|
|||||||
// items: a list of objects backing the terms which may be used in rendering
|
// items: a list of objects backing the terms which may be used in rendering
|
||||||
// components: a list of react components that can be used to render their corresponding item
|
// components: a list of react components that can be used to render their corresponding item
|
||||||
// selection: the term currently selected by the keyboard
|
// selection: the term currently selected by the keyboard
|
||||||
|
// completeOnSpace: whether or not space will trigger the term to be autocompleted
|
||||||
this.suggestions = new Map();
|
this.suggestions = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +79,8 @@ class SuggestionStore extends EventEmitter {
|
|||||||
terms: [],
|
terms: [],
|
||||||
items: [],
|
items: [],
|
||||||
components: [],
|
components: [],
|
||||||
selection: ''
|
selection: '',
|
||||||
|
completeOnSpace: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +95,12 @@ class SuggestionStore extends EventEmitter {
|
|||||||
suggestion.terms = [];
|
suggestion.terms = [];
|
||||||
suggestion.items = [];
|
suggestion.items = [];
|
||||||
suggestion.components = [];
|
suggestion.components = [];
|
||||||
|
suggestion.completeOnSpace = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection(id) {
|
||||||
|
const suggestion = this.suggestions.get(id);
|
||||||
|
|
||||||
suggestion.selection = '';
|
suggestion.selection = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +120,12 @@ class SuggestionStore extends EventEmitter {
|
|||||||
suggestion.matchedPretext = matchedPretext;
|
suggestion.matchedPretext = matchedPretext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCompleteOnSpace(id, completeOnSpace) {
|
||||||
|
const suggestion = this.suggestions.get(id);
|
||||||
|
|
||||||
|
suggestion.completeOnSpace = completeOnSpace;
|
||||||
|
}
|
||||||
|
|
||||||
addSuggestion(id, term, item, component) {
|
addSuggestion(id, term, item, component) {
|
||||||
const suggestion = this.suggestions.get(id);
|
const suggestion = this.suggestions.get(id);
|
||||||
|
|
||||||
@@ -175,6 +189,10 @@ class SuggestionStore extends EventEmitter {
|
|||||||
return this.suggestions.get(id).selection;
|
return this.suggestions.get(id).selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldCompleteOnSpace(id) {
|
||||||
|
return this.suggestions.get(id).completeOnSpace;
|
||||||
|
}
|
||||||
|
|
||||||
selectNext(id) {
|
selectNext(id) {
|
||||||
this.setSelectionByDelta(id, 1);
|
this.setSelectionByDelta(id, 1);
|
||||||
}
|
}
|
||||||
@@ -218,11 +236,13 @@ class SuggestionStore extends EventEmitter {
|
|||||||
this.emitSuggestionsChanged(id);
|
this.emitSuggestionsChanged(id);
|
||||||
break;
|
break;
|
||||||
case ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS:
|
case ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS:
|
||||||
this.setMatchedPretext(id, other.matchedPretext);
|
if (other.matchedPretext === this.getMatchedPretext(id)) {
|
||||||
this.addSuggestions(id, other.terms, other.items, other.componentType);
|
// ensure the matched pretext hasn't changed so that we don't receive suggestions for outdated pretext
|
||||||
|
this.addSuggestions(id, other.terms, other.items, other.component);
|
||||||
|
|
||||||
this.ensureSelectionExists(id);
|
this.ensureSelectionExists(id);
|
||||||
this.emitSuggestionsChanged(id);
|
this.emitSuggestionsChanged(id);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ActionTypes.SUGGESTION_SELECT_NEXT:
|
case ActionTypes.SUGGESTION_SELECT_NEXT:
|
||||||
this.selectNext(id);
|
this.selectNext(id);
|
||||||
@@ -237,10 +257,11 @@ class SuggestionStore extends EventEmitter {
|
|||||||
|
|
||||||
this.setPretext(id, '');
|
this.setPretext(id, '');
|
||||||
this.clearSuggestions(id);
|
this.clearSuggestions(id);
|
||||||
|
this.clearSelection(id);
|
||||||
this.emitSuggestionsChanged(id);
|
this.emitSuggestionsChanged(id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new SuggestionStore();
|
export default new SuggestionStore();
|
||||||
|
|||||||
@@ -741,3 +741,27 @@ export function savePreferences(preferences, success, error) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSuggestedCommands(command, suggestionId, component) {
|
||||||
|
client.executeCommand(
|
||||||
|
'',
|
||||||
|
command,
|
||||||
|
true,
|
||||||
|
(data) => {
|
||||||
|
// pull out the suggested commands from the returned data
|
||||||
|
const terms = data.suggestions.map((suggestion) => suggestion.suggestion);
|
||||||
|
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
|
||||||
|
id: suggestionId,
|
||||||
|
matchedPretext: command,
|
||||||
|
terms,
|
||||||
|
items: data.suggestions,
|
||||||
|
component
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
dispatchError(err, 'getCommandSuggestions');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -597,7 +597,7 @@ export function applyTheme(theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (theme.centerChannelBg) {
|
if (theme.centerChannelBg) {
|
||||||
changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .command-box, .modal .modal-content, .mentions-name, .mentions--top .mentions-box', 'background:' + theme.centerChannelBg, 1);
|
changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .suggestion-content, .modal .modal-content', 'background:' + theme.centerChannelBg, 1);
|
||||||
changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
|
changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
|
||||||
changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
|
changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
|
||||||
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);
|
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);
|
||||||
@@ -615,9 +615,9 @@ export function applyTheme(theme) {
|
|||||||
changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
|
changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
|
||||||
changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name, .tip-overlay', 'color:' + theme.centerChannelColor, 1);
|
changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name, .tip-overlay', 'color:' + theme.centerChannelColor, 1);
|
||||||
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
|
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
|
||||||
changeCss('.mentions--top, .command-box', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
|
changeCss('.mentions--top, .suggestion-list', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
|
||||||
changeCss('.mentions--top, .command-box', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
|
changeCss('.mentions--top, .suggestion-list', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
|
||||||
changeCss('.mentions--top, .command-box', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1);
|
changeCss('.mentions--top, .suggestion-list', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1);
|
||||||
changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3);
|
changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3);
|
||||||
changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2);
|
changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2);
|
||||||
changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1);
|
changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1);
|
||||||
@@ -626,7 +626,7 @@ export function applyTheme(theme) {
|
|||||||
changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
|
changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
|
||||||
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
|
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
|
||||||
changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
|
changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
|
||||||
changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .webhooks__container, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
|
changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .suggestion-content, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .webhooks__container, .dropdown-menu, .modal .modal-header, .popover', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
|
||||||
changeCss('.popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
|
changeCss('.popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
|
||||||
changeCss('.search-help-popover .search-autocomplete__divider span', 'color:' + changeOpacity(theme.centerChannelColor, 0.7), 1);
|
changeCss('.search-help-popover .search-autocomplete__divider span', 'color:' + changeOpacity(theme.centerChannelColor, 0.7), 1);
|
||||||
changeCss('.popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
|
changeCss('.popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
|
||||||
@@ -652,7 +652,7 @@ export function applyTheme(theme) {
|
|||||||
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
|
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
|
||||||
changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
|
changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
|
||||||
changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
|
changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
|
||||||
changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
|
changeCss('.command-name:hover, .mentions-name:hover, .suggestion--selected, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
|
||||||
changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
|
changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
|
||||||
changeCss('@media(min-width: 960px){.post.current--user:hover .post__body ', 'background: none;', 1);
|
changeCss('@media(min-width: 960px){.post.current--user:hover .post__body ', 'background: none;', 1);
|
||||||
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
|
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
|
||||||
|
|||||||
@@ -7,26 +7,9 @@
|
|||||||
@include border-radius(3px);
|
@include border-radius(3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mentions--top {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1060;
|
|
||||||
@extend %popover-box-shadow;
|
|
||||||
.mentions-box {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
background-color: #fff;
|
|
||||||
border: $border-gray;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: scroll;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mentions-name {
|
.mentions-name {
|
||||||
position:relative;
|
position:relative;
|
||||||
width:100%;
|
width:100%;
|
||||||
background-color:#fff;
|
|
||||||
height:36px;
|
height:36px;
|
||||||
padding:2px;
|
padding:2px;
|
||||||
z-index:101;
|
z-index:101;
|
||||||
@@ -57,4 +40,4 @@
|
|||||||
|
|
||||||
.mention-highlight {
|
.mention-highlight {
|
||||||
background-color:#fff2bb;
|
background-color:#fff2bb;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,3 +405,11 @@
|
|||||||
@include opacity(0.7);
|
@include opacity(0.7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-body.edit-modal-body {
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.suggestion-content {
|
||||||
|
max-height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,6 +93,10 @@
|
|||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.suggestion-content {
|
||||||
|
max-height: 120px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-right-container {
|
.sidebar-right-container {
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
.command-box {
|
.suggestion-list {
|
||||||
|
width: 100%;
|
||||||
|
z-index: 100;
|
||||||
|
@extend %popover-box-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-list--top {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #fff;
|
}
|
||||||
|
|
||||||
|
.suggestion-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-height: 292px;
|
||||||
|
background-color: #fff;
|
||||||
border: $border-gray;
|
border: $border-gray;
|
||||||
bottom: 38px;
|
overflow-x: hidden;
|
||||||
overflow: auto;
|
overflow-y: scroll;
|
||||||
z-index: 100;
|
}
|
||||||
@extend %popover-box-shadow;
|
|
||||||
.sidebar--right & {
|
.suggestion-content--top {
|
||||||
bottom: 100px;
|
position: absolute;
|
||||||
}
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion--selected {
|
||||||
|
// set by theme code
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-name {
|
.command-name {
|
||||||
@@ -20,9 +34,7 @@
|
|||||||
z-index: 101;
|
z-index: 101;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
&:hover {
|
|
||||||
background-color: #e8eaed;
|
|
||||||
}
|
|
||||||
.command__desc {
|
.command__desc {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
@include opacity(0.5);
|
@include opacity(0.5);
|
||||||
@@ -32,4 +44,4 @@
|
|||||||
|
|
||||||
.command-desc {
|
.command-desc {
|
||||||
color: #a7a8ab;
|
color: #a7a8ab;
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,6 @@
|
|||||||
@import "partials/modal";
|
@import "partials/modal";
|
||||||
@import "partials/forms";
|
@import "partials/forms";
|
||||||
@import "partials/mentions";
|
@import "partials/mentions";
|
||||||
@import "partials/command-box";
|
|
||||||
@import "partials/error";
|
@import "partials/error";
|
||||||
@import "partials/error-bar";
|
@import "partials/error-bar";
|
||||||
@import "partials/loading";
|
@import "partials/loading";
|
||||||
@@ -40,6 +39,7 @@
|
|||||||
@import "partials/markdown";
|
@import "partials/markdown";
|
||||||
@import "partials/tutorial";
|
@import "partials/tutorial";
|
||||||
@import "partials/statistics";
|
@import "partials/statistics";
|
||||||
|
@import "partials/suggestion_list";
|
||||||
|
|
||||||
// Elements
|
// Elements
|
||||||
@import "partials/tooltips";
|
@import "partials/tooltips";
|
||||||
|
|||||||
@@ -26,13 +26,13 @@
|
|||||||
<script>
|
<script>
|
||||||
window.setup_channel_page({{ .Props }}, {{ .Team }}, {{ .Channel }}, {{ .User }});
|
window.setup_channel_page({{ .Props }}, {{ .Team }}, {{ .Channel }}, {{ .User }});
|
||||||
$('body').tooltip( {selector: '[data-toggle=tooltip]'} );
|
$('body').tooltip( {selector: '[data-toggle=tooltip]'} );
|
||||||
|
var modals = $('.modal-body').not('.edit-modal-body');
|
||||||
if($(window).height() > 1200){
|
if($(window).height() > 1200){
|
||||||
$('.modal-body').css('max-height', 1000);
|
modals.css('max-height', 1000);
|
||||||
|
} else {
|
||||||
|
modals.css('max-height', $(window).height() - 200);
|
||||||
}
|
}
|
||||||
else {
|
modals.perfectScrollbar();
|
||||||
$('.modal-body').css('max-height', $(window).height() - 200);
|
|
||||||
}
|
|
||||||
$('.modal-body').perfectScrollbar();
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user