feat: implement autocomplete replacement

This commit is contained in:
Aviral Dasgupta 2016-07-03 22:15:13 +05:30
parent 8961c87cf9
commit cccc58b47f
13 changed files with 271 additions and 121 deletions

View file

@ -1,15 +1,23 @@
import React from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import classNames from 'classnames';
import _ from 'lodash';
import {getCompletions} from '../../../autocomplete/Autocompleter';
export default class Autocomplete extends React.Component {
constructor(props) {
super(props);
this.onConfirm = this.onConfirm.bind(this);
this.state = {
// list of completionResults, each containing completions
completions: [],
// array of completions, so we can look up current selection by offset quickly
completionList: [],
// how far down the completion list we are
selectionOffset: 0,
};
@ -31,8 +39,10 @@ export default class Autocomplete extends React.Component {
let newCompletions = Object.assign([], this.state.completions);
completionResult.completions = completions;
newCompletions[i] = completionResult;
this.setState({
completions: newCompletions,
completionList: _.flatMap(newCompletions, provider => provider.completions),
});
}, err => {
console.error(err);
@ -54,7 +64,7 @@ export default class Autocomplete extends React.Component {
onUpArrow(): boolean {
let completionCount = this.countCompletions(),
selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount;
this.setState({selectionOffset});
this.setSelection(selectionOffset);
return true;
}
@ -62,34 +72,49 @@ export default class Autocomplete extends React.Component {
onDownArrow(): boolean {
let completionCount = this.countCompletions(),
selectionOffset = (this.state.selectionOffset + 1) % completionCount;
this.setState({selectionOffset});
this.setSelection(selectionOffset);
return true;
}
/** called from MessageComposerInput
* @returns {boolean} whether confirmation was handled
*/
onConfirm(): boolean {
if (this.countCompletions() === 0)
return false;
let selectedCompletion = this.state.completionList[this.state.selectionOffset];
this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion);
return true;
}
setSelection(selectionOffset: number) {
this.setState({selectionOffset});
}
render() {
let position = 0;
let renderedCompletions = this.state.completions.map((completionResult, i) => {
let completions = completionResult.completions.map((completion, i) => {
let Component = completion.component;
let className = classNames('mx_Autocomplete_Completion', {
'selected': position === this.state.selectionOffset,
});
let componentPosition = position;
position++;
if (Component) {
return Component;
}
let onMouseOver = () => this.setState({selectionOffset: componentPosition});
let onMouseOver = () => this.setSelection(componentPosition),
onClick = () => {
this.setSelection(componentPosition);
this.onConfirm();
};
return (
<div key={i}
className={className}
onMouseOver={onMouseOver}>
<span style={{fontWeight: 600}}>{completion.title}</span>
<span>{completion.subtitle}</span>
<span style={{flex: 1}} />
<span style={{color: 'gray'}}>{completion.description}</span>
onMouseOver={onMouseOver}
onClick={onClick}>
{completion.component}
</div>
);
});

View file

@ -40,16 +40,17 @@ export default class MessageComposer extends React.Component {
this.state = {
autocompleteQuery: '',
selection: null
selection: null,
};
}
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't upload files. Please register to upload."
description: "Guest users can't upload files. Please register to upload.",
});
return;
}
@ -58,13 +59,13 @@ export default class MessageComposer extends React.Component {
}
onUploadFileSelected(ev) {
var files = ev.target.files;
let files = ev.target.files;
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var TintableSvg = sdk.getComponent("elements.TintableSvg");
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let TintableSvg = sdk.getComponent("elements.TintableSvg");
var fileList = [];
for(var i=0; i<files.length; i++) {
let fileList = [];
for (let i=0; i<files.length; i++) {
fileList.push(<li>
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
</li>);
@ -91,7 +92,7 @@ export default class MessageComposer extends React.Component {
}
this.refs.uploadInput.value = null;
}
},
});
}
@ -105,7 +106,7 @@ export default class MessageComposer extends React.Component {
action: 'hangup',
// hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead)
room_id: call.roomId
room_id: call.roomId,
});
}
@ -113,7 +114,7 @@ export default class MessageComposer extends React.Component {
dis.dispatch({
action: 'place_call',
type: ev.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId
room_id: this.props.room.roomId,
});
}
@ -121,14 +122,14 @@ export default class MessageComposer extends React.Component {
dis.dispatch({
action: 'place_call',
type: 'voice',
room_id: this.props.room.roomId
room_id: this.props.room.roomId,
});
}
onInputContentChanged(content: string, selection: {start: number, end: number}) {
this.setState({
autocompleteQuery: content,
selection
selection,
});
}
@ -171,11 +172,11 @@ export default class MessageComposer extends React.Component {
callButton =
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call">
<TintableSvg src="img/voice.svg" width="16" height="26"/>
</div>
</div>;
videoCallButton =
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call">
<TintableSvg src="img/call.svg" width="30" height="22"/>
</div>
</div>;
}
var canSendMessages = this.props.room.currentState.maySendMessage(
@ -198,9 +199,11 @@ export default class MessageComposer extends React.Component {
controls.push(
<MessageComposerInput
ref={c => this.messageComposerInput = c}
key="controls_input"
onResize={this.props.onResize}
room={this.props.room}
tryComplete={this.refs.autocomplete && this.refs.autocomplete.onConfirm}
onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow}
onTab={this.onTab}
@ -223,6 +226,7 @@ export default class MessageComposer extends React.Component {
<div className="mx_MessageComposer_autocomplete_wrapper">
<Autocomplete
ref="autocomplete"
onConfirm={this.messageComposerInput && this.messageComposerInput.onConfirmAutocompletion}
query={this.state.autocompleteQuery}
selection={this.state.selection} />
</div>

View file

@ -76,6 +76,7 @@ export default class MessageComposerInput extends React.Component {
this.onUpArrow = this.onUpArrow.bind(this);
this.onDownArrow = this.onDownArrow.bind(this);
this.onTab = this.onTab.bind(this);
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled');
if(isRichtextEnabled == null) {
@ -85,7 +86,7 @@ export default class MessageComposerInput extends React.Component {
this.state = {
isRichtextEnabled: isRichtextEnabled,
editorState: null
editorState: null,
};
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
@ -96,7 +97,7 @@ export default class MessageComposerInput extends React.Component {
static getKeyBinding(e: SyntheticKeyboardEvent): string {
// C-m => Toggles between rich text and markdown modes
if(e.keyCode == KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
return 'toggle-mode';
}
@ -212,7 +213,7 @@ export default class MessageComposerInput extends React.Component {
let content = convertFromRaw(JSON.parse(contentJSON));
component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content));
}
}
},
};
}
@ -234,7 +235,7 @@ export default class MessageComposerInput extends React.Component {
}
onAction(payload) {
var editor = this.refs.editor;
let editor = this.refs.editor;
switch (payload.action) {
case 'focus_composer':
@ -252,7 +253,7 @@ export default class MessageComposerInput extends React.Component {
payload.displayname
);
this.setState({
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters')
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
});
editor.focus();
}
@ -356,7 +357,7 @@ export default class MessageComposerInput extends React.Component {
if(this.props.onContentChanged) {
this.props.onContentChanged(editorState.getCurrentContent().getPlainText(),
RichText.getTextSelectionOffsets(editorState.getSelection(),
RichText.selectionStateToTextOffsets(editorState.getSelection(),
editorState.getCurrentContent().getBlocksAsArray()));
}
}
@ -418,12 +419,21 @@ export default class MessageComposerInput extends React.Component {
}
handleReturn(ev) {
if(ev.shiftKey)
if (ev.shiftKey) {
return false;
}
if(this.props.tryComplete) {
if(this.props.tryComplete()) {
return true;
}
}
const contentState = this.state.editorState.getCurrentContent();
if(!contentState.hasText())
if (!contentState.hasText()) {
return true;
}
let contentText = contentState.getPlainText(), contentHTML;
@ -509,17 +519,32 @@ export default class MessageComposerInput extends React.Component {
}
onTab(e) {
if(this.props.onTab) {
if(this.props.onTab()) {
if (this.props.onTab) {
if (this.props.onTab()) {
e.preventDefault();
}
}
}
onConfirmAutocompletion(range, content: string) {
let contentState = Modifier.replaceText(
this.state.editorState.getCurrentContent(),
RichText.textOffsetsToSelectionState(range, this.state.editorState.getCurrentContent().getBlocksAsArray()),
content
);
this.setState({
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
});
// for some reason, doing this right away does not update the editor :(
setTimeout(() => this.refs.editor.focus(), 50);
}
render() {
let className = "mx_MessageComposer_input";
if(this.state.isRichtextEnabled) {
if (this.state.isRichtextEnabled) {
className += " mx_MessageComposer_input_rte"; // placeholder indicator for RTE mode
}