Skip to content

Commit 02bab41

Browse files
committed
Convert Autocomplete to use promises
1 parent 6c98321 commit 02bab41

4 files changed

Lines changed: 120 additions & 104 deletions

File tree

app/assets/javascripts/discourse/components/autocomplete.js

Lines changed: 70 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,48 @@
55
**/
66
$.fn.autocomplete = function(options) {
77

8-
var addInputSelectedItem, autocompleteOptions, closeAutocomplete, completeEnd, completeStart, completeTerm, div, height;
9-
var inputSelectedItems, isInput, markSelected, me, oldClose, renderAutocomplete, selectedOption, updateAutoComplete, vals;
10-
var width, wrap, _this = this;
8+
var autocompletePlugin = this;
119

1210
if (this.length === 0) return;
1311

1412
if (options && options.cancel && this.data("closeAutocomplete")) {
1513
this.data("closeAutocomplete")();
1614
return this;
1715
}
16+
1817
if (this.length !== 1) {
1918
alert("only supporting one matcher at the moment");
2019
}
21-
autocompleteOptions = null;
22-
selectedOption = null;
23-
completeStart = null;
24-
completeEnd = null;
25-
me = this;
26-
div = null;
20+
21+
var wrap = null;
22+
var autocompleteOptions = null;
23+
var selectedOption = null;
24+
var completeStart = null;
25+
var completeEnd = null;
26+
var me = this;
27+
var div = null;
2728

2829
// input is handled differently
29-
isInput = this[0].tagName === "INPUT";
30-
inputSelectedItems = [];
30+
var isInput = this[0].tagName === "INPUT";
31+
var inputSelectedItems = [];
32+
33+
34+
var closeAutocomplete = function() {
35+
if (div) {
36+
div.hide().remove();
37+
}
38+
div = null;
39+
completeStart = null;
40+
autocompleteOptions = null;
41+
};
3142

32-
addInputSelectedItem = function(item) {
33-
var d, prev, transformed;
43+
var addInputSelectedItem = function(item) {
44+
var transformed;
3445
if (options.transformComplete) {
3546
transformed = options.transformComplete(item);
3647
}
37-
d = $("<div class='item'><span>" + (transformed || item) + "<a href='#'><i class='icon-remove'></i></a></span></div>");
38-
prev = me.parent().find('.item:last');
48+
var d = $("<div class='item'><span>" + (transformed || item) + "<a href='#'><i class='icon-remove'></i></a></span></div>");
49+
var prev = me.parent().find('.item:last');
3950
if (prev.length === 0) {
4051
me.parent().prepend(d);
4152
} else {
@@ -55,14 +66,32 @@ $.fn.autocomplete = function(options) {
5566
});
5667
};
5768

69+
var completeTerm = function(term) {
70+
if (term) {
71+
if (isInput) {
72+
me.val("");
73+
addInputSelectedItem(term);
74+
} else {
75+
if (options.transformComplete) {
76+
term = options.transformComplete(term);
77+
}
78+
var text = me.val();
79+
text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd + 1, text.length);
80+
me.val(text);
81+
Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length);
82+
}
83+
}
84+
closeAutocomplete();
85+
};
86+
5887
if (isInput) {
59-
width = this.width();
60-
height = this.height();
88+
var width = this.width();
89+
var height = this.height();
6190
wrap = this.wrap("<div class='ac-wrap clearfix'/>").parent();
6291
wrap.width(width);
6392
this.width(150);
6493
this.attr('name', this.attr('name') + "-renamed");
65-
vals = this.val().split(",");
94+
var vals = this.val().split(",");
6695
vals.each(function(x) {
6796
if (x !== "") {
6897
if (options.reverseTransform) {
@@ -74,19 +103,18 @@ $.fn.autocomplete = function(options) {
74103
this.val("");
75104
completeStart = 0;
76105
wrap.click(function() {
77-
_this.focus();
106+
autocompletePlugin.focus();
78107
return true;
79108
});
80109
}
81110

82-
markSelected = function() {
83-
var links;
84-
links = div.find('li a');
111+
var markSelected = function() {
112+
var links = div.find('li a');
85113
links.removeClass('selected');
86114
return $(links[selectedOption]).addClass('selected');
87115
};
88116

89-
renderAutocomplete = function() {
117+
var renderAutocomplete = function() {
90118
var borderTop, mePos, pos, ul;
91119
if (div) {
92120
div.hide().remove();
@@ -130,7 +158,7 @@ $.fn.autocomplete = function(options) {
130158
});
131159
};
132160

133-
updateAutoComplete = function(r) {
161+
var updateAutoComplete = function(r) {
134162
if (completeStart === null) return;
135163

136164
autocompleteOptions = r;
@@ -141,58 +169,27 @@ $.fn.autocomplete = function(options) {
141169
}
142170
};
143171

144-
closeAutocomplete = function() {
145-
if (div) {
146-
div.hide().remove();
147-
}
148-
div = null;
149-
completeStart = null;
150-
autocompleteOptions = null;
151-
};
152172

153173
// chain to allow multiples
154-
oldClose = me.data("closeAutocomplete");
174+
var oldClose = me.data("closeAutocomplete");
155175
me.data("closeAutocomplete", function() {
156176
if (oldClose) {
157177
oldClose();
158178
}
159179
return closeAutocomplete();
160180
});
161181

162-
completeTerm = function(term) {
163-
var text;
164-
if (term) {
165-
if (isInput) {
166-
me.val("");
167-
addInputSelectedItem(term);
168-
} else {
169-
if (options.transformComplete) {
170-
term = options.transformComplete(term);
171-
}
172-
text = me.val();
173-
text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd + 1, text.length);
174-
me.val(text);
175-
Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length);
176-
}
177-
}
178-
return closeAutocomplete();
179-
};
180-
181182
$(this).keypress(function(e) {
182-
var caretPosition, prevChar, term;
183-
if (!options.key) {
184-
return;
185-
}
186-
/* keep hunting backwards till you hit a
187-
*/
183+
if (!options.key) return;
188184

185+
// keep hunting backwards till you hit a
189186
if (e.which === options.key.charCodeAt(0)) {
190-
caretPosition = Discourse.Utilities.caretPosition(me[0]);
191-
prevChar = me.val().charAt(caretPosition - 1);
187+
var caretPosition = Discourse.Utilities.caretPosition(me[0]);
188+
var prevChar = me.val().charAt(caretPosition - 1);
192189
if (!prevChar || /\s/.test(prevChar)) {
193190
completeStart = completeEnd = caretPosition;
194-
term = "";
195-
options.dataSource(term, updateAutoComplete);
191+
var term = "";
192+
options.dataSource(term).then(updateAutoComplete);
196193
}
197194
}
198195
});
@@ -202,9 +199,7 @@ $.fn.autocomplete = function(options) {
202199
if (!options.key) {
203200
completeStart = 0;
204201
}
205-
if (e.which === 16) {
206-
return;
207-
}
202+
if (e.which === 16) return;
208203
if ((completeStart === null) && e.which === 8 && options.key) {
209204
c = Discourse.Utilities.caretPosition(me[0]);
210205
next = me[0].value[c];
@@ -222,45 +217,42 @@ $.fn.autocomplete = function(options) {
222217
completeStart = c;
223218
caretPosition = completeEnd = initial;
224219
term = me[0].value.substring(c + 1, initial);
225-
options.dataSource(term, updateAutoComplete);
220+
options.dataSource(term).then(updateAutoComplete);
226221
return true;
227222
}
228223
}
229224
prevIsGood = /[a-zA-Z\.]/.test(prev);
230225
}
231226
}
227+
228+
// ESC
232229
if (e.which === 27) {
233230
if (completeStart !== null) {
234231
closeAutocomplete();
235232
return false;
236233
}
237234
return true;
238235
}
236+
239237
if (completeStart !== null) {
240238
caretPosition = Discourse.Utilities.caretPosition(me[0]);
241-
/* If we've backspaced past the beginning, cancel unless no key
242-
*/
243239

240+
// If we've backspaced past the beginning, cancel unless no key
244241
if (caretPosition <= completeStart && options.key) {
245242
closeAutocomplete();
246243
return false;
247244
}
248-
/* Keyboard codes! So 80's.
249-
*/
250245

246+
// Keyboard codes! So 80's.
251247
switch (e.which) {
252248
case 13:
253249
case 39:
254250
case 9:
255-
if (!autocompleteOptions) {
256-
return true;
257-
}
251+
if (!autocompleteOptions) return true;
258252
if (selectedOption >= 0 && (userToComplete = autocompleteOptions[selectedOption])) {
259253
completeTerm(userToComplete);
260254
} else {
261-
/* We're cancelling it, really.
262-
*/
263-
255+
// We're cancelling it, really.
264256
return true;
265257
}
266258
closeAutocomplete();
@@ -284,9 +276,7 @@ $.fn.autocomplete = function(options) {
284276
markSelected();
285277
return false;
286278
default:
287-
/* otherwise they're typing - let's search for it!
288-
*/
289-
279+
// otherwise they're typing - let's search for it!
290280
completeEnd = caretPosition;
291281
if (e.which === 8) {
292282
caretPosition--;
@@ -309,7 +299,7 @@ $.fn.autocomplete = function(options) {
309299
term += ",";
310300
}
311301
}
312-
options.dataSource(term, updateAutoComplete);
302+
options.dataSource(term).then(updateAutoComplete);
313303
return true;
314304
}
315305
}

app/assets/javascripts/discourse/components/debounce.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
@method debounce
66
@module Discourse
77
@param {function} func The function to debounce
8-
@param {Numbers} wait how long to wait
8+
@param {Number} wait how long to wait
99
**/
1010
Discourse.debounce = function(func, wait) {
1111
var timeout = null;
@@ -36,3 +36,35 @@ Discourse.debounce = function(func, wait) {
3636
return timeout;
3737
};
3838
};
39+
40+
/**
41+
Debounce a javascript function that returns a promise. If it's called too soon it
42+
will return a promise that is never resolved.
43+
44+
@method debouncePromise
45+
@module Discourse
46+
@param {function} func The function to debounce
47+
@param {Number} wait how long to wait
48+
**/
49+
Discourse.debouncePromise = function(func, wait) {
50+
var timeout = null;
51+
var args = null;
52+
53+
return function() {
54+
var context = this;
55+
var promise = Ember.Deferred.create();
56+
args = arguments;
57+
58+
if (!timeout) {
59+
timeout = Em.run.later(function () {
60+
timeout = null;
61+
func.apply(context, args).then(function (y) {
62+
promise.resolve(y)
63+
});
64+
}, wait);
65+
}
66+
67+
return promise;
68+
}
69+
};
70+

app/assets/javascripts/discourse/components/user_search.js

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,34 @@ var cache = {};
99
var cacheTopicId = null;
1010
var cacheTime = null;
1111

12-
var doSearch = function(term, topicId, success) {
12+
var debouncedSearch = Discourse.debouncePromise(function(term, topicId) {
1313
return Discourse.ajax({
1414
url: Discourse.getURL('/users/search/users'),
15-
dataType: 'JSON',
1615
data: {
1716
term: term,
1817
topic_id: topicId
19-
},
20-
success: function(r) {
21-
cache[term] = r;
22-
cacheTime = new Date();
23-
return success(r);
2418
}
19+
}).then(function (r) {
20+
cache[term] = r;
21+
cacheTime = new Date();
22+
return r;
2523
});
26-
};
27-
28-
var debouncedSearch = Discourse.debounce(doSearch, 200);
24+
}, 200);
2925

3026
Discourse.UserSearch = {
3127

3228
search: function(options) {
3329
var term = options.term || "";
34-
var callback = options.callback;
3530
var exclude = options.exclude || [];
3631
var topicId = options.topicId;
3732
var limit = options.limit || 5;
38-
if (!callback) {
39-
throw "missing callback";
40-
}
33+
34+
var promise = Ember.Deferred.create();
4135

4236
// TODO site setting for allowed regex in username
4337
if (term.match(/[^a-zA-Z0-9\_\.]/)) {
44-
callback([]);
45-
return true;
38+
promise.resolve([]);
39+
return promise;
4640
}
4741
if ((new Date() - cacheTime) > 30000) {
4842
cache = {};
@@ -60,15 +54,15 @@ Discourse.UserSearch = {
6054
if (result.length > limit) return false;
6155
return true;
6256
});
63-
return callback(result);
57+
promise.resolve(result);
6458
};
6559

6660
if (cache[term]) {
6761
success(cache[term]);
6862
} else {
69-
debouncedSearch(term, topicId, success);
63+
debouncedSearch(term, topicId).then(success);
7064
}
71-
return true;
65+
return promise;
7266
}
7367

7468
};

0 commit comments

Comments
 (0)