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

Commit fb6eec0

Browse files
committed
Hide/show autocomplete based on selection state
1 parent f6a76ed commit fb6eec0

File tree

11 files changed

+114
-43
lines changed

11 files changed

+114
-43
lines changed

src/RichText.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
convertFromHTML,
66
DefaultDraftBlockRenderMap,
77
DefaultDraftInlineStyle,
8-
CompositeDecorator
8+
CompositeDecorator,
9+
SelectionState
910
} from 'draft-js';
1011
import * as sdk from './index';
1112

@@ -168,3 +169,28 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
168169

169170
return Modifier.replaceText(contentState, rangeToReplace, modifyFn(text), inlineStyle, entityKey);
170171
}
172+
173+
/**
174+
* Computes the plaintext offsets of the given SelectionState.
175+
* Note that this inherently means we make assumptions about what that means (no separator between ContentBlocks, etc)
176+
* Used by autocomplete to show completions when the current selection lies within, or at the edges of a command.
177+
*/
178+
export function getTextSelectionOffsets(selectionState: SelectionState,
179+
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
180+
let offset = 0, start = 0, end = 0;
181+
for(let block of contentBlocks) {
182+
if (selectionState.getStartKey() == block.getKey()) {
183+
start = offset + selectionState.getStartOffset();
184+
}
185+
if (selectionState.getEndKey() == block.getKey()) {
186+
end = offset + selectionState.getEndOffset();
187+
break;
188+
}
189+
offset += block.getLength();
190+
}
191+
192+
return {
193+
start,
194+
end
195+
}
196+
}

src/autocomplete/AutocompleteProvider.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,41 @@
1+
import Q from 'q';
2+
13
export default class AutocompleteProvider {
4+
constructor(commandRegex?: RegExp, fuseOpts?: any) {
5+
if(commandRegex) {
6+
if(!commandRegex.global) {
7+
throw new Error('commandRegex must have global flag set');
8+
}
9+
this.commandRegex = commandRegex;
10+
}
11+
}
12+
13+
/**
14+
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
15+
*/
16+
getCurrentCommand(query: string, selection: {start: number, end: number}): ?Array<string> {
17+
if(this.commandRegex == null)
18+
return null;
19+
20+
let match = null;
21+
while((match = this.commandRegex.exec(query)) != null) {
22+
let matchStart = match.index,
23+
matchEnd = matchStart + match[0].length;
24+
25+
console.log(match);
26+
27+
if(selection.start <= matchEnd && selection.end >= matchStart) {
28+
return match;
29+
}
30+
}
31+
this.commandRegex.lastIndex = 0;
32+
return null;
33+
}
34+
35+
getCompletions(query: String, selection: {start: number, end: number}) {
36+
return Q.when([]);
37+
}
38+
239
getName(): string {
340
return 'Default Provider';
441
}

src/autocomplete/Autocompleter.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ const PROVIDERS = [
1212
EmojiProvider
1313
].map(completer => completer.getInstance());
1414

15-
export function getCompletions(query: String) {
15+
export function getCompletions(query: string, selection: {start: number, end: number}) {
1616
return PROVIDERS.map(provider => {
1717
return {
18-
completions: provider.getCompletions(query),
18+
completions: provider.getCompletions(query, selection),
1919
provider
2020
};
2121
});

src/autocomplete/CommandProvider.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,23 @@ const COMMANDS = [
3939
}
4040
];
4141

42+
let COMMAND_RE = /(^\/\w*)/g;
43+
4244
let instance = null;
4345

4446
export default class CommandProvider extends AutocompleteProvider {
4547
constructor() {
46-
super();
48+
super(COMMAND_RE);
4749
this.fuse = new Fuse(COMMANDS, {
4850
keys: ['command', 'args', 'description']
4951
});
5052
}
5153

52-
getCompletions(query: String) {
54+
getCompletions(query: string, selection: {start: number, end: number}) {
5355
let completions = [];
54-
const matches = query.match(/(^\/\w*)/);
55-
if(!!matches) {
56-
const command = matches[0];
57-
completions = this.fuse.search(command).map(result => {
56+
const command = this.getCurrentCommand(query, selection);
57+
if(command) {
58+
completions = this.fuse.search(command[0]).map(result => {
5859
return {
5960
title: result.command,
6061
subtitle: result.args,

src/autocomplete/DuckDuckGoProvider.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,27 @@ import AutocompleteProvider from './AutocompleteProvider';
22
import Q from 'q';
33
import 'whatwg-fetch';
44

5-
const DDG_REGEX = /\/ddg\s+(.+)$/;
5+
const DDG_REGEX = /\/ddg\s+(.+)$/g;
66
const REFERER = 'vector';
77

88
let instance = null;
99

1010
export default class DuckDuckGoProvider extends AutocompleteProvider {
11+
constructor() {
12+
super(DDG_REGEX);
13+
}
14+
1115
static getQueryUri(query: String) {
1216
return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}`
1317
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERER)}`;
1418
}
1519

16-
getCompletions(query: String) {
17-
let match = DDG_REGEX.exec(query);
18-
if(!query || !match)
20+
getCompletions(query: string, selection: {start: number, end: number}) {
21+
let command = this.getCurrentCommand(query, selection);
22+
if(!query || !command)
1923
return Q.when([]);
2024

21-
return fetch(DuckDuckGoProvider.getQueryUri(match[1]), {
25+
return fetch(DuckDuckGoProvider.getQueryUri(command[1]), {
2226
method: 'GET'
2327
})
2428
.then(response => response.json())

src/autocomplete/EmojiProvider.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@ let instance = null;
1010

1111
export default class EmojiProvider extends AutocompleteProvider {
1212
constructor() {
13-
super();
13+
super(EMOJI_REGEX);
1414
this.fuse = new Fuse(EMOJI_SHORTNAMES);
1515
}
1616

17-
getCompletions(query: String) {
17+
getCompletions(query: string, selection: {start: number, end: number}) {
1818
let completions = [];
19-
let matches = query.match(EMOJI_REGEX);
20-
let command = matches && matches[0];
19+
let command = this.getCurrentCommand(query, selection);
2120
if(command) {
22-
completions = this.fuse.search(command).map(result => {
21+
completions = this.fuse.search(command[0]).map(result => {
2322
let shortname = EMOJI_SHORTNAMES[result];
2423
let imageHTML = shortnameToImage(shortname);
2524
return {

src/autocomplete/RoomProvider.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ let instance = null;
99

1010
export default class RoomProvider extends AutocompleteProvider {
1111
constructor() {
12-
super();
12+
super(ROOM_REGEX, {
13+
keys: ['displayName', 'userId']
14+
});
1315
this.fuse = new Fuse([], {
1416
keys: ['name', 'roomId', 'aliases']
1517
});
1618
}
1719

18-
getCompletions(query: String) {
20+
getCompletions(query: string, selection: {start: number, end: number}) {
1921
let client = MatrixClientPeg.get();
2022
let completions = [];
21-
const matches = query.match(ROOM_REGEX);
22-
const command = matches && matches[0];
23+
const command = this.getCurrentCommand(query, selection);
2324
if(command) {
2425
// the only reason we need to do this is because Fuse only matches on properties
2526
this.fuse.set(client.getRooms().filter(room => !!room).map(room => {
@@ -29,7 +30,7 @@ export default class RoomProvider extends AutocompleteProvider {
2930
aliases: room.getAliases()
3031
};
3132
}));
32-
completions = this.fuse.search(command).map(room => {
33+
completions = this.fuse.search(command[0]).map(room => {
3334
return {
3435
title: room.name,
3536
subtitle: room.roomId

src/autocomplete/UserProvider.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,27 @@ import AutocompleteProvider from './AutocompleteProvider';
22
import Q from 'q';
33
import Fuse from 'fuse.js';
44

5-
const ROOM_REGEX = /@[^\s]*/g;
5+
const USER_REGEX = /@[^\s]*/g;
66

77
let instance = null;
88

99
export default class UserProvider extends AutocompleteProvider {
1010
constructor() {
11-
super();
11+
super(USER_REGEX, {
12+
keys: ['displayName', 'userId']
13+
});
1214
this.users = [];
1315
this.fuse = new Fuse([], {
1416
keys: ['displayName', 'userId']
1517
})
1618
}
1719

18-
getCompletions(query: String) {
20+
getCompletions(query: string, selection: {start: number, end: number}) {
1921
let completions = [];
20-
let matches = query.match(ROOM_REGEX);
21-
let command = matches && matches[0];
22+
let command = this.getCurrentCommand(query, selection);
2223
if(command) {
2324
this.fuse.set(this.users);
24-
completions = this.fuse.search(command).map(user => {
25+
completions = this.fuse.search(command[0]).map(user => {
2526
return {
2627
title: user.displayName || user.userId,
2728
description: user.userId

src/components/views/rooms/Autocomplete.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,27 @@ export default class Autocomplete extends React.Component {
77
constructor(props) {
88
super(props);
99
this.state = {
10-
completions: []
10+
completions: [],
11+
12+
// how far down the completion list we are
13+
selectionOffset: 0
1114
};
1215
}
1316

1417
componentWillReceiveProps(props, state) {
1518
if(props.query == this.props.query) return;
1619

17-
getCompletions(props.query).map(completionResult => {
20+
getCompletions(props.query, props.selection).map(completionResult => {
1821
try {
19-
// console.log(`${completionResult.provider.getName()}: ${JSON.stringify(completionResult.completions)}`);
2022
completionResult.completions.then(completions => {
2123
let i = this.state.completions.findIndex(
2224
completion => completion.provider === completionResult.provider
2325
);
2426

2527
i = i == -1 ? this.state.completions.length : i;
26-
// console.log(completionResult);
2728
let newCompletions = Object.assign([], this.state.completions);
2829
completionResult.completions = completions;
2930
newCompletions[i] = completionResult;
30-
// console.log(newCompletions);
3131
this.setState({
3232
completions: newCompletions
3333
});
@@ -42,8 +42,7 @@ export default class Autocomplete extends React.Component {
4242
}
4343

4444
render() {
45-
const renderedCompletions = this.state.completions.map((completionResult, i) => {
46-
// console.log(completionResult);
45+
let renderedCompletions = this.state.completions.map((completionResult, i) => {
4746
let completions = completionResult.completions.map((completion, i) => {
4847
let Component = completion.component;
4948
if(Component) {

src/components/views/rooms/MessageComposer.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export default class MessageComposer extends React.Component {
3636
this.onInputContentChanged = this.onInputContentChanged.bind(this);
3737

3838
this.state = {
39-
autocompleteQuery: ''
39+
autocompleteQuery: '',
40+
selection: null
4041
};
4142
}
4243

@@ -121,11 +122,11 @@ export default class MessageComposer extends React.Component {
121122
});
122123
}
123124

124-
onInputContentChanged(content: string) {
125+
onInputContentChanged(content: string, selection: {start: number, end: number}) {
125126
this.setState({
126-
autocompleteQuery: content
127+
autocompleteQuery: content,
128+
selection
127129
});
128-
console.log(content);
129130
}
130131

131132
render() {
@@ -200,7 +201,7 @@ export default class MessageComposer extends React.Component {
200201
return (
201202
<div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}>
202203
<div className="mx_MessageComposer_autocomplete_wrapper">
203-
<Autocomplete query={this.state.autocompleteQuery} />
204+
<Autocomplete query={this.state.autocompleteQuery} selection={this.state.selection} />
204205
</div>
205206
<div className="mx_MessageComposer_wrapper">
206207
<div className="mx_MessageComposer_row">

0 commit comments

Comments
 (0)