Convert Autocomplete to use promises

This commit is contained in:
Robin Ward 2013-04-08 14:37:35 -04:00
parent 6c983218b3
commit 02bab415bd
4 changed files with 120 additions and 104 deletions

View File

@ -5,9 +5,7 @@
**/ **/
$.fn.autocomplete = function(options) { $.fn.autocomplete = function(options) {
var addInputSelectedItem, autocompleteOptions, closeAutocomplete, completeEnd, completeStart, completeTerm, div, height; var autocompletePlugin = this;
var inputSelectedItems, isInput, markSelected, me, oldClose, renderAutocomplete, selectedOption, updateAutoComplete, vals;
var width, wrap, _this = this;
if (this.length === 0) return; if (this.length === 0) return;
@ -15,27 +13,40 @@ $.fn.autocomplete = function(options) {
this.data("closeAutocomplete")(); this.data("closeAutocomplete")();
return this; return this;
} }
if (this.length !== 1) { if (this.length !== 1) {
alert("only supporting one matcher at the moment"); alert("only supporting one matcher at the moment");
} }
autocompleteOptions = null;
selectedOption = null; var wrap = null;
completeStart = null; var autocompleteOptions = null;
completeEnd = null; var selectedOption = null;
me = this; var completeStart = null;
div = null; var completeEnd = null;
var me = this;
var div = null;
// input is handled differently // input is handled differently
isInput = this[0].tagName === "INPUT"; var isInput = this[0].tagName === "INPUT";
inputSelectedItems = []; var inputSelectedItems = [];
addInputSelectedItem = function(item) {
var d, prev, transformed; var closeAutocomplete = function() {
if (div) {
div.hide().remove();
}
div = null;
completeStart = null;
autocompleteOptions = null;
};
var addInputSelectedItem = function(item) {
var transformed;
if (options.transformComplete) { if (options.transformComplete) {
transformed = options.transformComplete(item); transformed = options.transformComplete(item);
} }
d = $("<div class='item'><span>" + (transformed || item) + "<a href='#'><i class='icon-remove'></i></a></span></div>"); var d = $("<div class='item'><span>" + (transformed || item) + "<a href='#'><i class='icon-remove'></i></a></span></div>");
prev = me.parent().find('.item:last'); var prev = me.parent().find('.item:last');
if (prev.length === 0) { if (prev.length === 0) {
me.parent().prepend(d); me.parent().prepend(d);
} else { } else {
@ -55,14 +66,32 @@ $.fn.autocomplete = function(options) {
}); });
}; };
var completeTerm = function(term) {
if (term) {
if (isInput) {
me.val("");
addInputSelectedItem(term);
} else {
if (options.transformComplete) {
term = options.transformComplete(term);
}
var text = me.val();
text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd + 1, text.length);
me.val(text);
Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length);
}
}
closeAutocomplete();
};
if (isInput) { if (isInput) {
width = this.width(); var width = this.width();
height = this.height(); var height = this.height();
wrap = this.wrap("<div class='ac-wrap clearfix'/>").parent(); wrap = this.wrap("<div class='ac-wrap clearfix'/>").parent();
wrap.width(width); wrap.width(width);
this.width(150); this.width(150);
this.attr('name', this.attr('name') + "-renamed"); this.attr('name', this.attr('name') + "-renamed");
vals = this.val().split(","); var vals = this.val().split(",");
vals.each(function(x) { vals.each(function(x) {
if (x !== "") { if (x !== "") {
if (options.reverseTransform) { if (options.reverseTransform) {
@ -74,19 +103,18 @@ $.fn.autocomplete = function(options) {
this.val(""); this.val("");
completeStart = 0; completeStart = 0;
wrap.click(function() { wrap.click(function() {
_this.focus(); autocompletePlugin.focus();
return true; return true;
}); });
} }
markSelected = function() { var markSelected = function() {
var links; var links = div.find('li a');
links = div.find('li a');
links.removeClass('selected'); links.removeClass('selected');
return $(links[selectedOption]).addClass('selected'); return $(links[selectedOption]).addClass('selected');
}; };
renderAutocomplete = function() { var renderAutocomplete = function() {
var borderTop, mePos, pos, ul; var borderTop, mePos, pos, ul;
if (div) { if (div) {
div.hide().remove(); div.hide().remove();
@ -130,7 +158,7 @@ $.fn.autocomplete = function(options) {
}); });
}; };
updateAutoComplete = function(r) { var updateAutoComplete = function(r) {
if (completeStart === null) return; if (completeStart === null) return;
autocompleteOptions = r; autocompleteOptions = r;
@ -141,17 +169,9 @@ $.fn.autocomplete = function(options) {
} }
}; };
closeAutocomplete = function() {
if (div) {
div.hide().remove();
}
div = null;
completeStart = null;
autocompleteOptions = null;
};
// chain to allow multiples // chain to allow multiples
oldClose = me.data("closeAutocomplete"); var oldClose = me.data("closeAutocomplete");
me.data("closeAutocomplete", function() { me.data("closeAutocomplete", function() {
if (oldClose) { if (oldClose) {
oldClose(); oldClose();
@ -159,40 +179,17 @@ $.fn.autocomplete = function(options) {
return closeAutocomplete(); return closeAutocomplete();
}); });
completeTerm = function(term) {
var text;
if (term) {
if (isInput) {
me.val("");
addInputSelectedItem(term);
} else {
if (options.transformComplete) {
term = options.transformComplete(term);
}
text = me.val();
text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd + 1, text.length);
me.val(text);
Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length);
}
}
return closeAutocomplete();
};
$(this).keypress(function(e) { $(this).keypress(function(e) {
var caretPosition, prevChar, term; if (!options.key) return;
if (!options.key) {
return;
}
/* keep hunting backwards till you hit a
*/
// keep hunting backwards till you hit a
if (e.which === options.key.charCodeAt(0)) { if (e.which === options.key.charCodeAt(0)) {
caretPosition = Discourse.Utilities.caretPosition(me[0]); var caretPosition = Discourse.Utilities.caretPosition(me[0]);
prevChar = me.val().charAt(caretPosition - 1); var prevChar = me.val().charAt(caretPosition - 1);
if (!prevChar || /\s/.test(prevChar)) { if (!prevChar || /\s/.test(prevChar)) {
completeStart = completeEnd = caretPosition; completeStart = completeEnd = caretPosition;
term = ""; var term = "";
options.dataSource(term, updateAutoComplete); options.dataSource(term).then(updateAutoComplete);
} }
} }
}); });
@ -202,9 +199,7 @@ $.fn.autocomplete = function(options) {
if (!options.key) { if (!options.key) {
completeStart = 0; completeStart = 0;
} }
if (e.which === 16) { if (e.which === 16) return;
return;
}
if ((completeStart === null) && e.which === 8 && options.key) { if ((completeStart === null) && e.which === 8 && options.key) {
c = Discourse.Utilities.caretPosition(me[0]); c = Discourse.Utilities.caretPosition(me[0]);
next = me[0].value[c]; next = me[0].value[c];
@ -222,13 +217,15 @@ $.fn.autocomplete = function(options) {
completeStart = c; completeStart = c;
caretPosition = completeEnd = initial; caretPosition = completeEnd = initial;
term = me[0].value.substring(c + 1, initial); term = me[0].value.substring(c + 1, initial);
options.dataSource(term, updateAutoComplete); options.dataSource(term).then(updateAutoComplete);
return true; return true;
} }
} }
prevIsGood = /[a-zA-Z\.]/.test(prev); prevIsGood = /[a-zA-Z\.]/.test(prev);
} }
} }
// ESC
if (e.which === 27) { if (e.which === 27) {
if (completeStart !== null) { if (completeStart !== null) {
closeAutocomplete(); closeAutocomplete();
@ -236,31 +233,26 @@ $.fn.autocomplete = function(options) {
} }
return true; return true;
} }
if (completeStart !== null) { if (completeStart !== null) {
caretPosition = Discourse.Utilities.caretPosition(me[0]); caretPosition = Discourse.Utilities.caretPosition(me[0]);
/* If we've backspaced past the beginning, cancel unless no key
*/
// If we've backspaced past the beginning, cancel unless no key
if (caretPosition <= completeStart && options.key) { if (caretPosition <= completeStart && options.key) {
closeAutocomplete(); closeAutocomplete();
return false; return false;
} }
/* Keyboard codes! So 80's.
*/
// Keyboard codes! So 80's.
switch (e.which) { switch (e.which) {
case 13: case 13:
case 39: case 39:
case 9: case 9:
if (!autocompleteOptions) { if (!autocompleteOptions) return true;
return true;
}
if (selectedOption >= 0 && (userToComplete = autocompleteOptions[selectedOption])) { if (selectedOption >= 0 && (userToComplete = autocompleteOptions[selectedOption])) {
completeTerm(userToComplete); completeTerm(userToComplete);
} else { } else {
/* We're cancelling it, really. // We're cancelling it, really.
*/
return true; return true;
} }
closeAutocomplete(); closeAutocomplete();
@ -284,9 +276,7 @@ $.fn.autocomplete = function(options) {
markSelected(); markSelected();
return false; return false;
default: default:
/* otherwise they're typing - let's search for it! // otherwise they're typing - let's search for it!
*/
completeEnd = caretPosition; completeEnd = caretPosition;
if (e.which === 8) { if (e.which === 8) {
caretPosition--; caretPosition--;
@ -309,7 +299,7 @@ $.fn.autocomplete = function(options) {
term += ","; term += ",";
} }
} }
options.dataSource(term, updateAutoComplete); options.dataSource(term).then(updateAutoComplete);
return true; return true;
} }
} }

View File

@ -5,7 +5,7 @@
@method debounce @method debounce
@module Discourse @module Discourse
@param {function} func The function to debounce @param {function} func The function to debounce
@param {Numbers} wait how long to wait @param {Number} wait how long to wait
**/ **/
Discourse.debounce = function(func, wait) { Discourse.debounce = function(func, wait) {
var timeout = null; var timeout = null;
@ -36,3 +36,35 @@ Discourse.debounce = function(func, wait) {
return timeout; return timeout;
}; };
}; };
/**
Debounce a javascript function that returns a promise. If it's called too soon it
will return a promise that is never resolved.
@method debouncePromise
@module Discourse
@param {function} func The function to debounce
@param {Number} wait how long to wait
**/
Discourse.debouncePromise = function(func, wait) {
var timeout = null;
var args = null;
return function() {
var context = this;
var promise = Ember.Deferred.create();
args = arguments;
if (!timeout) {
timeout = Em.run.later(function () {
timeout = null;
func.apply(context, args).then(function (y) {
promise.resolve(y)
});
}, wait);
}
return promise;
}
};

View File

@ -9,40 +9,34 @@ var cache = {};
var cacheTopicId = null; var cacheTopicId = null;
var cacheTime = null; var cacheTime = null;
var doSearch = function(term, topicId, success) { var debouncedSearch = Discourse.debouncePromise(function(term, topicId) {
return Discourse.ajax({ return Discourse.ajax({
url: Discourse.getURL('/users/search/users'), url: Discourse.getURL('/users/search/users'),
dataType: 'JSON',
data: { data: {
term: term, term: term,
topic_id: topicId topic_id: topicId
},
success: function(r) {
cache[term] = r;
cacheTime = new Date();
return success(r);
} }
}).then(function (r) {
cache[term] = r;
cacheTime = new Date();
return r;
}); });
}; }, 200);
var debouncedSearch = Discourse.debounce(doSearch, 200);
Discourse.UserSearch = { Discourse.UserSearch = {
search: function(options) { search: function(options) {
var term = options.term || ""; var term = options.term || "";
var callback = options.callback;
var exclude = options.exclude || []; var exclude = options.exclude || [];
var topicId = options.topicId; var topicId = options.topicId;
var limit = options.limit || 5; var limit = options.limit || 5;
if (!callback) {
throw "missing callback"; var promise = Ember.Deferred.create();
}
// TODO site setting for allowed regex in username // TODO site setting for allowed regex in username
if (term.match(/[^a-zA-Z0-9\_\.]/)) { if (term.match(/[^a-zA-Z0-9\_\.]/)) {
callback([]); promise.resolve([]);
return true; return promise;
} }
if ((new Date() - cacheTime) > 30000) { if ((new Date() - cacheTime) > 30000) {
cache = {}; cache = {};
@ -60,15 +54,15 @@ Discourse.UserSearch = {
if (result.length > limit) return false; if (result.length > limit) return false;
return true; return true;
}); });
return callback(result); promise.resolve(result);
}; };
if (cache[term]) { if (cache[term]) {
success(cache[term]); success(cache[term]);
} else { } else {
debouncedSearch(term, topicId, success); debouncedSearch(term, topicId).then(success);
} }
return true; return promise;
} }
}; };

View File

@ -183,10 +183,9 @@ Discourse.ComposerView = Discourse.View.extend({
$wmdInput.data('init', true); $wmdInput.data('init', true);
$wmdInput.autocomplete({ $wmdInput.autocomplete({
template: template, template: template,
dataSource: function(term, callback) { dataSource: function(term) {
return Discourse.UserSearch.search({ return Discourse.UserSearch.search({
term: term, term: term,
callback: callback,
topicId: _this.get('controller.controllers.topic.content.id') topicId: _this.get('controller.controllers.topic.content.id')
}); });
}, },
@ -198,13 +197,14 @@ Discourse.ComposerView = Discourse.View.extend({
$('#private-message-users').val(this.get('content.targetUsernames')).autocomplete({ $('#private-message-users').val(this.get('content.targetUsernames')).autocomplete({
template: template, template: template,
dataSource: function(term, callback) { dataSource: function(term) {
return Discourse.UserSearch.search({ return Discourse.UserSearch.search({
term: term, term: term,
callback: callback, topicId: _this.get('controller.controllers.topic.content.id'),
exclude: selected.concat([Discourse.get('currentUser.username')]) exclude: selected.concat([Discourse.get('currentUser.username')])
}); });
}, },
onChangeItems: function(items) { onChangeItems: function(items) {
items = $.map(items, function(i) { items = $.map(items, function(i) {
if (i.username) { if (i.username) {