From 02bab415bd9f7bac7ed18c5ff4ab41a4b1538ffe Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 8 Apr 2013 14:37:35 -0400 Subject: [PATCH] Convert Autocomplete to use promises --- .../discourse/components/autocomplete.js | 150 ++++++++---------- .../discourse/components/debounce.js | 34 +++- .../discourse/components/user_search.js | 32 ++-- .../discourse/views/composer_view.js | 8 +- 4 files changed, 120 insertions(+), 104 deletions(-) diff --git a/app/assets/javascripts/discourse/components/autocomplete.js b/app/assets/javascripts/discourse/components/autocomplete.js index 1383e0c1c74..3cfca2b9ebe 100644 --- a/app/assets/javascripts/discourse/components/autocomplete.js +++ b/app/assets/javascripts/discourse/components/autocomplete.js @@ -5,9 +5,7 @@ **/ $.fn.autocomplete = function(options) { - var addInputSelectedItem, autocompleteOptions, closeAutocomplete, completeEnd, completeStart, completeTerm, div, height; - var inputSelectedItems, isInput, markSelected, me, oldClose, renderAutocomplete, selectedOption, updateAutoComplete, vals; - var width, wrap, _this = this; + var autocompletePlugin = this; if (this.length === 0) return; @@ -15,27 +13,40 @@ $.fn.autocomplete = function(options) { this.data("closeAutocomplete")(); return this; } + if (this.length !== 1) { alert("only supporting one matcher at the moment"); } - autocompleteOptions = null; - selectedOption = null; - completeStart = null; - completeEnd = null; - me = this; - div = null; + + var wrap = null; + var autocompleteOptions = null; + var selectedOption = null; + var completeStart = null; + var completeEnd = null; + var me = this; + var div = null; // input is handled differently - isInput = this[0].tagName === "INPUT"; - inputSelectedItems = []; + var isInput = this[0].tagName === "INPUT"; + 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) { transformed = options.transformComplete(item); } - d = $("
" + (transformed || item) + "
"); - prev = me.parent().find('.item:last'); + var d = $("
" + (transformed || item) + "
"); + var prev = me.parent().find('.item:last'); if (prev.length === 0) { me.parent().prepend(d); } 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) { - width = this.width(); - height = this.height(); + var width = this.width(); + var height = this.height(); wrap = this.wrap("
").parent(); wrap.width(width); this.width(150); this.attr('name', this.attr('name') + "-renamed"); - vals = this.val().split(","); + var vals = this.val().split(","); vals.each(function(x) { if (x !== "") { if (options.reverseTransform) { @@ -74,19 +103,18 @@ $.fn.autocomplete = function(options) { this.val(""); completeStart = 0; wrap.click(function() { - _this.focus(); + autocompletePlugin.focus(); return true; }); } - markSelected = function() { - var links; - links = div.find('li a'); + var markSelected = function() { + var links = div.find('li a'); links.removeClass('selected'); return $(links[selectedOption]).addClass('selected'); }; - renderAutocomplete = function() { + var renderAutocomplete = function() { var borderTop, mePos, pos, ul; if (div) { div.hide().remove(); @@ -130,7 +158,7 @@ $.fn.autocomplete = function(options) { }); }; - updateAutoComplete = function(r) { + var updateAutoComplete = function(r) { if (completeStart === null) return; 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 - oldClose = me.data("closeAutocomplete"); + var oldClose = me.data("closeAutocomplete"); me.data("closeAutocomplete", function() { if (oldClose) { oldClose(); @@ -159,40 +179,17 @@ $.fn.autocomplete = function(options) { 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) { - var caretPosition, prevChar, term; - if (!options.key) { - return; - } - /* keep hunting backwards till you hit a - */ + if (!options.key) return; + // keep hunting backwards till you hit a if (e.which === options.key.charCodeAt(0)) { - caretPosition = Discourse.Utilities.caretPosition(me[0]); - prevChar = me.val().charAt(caretPosition - 1); + var caretPosition = Discourse.Utilities.caretPosition(me[0]); + var prevChar = me.val().charAt(caretPosition - 1); if (!prevChar || /\s/.test(prevChar)) { completeStart = completeEnd = caretPosition; - term = ""; - options.dataSource(term, updateAutoComplete); + var term = ""; + options.dataSource(term).then(updateAutoComplete); } } }); @@ -202,9 +199,7 @@ $.fn.autocomplete = function(options) { if (!options.key) { completeStart = 0; } - if (e.which === 16) { - return; - } + if (e.which === 16) return; if ((completeStart === null) && e.which === 8 && options.key) { c = Discourse.Utilities.caretPosition(me[0]); next = me[0].value[c]; @@ -222,13 +217,15 @@ $.fn.autocomplete = function(options) { completeStart = c; caretPosition = completeEnd = initial; term = me[0].value.substring(c + 1, initial); - options.dataSource(term, updateAutoComplete); + options.dataSource(term).then(updateAutoComplete); return true; } } prevIsGood = /[a-zA-Z\.]/.test(prev); } } + + // ESC if (e.which === 27) { if (completeStart !== null) { closeAutocomplete(); @@ -236,31 +233,26 @@ $.fn.autocomplete = function(options) { } return true; } + if (completeStart !== null) { 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) { closeAutocomplete(); return false; } - /* Keyboard codes! So 80's. - */ + // Keyboard codes! So 80's. switch (e.which) { case 13: case 39: case 9: - if (!autocompleteOptions) { - return true; - } + if (!autocompleteOptions) return true; if (selectedOption >= 0 && (userToComplete = autocompleteOptions[selectedOption])) { completeTerm(userToComplete); } else { - /* We're cancelling it, really. - */ - + // We're cancelling it, really. return true; } closeAutocomplete(); @@ -284,9 +276,7 @@ $.fn.autocomplete = function(options) { markSelected(); return false; default: - /* otherwise they're typing - let's search for it! - */ - + // otherwise they're typing - let's search for it! completeEnd = caretPosition; if (e.which === 8) { caretPosition--; @@ -309,7 +299,7 @@ $.fn.autocomplete = function(options) { term += ","; } } - options.dataSource(term, updateAutoComplete); + options.dataSource(term).then(updateAutoComplete); return true; } } diff --git a/app/assets/javascripts/discourse/components/debounce.js b/app/assets/javascripts/discourse/components/debounce.js index 88887cbb685..d1b069d56b8 100644 --- a/app/assets/javascripts/discourse/components/debounce.js +++ b/app/assets/javascripts/discourse/components/debounce.js @@ -5,7 +5,7 @@ @method debounce @module Discourse @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) { var timeout = null; @@ -36,3 +36,35 @@ Discourse.debounce = function(func, wait) { 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; + } +}; + diff --git a/app/assets/javascripts/discourse/components/user_search.js b/app/assets/javascripts/discourse/components/user_search.js index ab11d393272..6bd2b6bad47 100644 --- a/app/assets/javascripts/discourse/components/user_search.js +++ b/app/assets/javascripts/discourse/components/user_search.js @@ -9,40 +9,34 @@ var cache = {}; var cacheTopicId = null; var cacheTime = null; -var doSearch = function(term, topicId, success) { +var debouncedSearch = Discourse.debouncePromise(function(term, topicId) { return Discourse.ajax({ url: Discourse.getURL('/users/search/users'), - dataType: 'JSON', data: { term: term, 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; }); -}; - -var debouncedSearch = Discourse.debounce(doSearch, 200); +}, 200); Discourse.UserSearch = { search: function(options) { var term = options.term || ""; - var callback = options.callback; var exclude = options.exclude || []; var topicId = options.topicId; var limit = options.limit || 5; - if (!callback) { - throw "missing callback"; - } + + var promise = Ember.Deferred.create(); // TODO site setting for allowed regex in username if (term.match(/[^a-zA-Z0-9\_\.]/)) { - callback([]); - return true; + promise.resolve([]); + return promise; } if ((new Date() - cacheTime) > 30000) { cache = {}; @@ -60,15 +54,15 @@ Discourse.UserSearch = { if (result.length > limit) return false; return true; }); - return callback(result); + promise.resolve(result); }; if (cache[term]) { success(cache[term]); } else { - debouncedSearch(term, topicId, success); + debouncedSearch(term, topicId).then(success); } - return true; + return promise; } }; diff --git a/app/assets/javascripts/discourse/views/composer_view.js b/app/assets/javascripts/discourse/views/composer_view.js index ec4e7944cc2..7e72edb53b3 100644 --- a/app/assets/javascripts/discourse/views/composer_view.js +++ b/app/assets/javascripts/discourse/views/composer_view.js @@ -183,10 +183,9 @@ Discourse.ComposerView = Discourse.View.extend({ $wmdInput.data('init', true); $wmdInput.autocomplete({ template: template, - dataSource: function(term, callback) { + dataSource: function(term) { return Discourse.UserSearch.search({ term: term, - callback: callback, 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({ template: template, - dataSource: function(term, callback) { + dataSource: function(term) { return Discourse.UserSearch.search({ term: term, - callback: callback, + topicId: _this.get('controller.controllers.topic.content.id'), exclude: selected.concat([Discourse.get('currentUser.username')]) }); }, + onChangeItems: function(items) { items = $.map(items, function(i) { if (i.username) {