ES6: user-search lib and autocomplete. Cancels many promises rather than

leaving them as pending forever.
This commit is contained in:
Robin Ward 2014-08-08 13:10:13 -04:00
parent d7f28baf77
commit 3b76fd82fd
7 changed files with 119 additions and 126 deletions

View file

@ -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);
}
};

View file

@ -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;
}
};

View file

@ -4,6 +4,8 @@
@module $.fn.autocomplete @module $.fn.autocomplete
**/ **/
export var CANCELLED_STATUS = "__CANCELLED";
var shiftMap = []; var shiftMap = [];
shiftMap[192] = "~"; shiftMap[192] = "~";
shiftMap[49] = "!"; shiftMap[49] = "!";
@ -43,7 +45,7 @@ function mapKeyPressToActualCharacter(isShiftKey, characterCode) {
return stringValue; return stringValue;
} }
$.fn.autocomplete = function(options) { export default function(options) {
var autocompletePlugin = this; var autocompletePlugin = this;
if (this.length === 0) return; if (this.length === 0) return;
@ -239,6 +241,12 @@ $.fn.autocomplete = function(options) {
return; return;
} }
// Allow an update method to cancel. This allows us to debounce
// promises without leaking
if (r === CANCELLED_STATUS) {
return;
}
autocompleteOptions = r; autocompleteOptions = r;
if (!r || r.length === 0) { if (!r || r.length === 0) {
closeAutocomplete(); closeAutocomplete();
@ -409,4 +417,4 @@ $.fn.autocomplete = function(options) {
}); });
return this; return this;
}; }

View file

@ -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);
}
});
}

View file

@ -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;
}
};

View file

@ -1,13 +1,7 @@
/*global assetPath:true */ /*global assetPath:true */
/** import userSearch from 'discourse/lib/user-search';
This view handles rendering of the composer
@class ComposerView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
var ComposerView = Discourse.View.extend(Ember.Evented, { var ComposerView = Discourse.View.extend(Ember.Evented, {
templateName: 'composer', templateName: 'composer',
elementId: 'reply-control', elementId: 'reply-control',
@ -183,7 +177,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
$wmdInput.autocomplete({ $wmdInput.autocomplete({
template: template, template: template,
dataSource: function(term) { dataSource: function(term) {
return Discourse.UserSearch.search({ return userSearch({
term: term, term: term,
topicId: self.get('controller.controllers.topic.model.id'), topicId: self.get('controller.controllers.topic.model.id'),
include_groups: true include_groups: true

View file

@ -1,4 +1,5 @@
import TextField from 'discourse/components/text-field'; import TextField from 'discourse/components/text-field';
import userSearch from 'discourse/lib/user-search';
var compiled; var compiled;
function templateFunction() { function templateFunction() {
@ -61,7 +62,7 @@ var UserSelector = TextField.extend({
allowAny: this.get('allowAny'), allowAny: this.get('allowAny'),
dataSource: function(term) { dataSource: function(term) {
return Discourse.UserSearch.search({ return userSearch({
term: term, term: term,
topicId: userSelectorView.get('topicId'), topicId: userSelectorView.get('topicId'),
exclude: excludedUsernames(), exclude: excludedUsernames(),