Files
mattermost/web/react/stores/suggestion_store.jsx

273 lines
8.7 KiB
JavaScript

// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Constants from '../utils/constants.jsx';
import EventEmitter from 'events';
const ActionTypes = Constants.ActionTypes;
const COMPLETE_WORD_EVENT = 'complete_word';
const PRETEXT_CHANGED_EVENT = 'pretext_changed';
const SUGGESTIONS_CHANGED_EVENT = 'suggestions_changed';
class SuggestionStore extends EventEmitter {
constructor() {
super();
this.addSuggestionsChangedListener = this.addSuggestionsChangedListener.bind(this);
this.removeSuggestionsChangedListener = this.removeSuggestionsChangedListener.bind(this);
this.emitSuggestionsChanged = this.emitSuggestionsChanged.bind(this);
this.addPretextChangedListener = this.addPretextChangedListener.bind(this);
this.removePretextChangedListener = this.removePretextChangedListener.bind(this);
this.emitPretextChanged = this.emitPretextChanged.bind(this);
this.addCompleteWordListener = this.addCompleteWordListener.bind(this);
this.removeCompleteWordListener = this.removeCompleteWordListener.bind(this);
this.emitCompleteWord = this.emitCompleteWord.bind(this);
this.handleEventPayload = this.handleEventPayload.bind(this);
this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
// this.suggestions stores the state of all SuggestionBoxes by mapping their unique identifier to an
// object with the following fields:
// pretext: the text before the cursor
// matchedPretext: the text before the cursor that will be replaced if an autocomplete term is selected
// 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
// components: a list of react components that can be used to render their corresponding item
// selection: the term currently selected by the keyboard
// completeOnSpace: whether or not space will trigger the term to be autocompleted
this.suggestions = new Map();
}
addSuggestionsChangedListener(id, callback) {
this.on(SUGGESTIONS_CHANGED_EVENT + id, callback);
}
removeSuggestionsChangedListener(id, callback) {
this.removeListener(SUGGESTIONS_CHANGED_EVENT + id, callback);
}
emitSuggestionsChanged(id) {
this.emit(SUGGESTIONS_CHANGED_EVENT + id);
}
addPretextChangedListener(id, callback) {
this.on(PRETEXT_CHANGED_EVENT + id, callback);
}
removePretextChangedListener(id, callback) {
this.removeListener(PRETEXT_CHANGED_EVENT + id, callback);
}
emitPretextChanged(id, pretext) {
this.emit(PRETEXT_CHANGED_EVENT + id, pretext);
}
addCompleteWordListener(id, callback) {
this.on(COMPLETE_WORD_EVENT + id, callback);
}
removeCompleteWordListener(id, callback) {
this.removeListener(COMPLETE_WORD_EVENT + id, callback);
}
emitCompleteWord(id, term) {
this.emit(COMPLETE_WORD_EVENT + id, term);
}
registerSuggestionBox(id) {
this.suggestions.set(id, {
pretext: '',
matchedPretext: '',
terms: [],
items: [],
components: [],
selection: '',
completeOnSpace: true
});
}
unregisterSuggestionBox(id) {
this.suggestions.delete(id);
}
clearSuggestions(id) {
const suggestion = this.suggestions.get(id);
suggestion.matchedPretext = '';
suggestion.terms = [];
suggestion.items = [];
suggestion.components = [];
suggestion.completeOnSpace = true;
}
clearSelection(id) {
const suggestion = this.suggestions.get(id);
suggestion.selection = '';
}
hasSuggestions(id) {
return this.suggestions.get(id).terms.length > 0;
}
setPretext(id, pretext) {
const suggestion = this.suggestions.get(id);
suggestion.pretext = pretext;
}
setMatchedPretext(id, matchedPretext) {
const suggestion = this.suggestions.get(id);
suggestion.matchedPretext = matchedPretext;
}
setCompleteOnSpace(id, completeOnSpace) {
const suggestion = this.suggestions.get(id);
suggestion.completeOnSpace = completeOnSpace;
}
addSuggestion(id, term, item, component) {
const suggestion = this.suggestions.get(id);
suggestion.terms.push(term);
suggestion.items.push(item);
suggestion.components.push(component);
}
addSuggestions(id, terms, items, component) {
const suggestion = this.suggestions.get(id);
suggestion.terms.push(...terms);
suggestion.items.push(...items);
for (let i = 0; i < terms.length; i++) {
suggestion.components.push(component);
}
}
// make sure that if suggestions exist, then one of them is selected. return true if the selection changes.
ensureSelectionExists(id) {
const suggestion = this.suggestions.get(id);
if (suggestion.terms.length > 0) {
// if the current selection is no longer in the map, select the first term in the list
if (!suggestion.selection || suggestion.terms.indexOf(suggestion.selection) === -1) {
suggestion.selection = suggestion.terms[0];
return true;
}
} else if (suggestion.selection) {
suggestion.selection = '';
return true;
}
return false;
}
getPretext(id) {
return this.suggestions.get(id).pretext;
}
getMatchedPretext(id) {
return this.suggestions.get(id).matchedPretext;
}
getItems(id) {
return this.suggestions.get(id).items;
}
getTerms(id) {
return this.suggestions.get(id).terms;
}
getComponents(id) {
return this.suggestions.get(id).components;
}
getSelection(id) {
return this.suggestions.get(id).selection;
}
shouldCompleteOnSpace(id) {
return this.suggestions.get(id).completeOnSpace;
}
selectNext(id) {
this.setSelectionByDelta(id, 1);
}
selectPrevious(id) {
this.setSelectionByDelta(id, -1);
}
setSelectionByDelta(id, delta) {
const suggestion = this.suggestions.get(id);
let selectionIndex = suggestion.terms.indexOf(suggestion.selection);
if (selectionIndex === -1) {
// this should never happen since selection should always be in terms
throw new Error('selection is not in terms');
}
selectionIndex += delta;
if (selectionIndex < 0) {
selectionIndex = 0;
} else if (selectionIndex > suggestion.terms.length - 1) {
selectionIndex = suggestion.terms.length - 1;
}
suggestion.selection = suggestion.terms[selectionIndex];
}
handleEventPayload(payload) {
const {type, id, ...other} = payload.action; // eslint-disable-line no-redeclare
switch (type) {
case ActionTypes.SUGGESTION_PRETEXT_CHANGED:
this.clearSuggestions(id);
this.setPretext(id, other.pretext);
this.emitPretextChanged(id, other.pretext);
this.ensureSelectionExists(id);
this.emitSuggestionsChanged(id);
break;
case ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS:
if (other.matchedPretext === this.getMatchedPretext(id)) {
// ensure the matched pretext hasn't changed so that we don't receive suggestions for outdated pretext
this.addSuggestions(id, other.terms, other.items, other.component);
this.ensureSelectionExists(id);
this.emitSuggestionsChanged(id);
}
break;
case ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS:
this.clearSuggestions(id);
this.clearSelection(id);
this.emitSuggestionsChanged(id);
break;
case ActionTypes.SUGGESTION_SELECT_NEXT:
this.selectNext(id);
this.emitSuggestionsChanged(id);
break;
case ActionTypes.SUGGESTION_SELECT_PREVIOUS:
this.selectPrevious(id);
this.emitSuggestionsChanged(id);
break;
case ActionTypes.SUGGESTION_COMPLETE_WORD:
this.emitCompleteWord(id, other.term || this.getSelection(id), this.getMatchedPretext(id));
this.setPretext(id, '');
this.clearSuggestions(id);
this.clearSelection(id);
this.emitSuggestionsChanged(id);
break;
}
}
}
export default new SuggestionStore();