Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit b979a16

Browse files
committed
initial version of autocomplete
1 parent a145ab7 commit b979a16

File tree

7 files changed

+164
-1
lines changed

7 files changed

+164
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"favico.js": "^0.3.10",
2727
"filesize": "^3.1.2",
2828
"flux": "^2.0.3",
29+
"fuse.js": "^2.2.0",
2930
"glob": "^5.0.14",
3031
"highlight.js": "^8.9.1",
3132
"linkifyjs": "^2.0.0-beta.4",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default class AutocompleteProvider {
2+
3+
}

src/autocomplete/Autocompleter.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import CommandProvider from './CommandProvider';
2+
3+
const COMPLETERS = [CommandProvider].map(completer => new completer());
4+
5+
export function getCompletions(query: String) {
6+
return COMPLETERS.map(completer => completer.getCompletions(query));
7+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import AutocompleteProvider from './AutocompleteProvider';
2+
import Q from 'q';
3+
import Fuse from 'fuse.js';
4+
5+
const COMMANDS = [
6+
{
7+
command: '/me',
8+
args: '<message>',
9+
description: 'Displays action'
10+
},
11+
{
12+
command: '/ban',
13+
args: '<user-id> [reason]',
14+
description: 'Bans user with given id'
15+
},
16+
{
17+
command: '/deop'
18+
},
19+
{
20+
command: '/encrypt'
21+
},
22+
{
23+
command: '/invite'
24+
},
25+
{
26+
command: '/join',
27+
args: '<room-alias>',
28+
description: 'Joins room with given alias'
29+
},
30+
{
31+
command: '/kick',
32+
args: '<user-id> [reason]',
33+
description: 'Kicks user with given id'
34+
},
35+
{
36+
command: '/nick',
37+
args: '<display-name>',
38+
description: 'Changes your display nickname'
39+
}
40+
];
41+
42+
export default class CommandProvider extends AutocompleteProvider {
43+
constructor() {
44+
super();
45+
this.fuse = new Fuse(COMMANDS, {
46+
keys: ['command', 'args', 'description']
47+
});
48+
}
49+
50+
getCompletions(query: String) {
51+
let completions = [];
52+
const matches = query.match(/(^\/\w+)/);
53+
if(!!matches) {
54+
const command = matches[0];
55+
completions = this.fuse.search(command).map(result => {
56+
return {
57+
title: result.command,
58+
subtitle: result.args,
59+
description: result.description
60+
};
61+
});
62+
}
63+
return Q.when(completions);
64+
}
65+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
3+
import {getCompletions} from '../../../autocomplete/Autocompleter';
4+
5+
export default class Autocomplete extends React.Component {
6+
constructor(props) {
7+
super(props);
8+
this.state = {
9+
completions: []
10+
};
11+
}
12+
13+
componentWillReceiveProps(props, state) {
14+
getCompletions(props.query)[0].then(completions => {
15+
console.log(completions);
16+
this.setState({
17+
completions
18+
});
19+
});
20+
}
21+
22+
render() {
23+
const pinElement = document.querySelector(this.props.pinSelector);
24+
if(!pinElement) return null;
25+
26+
const position = pinElement.getBoundingClientRect();
27+
28+
const style = {
29+
position: 'fixed',
30+
border: '1px solid gray',
31+
background: 'white',
32+
borderRadius: '4px'
33+
};
34+
35+
this.props.pinTo.forEach(direction => {
36+
console.log(`${direction} = ${position[direction]}`);
37+
style[direction] = position[direction];
38+
});
39+
40+
const renderedCompletions = this.state.completions.map((completion, i) => {
41+
return (
42+
<div key={i} class="mx_Autocomplete_Completion">
43+
<strong>{completion.title}</strong>
44+
<em>{completion.subtitle}</em>
45+
<span style={{color: 'gray', float: 'right'}}>{completion.description}</span>
46+
</div>
47+
);
48+
});
49+
50+
return (
51+
<div className="mx_Autocomplete" style={style}>
52+
{renderedCompletions}
53+
</div>
54+
);
55+
}
56+
}
57+
58+
Autocomplete.propTypes = {
59+
// the query string for which to show autocomplete suggestions
60+
query: React.PropTypes.string.isRequired,
61+
62+
// CSS selector indicating which element to pin the autocomplete to
63+
pinSelector: React.PropTypes.string.isRequired,
64+
65+
// attributes on which the autocomplete should match the pinElement
66+
pinTo: React.PropTypes.array.isRequired
67+
};

src/components/views/rooms/MessageComposer.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
2020
var Modal = require('../../../Modal');
2121
var sdk = require('../../../index');
2222
var dis = require('../../../dispatcher');
23+
import Autocomplete from './Autocomplete';
2324

2425

2526
module.exports = React.createClass({
@@ -45,6 +46,12 @@ module.exports = React.createClass({
4546
opacity: React.PropTypes.number,
4647
},
4748

49+
getInitialState: function () {
50+
return {
51+
autocompleteQuery: ''
52+
};
53+
},
54+
4855
onUploadClick: function(ev) {
4956
this.refs.uploadInput.click();
5057
},
@@ -117,6 +124,12 @@ module.exports = React.createClass({
117124
});
118125
},
119126

127+
onInputContentChanged(content: String) {
128+
this.setState({
129+
autocompleteQuery: content
130+
})
131+
},
132+
120133
render: function() {
121134
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
122135
var uploadInputStyle = {display: 'none'};
@@ -170,7 +183,8 @@ module.exports = React.createClass({
170183

171184
controls.push(
172185
<MessageComposerInput key="controls_input" tabComplete={this.props.tabComplete}
173-
onResize={this.props.onResize} room={this.props.room} />,
186+
onResize={this.props.onResize} room={this.props.room}
187+
onContentChanged={(content) => this.onInputContentChanged(content) } />,
174188
uploadButton,
175189
hangupButton,
176190
callButton,
@@ -191,6 +205,8 @@ module.exports = React.createClass({
191205
{controls}
192206
</div>
193207
</div>
208+
209+
<Autocomplete query={this.state.autocompleteQuery} pinSelector=".mx_RoomView_statusArea" pinTo={['top', 'left', 'width']} />
194210
</div>
195211
);
196212
}

src/components/views/rooms/MessageComposerInput.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ module.exports = React.createClass({
7373

7474
// js-sdk Room object
7575
room: React.PropTypes.object.isRequired,
76+
77+
onContentChanged: React.PropTypes.func
7678
},
7779

7880
componentWillMount: function() {
@@ -276,6 +278,8 @@ module.exports = React.createClass({
276278
{
277279
this.resizeInput();
278280
}
281+
282+
this.props.onContentChanged && this.props.onContentChanged(this.refs.textarea.value);
279283
},
280284

281285
onEnter: function(ev) {

0 commit comments

Comments
 (0)