Replaced MentionList and Mention components with AtMentionProvider

Этот коммит содержится в:
hmhealey
2015-11-30 16:20:42 -05:00
родитель e87c89be25
Коммит 6d10b10318
13 изменённых файлов: 278 добавлений и 601 удалений

100
web/react/components/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 += ' mentions-focus';
}
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);
}
}
}

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

@@ -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
};

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

@@ -7,6 +7,7 @@ import SearchStore from '../stores/search_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import SuggestionBox from '../components/suggestion_box.jsx';
import SearchChannelProvider from '../components/search_channel_provider.jsx';
import SearchSuggestionList from '../components/search_suggestion_list.jsx';
import SearchUserProvider from '../components/search_user_provider.jsx';
import * as utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
@@ -164,6 +165,7 @@ export default class SearchBar extends React.Component {
onFocus={this.handleUserFocus}
onBlur={this.handleUserBlur}
onUserInput={this.handleUserInput}
listComponent={SearchSuggestionList}
providers={this.suggestionProviders}
/>
{isSearching}

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

@@ -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) {
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 = {
suggestionId: React.PropTypes.string.isRequired
};

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

@@ -3,7 +3,6 @@
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Constants from '../utils/constants.jsx';
import SuggestionList from './suggestion_list.jsx';
import SuggestionStore from '../stores/suggestion_store.jsx';
import * as Utils from '../utils/utils.jsx';
@@ -45,6 +44,11 @@ export default class SuggestionBox extends React.Component {
$(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) {
if (!this.state.focused) {
return;
@@ -143,26 +147,48 @@ export default class SuggestionBox extends React.Component {
}
render() {
const SuggestionList = this.props.listComponent;
const newProps = Object.assign({}, this.props, {
onFocus: this.handleFocus,
onChange: this.handleChange,
onKeyDown: this.handleKeyDown
});
return (
<div>
let textbox = null;
if (this.props.type === 'input') {
textbox = (
<input
ref='textbox'
type='text'
{...newProps}
/>
);
} else if (this.props.type === 'textarea') {
textbox = (
<textarea
ref='textbox'
{...newProps}
/>
);
}
return (
<div>
{textbox}
<SuggestionList suggestionId={this.suggestionId} />
</div>
);
}
}
SuggestionBox.defaultProps = {
type: 'input'
};
SuggestionBox.propTypes = {
listComponent: React.PropTypes.func.isRequired,
type: React.PropTypes.oneOf(['input', 'textarea']).isRequired,
value: React.PropTypes.string.isRequired,
onUserInput: React.PropTypes.func,
providers: React.PropTypes.arrayOf(React.PropTypes.object),

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

@@ -4,12 +4,13 @@
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Constants from '../utils/constants.jsx';
import SuggestionStore from '../stores/suggestion_store.jsx';
import * as Utils from '../utils/utils.jsx';
export default class SuggestionList extends React.Component {
constructor(props) {
super(props);
this.getContent = this.getContent.bind(this);
this.handleItemClick = this.handleItemClick.bind(this);
this.handleSuggestionsChanged = this.handleSuggestionsChanged.bind(this);
@@ -27,17 +28,14 @@ export default class SuggestionList extends React.Component {
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() {
SuggestionStore.removeSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
}
getContent() {
return $(ReactDOM.findDOMNode(this.refs.content));
}
handleItemClick(term, e) {
AppDispatcher.handleViewAction({
type: Constants.ActionTypes.SUGGESTION_COMPLETE_WORD,
@@ -64,7 +62,7 @@ export default class SuggestionList extends React.Component {
}
scrollToItem(term) {
const content = $(ReactDOM.findDOMNode(this.refs.popover)).find('.popover-content');
const content = this.getContent();
const visibleContentHeight = content[0].clientHeight;
const actualContentHeight = content[0].scrollHeight;
@@ -75,7 +73,8 @@ export default class SuggestionList extends React.Component {
const item = $(ReactDOM.findDOMNode(this.refs[term]));
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) {
// the item is off the top of the visible space
@@ -87,24 +86,6 @@ 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() {
if (this.state.items.length === 0) {
return null;
@@ -119,15 +100,6 @@ export default class SuggestionList extends React.Component {
// 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}
@@ -140,18 +112,18 @@ export default class SuggestionList extends React.Component {
}
return (
<ReactBootstrap.Popover
ref='popover'
id='search-autocomplete__popover'
className='search-help-popover autocomplete visible'
placement='bottom'
>
{items}
</ReactBootstrap.Popover>
<div className='suggestion-list suggestion-list--top'>
<div
ref='content'
className='suggestion-content suggestion-content--top'
>
{items}
</div>
</div>
);
}
}
SuggestionList.propTypes = {
suggestionId: React.PropTypes.string.isRequired
};
};

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

@@ -1,15 +1,15 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import SearchStore from '../stores/search_store.jsx';
import CommandList from './command_list.jsx';
import AtMentionProvider from './at_mention_provider.jsx';
import SuggestionList from './suggestion_list.jsx';
import SuggestionBox from './suggestion_box.jsx';
import ErrorStore from '../stores/error_store.jsx';
import * as TextFormatting from '../utils/text_formatting.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
@@ -18,32 +18,23 @@ export default class Textbox extends React.Component {
super(props);
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onListenerChange = this.onListenerChange.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.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.handleFocus = this.handleFocus.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handlePaste = this.handlePaste.bind(this);
this.showPreview = this.showPreview.bind(this);
this.state = {
mentionText: '-1',
mentions: [],
connection: ''
};
this.caret = -1;
this.addedMention = false;
this.doProcessMentions = false;
this.mentions = [];
this.suggestionProviders = [new AtMentionProvider()];
}
getStateFromStores() {
@@ -57,24 +48,15 @@ export default class Textbox extends React.Component {
}
componentDidMount() {
SearchStore.addAddMentionListener(this.onListenerChange);
ErrorStore.addChangeListener(this.onRecievedError);
this.resize();
this.updateMentionTab(null);
}
componentWillUnmount() {
SearchStore.removeAddMentionListener(this.onListenerChange);
ErrorStore.removeChangeListener(this.onRecievedError);
}
onListenerChange(id, username) {
if (id === this.props.id) {
this.addMention(username);
}
}
onRecievedError() {
const errorState = ErrorStore.getLastError();
@@ -87,72 +69,29 @@ export default class Textbox extends React.Component {
componentDidUpdate() {
if (this.caret >= 0) {
Utils.setCaretPosition(ReactDOM.findDOMNode(this.refs.message), this.caret);
Utils.setCaretPosition(this.refs.message.getTextbox(), this.caret);
this.caret = -1;
}
if (this.doProcessMentions) {
this.updateMentionTab(null);
this.doProcessMentions = false;
}
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) {
const text = ReactDOM.findDOMNode(this.refs.message).value;
const text = this.refs.message.getTextbox().value;
if (!this.refs.commands.isEmpty() && text.indexOf('/') === 0 && e.which === 13) {
if (!this.refs.commands.isEmpty() && text.indexOf('/') === 0 && e.which === KeyCodes.ENTER) {
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);
}
handleKeyDown(e) {
if (Utils.getSelectedText(ReactDOM.findDOMNode(this.refs.message)) !== '') {
this.doProcessMentions = true;
}
if (e.keyCode === KeyCodes.BACKSPACE) {
this.handleBackspace(e);
} else if (this.props.onKeyDown) {
@@ -161,83 +100,18 @@ export default class Textbox extends React.Component {
}
handleBackspace() {
const text = ReactDOM.findDOMNode(this.refs.message).value;
const text = this.refs.message.getTextbox().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();
this.props.onUserInput(cmd);
}
resize() {
const e = ReactDOM.findDOMNode(this.refs.message);
const e = this.refs.message.getTextbox();
const w = ReactDOM.findDOMNode(this.refs.wrapper);
const prevHeight = $(e).height();
@@ -272,23 +146,19 @@ export default class Textbox extends React.Component {
}
handleFocus() {
const elm = ReactDOM.findDOMNode(this.refs.message);
const elm = this.refs.message.getTextbox();
if (elm.title === elm.value) {
elm.value = '';
}
}
handleBlur() {
const elm = ReactDOM.findDOMNode(this.refs.message);
const elm = this.refs.message.getTextbox();
if (elm.value === '') {
elm.value = elm.title;
}
}
handlePaste() {
this.doProcessMentions = true;
}
showPreview(e) {
e.preventDefault();
e.target.blur();
@@ -328,10 +198,11 @@ export default class Textbox extends React.Component {
addCommand={this.addCommand}
channelId={this.props.channelId}
/>
<textarea
<SuggestionBox
id={this.props.id}
ref='message'
className={`form-control custom-textarea ${this.state.connection}`}
type='textarea'
spellCheck='true'
autoComplete='off'
autoCorrect='off'
@@ -339,14 +210,15 @@ export default class Textbox extends React.Component {
maxLength={Constants.MAX_POST_LEN}
placeholder={this.props.createMessage}
value={this.props.messageText}
onInput={this.handleChange}
onChange={this.handleChange}
onUserInput={this.props.onUserInput}
onKeyPress={this.handleKeyPress}
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onPaste={this.handlePaste}
style={{visibility: this.state.preview ? 'hidden' : 'visible'}}
listComponent={SuggestionList}
providers={this.suggestionProviders}
/>
<div
ref='preview'

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

@@ -6,7 +6,6 @@ import ChannelLoader from '../components/channel_loader.jsx';
import ErrorBar from '../components/error_bar.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 RenameChannelModal from '../components/rename_channel_modal.jsx';
import EditPostModal from '../components/edit_post_modal.jsx';
@@ -47,21 +46,6 @@ function setupChannelPage(props, team, channel) {
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
//

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

@@ -12,8 +12,6 @@ var ActionTypes = Constants.ActionTypes;
var CHANGE_EVENT = 'change';
var SEARCH_CHANGE_EVENT = 'search_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';
class SearchStoreClass extends EventEmitter {
@@ -32,10 +30,6 @@ class SearchStoreClass extends EventEmitter {
this.addSearchTermChangeListener = this.addSearchTermChangeListener.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.addShowSearchListener = this.addShowSearchListener.bind(this);
this.removeShowSearchListener = this.removeShowSearchListener.bind(this);
@@ -113,30 +107,6 @@ class SearchStoreClass extends EventEmitter {
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) {
BrowserStore.setItem('search_results', results);
BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
@@ -157,12 +127,6 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => {
SearchStore.storeSearchTerm(action.term);
SearchStore.emitSearchTermChange(action.do_search, action.is_mention_search);
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:
SearchStore.emitShowSearch();
break;

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

@@ -7,22 +7,6 @@
@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 {
position:relative;
width:100%;
@@ -57,4 +41,4 @@
.mention-highlight {
background-color:#fff2bb;
}
}

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

@@ -0,0 +1,23 @@
.suggestion-list {
width: 100%;
z-index: 100;
@extend %popover-box-shadow;
}
.suggestion-list--top {
position: absolute;
}
.suggestion-content {
width: 100%;
max-height: 292px;
background-color: #fff;
border: $border-gray;
overflow-x: hidden;
overflow-y: scroll;
}
.suggestion-content--top {
position: absolute;
bottom: 0;
}

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

@@ -40,6 +40,7 @@
@import "partials/markdown";
@import "partials/tutorial";
@import "partials/statistics";
@import "partials/suggestion_list";
// Elements
@import "partials/tooltips";