Added endless scroll (closed #5)

The code for paged collections now feels like playing ping-pong with
callbacks, and like I have no idea on who should render who.

It works, though.
This commit is contained in:
Marcin Kurczewski 2014-09-12 22:58:07 +02:00
parent 0828a0aa89
commit 12b43b1bb8
5 changed files with 170 additions and 108 deletions

View file

@ -1,73 +1,101 @@
var App = App || {}; var App = App || {};
App.Presenters = App.Presenters || {}; App.Presenters = App.Presenters || {};
App.Presenters.PagedCollectionPresenter = function(_, util, promise, api, mousetrap, router) { App.Presenters.PagedCollectionPresenter = function(
_,
jQuery,
util,
promise,
api,
mousetrap,
router,
browsingSettings) {
var endlessScroll = browsingSettings.getSettings().endlessScroll;
var scrollInterval;
var template;
var totalPages;
var forceClear;
var searchOrder;
var searchQuery;
var pageNumber; var pageNumber;
var searchParams;
var baseUri; var baseUri;
var backendUri; var backendUri;
var renderCallback; var updateCallback;
var failCallback; var failCallback;
var template;
var pageSize;
var totalPages;
var totalRecords;
function init(args) { function init(args) {
forceClear = !_.isEqual(args.searchParams, searchParams) || parseInt(args.page) !== pageNumber + 1;
searchParams = args.searchParams;
pageNumber = parseInt(args.page) || 1; pageNumber = parseInt(args.page) || 1;
searchOrder = args.order;
searchQuery = args.query;
baseUri = args.baseUri; baseUri = args.baseUri;
backendUri = args.backendUri; backendUri = args.backendUri;
renderCallback = args.renderCallback; updateCallback = args.updateCallback;
failCallback = args.failCallback; failCallback = args.failCallback;
promise.wait(util.promiseTemplate('pager')).then(function(html) { promise.wait(util.promiseTemplate('pager')).then(function(html) {
template = _.template(html); template = _.template(html);
changePage(pageNumber); softChangePage(pageNumber);
mousetrap.bind('a', prevPage); if (!endlessScroll) {
mousetrap.bind('d', nextPage); mousetrap.bind('a', function(e) {
if (!e.altKey && !e.ctrlKey) {
prevPage();
}
});
mousetrap.bind('d', function(e) {
if (!e.altKey && !e.ctrlKey) {
nextPage();
}
});
}
}); });
} }
function prevPage(e) { function prevPage() {
if (e.altKey || e.ctrlKey) {
return;
}
if (pageNumber > 1) { if (pageNumber > 1) {
router.navigate(getPageChangeLink(pageNumber - 1)); changePage(pageNumber - 1);
} }
} }
function nextPage(e) { function nextPage() {
if (e.altKey || e.ctrlKey) {
return;
}
if (pageNumber < totalPages) { if (pageNumber < totalPages) {
router.navigate(getPageChangeLink(pageNumber + 1)); changePage(pageNumber + 1);
} }
} }
function nextPageInplace() {
if (pageNumber < totalPages) {
changePageInplace(pageNumber + 1);
}
}
function changePageInplace(newPageNumber) {
router.navigateInplace(getPageChangeLink(newPageNumber));
}
function changePage(newPageNumber) { function changePage(newPageNumber) {
router.navigate(getPageChangeLink(newPageNumber));
}
function softChangePage(newPageNumber) {
pageNumber = newPageNumber; pageNumber = newPageNumber;
promise.wait( promise.wait(
api.get(backendUri, { api.get(backendUri, _.extend({}, searchParams, {page: pageNumber})))
order: searchOrder,
query: searchQuery,
page: pageNumber
}))
.then(function(response) { .then(function(response) {
pageSize = response.json.pageSize; var pageSize = response.json.pageSize;
totalRecords = response.json.totalRecords; var totalRecords = response.json.totalRecords;
totalPages = Math.ceil(totalRecords / pageSize); totalPages = Math.ceil(totalRecords / pageSize);
renderCallback({
var $target = updateCallback({
entities: response.json.data, entities: response.json.data,
totalRecords: response.json.totalRecords}); totalRecords: totalRecords},
forceClear || !endlessScroll);
forceClear = false;
render($target);
}).fail(function(response) { }).fail(function(response) {
if (typeof(failCallback) !== 'undefined') { if (typeof(failCallback) !== 'undefined') {
failCallback(response); failCallback(response);
@ -78,6 +106,29 @@ App.Presenters.PagedCollectionPresenter = function(_, util, promise, api, mouset
} }
function render($target) { function render($target) {
var pages = getVisiblePages();
if (!endlessScroll) {
$target.find('.pager').remove();
$target.append(template({
pages: pages,
pageNumber: pageNumber,
link: getPageChangeLink,
}));
} else {
var $scroller = jQuery('<div/>');
window.clearInterval(scrollInterval);
scrollInterval = window.setInterval(function() {
if ($scroller.is(':visible')) {
nextPageInplace();
window.clearInterval(scrollInterval);
}
}, 50);
$target.append($scroller);
}
}
function getVisiblePages() {
var pages = [1, totalPages]; var pages = [1, totalPages];
var pagesAroundCurrent = 2; var pagesAroundCurrent = 2;
for (var i = -pagesAroundCurrent; i <= pagesAroundCurrent; i ++) { for (var i = -pagesAroundCurrent; i <= pagesAroundCurrent; i ++) {
@ -96,46 +147,25 @@ App.Presenters.PagedCollectionPresenter = function(_, util, promise, api, mouset
return !pos || item !== pages[pos - 1]; return !pos || item !== pages[pos - 1];
}); });
$target.html(template({ return pages;
pages: pages,
pageNumber: pageNumber,
link: getPageChangeLink,
}));
} }
function getSearchQueryChangeLink(newSearchQuery) { function getSearchChangeLink(newSearchParams) {
return util.compileComplexRouteArgs(baseUri, { return util.compileComplexRouteArgs(baseUri, _.extend({}, searchParams, newSearchParams, {page: 1}));
page: 1,
order: searchOrder,
query: newSearchQuery,
});
}
function getSearchOrderChangeLink(newSearchOrder) {
return util.compileComplexRouteArgs(baseUri, {
page: 1,
order: newSearchOrder,
query: searchQuery,
});
} }
function getPageChangeLink(newPageNumber) { function getPageChangeLink(newPageNumber) {
return util.compileComplexRouteArgs(baseUri, { return util.compileComplexRouteArgs(baseUri, _.extend({}, searchParams, {page: newPageNumber}));
page: newPageNumber,
order: searchOrder,
query: searchQuery,
});
} }
return { return {
init: init, init: init,
render: render, render: render,
changePage: changePage, changePage: changePage,
getSearchQueryChangeLink: getSearchQueryChangeLink, getSearchChangeLink: getSearchChangeLink,
getSearchOrderChangeLink: getSearchOrderChangeLink,
getPageChangeLink: getPageChangeLink getPageChangeLink: getPageChangeLink
}; };
}; };
App.DI.register('pagedCollectionPresenter', ['_', 'util', 'promise', 'api', 'mousetrap', 'router'], App.Presenters.PagedCollectionPresenter); App.DI.register('pagedCollectionPresenter', ['_', 'jQuery', 'util', 'promise', 'api', 'mousetrap', 'router', 'browsingSettings'], App.Presenters.PagedCollectionPresenter);

View file

@ -13,33 +13,41 @@ App.Presenters.UserListPresenter = function(
messagePresenter) { messagePresenter) {
var $el = jQuery('#content'); var $el = jQuery('#content');
var template; var listTemplate;
var userList = []; var itemTemplate;
var activeSearchOrder;
function init(args) { function init(args) {
topNavigationPresenter.select('users'); topNavigationPresenter.select('users');
topNavigationPresenter.changeTitle('Users'); topNavigationPresenter.changeTitle('Users');
promise.wait(util.promiseTemplate('user-list')).then(function(html) { promise.waitAll(
template = _.template(html); util.promiseTemplate('user-list'),
initPaginator(args); util.promiseTemplate('user-list-item')).then(function(listHtml, itemHtml) {
listTemplate = _.template(listHtml);
itemTemplate = _.template(itemHtml);
render();
reinit(args);
}); });
} }
function initPaginator(args) { function reinit(args) {
var searchArgs = util.parseComplexRouteArgs(args.searchArgs); var searchArgs = util.parseComplexRouteArgs(args.searchArgs);
searchArgs.order = searchArgs.order || 'name'; searchArgs.order = searchArgs.order || 'name';
activeSearchOrder = searchArgs.order;
updateActiveOrder(searchArgs.order);
initPaginator(searchArgs);
}
function initPaginator(searchArgs) {
pagedCollectionPresenter.init({ pagedCollectionPresenter.init({
page: searchArgs.page, page: searchArgs.page,
order: searchArgs.order, searchParams: {order: searchArgs.order},
baseUri: '#/users', baseUri: '#/users',
backendUri: '/users', backendUri: '/users',
renderCallback: function updateCollection(data) { updateCallback: function(data, clear) {
userList = data.entities; renderUsers(data.entities, clear);
render(); return $el.find('.pagination-content');
}, },
failCallback: function(response) { failCallback: function(response) {
$el.empty(); $el.empty();
@ -48,28 +56,44 @@ App.Presenters.UserListPresenter = function(
} }
function render() { function render() {
$el.html(template({ $el.html(listTemplate());
userList: userList,
formatRelativeTime: util.formatRelativeTime,
}));
$el.find('.order a').click(orderLinkClicked); $el.find('.order a').click(orderLinkClicked);
$el.find('.order [data-order="' + activeSearchOrder + '"]').parent('li').addClass('active'); }
var $pager = $el.find('.pager'); function updateActiveOrder(activeOrder) {
pagedCollectionPresenter.render($pager); $el.find('.order li').removeClass('active');
$el.find('.order [data-order="' + activeOrder + '"]').parent('li').addClass('active');
}
function renderUsers(users, clear) {
var $target = $el.find('.users');
var all = '';
_.each(users, function(user) {
all += itemTemplate({
user: user,
formatRelativeTime: util.formatRelativeTime,
});
});
if (clear) {
$target.html(all);
} else {
$target.append(all);
}
} }
function orderLinkClicked(e) { function orderLinkClicked(e) {
e.preventDefault(); e.preventDefault();
var $orderLink = jQuery(this); var $orderLink = jQuery(this);
activeSearchOrder = $orderLink.attr('data-order'); var activeSearchOrder = $orderLink.attr('data-order');
router.navigate(pagedCollectionPresenter.getSearchOrderChangeLink(activeSearchOrder)); router.navigate(pagedCollectionPresenter.getSearchChangeLink({order: activeSearchOrder}));
} }
return { return {
init: init, init: init,
reinit: initPaginator, reinit: reinit,
render: render render: render,
}; };
}; };

View file

@ -14,6 +14,15 @@ App.Router = function(pathJs, _, jQuery, util, appState, presenterManager) {
window.location.href = url; window.location.href = url;
} }
function navigateInplace(url) {
if ('replaceState' in history) {
history.replaceState('', '', url);
pathJs.dispatch(document.location.hash);
} else {
navigate(url);
}
}
function start() { function start() {
pathJs.listen(); pathJs.listen();
} }
@ -54,6 +63,7 @@ App.Router = function(pathJs, _, jQuery, util, appState, presenterManager) {
return { return {
start: start, start: start,
navigate: navigate, navigate: navigate,
navigateInplace: navigateInplace,
navigateToMainPage: navigateToMainPage, navigateToMainPage: navigateToMainPage,
}; };

View file

@ -0,0 +1,18 @@
<li class="user">
<a href="#/user/<%= user.name %>">
<img src="/api/users/<%= user.name %>/avatar/80" alt="<%= user.name %>"/>
</a>
<div class="details">
<h1>
<a href="#/user/<%= user.name %>">
<%= user.name %>
</a>
</h1>
<div class="date-joined" title="<%= user.registrationTime %>">
Joined: <%= formatRelativeTime(user.registrationTime) %>
</div>
<div class="date-seen">
Last seen: <%= formatRelativeTime(user.lastLoginTime) %>
</div>
</div>
</li>

View file

@ -14,28 +14,8 @@
</li> </li>
</ul> </ul>
<ul class="users"> <div class="pagination-content">
<% _.each(userList, function(user) { %> <ul class="users">
<li class="user"> </ul>
<a href="#/user/<%= user.name %>"> </div>
<img src="/api/users/<%= user.name %>/avatar/80" alt="<%= user.name %>"/>
</a>
<div class="details">
<h1>
<a href="#/user/<%= user.name %>">
<%= user.name %>
</a>
</h1>
<div class="date-joined" title="<%= user.registrationTime %>">
Joined: <%= formatRelativeTime(user.registrationTime) %>
</div>
<div class="date-seen">
Last seen: <%= formatRelativeTime(user.lastLoginTime) %>
</div>
</div>
</li>
<% }); %>
</ul>
<div class="pager"></div>
</div> </div>