From 3b76fd82fd9b00f71c5b2424e8086fd82e516cdc Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 8 Aug 2014 13:10:13 -0400 Subject: [PATCH] ES6: user-search lib and autocomplete. Cancels many promises rather than leaving them as pending forever. --- .../discourse/initializers/bootbox.js.es6 | 12 -- .../initializers/jquery-plugins.js.es6 | 14 +++ .../{autocomplete.js => autocomplete.js.es6} | 12 +- .../discourse/lib/user-search.js.es6 | 91 ++++++++++++++++ .../javascripts/discourse/lib/user_search.js | 103 ------------------ .../discourse/views/composer.js.es6 | 10 +- .../discourse/views/user-selector.js.es6 | 3 +- 7 files changed, 119 insertions(+), 126 deletions(-) delete mode 100644 app/assets/javascripts/discourse/initializers/bootbox.js.es6 create mode 100644 app/assets/javascripts/discourse/initializers/jquery-plugins.js.es6 rename app/assets/javascripts/discourse/lib/{autocomplete.js => autocomplete.js.es6} (97%) create mode 100644 app/assets/javascripts/discourse/lib/user-search.js.es6 delete mode 100644 app/assets/javascripts/discourse/lib/user_search.js diff --git a/app/assets/javascripts/discourse/initializers/bootbox.js.es6 b/app/assets/javascripts/discourse/initializers/bootbox.js.es6 deleted file mode 100644 index 4715f2fa7..000000000 --- a/app/assets/javascripts/discourse/initializers/bootbox.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -/** - Default settings for bootbox -**/ -export default { - name: "bootbox", - initialize: function() { - bootbox.animate(false); - - // clicking outside a bootbox modal closes it - bootbox.backdrop(true); - } -}; diff --git a/app/assets/javascripts/discourse/initializers/jquery-plugins.js.es6 b/app/assets/javascripts/discourse/initializers/jquery-plugins.js.es6 new file mode 100644 index 000000000..c151e1a30 --- /dev/null +++ b/app/assets/javascripts/discourse/initializers/jquery-plugins.js.es6 @@ -0,0 +1,14 @@ +import autocomplete from 'discourse/lib/autocomplete'; + +export default { + name: "jquery-plugins", + initialize: function() { + + // Settings for bootbox + bootbox.animate(false); + bootbox.backdrop(true); + + // Initialize the autocomplete tool + $.fn.autocomplete = autocomplete; + } +}; diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 similarity index 97% rename from app/assets/javascripts/discourse/lib/autocomplete.js rename to app/assets/javascripts/discourse/lib/autocomplete.js.es6 index 9057afe04..b8110dd3d 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -4,6 +4,8 @@ @module $.fn.autocomplete **/ +export var CANCELLED_STATUS = "__CANCELLED"; + var shiftMap = []; shiftMap[192] = "~"; shiftMap[49] = "!"; @@ -43,7 +45,7 @@ function mapKeyPressToActualCharacter(isShiftKey, characterCode) { return stringValue; } -$.fn.autocomplete = function(options) { +export default function(options) { var autocompletePlugin = this; if (this.length === 0) return; @@ -239,6 +241,12 @@ $.fn.autocomplete = function(options) { return; } + // Allow an update method to cancel. This allows us to debounce + // promises without leaking + if (r === CANCELLED_STATUS) { + return; + } + autocompleteOptions = r; if (!r || r.length === 0) { closeAutocomplete(); @@ -409,4 +417,4 @@ $.fn.autocomplete = function(options) { }); return this; -}; +} diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6 new file mode 100644 index 000000000..d188157a1 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/user-search.js.es6 @@ -0,0 +1,91 @@ +import { CANCELLED_STATUS } from 'discourse/lib/autocomplete'; + +var cache = {}, + cacheTopicId, + cacheTime, + currentTerm; + +function performSearch(term, topicId, includeGroups, resultsFn) { + var cached = cache[term]; + if (cached) { + resultsFn(cached); + return true; + } + + Discourse.ajax('/users/search/users', { + data: { term: term, + topic_id: topicId, + include_groups: includeGroups } + }).then(function (r) { + cache[term] = r; + cacheTime = new Date(); + + // If there is a newer search term, return null + if (term !== currentTerm) { r = CANCELLED_STATUS; } + resultsFn(r); + }); + return true; +} +var debouncedSearch = _.debounce(performSearch, 300); + +function organizeResults(r, options) { + if (r === CANCELLED_STATUS) { return r; } + + var exclude = options.exclude || [], + limit = options.limit || 5, + users = [], + groups = [], + results = []; + + r.users.every(function(u) { + if (exclude.indexOf(u.username) === -1) { + users.push(u); + results.push(u); + } + return results.length <= limit; + }); + + r.groups.every(function(g) { + if (results.length > limit) return false; + if (exclude.indexOf(g.name) === -1) { + groups.push(g); + results.push(g); + } + return true; + }); + + results.users = users; + results.groups = groups; + return results; +} + + +export default function userSearch(options) { + var term = options.term || "", + includeGroups = !!options.include_groups, + topicId = options.topicId; + + currentTerm = term; + + return new Ember.RSVP.Promise(function(resolve) { + // TODO site setting for allowed regex in username + if (term.match(/[^a-zA-Z0-9_\.]/)) { + resolve([]); + return; + } + if (((new Date() - cacheTime) > 30000) || (cacheTopicId !== topicId)) { + cache = {}; + } + + cacheTopicId = topicId; + var executed = debouncedSearch(term, topicId, includeGroups, function(r) { + resolve(organizeResults(r, options)); + }); + + // TODO: This doesn't cancel all debounced promises, we should figure out + // a way to handle that. + if (!executed) { + resolve(CANCELLED_STATUS); + } + }); +} diff --git a/app/assets/javascripts/discourse/lib/user_search.js b/app/assets/javascripts/discourse/lib/user_search.js deleted file mode 100644 index ddd8524ce..000000000 --- a/app/assets/javascripts/discourse/lib/user_search.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - Helper for searching for Users - - @class UserSearch - @namespace Discourse - @module Discourse -**/ -var cache = {}; -var cacheTopicId = null; -var cacheTime = null; - -var currentTerm; - -var debouncedSearch = _.debounce(function(term, topicId, include_groups, resultsFn) { - - Discourse.ajax('/users/search/users', { - data: { - term: term, - topic_id: topicId, - include_groups: include_groups - } - }).then(function (r) { - - cache[term] = r; - cacheTime = new Date(); - - if(term === currentTerm){ - resultsFn(r); - } - }); - -}, 300); - -Discourse.UserSearch = { - - search: function(options) { - var term = options.term || ""; - currentTerm = term; - - var include_groups = options.include_groups || false; - var exclude = options.exclude || []; - var topicId = options.topicId; - var limit = options.limit || 5; - - var promise = Ember.Deferred.create(); - - // TODO site setting for allowed regex in username - if (term.match(/[^a-zA-Z0-9_\.]/)) { - promise.resolve([]); - return promise; - } - if ((new Date() - cacheTime) > 30000) { - cache = {}; - } - if (cacheTopicId !== topicId) { - cache = {}; - } - - cacheTopicId = topicId; - - var organizeResults = function(r) { - var users = [], groups = [], results = []; - _.each(r.users,function(u) { - if (exclude.indexOf(u.username) === -1) { - users.push(u); - results.push(u); - } - return results.length <= limit; - }); - - _.each(r.groups,function(g) { - if (results.length > limit) return false; - if (exclude.indexOf(g.name) === -1) { - groups.push(g); - results.push(g); - } - return true; - }); - - results.users = users; - results.groups = groups; - - promise.resolve(results); - }; - - if (cache[term]) { - // inject a delay to avoid too much repainting - setTimeout(function(){ - if(term !== currentTerm) { - return; - } - organizeResults(cache[term]); - }, 300); - } else { - debouncedSearch(term, topicId, include_groups, organizeResults); - } - - return promise; - } - -}; - - diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index f962efcfa..64b568fec 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -1,13 +1,7 @@ /*global assetPath:true */ -/** - This view handles rendering of the composer +import userSearch from 'discourse/lib/user-search'; - @class ComposerView - @extends Discourse.View - @namespace Discourse - @module Discourse -**/ var ComposerView = Discourse.View.extend(Ember.Evented, { templateName: 'composer', elementId: 'reply-control', @@ -183,7 +177,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, { $wmdInput.autocomplete({ template: template, dataSource: function(term) { - return Discourse.UserSearch.search({ + return userSearch({ term: term, topicId: self.get('controller.controllers.topic.model.id'), include_groups: true diff --git a/app/assets/javascripts/discourse/views/user-selector.js.es6 b/app/assets/javascripts/discourse/views/user-selector.js.es6 index f3e01178c..89160244f 100644 --- a/app/assets/javascripts/discourse/views/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/views/user-selector.js.es6 @@ -1,4 +1,5 @@ import TextField from 'discourse/components/text-field'; +import userSearch from 'discourse/lib/user-search'; var compiled; function templateFunction() { @@ -61,7 +62,7 @@ var UserSelector = TextField.extend({ allowAny: this.get('allowAny'), dataSource: function(term) { - return Discourse.UserSearch.search({ + return userSearch({ term: term, topicId: userSelectorView.get('topicId'), exclude: excludedUsernames(),