Infinite scroll improvement (webmail).

see #585
This commit is contained in:
Antoine Nguyen
2014-09-24 11:46:22 +02:00
parent 4659f97327
commit b2fbf5e009
8 changed files with 299 additions and 89 deletions
+9
View File
@@ -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)
@@ -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) {
@@ -3,16 +3,16 @@
{% if with_top_div %}<div id="emails">{% endif %}
{% for email in email_list %}
<div id="{{ email.imapid }}" class="row email{% if email.style %} {{ email.style }}{% endif %}">
<div id="{{ email.imapid }}" class="row email{% if email.style %} {{ email.style }}{% endif %}" data-page="{{ page }}">
<div class="hidden-xs col-sm-1">
<img name="drag" src="{{ STATIC_URL }}pics/grippy.png" class="draggable" />
<input type="checkbox" />
</div>
<div class="col-xs-9 col-sm-9 openable">
{{ email.from|parse_imap_header:"from" }}
{{ email.subject|parse_imap_header:"subject" }}
<p class="text-muted">
{{ email.subject|parse_imap_header:"subject" }}
{{ email.from|parse_imap_header:"from" }}
</p>
</div>
<div class="col-xs-3 col-sm-2 text-right">
@@ -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")},
+7 -3
View File
@@ -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'))
+10 -3
View File
@@ -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:
+86 -47
View File
@@ -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));
}
};
};
+127 -19
View File
@@ -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: ''
};