summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2016-05-31 16:14:28 -0400
committerJoram Wilander <jwawilander@gmail.com>2016-05-31 16:14:28 -0400
commit12f659372786c9b9dd3261b4663a4e840da64372 (patch)
treeed7b3234df9ef277fa79759ced5897cc59e83049 /webapp/components
parent6e6257fccaa0c5837101dedbe7f547bc3cc6c6bb (diff)
downloadchat-12f659372786c9b9dd3261b4663a4e840da64372.tar.gz
chat-12f659372786c9b9dd3261b4663a4e840da64372.tar.bz2
chat-12f659372786c9b9dd3261b4663a4e840da64372.zip
PLT-2643 Fixed asynchronous autocomplete incorrectly replacing text (#3167)
* Allowed different suggestions to match different text. Added a Suggestion base component. Improved text replacement used when filling in suggestions * Fixed formatting
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/suggestion/at_mention_provider.jsx20
-rw-r--r--webapp/components/suggestion/command_provider.jsx14
-rw-r--r--webapp/components/suggestion/emoticon_provider.jsx20
-rw-r--r--webapp/components/suggestion/search_channel_provider.jsx18
-rw-r--r--webapp/components/suggestion/search_suggestion_list.jsx4
-rw-r--r--webapp/components/suggestion/search_user_provider.jsx17
-rw-r--r--webapp/components/suggestion/suggestion.jsx28
-rw-r--r--webapp/components/suggestion/suggestion_box.jsx32
-rw-r--r--webapp/components/suggestion/suggestion_list.jsx50
9 files changed, 117 insertions, 86 deletions
diff --git a/webapp/components/suggestion/at_mention_provider.jsx b/webapp/components/suggestion/at_mention_provider.jsx
index 760f048bd..2e297a175 100644
--- a/webapp/components/suggestion/at_mention_provider.jsx
+++ b/webapp/components/suggestion/at_mention_provider.jsx
@@ -1,20 +1,21 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import React from 'react';
+
import SuggestionStore from 'stores/suggestion_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
import Client from 'utils/web_client.jsx';
import {FormattedMessage} from 'react-intl';
+import Suggestion from './suggestion.jsx';
const MaxUserSuggestions = 40;
-import React from 'react';
-
-class AtMentionSuggestion extends React.Component {
+class AtMentionSuggestion extends Suggestion {
render() {
- const {item, isSelection, onClick} = this.props;
+ const {item, isSelection} = this.props;
let username;
let description;
@@ -56,7 +57,7 @@ class AtMentionSuggestion extends React.Component {
return (
<div
className={className}
- onClick={onClick}
+ onClick={this.handleClick}
>
<div className='pull-left'>
{icon}
@@ -74,12 +75,6 @@ class AtMentionSuggestion extends React.Component {
}
}
-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);
@@ -112,8 +107,7 @@ export default class AtMentionProvider {
const mentions = filtered.map((user) => '@' + user.username);
- SuggestionStore.setMatchedPretext(suggestionId, captured[0]);
- SuggestionStore.addSuggestions(suggestionId, mentions, filtered, AtMentionSuggestion);
+ SuggestionStore.addSuggestions(suggestionId, mentions, filtered, AtMentionSuggestion, captured[0]);
}
}
}
diff --git a/webapp/components/suggestion/command_provider.jsx b/webapp/components/suggestion/command_provider.jsx
index 36860fa66..73ae4deaa 100644
--- a/webapp/components/suggestion/command_provider.jsx
+++ b/webapp/components/suggestion/command_provider.jsx
@@ -1,11 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import React from 'react';
+
import * as AsyncClient from 'utils/async_client.jsx';
-import React from 'react';
+import Suggestion from './suggestion.jsx';
-class CommandSuggestion extends React.Component {
+class CommandSuggestion extends Suggestion {
render() {
const {item, isSelection, onClick} = this.props;
@@ -30,16 +32,10 @@ class CommandSuggestion extends React.Component {
}
}
-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('/')) {
- AsyncClient.getSuggestedCommands(pretext, suggestionId, CommandSuggestion);
+ AsyncClient.getSuggestedCommands(pretext, suggestionId, CommandSuggestion, pretext);
}
}
}
diff --git a/webapp/components/suggestion/emoticon_provider.jsx b/webapp/components/suggestion/emoticon_provider.jsx
index b7f4cd513..a110796e1 100644
--- a/webapp/components/suggestion/emoticon_provider.jsx
+++ b/webapp/components/suggestion/emoticon_provider.jsx
@@ -1,14 +1,16 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import SuggestionStore from 'stores/suggestion_store.jsx';
+import React from 'react';
+
import * as Emoticons from 'utils/emoticons.jsx';
+import SuggestionStore from 'stores/suggestion_store.jsx';
-const MAX_EMOTICON_SUGGESTIONS = 40;
+import Suggestion from './suggestion.jsx';
-import React from 'react';
+const MAX_EMOTICON_SUGGESTIONS = 40;
-class EmoticonSuggestion extends React.Component {
+class EmoticonSuggestion extends Suggestion {
render() {
const text = this.props.term;
const emoticon = this.props.item;
@@ -39,13 +41,6 @@ class EmoticonSuggestion extends React.Component {
}
}
-EmoticonSuggestion.propTypes = {
- item: React.PropTypes.object.isRequired,
- term: React.PropTypes.string.isRequired,
- isSelection: React.PropTypes.bool,
- onClick: React.PropTypes.func
-};
-
export default class EmoticonProvider {
handlePretextChanged(suggestionId, pretext) {
const captured = (/(?:^|\s)(:([a-zA-Z0-9_+\-]*))$/g).exec(pretext);
@@ -82,8 +77,7 @@ export default class EmoticonProvider {
const terms = matched.map((emoticon) => ':' + emoticon.alias + ':');
if (terms.length > 0) {
- SuggestionStore.setMatchedPretext(suggestionId, text);
- SuggestionStore.addSuggestions(suggestionId, terms, matched, EmoticonSuggestion);
+ SuggestionStore.addSuggestions(suggestionId, terms, matched, EmoticonSuggestion, text);
// force the selection to be cleared since the order of elements may have changed
SuggestionStore.clearSelection(suggestionId);
diff --git a/webapp/components/suggestion/search_channel_provider.jsx b/webapp/components/suggestion/search_channel_provider.jsx
index 2e3195c1d..2b8005204 100644
--- a/webapp/components/suggestion/search_channel_provider.jsx
+++ b/webapp/components/suggestion/search_channel_provider.jsx
@@ -1,13 +1,15 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import React from 'react';
+
import ChannelStore from 'stores/channel_store.jsx';
import Constants from 'utils/constants.jsx';
import SuggestionStore from 'stores/suggestion_store.jsx';
-import React from 'react';
+import Suggestion from './suggestion.jsx';
-class SearchChannelSuggestion extends React.Component {
+class SearchChannelSuggestion extends Suggestion {
render() {
const {item, isSelection, onClick} = this.props;
@@ -27,12 +29,6 @@ class SearchChannelSuggestion extends React.Component {
}
}
-SearchChannelSuggestion.propTypes = {
- item: React.PropTypes.object.isRequired,
- isSelection: React.PropTypes.bool,
- onClick: React.PropTypes.func
-};
-
export default class SearchChannelProvider {
handlePretextChanged(suggestionId, pretext) {
const captured = (/\b(?:in|channel):\s*(\S*)$/i).exec(pretext);
@@ -62,10 +58,8 @@ export default class SearchChannelProvider {
privateChannels.sort((a, b) => a.name.localeCompare(b.name));
const privateChannelNames = privateChannels.map((channel) => channel.name);
- SuggestionStore.setMatchedPretext(suggestionId, channelPrefix);
-
- SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion);
- SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion);
+ SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion, channelPrefix);
+ SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion, channelPrefix);
}
}
}
diff --git a/webapp/components/suggestion/search_suggestion_list.jsx b/webapp/components/suggestion/search_suggestion_list.jsx
index 57aaee8ff..628f93af0 100644
--- a/webapp/components/suggestion/search_suggestion_list.jsx
+++ b/webapp/components/suggestion/search_suggestion_list.jsx
@@ -72,8 +72,10 @@ export default class SearchSuggestionList extends SuggestionList {
key={term}
ref={term}
item={item}
+ term={term}
+ matchedPretext={this.state.matchedPretext[i]}
isSelection={isSelection}
- onClick={this.handleItemClick.bind(this, term)}
+ onClick={this.handleItemClick}
/>
);
}
diff --git a/webapp/components/suggestion/search_user_provider.jsx b/webapp/components/suggestion/search_user_provider.jsx
index b7234469a..5a9e70d0f 100644
--- a/webapp/components/suggestion/search_user_provider.jsx
+++ b/webapp/components/suggestion/search_user_provider.jsx
@@ -1,13 +1,15 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import React from 'react';
+
+import Client from 'utils/web_client.jsx';
import SuggestionStore from 'stores/suggestion_store.jsx';
import UserStore from 'stores/user_store.jsx';
-import Client from 'utils/web_client.jsx';
-import React from 'react';
+import Suggestion from './suggestion.jsx';
-class SearchUserSuggestion extends React.Component {
+class SearchUserSuggestion extends Suggestion {
render() {
const {item, isSelection, onClick} = this.props;
@@ -31,12 +33,6 @@ class SearchUserSuggestion extends React.Component {
}
}
-SearchUserSuggestion.propTypes = {
- item: React.PropTypes.object.isRequired,
- isSelection: React.PropTypes.bool,
- onClick: React.PropTypes.func
-};
-
export default class SearchUserProvider {
handlePretextChanged(suggestionId, pretext) {
const captured = (/\bfrom:\s*(\S*)$/i).exec(pretext);
@@ -58,8 +54,7 @@ export default class SearchUserProvider {
const usernames = filtered.map((user) => user.username);
- SuggestionStore.setMatchedPretext(suggestionId, usernamePrefix);
- SuggestionStore.addSuggestions(suggestionId, usernames, filtered, SearchUserSuggestion);
+ SuggestionStore.addSuggestions(suggestionId, usernames, filtered, SearchUserSuggestion, usernamePrefix);
}
}
}
diff --git a/webapp/components/suggestion/suggestion.jsx b/webapp/components/suggestion/suggestion.jsx
new file mode 100644
index 000000000..8547d50d0
--- /dev/null
+++ b/webapp/components/suggestion/suggestion.jsx
@@ -0,0 +1,28 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+export default class Suggestion extends React.Component {
+ static get propTypes() {
+ return {
+ item: React.PropTypes.object.isRequired,
+ term: React.PropTypes.string.isRequired,
+ matchedPretext: React.PropTypes.string.isRequired,
+ isSelection: React.PropTypes.bool,
+ onClick: React.PropTypes.func
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleClick = this.handleClick.bind(this);
+ }
+
+ handleClick(e) {
+ e.preventDefault();
+
+ this.props.onClick(this.props.term, this.props.matchedPretext);
+ }
+} \ No newline at end of file
diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx
index 86d349a1a..f81cc6765 100644
--- a/webapp/components/suggestion/suggestion_box.jsx
+++ b/webapp/components/suggestion/suggestion_box.jsx
@@ -27,10 +27,10 @@ export default class SuggestionBox extends React.Component {
this.handlePretextChanged = this.handlePretextChanged.bind(this);
this.suggestionId = Utils.generateId();
+ SuggestionStore.registerSuggestionBox(this.suggestionId);
}
componentDidMount() {
- SuggestionStore.registerSuggestionBox(this.suggestionId);
$(document).on('click', this.handleDocumentClick);
SuggestionStore.addCompleteWordListener(this.suggestionId, this.handleCompleteWord);
@@ -81,12 +81,24 @@ export default class SuggestionBox extends React.Component {
}
}
- handleCompleteWord(term) {
+ handleCompleteWord(term, matchedPretext) {
const textbox = ReactDOM.findDOMNode(this.refs.textbox);
const caret = Utils.getCaretPosition(textbox);
const text = this.props.value;
- const prefix = text.substring(0, caret - SuggestionStore.getMatchedPretext(this.suggestionId).length);
+ const pretext = text.substring(0, caret);
+
+ let prefix;
+ if (pretext.endsWith(matchedPretext)) {
+ prefix = pretext.substring(0, pretext.length - matchedPretext.length);
+ } else {
+ // the pretext has changed since we got a term to complete so see if the term still fits the pretext
+ const termWithoutMatched = term.substring(matchedPretext.length);
+ const overlap = SuggestionBox.findOverlap(pretext, termWithoutMatched);
+
+ prefix = pretext.substring(0, pretext.length - overlap.length - matchedPretext.length);
+ }
+
const suffix = text.substring(caret);
if (this.props.onUserInput) {
@@ -168,6 +180,20 @@ export default class SuggestionBox extends React.Component {
</div>
);
}
+
+ // Finds the longest substring that's at both the end of b and the start of a. For example,
+ // if a = "firepit" and b = "pitbull", findOverlap would return "pit".
+ static findOverlap(a, b) {
+ for (let i = b.length; i > 0; i--) {
+ const substring = b.substring(0, i);
+
+ if (a.endsWith(substring)) {
+ return substring;
+ }
+ }
+
+ return '';
+ }
}
SuggestionBox.defaultProps = {
diff --git a/webapp/components/suggestion/suggestion_list.jsx b/webapp/components/suggestion/suggestion_list.jsx
index 91f7443cb..7774f9a7d 100644
--- a/webapp/components/suggestion/suggestion_list.jsx
+++ b/webapp/components/suggestion/suggestion_list.jsx
@@ -12,6 +12,8 @@ export default class SuggestionList extends React.Component {
constructor(props) {
super(props);
+ this.getStateFromStores = this.getStateFromStores.bind(this);
+
this.getContent = this.getContent.bind(this);
this.handleItemClick = this.handleItemClick.bind(this);
@@ -19,11 +21,18 @@ export default class SuggestionList extends React.Component {
this.scrollToItem = this.scrollToItem.bind(this);
- this.state = {
- items: [],
- terms: [],
- components: [],
- selection: ''
+ this.state = this.getStateFromStores(props.suggestionId);
+ }
+
+ getStateFromStores(suggestionId) {
+ const suggestions = SuggestionStore.getSuggestions(suggestionId || this.props.suggestionId);
+
+ return {
+ matchedPretext: suggestions.matchedPretext,
+ items: suggestions.items,
+ terms: suggestions.terms,
+ components: suggestions.components,
+ selection: suggestions.selection
};
}
@@ -31,6 +40,12 @@ export default class SuggestionList extends React.Component {
SuggestionStore.addSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
}
+ componentDidUpdate(prevProps, prevState) {
+ if (this.state.selection !== prevState.selection && this.state.selection) {
+ this.scrollToItem(this.state.selection);
+ }
+ }
+
componentWillUnmount() {
SuggestionStore.removeSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
}
@@ -39,25 +54,12 @@ export default class SuggestionList extends React.Component {
return $(ReactDOM.findDOMNode(this.refs.content));
}
- handleItemClick(term, e) {
- GlobalActions.emitCompleteWordSuggestion(this.props.suggestionId, term);
-
- e.preventDefault();
+ handleItemClick(term, matchedPretext) {
+ GlobalActions.emitCompleteWordSuggestion(this.props.suggestionId, term, matchedPretext);
}
handleSuggestionsChanged() {
- const selection = SuggestionStore.getSelection(this.props.suggestionId);
-
- this.setState({
- items: SuggestionStore.getItems(this.props.suggestionId),
- terms: SuggestionStore.getTerms(this.props.suggestionId),
- components: SuggestionStore.getComponents(this.props.suggestionId),
- selection
- });
-
- if (selection) {
- window.requestAnimationFrame(() => this.scrollToItem(this.state.selection));
- }
+ this.setState(this.getStateFromStores());
}
scrollToItem(term) {
@@ -96,7 +98,6 @@ export default class SuggestionList extends React.Component {
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;
@@ -107,10 +108,11 @@ export default class SuggestionList extends React.Component {
<Component
key={term}
ref={term}
- item={item}
+ item={this.state.items[i]}
term={term}
+ matchedPretext={this.state.matchedPretext[i]}
isSelection={isSelection}
- onClick={this.handleItemClick.bind(this, term)}
+ onClick={this.handleItemClick}
/>
);
}