Files
ThreadNet-Web/src/components/views/rooms/Autocomplete.js
T

289 lines
9.8 KiB
JavaScript
Raw Normal View History

2016-06-01 16:54:21 +05:30
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
2016-06-21 18:33:39 +05:30
import classNames from 'classnames';
2016-07-04 21:56:09 +05:30
import flatMap from 'lodash/flatMap';
2016-09-13 15:41:52 +05:30
import isEqual from 'lodash/isEqual';
import sdk from '../../../index';
2017-06-02 21:35:55 +01:00
import type {Completion} from '../../../autocomplete/Autocompleter';
2017-07-12 13:58:14 +01:00
import Promise from 'bluebird';
2017-02-20 19:26:40 +05:30
import UserSettingsStore from '../../../UserSettingsStore';
2016-06-01 16:54:21 +05:30
import Autocompleter from '../../../autocomplete/Autocompleter';
2016-06-01 16:54:21 +05:30
2016-09-13 15:41:52 +05:30
const COMPOSER_SELECTED = 0;
2016-06-01 16:54:21 +05:30
export default class Autocomplete extends React.Component {
2016-09-13 15:41:52 +05:30
2016-06-01 16:54:21 +05:30
constructor(props) {
super(props);
2016-07-03 22:15:13 +05:30
this.autocompleter = new Autocompleter(props.room);
2016-09-13 16:46:20 +05:30
this.completionPromise = null;
this.hide = this.hide.bind(this);
2016-09-21 07:40:48 +05:30
this.onCompletionClicked = this.onCompletionClicked.bind(this);
2016-07-03 22:15:13 +05:30
2016-06-01 16:54:21 +05:30
this.state = {
2016-07-03 22:15:13 +05:30
// list of completionResults, each containing completions
completions: [],
2016-07-03 22:15:13 +05:30
// array of completions, so we can look up current selection by offset quickly
completionList: [],
2016-09-13 15:41:52 +05:30
// how far down the completion list we are (THIS IS 1-INDEXED!)
selectionOffset: COMPOSER_SELECTED,
// whether we should show completions if they're available
shouldShowCompletions: true,
hide: false,
forceComplete: false,
2016-06-01 16:54:21 +05:30
};
}
componentWillReceiveProps(newProps, state) {
if (this.props.room.roomId !== newProps.room.roomId) {
this.autocompleter.destroy();
this.autocompleter = new Autocompleter();
}
// Query hasn't changed so don't try to complete it
if (newProps.query === this.props.query) {
2016-07-03 01:11:34 +05:30
return;
}
this.complete(newProps.query, newProps.selection);
}
componentWillUnmount() {
this.autocompleter.destroy();
}
complete(query, selection) {
this.queryRequested = query;
if (this.debounceCompletionsRequest) {
clearTimeout(this.debounceCompletionsRequest);
}
if (query === "") {
this.setState({
// Clear displayed completions
completions: [],
completionList: [],
// Reset selected completion
selectionOffset: COMPOSER_SELECTED,
// Hide the autocomplete box
hide: true,
});
2017-07-12 14:02:00 +01:00
return Promise.resolve(null);
}
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
// Don't debounce if we are already showing completions
if (this.state.completions.length > 0 || this.state.forceComplete) {
autocompleteDelay = 0;
}
2017-07-12 14:04:20 +01:00
const deferred = Promise.defer();
this.debounceCompletionsRequest = setTimeout(() => {
this.processQuery(query, selection).then(() => {
deferred.resolve();
});
}, autocompleteDelay);
return deferred.promise;
}
processQuery(query, selection) {
return this.autocompleter.getCompletions(
query, selection, this.state.forceComplete,
).then((completions) => {
// Only ever process the completions for the most recent query being processed
if (query !== this.queryRequested) {
return;
}
this.processCompletions(completions);
});
}
processCompletions(completions) {
const completionList = flatMap(completions, (provider) => provider.completions);
2016-09-13 15:41:52 +05:30
// Reset selection when completion list becomes empty.
let selectionOffset = COMPOSER_SELECTED;
if (completionList.length > 0) {
/* If the currently selected completion is still in the completion list,
try to find it and jump to it. If not, select composer.
*/
const currentSelection = this.state.selectionOffset === 0 ? null :
this.state.completionList[this.state.selectionOffset - 1].completion;
selectionOffset = completionList.findIndex(
(completion) => completion.completion === currentSelection);
2016-09-13 15:41:52 +05:30
if (selectionOffset === -1) {
selectionOffset = COMPOSER_SELECTED;
} else {
selectionOffset++; // selectionOffset is 1-indexed!
}
2016-09-13 15:41:52 +05:30
}
let hide = this.state.hide;
// If `completion.command.command` is truthy, then a provider has matched with the query
const anyMatches = completions.some((completion) => !!completion.command.command);
hide = !anyMatches;
2016-09-13 15:41:52 +05:30
this.setState({
completions,
completionList,
selectionOffset,
hide,
// Force complete is turned off each time since we can't edit the query in that case
forceComplete: false,
2016-06-01 16:54:21 +05:30
});
}
2016-07-03 01:11:34 +05:30
countCompletions(): number {
2016-09-13 15:41:52 +05:30
return this.state.completionList.length;
2016-07-03 01:11:34 +05:30
}
// called from MessageComposerInput
2016-09-13 15:41:52 +05:30
onUpArrow(): ?Completion {
const completionCount = this.countCompletions();
// completionCount + 1, since 0 means composer is selected
const selectionOffset = (completionCount + 1 + this.state.selectionOffset - 1)
% (completionCount + 1);
if (!completionCount) {
2016-09-13 15:41:52 +05:30
return null;
}
2016-07-03 22:15:13 +05:30
this.setSelection(selectionOffset);
2016-06-21 18:33:39 +05:30
}
2016-07-03 01:11:34 +05:30
// called from MessageComposerInput
2016-09-13 15:41:52 +05:30
onDownArrow(): ?Completion {
const completionCount = this.countCompletions();
// completionCount + 1, since 0 means composer is selected
const selectionOffset = (this.state.selectionOffset + 1) % (completionCount + 1);
if (!completionCount) {
2016-09-13 15:41:52 +05:30
return null;
}
2016-07-03 22:15:13 +05:30
this.setSelection(selectionOffset);
2016-09-13 15:41:52 +05:30
}
onEscape(e): boolean {
const completionCount = this.countCompletions();
if (completionCount === 0) {
// autocomplete is already empty, so don't preventDefault
return;
}
e.preventDefault();
// selectionOffset = 0, so we don't end up completing when autocomplete is hidden
this.hide();
}
hide() {
this.setState({hide: true, selectionOffset: 0, completions: [], completionList: []});
2016-09-13 15:41:52 +05:30
}
forceComplete() {
2017-07-12 14:04:20 +01:00
const done = Promise.defer();
2016-09-13 15:41:52 +05:30
this.setState({
forceComplete: true,
2017-02-10 02:06:06 +05:30
hide: false,
2016-09-13 15:41:52 +05:30
}, () => {
this.complete(this.props.query, this.props.selection).then(() => {
done.resolve(this.countCompletions());
});
2016-09-13 15:41:52 +05:30
});
return done.promise;
2016-06-21 18:33:39 +05:30
}
2016-09-21 07:40:48 +05:30
onCompletionClicked(): boolean {
2016-09-13 15:41:52 +05:30
if (this.countCompletions() === 0 || this.state.selectionOffset === COMPOSER_SELECTED) {
2016-07-03 22:15:13 +05:30
return false;
2016-07-04 21:44:35 +05:30
}
2016-07-03 22:15:13 +05:30
2016-09-21 07:40:48 +05:30
this.props.onConfirm(this.state.completionList[this.state.selectionOffset - 1]);
this.hide();
2016-07-03 22:15:13 +05:30
return true;
}
setSelection(selectionOffset: number) {
this.setState({selectionOffset, hide: false});
2017-08-23 16:22:14 +01:00
if (this.props.onSelectionChange) {
this.props.onSelectionChange(this.state.completionList[selectionOffset - 1]);
}
2016-07-03 22:15:13 +05:30
}
componentDidUpdate() {
// this is the selected completion, so scroll it into view if needed
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`];
if (selectedCompletion && this.container) {
2016-08-23 00:36:31 +05:30
const domNode = ReactDOM.findDOMNode(selectedCompletion);
const offsetTop = domNode && domNode.offsetTop;
if (offsetTop > this.container.scrollTop + this.container.offsetHeight ||
offsetTop < this.container.scrollTop) {
this.container.scrollTop = offsetTop - this.container.offsetTop;
}
}
}
2017-02-10 02:06:06 +05:30
setState(state, func) {
super.setState(state, func);
}
2016-06-01 16:54:21 +05:30
render() {
const EmojiText = sdk.getComponent('views.elements.EmojiText');
2016-09-13 15:41:52 +05:30
let position = 1;
const renderedCompletions = this.state.completions.map((completionResult, i) => {
const completions = completionResult.completions.map((completion, i) => {
const className = classNames('mx_Autocomplete_Completion', {
2016-07-03 01:11:34 +05:30
'selected': position === this.state.selectionOffset,
2016-06-21 18:33:39 +05:30
});
const componentPosition = position;
2016-06-21 18:33:39 +05:30
position++;
2016-07-03 01:11:34 +05:30
2017-10-12 11:31:31 +01:00
const onMouseMove = () => this.setSelection(componentPosition);
const onClick = () => {
2016-07-04 21:44:35 +05:30
this.setSelection(componentPosition);
2016-09-21 07:40:48 +05:30
this.onCompletionClicked();
2016-07-04 21:44:35 +05:30
};
2016-07-03 22:15:13 +05:30
return React.cloneElement(completion.component, {
key: i,
2016-09-13 15:41:52 +05:30
ref: `completion${position - 1}`,
className,
2017-10-12 11:31:31 +01:00
onMouseMove,
onClick,
});
});
return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection">
2017-10-11 17:56:17 +01:00
<EmojiText element="div" className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</EmojiText>
{ completionResult.provider.renderCompletions(completions) }
2016-06-01 16:54:21 +05:30
</div>
) : null;
}).filter((completion) => !!completion);
2016-06-01 16:54:21 +05:30
2016-09-13 15:41:52 +05:30
return !this.state.hide && renderedCompletions.length > 0 ? (
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
2017-10-11 17:56:17 +01:00
{ renderedCompletions }
2016-06-01 16:54:21 +05:30
</div>
) : null;
2016-06-01 16:54:21 +05:30
}
}
Autocomplete.propTypes = {
// the query string for which to show autocomplete suggestions
query: PropTypes.string.isRequired,
2016-07-04 21:44:35 +05:30
// method invoked with range and text content when completion is confirmed
onConfirm: PropTypes.func.isRequired,
// The room in which we're autocompleting
room: PropTypes.object,
2016-06-01 16:54:21 +05:30
};