diff --git a/web/react/components/at_mention_provider.jsx b/web/react/components/at_mention_provider.jsx
new file mode 100644
index 0000000000..4d68fef9da
--- /dev/null
+++ b/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 = ;
+ } else if (item.username === 'channel') {
+ username = 'channel';
+ description = 'Notifies everyone in the channel';
+ icon = ;
+ } else {
+ username = item.username;
+ description = Utils.getFullName(item);
+ icon = (
+
+ );
+ }
+
+ let className = 'mentions-name';
+ if (isSelection) {
+ className += ' mentions-focus';
+ }
+
+ return (
+
+ let textbox = null;
+ if (this.props.type === 'input') {
+ textbox = (
+ );
+ } else if (this.props.type === 'textarea') {
+ textbox = (
+
+ );
+ }
+
+ return (
+
+ {textbox}
);
}
}
+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),
diff --git a/web/react/components/suggestion_list.jsx b/web/react/components/suggestion_list.jsx
index 04d8f3e608..ec3888ebbe 100644
--- a/web/react/components/suggestion_list.jsx
+++ b/web/react/components/suggestion_list.jsx
@@ -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 (
-
- {text}
-
- );
- }
-
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(
- {items}
-
+
);
}
}
SuggestionList.propTypes = {
suggestionId: React.PropTypes.string.isRequired
-};
\ No newline at end of file
+};
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index 10b3c0069d..fde8f64d31 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -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}
/>
-
,
- document.getElementById('post_mention_tab')
- );
-
- ReactDOM.render(
- ,
- document.getElementById('reply_mention_tab')
- );
-
- ReactDOM.render(
- ,
- document.getElementById('edit_mention_tab')
- );
-
//
// Modals
//
diff --git a/web/react/stores/search_store.jsx b/web/react/stores/search_store.jsx
index e8ab6a2ae4..f932c379ac 100644
--- a/web/react/stores/search_store.jsx
+++ b/web/react/stores/search_store.jsx
@@ -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;
diff --git a/web/sass-files/sass/partials/_mentions.scss b/web/sass-files/sass/partials/_mentions.scss
index f59cefbc6e..a30490bd12 100644
--- a/web/sass-files/sass/partials/_mentions.scss
+++ b/web/sass-files/sass/partials/_mentions.scss
@@ -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;
-}
\ No newline at end of file
+}
diff --git a/web/sass-files/sass/partials/_suggestion_list.scss b/web/sass-files/sass/partials/_suggestion_list.scss
new file mode 100644
index 0000000000..7203779eda
--- /dev/null
+++ b/web/sass-files/sass/partials/_suggestion_list.scss
@@ -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;
+}
diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss
index 01f654eec1..a13496e238 100644
--- a/web/sass-files/sass/styles.scss
+++ b/web/sass-files/sass/styles.scss
@@ -40,6 +40,7 @@
@import "partials/markdown";
@import "partials/tutorial";
@import "partials/statistics";
+@import "partials/suggestion_list";
// Elements
@import "partials/tooltips";