From b2fbf5e009175595aa4cc1c87aeb82010d1b4763 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 24 Sep 2014 11:46:22 +0200 Subject: [PATCH] Infinite scroll improvement (webmail). see #585 --- modoboa/extensions/webmail/lib/utils.py | 9 ++ .../webmail/static/webmail/js/webmail.js | 67 ++++++-- .../webmail/templates/webmail/email_list.html | 6 +- .../webmail/templatetags/webmail_tags.py | 4 +- modoboa/extensions/webmail/views.py | 10 +- modoboa/lib/webutils.py | 13 +- modoboa/static/js/history.js | 133 ++++++++++------ modoboa/static/js/jquery.infinitescroll.js | 146 +++++++++++++++--- 8 files changed, 299 insertions(+), 89 deletions(-) diff --git a/modoboa/extensions/webmail/lib/utils.py b/modoboa/extensions/webmail/lib/utils.py index fc2569ad9..d48dd20ab 100644 --- a/modoboa/extensions/webmail/lib/utils.py +++ b/modoboa/extensions/webmail/lib/utils.py @@ -33,3 +33,12 @@ class WebmailNavigationParameters(NavigationParameters): ) if defmailbox is not None: self.parameters += [('mbox', defmailbox, False)] + + def _store_page(self): + """Specific method to store the current page.""" + if self.request.GET.get("reset_page", None) or "page" not in self: + self["page"] = 1 + else: + page = self.request.GET.get("page", None) + if page is not None: + self["page"] = int(page) diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js index d996f3251..6aa284342 100644 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ b/modoboa/extensions/webmail/static/webmail/js/webmail.js @@ -26,7 +26,7 @@ Webmail.prototype = { this.editorid = "id_body"; this.navobject = new History({ - deflocation: "?action=listmailbox", + deflocation: "?action=listmailbox&reset_page=true", defcallback: $.proxy(this.listmailbox_callback, this) }); this.poller = new Poller(this.options.poller_url, { @@ -158,21 +158,42 @@ Webmail.prototype = { /** * Setup an "infinite scroll" behaviour for the email list. */ - setup_infinite_scroll: function() { + setup_infinite_scroll: function(data) { + var $container = $("#listing"); var that = this; - $("#listing").infinite_scroll({ + if ($container.data("infinite-scroll") !== undefined) { + $container.scrollTop(10); + $container.infinite_scroll( + "reset_loaded_pages", parseInt(data.page)); + $container.infinite_scroll("resume"); + return; + } + + $container.infinite_scroll({ + initial_page: parseInt(data.page), url: this.options.listing_url, calculate_bottom: function($element) { return $("#emails").height() - $element.height(); }, get_args: function() { - return that.navparams; + var args = that.navparams; + args.scroll = true; + return args; }, - process_results: function(data) { - var content = $("#emails").html() + data.listing; + process_results: function(data, direction) { + var $emails = $("#emails"); - $("#emails").html(content); + if (direction === "down") { + $emails.html($emails.html() + data.listing); + } else { + var row_id = $emails.children(".email").first().attr("id"); + + $emails.html(data.listing + $emails.html()); + + var $row = $("#" + row_id); + $("#listing").scrollTop($row.offset().top); + } }, end_of_list_reached: function($element) { $element.append( @@ -184,6 +205,17 @@ Webmail.prototype = { }); }, + /** + * Disable the infinite scroll mode. + */ + disable_infinite_scroll: function() { + var $container = $("#listing"); + + if ($container.data("infinite-scroll") !== undefined) { + $container.infinite_scroll("pause"); + } + }, + record_unseen_messages: function() { var unseen_counters = {}; @@ -319,7 +351,14 @@ Webmail.prototype = { $("#pagination").html(response.navbar); $("#pagination-responsive").html(response.navbar); }*/ + $("#listing").html(response.listing); + + if (this.navobject.getparam("action") == "listmailbox") { + this.setup_infinite_scroll(response); + } else { + this.disable_infinite_scroll(); + } }, /* @@ -871,7 +910,7 @@ Webmail.prototype = { $("body").notify("success", gettext("Message sent"), 2000); }, this)).fail($.proxy(function(jqxhr) { var data = $.parseJSON(jqxhr.responseText); - if (data != undefined) { + if (data !== undefined) { this.compose_callback(data); } $("a[name=sendmail]").attr("disabled", null); @@ -917,7 +956,9 @@ Webmail.prototype = { this.htmltable = $("#emails").data("htmltable"); this.init_draggables(); $("#listing").css("overflow", "auto"); - this.setup_infinite_scroll(); + if (this.navobject.hasparam("reset_page")) { + this.navobject.delparam("reset_page").update(false, true); + } }, add_field: function(e, name) { @@ -965,10 +1006,10 @@ Webmail.prototype = { viewmail_callback: function(resp) { this.page_update(resp); $("#listing").css("overflow", "hidden"); - $("a[name=back]").click($.proxy(function(e) { - e.preventDefault(); - this.go_back_to_listing(); - }, this)); + // $("a[name=back]").click($.proxy(function(e) { + // e.preventDefault(); + // this.go_back_to_listing(); + // }, this)); }, mark_callback: function(data) { diff --git a/modoboa/extensions/webmail/templates/webmail/email_list.html b/modoboa/extensions/webmail/templates/webmail/email_list.html index cf32f3042..60ecf273c 100644 --- a/modoboa/extensions/webmail/templates/webmail/email_list.html +++ b/modoboa/extensions/webmail/templates/webmail/email_list.html @@ -3,16 +3,16 @@ {% if with_top_div %}
{% endif %} {% for email in email_list %} -
+
- {{ email.from|parse_imap_header:"from" }} + {{ email.subject|parse_imap_header:"subject" }}

- {{ email.subject|parse_imap_header:"subject" }} + {{ email.from|parse_imap_header:"from" }}

diff --git a/modoboa/extensions/webmail/templatetags/webmail_tags.py b/modoboa/extensions/webmail/templatetags/webmail_tags.py index 396e420cd..de0bdbfcd 100644 --- a/modoboa/extensions/webmail/templatetags/webmail_tags.py +++ b/modoboa/extensions/webmail/templatetags/webmail_tags.py @@ -18,7 +18,9 @@ register = template.Library() def viewmail_menu(selection, folder, user, mail_id=None): entries = [ {"name": "back", - "url": "javascript:history.go(-1);", + # "url": reverse("modoboa.extensions.webmail.views.index") + # + "#?action=listmailbox&mbox=%s" % folder, + "url": "javascript:history.go(-1)", "img": "fa fa-arrow-left", "class": "btn-default sm-margin-left-back", "label": _("Back")}, diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index c7e366432..9d6f59386 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -335,6 +335,7 @@ def listmailbox(request, defmailbox="INBOX", update_session=True): :return: a dictionnary """ navparams = WebmailNavigationParameters(request, defmailbox) + previous_page_id = int(navparams["page"]) if update_session: navparams.store() mbox = navparams.get('mbox') @@ -345,7 +346,7 @@ def listmailbox(request, defmailbox="INBOX", update_session=True): elems_per_page=int(parameters.get_user(request.user, "MESSAGES_PER_PAGE")), **request.session["webmail_navparams"] ) - page_id = int(navparams.get('page')) + page_id = int(navparams["page"]) page = lst.paginator.getpage(page_id) if page is not None: email_list = lst.mbc.fetch( @@ -353,11 +354,14 @@ def listmailbox(request, defmailbox="INBOX", update_session=True): nbelems=int(parameters.get_user(request.user, "MESSAGES_PER_PAGE")) ) content = _render_to_string(request, "webmail/email_list.html", { - "email_list": email_list, "with_top_div": page_id == 1 + "email_list": email_list, + "page": page_id, + "with_top_div": request.GET.get("scroll", "false") == "false" }) else: content = "" - return {"listing": content, "length": len(content)} + navparams["page"] = previous_page_id + return {"listing": content, "length": len(content), "page": page_id} #return lst.render(request, navparams.get('page')) diff --git a/modoboa/lib/webutils.py b/modoboa/lib/webutils.py index ac882535c..20bdcebb6 100644 --- a/modoboa/lib/webutils.py +++ b/modoboa/lib/webutils.py @@ -181,18 +181,25 @@ class NavigationParameters(object): return self.request.session[self.sessionkey][key] def __contains__(self, key): - """ - """ + """Check if key is present.""" return key in self.request.session[self.sessionkey] + def __setitem__(self, key, value): + """Set a new item.""" + self.request.session[self.sessionkey][key] = value + + def _store_page(self): + """Specific method to store the current page.""" + self["page"] = int(self.request.GET.get("page", 1)) + def store(self): """Store navigation parameters into session. """ if not self.sessionkey in self.request.session: self.request.session[self.sessionkey] = {} + self._store_page() navparams = self.request.session[self.sessionkey] navparams["order"] = self.request.GET.get("sort_order", "-date") - navparams["page"] = int(self.request.GET.get("page", 1)) for param, defvalue, escape in self.parameters: value = self.request.GET.get(param, defvalue) if value is None: diff --git a/modoboa/static/js/history.js b/modoboa/static/js/history.js index 2d21dca4f..e659c09b9 100644 --- a/modoboa/static/js/history.js +++ b/modoboa/static/js/history.js @@ -1,5 +1,22 @@ +/** + * Creates an instance of History. + * + * @constructor + * @this {History} + * @param {dictionary} options - instance options + */ var History = function(options) { - this.initialize(options); + this.options = $.extend({}, this.defaults, options); + this.callbacks = {}; + this.base = ''; + this.serialized = null; + this.updatenext = true; + this.load_params(); + if (this.options.defcallback) { + this.register_callback("default", this.options.defcallback); + this.check_id = + setInterval($.proxy(this.check, this), this.options.checkinterval); + } }; History.prototype = { @@ -11,21 +28,7 @@ History.prototype = { defcallback: null }, - initialize: function(options) { - this.options = $.extend({}, this.defaults, options); - this.callbacks = {}; - this.base = ''; - this.serialized = null; - this.updatenext = true; - this.load_params(); - if (this.options.defcallback) { - this.register_callback("default", this.options.defcallback); - this.check_id = - setInterval($.proxy(this.check, this), this.options.checkinterval); - } - }, - - /* + /** * Return the encoded hash part of the URL (some browsers like FF * automatically decode the location.hash variable). */ @@ -61,10 +64,10 @@ History.prototype = { }, parse_string: function(value, reset) { - var splits = (value.charAt(0) == '#') - ? value.substring(1).split('?') : value.split('?'); + var splits = (value.charAt(0) == '#') ? + value.substring(1).split('?') : value.split('?'); - if (splits.length == 0) { + if (splits.length === 0) { this.base = this.options.deflocation; return this; } @@ -75,7 +78,7 @@ History.prototype = { this.reset(); } - if ((this.base = splits[0]) != "") { + if ((this.base = splits[0]) !== "") { var re = new RegExp("/$"); if (!this.base.match(re)) { this.base += '/'; @@ -101,7 +104,7 @@ History.prototype = { var res = this.base; var args = ""; - if (this.paramslength() != 0) { + if (this.paramslength() !== 0) { args += "?"; $.each(this.params, function(key, value) { if (args != "?") { @@ -113,6 +116,12 @@ History.prototype = { return res + args; }, + /** + * Update the current location. + * + * @param {boolean} force - force the update + * @param {boolean} noupdate - disable the next update + */ update: function(force, noupdate) { location.hash = this.serialize(); @@ -128,20 +137,39 @@ History.prototype = { } }, - updateparams: function(str) { - if (str.charAt(0) == '?') { - str = str.substring(1); - } - var elems = str.split('&'); - for (var i = 0; i < elems.length; i++) { - var pdef = elems[i].split('='); - this.setparam(pdef[0], pdef[1]); - } - return this; + /** + * Check if a parameter is present. + * + * @param {string} name - name of the parameter + * @return {boolean} true if parameter is present, false otherwise + */ + hasparam: function(name) { + return this.params[name] !== undefined; }, + /** + * Retrieve a parameter. + * + * @param {string} name - name of the parameter + * @return {string} the parameter's value (URI decoded) + */ + getparam: function(name, defvalue) { + if (this.params[name] === undefined) { + return (defvalue !== undefined) ? defvalue : undefined; + } + return decodeURIComponent(this.params[name]); + }, + + /** + * Set a parameter. + * + * @param {string} name - name of the parameter + * @param {string} value - parameter's value + * @param {boolean} encode - URI encode the parameter if true + * @return {History} + */ setparam: function(name, value, encode) { - if (encode == undefined || encode) { + if (encode === undefined || encode) { this.params[name] = encodeURIComponent(value); } else { this.params[name] = value; @@ -154,15 +182,34 @@ History.prototype = { return this; }, - getparam: function(name, defvalue) { - if (this.params[name] === undefined) { - return (defvalue != undefined) ? defvalue : undefined; + updateparams: function(str) { + if (str.charAt(0) == '?') { + str = str.substring(1); } - return decodeURIComponent(this.params[name]); + var elems = str.split('&'); + for (var i = 0; i < elems.length; i++) { + var pdef = elems[i].split('='); + this.setparam(pdef[0], pdef[1]); + } + return this; + }, + + /** + * Delete a parameter. + * + * @param {string} name - name of the parameter + * @return {History} + */ + delparam: function(name) { + if (this.params[name] === undefined) { + return this; + } + delete(this.params[name]); + return this; }, baseurl: function(value, noreset) { - if (noreset === undefined || noreset == 0) { + if (noreset === undefined || noreset === 0) { this.reset(); } this.base = value; @@ -182,14 +229,6 @@ History.prototype = { return decodeURIComponent(this.base.substr(0, this.base.length - 1)); }, - delparam: function(name) { - if (this.params[name] == undefined) { - return this; - } - delete(this.params[name]); - return this; - }, - register_callback: function(name, callback) { this.callbacks[name] = callback; }, @@ -229,7 +268,7 @@ History.prototype = { cache: false, dataType: 'json' }).done($.proxy(function(resp) { - var callback = (resp.callback != undefined) ? resp.callback : "default"; + var callback = (resp.callback !== undefined) ? resp.callback : "default"; this.callbacks[callback](resp); if (resp.respmsg) { @@ -237,4 +276,4 @@ History.prototype = { } }, this)); } -}; \ No newline at end of file +}; diff --git a/modoboa/static/js/jquery.infinitescroll.js b/modoboa/static/js/jquery.infinitescroll.js index 841e9ae48..e577b09b2 100644 --- a/modoboa/static/js/jquery.infinitescroll.js +++ b/modoboa/static/js/jquery.infinitescroll.js @@ -1,77 +1,185 @@ /** * Infinite scroll plugin for jQuery. * + * @author {Antoine Nguyen} */ !function($) { 'use strict'; + /** + * Creates an instance of InfiniteScroll. + * + * @constructor + * @this {InfiniteScroll} + * @param {object} element + * @param {dictionary} options - instance options + */ var InfiniteScroll = function(element, options) { this.$element = $(element); this.$data = $(element).data(); this.$options = options; this.executing = false; this.end_of_results = false; - this.current_page = 1; - var that = this; + this.current_page = (isNaN(this.$options.initial_page)) ? + 1 : this.$options.initial_page; + this.loaded_pages = [this.current_page]; - this.$element.scroll(function() { - if (that.$element.scrollTop() >= that.$options.calculate_bottom(that.$element)) { - that.load_more(); - } - }); + this.resume(); }; InfiniteScroll.prototype = { constructor: InfiniteScroll, - load_more: function() { + /** + * Scroll event handler. + * + * @this {InfiniteScroll} + */ + on_scroll: function() { + var bottom = this.$options.calculate_bottom(this.$element); + + if (this.$element.scrollTop() >= bottom) { + this.load_next_page(); + } else if (this.$element.scrollTop() === 0) { + this.load_previous_page(); + } + }, + + /** + * Load the previous page from the server. + * + * @private + * @this {InfiniteScroll} + */ + load_previous_page: function() { + var $this = this; + + if ($this.executing || $.inArray(1, $this.loaded_pages) != -1) { + return; + } + + while ($this.current_page > 0) { + $this.current_page -= 1; + if ($.inArray($this.current_page, $this.loaded_pages) == -1) { + break; + } + } + if ($this.current_page === 0) { + $this.current_page = 1; + return; + } + this.load_page("up"); + }, + + /** + * Load the next page from the server. + * + * @this {InfiniteScroll} + */ + load_next_page: function() { var $this = this; if ($this.executing || $this.end_of_results) { return; } - /*$this.$element.find('.spinner').removeClass('hide');*/ - $this.executing = true; - $this.current_page += 1; - var args = $this.$options.get_args(); - args.page = $this.current_page; + while (true) { + $this.current_page += 1; + if ($.inArray($this.current_page, $this.loaded_pages) == -1) { + break; + } + } + this.load_page("down"); + }, + + /** + * Load a page's content. + * + * @private + * @this {InfiniteScroll} + * @param {string} direction - direction of the scroll (up or down) + */ + load_page: function(direction) { + var args = this.$options.get_args(); + var $this = this; + + this.executing = true; + args.page = this.current_page; $.ajax({ contentType: 'application/json; charset=UTF-8', data: args, url: $this.$options.url, type: 'GET', success: function(data) { - $this.$options.process_results(data); + $this.$options.process_results(data, direction); if (data.length === 0) { $this.end_of_results = true; $this.$options.end_of_list_reached($this.$element); } - //$this.$element.find('.spinner').addClass('hide'); $this.executing = false; + $this.loaded_pages.push($this.current_page); + console.log("page", $this.current_page, "loaded successfuly"); } }); + }, + + /** + * Reset the loaded pages cache. + * + * @this {InfiniteScroll} + * @param {Array} current_page - the new current_page + */ + reset_loaded_pages: function(current_page) { + this.current_page = current_page; + this.loaded_pages = [this.current_page]; + }, + + /** + * Suspend the "infinite scroll" mode. + * + * @this {InfiniteScroll} + */ + pause: function() { + this.$element.unbind("scroll"); + }, + + /** + * Resume the "infinite scroll" mode. + * + * @this {InfiniteScroll} + */ + resume: function() { + this.$element.scroll($.proxy(this.on_scroll, this)); } }; $.fn.infinite_scroll = function (option) { + var args = arguments; + return this.each(function () { var $this = $(this), data = $this.data('infinite-scroll'), - options = $.extend({}, $.fn.infinite_scroll.defaults, typeof option == 'object' && option); + options = $.extend( + {}, $.fn.infinite_scroll.defaults, + typeof option == 'object' && option + ); if (!data) { - $this.data('infinite-scroll', (data = new InfiniteScroll(this, options))); + $this.data('infinite-scroll', ( + data = new InfiniteScroll(this, options)) + ); } - if (typeof options == 'string') { - data[options](); + if (typeof option == 'string') { + data[option].apply(data, Array.prototype.slice.call(args, 1)); } }); }; $.fn.infinite_scroll.defaults = { + initial_page: 1, calculate_bottom: null, get_data: null, process_results: null, + end_of_list_reached: null, url: '' };