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
Этот коммит содержится в:
коммит произвёл
Joram Wilander
родитель
6e6257fcca
Коммит
12f6593727
@@ -1,20 +1,21 @@
|
|||||||
// 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 React from 'react';
|
||||||
|
|
||||||
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';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import Client from 'utils/web_client.jsx';
|
import Client from 'utils/web_client.jsx';
|
||||||
|
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import Suggestion from './suggestion.jsx';
|
||||||
|
|
||||||
const MaxUserSuggestions = 40;
|
const MaxUserSuggestions = 40;
|
||||||
|
|
||||||
import React from 'react';
|
class AtMentionSuggestion extends Suggestion {
|
||||||
|
|
||||||
class AtMentionSuggestion extends React.Component {
|
|
||||||
render() {
|
render() {
|
||||||
const {item, isSelection, onClick} = this.props;
|
const {item, isSelection} = this.props;
|
||||||
|
|
||||||
let username;
|
let username;
|
||||||
let description;
|
let description;
|
||||||
@@ -56,7 +57,7 @@ class AtMentionSuggestion extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={className}
|
className={className}
|
||||||
onClick={onClick}
|
onClick={this.handleClick}
|
||||||
>
|
>
|
||||||
<div className='pull-left'>
|
<div className='pull-left'>
|
||||||
{icon}
|
{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 {
|
export default class AtMentionProvider {
|
||||||
handlePretextChanged(suggestionId, pretext) {
|
handlePretextChanged(suggestionId, pretext) {
|
||||||
const captured = (/@([a-z0-9\-\._]*)$/i).exec(pretext);
|
const captured = (/@([a-z0-9\-\._]*)$/i).exec(pretext);
|
||||||
@@ -112,8 +107,7 @@ export default class AtMentionProvider {
|
|||||||
|
|
||||||
const mentions = filtered.map((user) => '@' + user.username);
|
const mentions = filtered.map((user) => '@' + user.username);
|
||||||
|
|
||||||
SuggestionStore.setMatchedPretext(suggestionId, captured[0]);
|
SuggestionStore.addSuggestions(suggestionId, mentions, filtered, AtMentionSuggestion, captured[0]);
|
||||||
SuggestionStore.addSuggestions(suggestionId, mentions, filtered, AtMentionSuggestion);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// 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 * as AsyncClient from 'utils/async_client.jsx';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
class CommandSuggestion extends React.Component {
|
import * as AsyncClient from 'utils/async_client.jsx';
|
||||||
|
|
||||||
|
import Suggestion from './suggestion.jsx';
|
||||||
|
|
||||||
|
class CommandSuggestion extends Suggestion {
|
||||||
render() {
|
render() {
|
||||||
const {item, isSelection, onClick} = this.props;
|
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 {
|
export default class CommandProvider {
|
||||||
handlePretextChanged(suggestionId, pretext) {
|
handlePretextChanged(suggestionId, pretext) {
|
||||||
if (pretext.startsWith('/')) {
|
if (pretext.startsWith('/')) {
|
||||||
AsyncClient.getSuggestedCommands(pretext, suggestionId, CommandSuggestion);
|
AsyncClient.getSuggestedCommands(pretext, suggestionId, CommandSuggestion, pretext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
// 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 React from 'react';
|
||||||
|
|
||||||
import * as Emoticons from 'utils/emoticons.jsx';
|
import * as Emoticons from 'utils/emoticons.jsx';
|
||||||
|
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||||
|
|
||||||
|
import Suggestion from './suggestion.jsx';
|
||||||
|
|
||||||
const MAX_EMOTICON_SUGGESTIONS = 40;
|
const MAX_EMOTICON_SUGGESTIONS = 40;
|
||||||
|
|
||||||
import React from 'react';
|
class EmoticonSuggestion extends Suggestion {
|
||||||
|
|
||||||
class EmoticonSuggestion extends React.Component {
|
|
||||||
render() {
|
render() {
|
||||||
const text = this.props.term;
|
const text = this.props.term;
|
||||||
const emoticon = this.props.item;
|
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 {
|
export default class EmoticonProvider {
|
||||||
handlePretextChanged(suggestionId, pretext) {
|
handlePretextChanged(suggestionId, pretext) {
|
||||||
const captured = (/(?:^|\s)(:([a-zA-Z0-9_+\-]*))$/g).exec(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 + ':');
|
const terms = matched.map((emoticon) => ':' + emoticon.alias + ':');
|
||||||
|
|
||||||
if (terms.length > 0) {
|
if (terms.length > 0) {
|
||||||
SuggestionStore.setMatchedPretext(suggestionId, text);
|
SuggestionStore.addSuggestions(suggestionId, terms, matched, EmoticonSuggestion, text);
|
||||||
SuggestionStore.addSuggestions(suggestionId, terms, matched, EmoticonSuggestion);
|
|
||||||
|
|
||||||
// force the selection to be cleared since the order of elements may have changed
|
// force the selection to be cleared since the order of elements may have changed
|
||||||
SuggestionStore.clearSelection(suggestionId);
|
SuggestionStore.clearSelection(suggestionId);
|
||||||
|
|||||||
@@ -1,13 +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 React from 'react';
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
import React from 'react';
|
import Suggestion from './suggestion.jsx';
|
||||||
|
|
||||||
class SearchChannelSuggestion extends React.Component {
|
class SearchChannelSuggestion extends Suggestion {
|
||||||
render() {
|
render() {
|
||||||
const {item, isSelection, onClick} = this.props;
|
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 {
|
export default class SearchChannelProvider {
|
||||||
handlePretextChanged(suggestionId, pretext) {
|
handlePretextChanged(suggestionId, pretext) {
|
||||||
const captured = (/\b(?:in|channel):\s*(\S*)$/i).exec(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));
|
privateChannels.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
const privateChannelNames = privateChannels.map((channel) => channel.name);
|
const privateChannelNames = privateChannels.map((channel) => channel.name);
|
||||||
|
|
||||||
SuggestionStore.setMatchedPretext(suggestionId, channelPrefix);
|
SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion, channelPrefix);
|
||||||
|
SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion, channelPrefix);
|
||||||
SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion);
|
|
||||||
SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,10 @@ export default class SearchSuggestionList extends SuggestionList {
|
|||||||
key={term}
|
key={term}
|
||||||
ref={term}
|
ref={term}
|
||||||
item={item}
|
item={item}
|
||||||
|
term={term}
|
||||||
|
matchedPretext={this.state.matchedPretext[i]}
|
||||||
isSelection={isSelection}
|
isSelection={isSelection}
|
||||||
onClick={this.handleItemClick.bind(this, term)}
|
onClick={this.handleItemClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +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 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 React from 'react';
|
||||||
|
|
||||||
class SearchUserSuggestion extends React.Component {
|
import Client from 'utils/web_client.jsx';
|
||||||
|
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||||
|
import UserStore from 'stores/user_store.jsx';
|
||||||
|
|
||||||
|
import Suggestion from './suggestion.jsx';
|
||||||
|
|
||||||
|
class SearchUserSuggestion extends Suggestion {
|
||||||
render() {
|
render() {
|
||||||
const {item, isSelection, onClick} = this.props;
|
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 {
|
export default class SearchUserProvider {
|
||||||
handlePretextChanged(suggestionId, pretext) {
|
handlePretextChanged(suggestionId, pretext) {
|
||||||
const captured = (/\bfrom:\s*(\S*)$/i).exec(pretext);
|
const captured = (/\bfrom:\s*(\S*)$/i).exec(pretext);
|
||||||
@@ -58,8 +54,7 @@ export default class SearchUserProvider {
|
|||||||
|
|
||||||
const usernames = filtered.map((user) => user.username);
|
const usernames = filtered.map((user) => user.username);
|
||||||
|
|
||||||
SuggestionStore.setMatchedPretext(suggestionId, usernamePrefix);
|
SuggestionStore.addSuggestions(suggestionId, usernames, filtered, SearchUserSuggestion, usernamePrefix);
|
||||||
SuggestionStore.addSuggestions(suggestionId, usernames, filtered, SearchUserSuggestion);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
webapp/components/suggestion/suggestion.jsx
Обычный файл
28
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,10 +27,10 @@ export default class SuggestionBox extends React.Component {
|
|||||||
this.handlePretextChanged = this.handlePretextChanged.bind(this);
|
this.handlePretextChanged = this.handlePretextChanged.bind(this);
|
||||||
|
|
||||||
this.suggestionId = Utils.generateId();
|
this.suggestionId = Utils.generateId();
|
||||||
|
SuggestionStore.registerSuggestionBox(this.suggestionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
SuggestionStore.registerSuggestionBox(this.suggestionId);
|
|
||||||
$(document).on('click', this.handleDocumentClick);
|
$(document).on('click', this.handleDocumentClick);
|
||||||
|
|
||||||
SuggestionStore.addCompleteWordListener(this.suggestionId, this.handleCompleteWord);
|
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 textbox = ReactDOM.findDOMNode(this.refs.textbox);
|
||||||
const caret = Utils.getCaretPosition(textbox);
|
const caret = Utils.getCaretPosition(textbox);
|
||||||
|
|
||||||
const text = this.props.value;
|
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);
|
const suffix = text.substring(caret);
|
||||||
|
|
||||||
if (this.props.onUserInput) {
|
if (this.props.onUserInput) {
|
||||||
@@ -168,6 +180,20 @@ export default class SuggestionBox extends React.Component {
|
|||||||
</div>
|
</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 = {
|
SuggestionBox.defaultProps = {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export default class SuggestionList extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||||
|
|
||||||
this.getContent = this.getContent.bind(this);
|
this.getContent = this.getContent.bind(this);
|
||||||
|
|
||||||
this.handleItemClick = this.handleItemClick.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.scrollToItem = this.scrollToItem.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = this.getStateFromStores(props.suggestionId);
|
||||||
items: [],
|
}
|
||||||
terms: [],
|
|
||||||
components: [],
|
getStateFromStores(suggestionId) {
|
||||||
selection: ''
|
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);
|
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() {
|
componentWillUnmount() {
|
||||||
SuggestionStore.removeSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
|
SuggestionStore.removeSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
|
||||||
}
|
}
|
||||||
@@ -39,25 +54,12 @@ export default class SuggestionList extends React.Component {
|
|||||||
return $(ReactDOM.findDOMNode(this.refs.content));
|
return $(ReactDOM.findDOMNode(this.refs.content));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleItemClick(term, e) {
|
handleItemClick(term, matchedPretext) {
|
||||||
GlobalActions.emitCompleteWordSuggestion(this.props.suggestionId, term);
|
GlobalActions.emitCompleteWordSuggestion(this.props.suggestionId, term, matchedPretext);
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSuggestionsChanged() {
|
handleSuggestionsChanged() {
|
||||||
const selection = SuggestionStore.getSelection(this.props.suggestionId);
|
this.setState(this.getStateFromStores());
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToItem(term) {
|
scrollToItem(term) {
|
||||||
@@ -96,7 +98,6 @@ export default class SuggestionList extends React.Component {
|
|||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
for (let i = 0; i < this.state.items.length; i++) {
|
for (let i = 0; i < this.state.items.length; i++) {
|
||||||
const item = this.state.items[i];
|
|
||||||
const term = this.state.terms[i];
|
const term = this.state.terms[i];
|
||||||
const isSelection = term === this.state.selection;
|
const isSelection = term === this.state.selection;
|
||||||
|
|
||||||
@@ -107,10 +108,11 @@ export default class SuggestionList extends React.Component {
|
|||||||
<Component
|
<Component
|
||||||
key={term}
|
key={term}
|
||||||
ref={term}
|
ref={term}
|
||||||
item={item}
|
item={this.state.items[i]}
|
||||||
term={term}
|
term={term}
|
||||||
|
matchedPretext={this.state.matchedPretext[i]}
|
||||||
isSelection={isSelection}
|
isSelection={isSelection}
|
||||||
onClick={this.handleItemClick.bind(this, term)}
|
onClick={this.handleItemClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class SuggestionStore extends EventEmitter {
|
|||||||
// this.suggestions stores the state of all SuggestionBoxes by mapping their unique identifier to an
|
// this.suggestions stores the state of all SuggestionBoxes by mapping their unique identifier to an
|
||||||
// object with the following fields:
|
// object with the following fields:
|
||||||
// pretext: the text before the cursor
|
// pretext: the text before the cursor
|
||||||
// matchedPretext: the text before the cursor that will be replaced if an autocomplete term is selected
|
// matchedPretext: a list of the text before the cursor that will be replaced if the corresponding autocomplete term is selected
|
||||||
// terms: a list of strings which the previously typed text may be replaced by
|
// terms: a list of strings which the previously typed text may be replaced by
|
||||||
// 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
|
||||||
@@ -67,14 +67,14 @@ class SuggestionStore extends EventEmitter {
|
|||||||
removeCompleteWordListener(id, callback) {
|
removeCompleteWordListener(id, callback) {
|
||||||
this.removeListener(COMPLETE_WORD_EVENT + id, callback);
|
this.removeListener(COMPLETE_WORD_EVENT + id, callback);
|
||||||
}
|
}
|
||||||
emitCompleteWord(id, term) {
|
emitCompleteWord(id, term, matchedPretext) {
|
||||||
this.emit(COMPLETE_WORD_EVENT + id, term);
|
this.emit(COMPLETE_WORD_EVENT + id, term, matchedPretext);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSuggestionBox(id) {
|
registerSuggestionBox(id) {
|
||||||
this.suggestions.set(id, {
|
this.suggestions.set(id, {
|
||||||
pretext: '',
|
pretext: '',
|
||||||
matchedPretext: '',
|
matchedPretext: [],
|
||||||
terms: [],
|
terms: [],
|
||||||
items: [],
|
items: [],
|
||||||
components: [],
|
components: [],
|
||||||
@@ -89,7 +89,7 @@ class SuggestionStore extends EventEmitter {
|
|||||||
clearSuggestions(id) {
|
clearSuggestions(id) {
|
||||||
const suggestion = this.suggestions.get(id);
|
const suggestion = this.suggestions.get(id);
|
||||||
|
|
||||||
suggestion.matchedPretext = '';
|
suggestion.matchedPretext = [];
|
||||||
suggestion.terms = [];
|
suggestion.terms = [];
|
||||||
suggestion.items = [];
|
suggestion.items = [];
|
||||||
suggestion.components = [];
|
suggestion.components = [];
|
||||||
@@ -111,21 +111,16 @@ class SuggestionStore extends EventEmitter {
|
|||||||
suggestion.pretext = pretext;
|
suggestion.pretext = pretext;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMatchedPretext(id, matchedPretext) {
|
addSuggestion(id, term, item, component, matchedPretext) {
|
||||||
const suggestion = this.suggestions.get(id);
|
|
||||||
|
|
||||||
suggestion.matchedPretext = matchedPretext;
|
|
||||||
}
|
|
||||||
|
|
||||||
addSuggestion(id, term, item, component) {
|
|
||||||
const suggestion = this.suggestions.get(id);
|
const suggestion = this.suggestions.get(id);
|
||||||
|
|
||||||
suggestion.terms.push(term);
|
suggestion.terms.push(term);
|
||||||
suggestion.items.push(item);
|
suggestion.items.push(item);
|
||||||
suggestion.components.push(component);
|
suggestion.components.push(component);
|
||||||
|
suggestion.matchedPretext.push(matchedPretext);
|
||||||
}
|
}
|
||||||
|
|
||||||
addSuggestions(id, terms, items, component) {
|
addSuggestions(id, terms, items, component, matchedPretext) {
|
||||||
const suggestion = this.suggestions.get(id);
|
const suggestion = this.suggestions.get(id);
|
||||||
|
|
||||||
suggestion.terms.push(...terms);
|
suggestion.terms.push(...terms);
|
||||||
@@ -133,6 +128,7 @@ class SuggestionStore extends EventEmitter {
|
|||||||
|
|
||||||
for (let i = 0; i < terms.length; i++) {
|
for (let i = 0; i < terms.length; i++) {
|
||||||
suggestion.components.push(component);
|
suggestion.components.push(component);
|
||||||
|
suggestion.matchedPretext.push(matchedPretext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +156,16 @@ class SuggestionStore extends EventEmitter {
|
|||||||
return this.suggestions.get(id).pretext;
|
return this.suggestions.get(id).pretext;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMatchedPretext(id) {
|
getSelectedMatchedPretext(id) {
|
||||||
return this.suggestions.get(id).matchedPretext;
|
const suggestion = this.suggestions.get(id);
|
||||||
|
|
||||||
|
for (let i = 0; i < suggestion.terms.length; i++) {
|
||||||
|
if (suggestion.terms[i] === suggestion.selection) {
|
||||||
|
return suggestion.matchedPretext[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getItems(id) {
|
getItems(id) {
|
||||||
@@ -176,6 +180,10 @@ class SuggestionStore extends EventEmitter {
|
|||||||
return this.suggestions.get(id).components;
|
return this.suggestions.get(id).components;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSuggestions(id) {
|
||||||
|
return this.suggestions.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
getSelection(id) {
|
getSelection(id) {
|
||||||
return this.suggestions.get(id).selection;
|
return this.suggestions.get(id).selection;
|
||||||
}
|
}
|
||||||
@@ -223,15 +231,11 @@ class SuggestionStore extends EventEmitter {
|
|||||||
this.emitSuggestionsChanged(id);
|
this.emitSuggestionsChanged(id);
|
||||||
break;
|
break;
|
||||||
case ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS:
|
case ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS:
|
||||||
if (this.getMatchedPretext(id) === '') {
|
|
||||||
this.setMatchedPretext(id, other.matchedPretext);
|
|
||||||
|
|
||||||
// ensure the matched pretext hasn't changed so that we don't receive suggestions for outdated pretext
|
// 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.addSuggestions(id, other.terms, other.items, other.component, other.matchedPretext);
|
||||||
|
|
||||||
this.ensureSelectionExists(id);
|
this.ensureSelectionExists(id);
|
||||||
this.emitSuggestionsChanged(id);
|
this.emitSuggestionsChanged(id);
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS:
|
case ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS:
|
||||||
this.clearSuggestions(id);
|
this.clearSuggestions(id);
|
||||||
@@ -247,7 +251,7 @@ class SuggestionStore extends EventEmitter {
|
|||||||
this.emitSuggestionsChanged(id);
|
this.emitSuggestionsChanged(id);
|
||||||
break;
|
break;
|
||||||
case ActionTypes.SUGGESTION_COMPLETE_WORD:
|
case ActionTypes.SUGGESTION_COMPLETE_WORD:
|
||||||
this.emitCompleteWord(id, other.term || this.getSelection(id), this.getMatchedPretext(id));
|
this.emitCompleteWord(id, other.term || this.getSelection(id), other.matchedPretext || this.getSelectedMatchedPretext(id));
|
||||||
|
|
||||||
this.setPretext(id, '');
|
this.setPretext(id, '');
|
||||||
this.clearSuggestions(id);
|
this.clearSuggestions(id);
|
||||||
|
|||||||
20
webapp/tests/suggestion_box.test.jsx
Обычный файл
20
webapp/tests/suggestion_box.test.jsx
Обычный файл
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
|
import SuggestionBox from 'components/suggestion/suggestion_box.jsx';
|
||||||
|
|
||||||
|
describe('SuggestionBox', function() {
|
||||||
|
it('findOverlap', function(done) {
|
||||||
|
assert.equal(SuggestionBox.findOverlap('', 'blue'), '');
|
||||||
|
assert.equal(SuggestionBox.findOverlap('red', ''), '');
|
||||||
|
assert.equal(SuggestionBox.findOverlap('red', 'blue'), '');
|
||||||
|
assert.equal(SuggestionBox.findOverlap('red', 'dog'), 'd');
|
||||||
|
assert.equal(SuggestionBox.findOverlap('red', 'education'), 'ed');
|
||||||
|
assert.equal(SuggestionBox.findOverlap('red', 'reduce'), 'red');
|
||||||
|
assert.equal(SuggestionBox.findOverlap('black', 'ack'), 'ack');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
Ссылка в новой задаче
Block a user