diff --git a/package.json b/package.json index 3f4a862f6f..d46e9b2621 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "favico.js": "^0.3.10", "filesize": "^3.1.2", "flux": "^2.0.3", + "fuse.js": "^2.2.0", "glob": "^5.0.14", "highlight.js": "^8.9.1", "linkifyjs": "^2.0.0-beta.4", diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js new file mode 100644 index 0000000000..3b2aae920b --- /dev/null +++ b/src/autocomplete/AutocompleteProvider.js @@ -0,0 +1,3 @@ +export default class AutocompleteProvider { + +} diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js new file mode 100644 index 0000000000..e49dbb7ad6 --- /dev/null +++ b/src/autocomplete/Autocompleter.js @@ -0,0 +1,7 @@ +import CommandProvider from './CommandProvider'; + +const COMPLETERS = [CommandProvider].map(completer => new completer()); + +export function getCompletions(query: String) { + return COMPLETERS.map(completer => completer.getCompletions(query)); +} diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js new file mode 100644 index 0000000000..cc95d96fd3 --- /dev/null +++ b/src/autocomplete/CommandProvider.js @@ -0,0 +1,65 @@ +import AutocompleteProvider from './AutocompleteProvider'; +import Q from 'q'; +import Fuse from 'fuse.js'; + +const COMMANDS = [ + { + command: '/me', + args: '', + description: 'Displays action' + }, + { + command: '/ban', + args: ' [reason]', + description: 'Bans user with given id' + }, + { + command: '/deop' + }, + { + command: '/encrypt' + }, + { + command: '/invite' + }, + { + command: '/join', + args: '', + description: 'Joins room with given alias' + }, + { + command: '/kick', + args: ' [reason]', + description: 'Kicks user with given id' + }, + { + command: '/nick', + args: '', + description: 'Changes your display nickname' + } +]; + +export default class CommandProvider extends AutocompleteProvider { + constructor() { + super(); + this.fuse = new Fuse(COMMANDS, { + keys: ['command', 'args', 'description'] + }); + } + + getCompletions(query: String) { + let completions = []; + const matches = query.match(/(^\/\w+)/); + if(!!matches) { + const command = matches[0]; + completions = this.fuse.search(command).map(result => { + return { + title: result.command, + subtitle: result.args, + description: result.description + }; + }); + } + return Q.when(completions); + } +} diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js new file mode 100644 index 0000000000..80208892b0 --- /dev/null +++ b/src/components/views/rooms/Autocomplete.js @@ -0,0 +1,67 @@ +import React from 'react'; + +import {getCompletions} from '../../../autocomplete/Autocompleter'; + +export default class Autocomplete extends React.Component { + constructor(props) { + super(props); + this.state = { + completions: [] + }; + } + + componentWillReceiveProps(props, state) { + getCompletions(props.query)[0].then(completions => { + console.log(completions); + this.setState({ + completions + }); + }); + } + + render() { + const pinElement = document.querySelector(this.props.pinSelector); + if(!pinElement) return null; + + const position = pinElement.getBoundingClientRect(); + + const style = { + position: 'fixed', + border: '1px solid gray', + background: 'white', + borderRadius: '4px' + }; + + this.props.pinTo.forEach(direction => { + console.log(`${direction} = ${position[direction]}`); + style[direction] = position[direction]; + }); + + const renderedCompletions = this.state.completions.map((completion, i) => { + return ( +
+ {completion.title} + {completion.subtitle} + {completion.description} +
+ ); + }); + + return ( +
+ {renderedCompletions} +
+ ); + } +} + +Autocomplete.propTypes = { + // the query string for which to show autocomplete suggestions + query: React.PropTypes.string.isRequired, + + // CSS selector indicating which element to pin the autocomplete to + pinSelector: React.PropTypes.string.isRequired, + + // attributes on which the autocomplete should match the pinElement + pinTo: React.PropTypes.array.isRequired +}; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 20785c4c70..2d17accd45 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -20,6 +20,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg'); var Modal = require('../../../Modal'); var sdk = require('../../../index'); var dis = require('../../../dispatcher'); +import Autocomplete from './Autocomplete'; module.exports = React.createClass({ @@ -45,6 +46,12 @@ module.exports = React.createClass({ opacity: React.PropTypes.number, }, + getInitialState: function () { + return { + autocompleteQuery: '' + }; + }, + onUploadClick: function(ev) { this.refs.uploadInput.click(); }, @@ -117,6 +124,12 @@ module.exports = React.createClass({ }); }, + onInputContentChanged(content: String) { + this.setState({ + autocompleteQuery: content + }) + }, + render: function() { var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); var uploadInputStyle = {display: 'none'}; @@ -170,7 +183,8 @@ module.exports = React.createClass({ controls.push( , + onResize={this.props.onResize} room={this.props.room} + onContentChanged={(content) => this.onInputContentChanged(content) } />, uploadButton, hangupButton, callButton, @@ -191,6 +205,8 @@ module.exports = React.createClass({ {controls} + + ); } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 733d9e6056..e8db496abf 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -73,6 +73,8 @@ module.exports = React.createClass({ // js-sdk Room object room: React.PropTypes.object.isRequired, + + onContentChanged: React.PropTypes.func }, componentWillMount: function() { @@ -276,6 +278,8 @@ module.exports = React.createClass({ { this.resizeInput(); } + + this.props.onContentChanged && this.props.onContentChanged(this.refs.textarea.value); }, onEnter: function(ev) {