From 12b43b1bb896d3cb60e597a5c0766f4e70d89d90 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Fri, 12 Sep 2014 22:58:07 +0200 Subject: [PATCH] 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. --- .../js/Presenters/PagedCollectionPresenter.js | 152 +++++++++++------- .../js/Presenters/UserListPresenter.js | 70 +++++--- public_html/js/Router.js | 10 ++ public_html/templates/user-list-item.tpl | 18 +++ public_html/templates/user-list.tpl | 28 +--- 5 files changed, 170 insertions(+), 108 deletions(-) create mode 100644 public_html/templates/user-list-item.tpl diff --git a/public_html/js/Presenters/PagedCollectionPresenter.js b/public_html/js/Presenters/PagedCollectionPresenter.js index c39aac33..cad4f4f4 100644 --- a/public_html/js/Presenters/PagedCollectionPresenter.js +++ b/public_html/js/Presenters/PagedCollectionPresenter.js @@ -1,73 +1,101 @@ var App = App || {}; 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 searchParams; var baseUri; var backendUri; - var renderCallback; + var updateCallback; var failCallback; - var template; - var pageSize; - var totalPages; - var totalRecords; - function init(args) { + forceClear = !_.isEqual(args.searchParams, searchParams) || parseInt(args.page) !== pageNumber + 1; + searchParams = args.searchParams; pageNumber = parseInt(args.page) || 1; - searchOrder = args.order; - searchQuery = args.query; + baseUri = args.baseUri; backendUri = args.backendUri; - renderCallback = args.renderCallback; + updateCallback = args.updateCallback; failCallback = args.failCallback; promise.wait(util.promiseTemplate('pager')).then(function(html) { template = _.template(html); - changePage(pageNumber); + softChangePage(pageNumber); - mousetrap.bind('a', prevPage); - mousetrap.bind('d', nextPage); + if (!endlessScroll) { + 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) { - if (e.altKey || e.ctrlKey) { - return; - } + function prevPage() { if (pageNumber > 1) { - router.navigate(getPageChangeLink(pageNumber - 1)); + changePage(pageNumber - 1); } } - function nextPage(e) { - if (e.altKey || e.ctrlKey) { - return; - } + function nextPage() { 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) { + router.navigate(getPageChangeLink(newPageNumber)); + } + + function softChangePage(newPageNumber) { pageNumber = newPageNumber; promise.wait( - api.get(backendUri, { - order: searchOrder, - query: searchQuery, - page: pageNumber - })) + api.get(backendUri, _.extend({}, searchParams, {page: pageNumber}))) .then(function(response) { - pageSize = response.json.pageSize; - totalRecords = response.json.totalRecords; + var pageSize = response.json.pageSize; + var totalRecords = response.json.totalRecords; totalPages = Math.ceil(totalRecords / pageSize); - renderCallback({ + + var $target = updateCallback({ entities: response.json.data, - totalRecords: response.json.totalRecords}); + totalRecords: totalRecords}, + forceClear || !endlessScroll); + forceClear = false; + + render($target); }).fail(function(response) { if (typeof(failCallback) !== 'undefined') { failCallback(response); @@ -78,6 +106,29 @@ App.Presenters.PagedCollectionPresenter = function(_, util, promise, api, mouset } 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('
'); + 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 pagesAroundCurrent = 2; 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]; }); - $target.html(template({ - pages: pages, - pageNumber: pageNumber, - link: getPageChangeLink, - })); + return pages; } - function getSearchQueryChangeLink(newSearchQuery) { - return util.compileComplexRouteArgs(baseUri, { - page: 1, - order: searchOrder, - query: newSearchQuery, - }); - } - - function getSearchOrderChangeLink(newSearchOrder) { - return util.compileComplexRouteArgs(baseUri, { - page: 1, - order: newSearchOrder, - query: searchQuery, - }); + function getSearchChangeLink(newSearchParams) { + return util.compileComplexRouteArgs(baseUri, _.extend({}, searchParams, newSearchParams, {page: 1})); } function getPageChangeLink(newPageNumber) { - return util.compileComplexRouteArgs(baseUri, { - page: newPageNumber, - order: searchOrder, - query: searchQuery, - }); + return util.compileComplexRouteArgs(baseUri, _.extend({}, searchParams, {page: newPageNumber})); } return { init: init, render: render, changePage: changePage, - getSearchQueryChangeLink: getSearchQueryChangeLink, - getSearchOrderChangeLink: getSearchOrderChangeLink, + getSearchChangeLink: getSearchChangeLink, 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); diff --git a/public_html/js/Presenters/UserListPresenter.js b/public_html/js/Presenters/UserListPresenter.js index 0499f496..6935ac18 100644 --- a/public_html/js/Presenters/UserListPresenter.js +++ b/public_html/js/Presenters/UserListPresenter.js @@ -13,33 +13,41 @@ App.Presenters.UserListPresenter = function( messagePresenter) { var $el = jQuery('#content'); - var template; - var userList = []; - var activeSearchOrder; + var listTemplate; + var itemTemplate; function init(args) { topNavigationPresenter.select('users'); topNavigationPresenter.changeTitle('Users'); - promise.wait(util.promiseTemplate('user-list')).then(function(html) { - template = _.template(html); - initPaginator(args); + promise.waitAll( + util.promiseTemplate('user-list'), + 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); searchArgs.order = searchArgs.order || 'name'; - activeSearchOrder = searchArgs.order; + updateActiveOrder(searchArgs.order); + initPaginator(searchArgs); + } + + function initPaginator(searchArgs) { pagedCollectionPresenter.init({ page: searchArgs.page, - order: searchArgs.order, + searchParams: {order: searchArgs.order}, baseUri: '#/users', backendUri: '/users', - renderCallback: function updateCollection(data) { - userList = data.entities; - render(); + updateCallback: function(data, clear) { + renderUsers(data.entities, clear); + return $el.find('.pagination-content'); }, failCallback: function(response) { $el.empty(); @@ -48,28 +56,44 @@ App.Presenters.UserListPresenter = function( } function render() { - $el.html(template({ - userList: userList, - formatRelativeTime: util.formatRelativeTime, - })); + $el.html(listTemplate()); $el.find('.order a').click(orderLinkClicked); - $el.find('.order [data-order="' + activeSearchOrder + '"]').parent('li').addClass('active'); + } - var $pager = $el.find('.pager'); - pagedCollectionPresenter.render($pager); + function updateActiveOrder(activeOrder) { + $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) { e.preventDefault(); var $orderLink = jQuery(this); - activeSearchOrder = $orderLink.attr('data-order'); - router.navigate(pagedCollectionPresenter.getSearchOrderChangeLink(activeSearchOrder)); + var activeSearchOrder = $orderLink.attr('data-order'); + router.navigate(pagedCollectionPresenter.getSearchChangeLink({order: activeSearchOrder})); } return { init: init, - reinit: initPaginator, - render: render + reinit: reinit, + render: render, }; }; diff --git a/public_html/js/Router.js b/public_html/js/Router.js index e920167b..3c34e931 100644 --- a/public_html/js/Router.js +++ b/public_html/js/Router.js @@ -14,6 +14,15 @@ App.Router = function(pathJs, _, jQuery, util, appState, presenterManager) { window.location.href = url; } + function navigateInplace(url) { + if ('replaceState' in history) { + history.replaceState('', '', url); + pathJs.dispatch(document.location.hash); + } else { + navigate(url); + } + } + function start() { pathJs.listen(); } @@ -54,6 +63,7 @@ App.Router = function(pathJs, _, jQuery, util, appState, presenterManager) { return { start: start, navigate: navigate, + navigateInplace: navigateInplace, navigateToMainPage: navigateToMainPage, }; diff --git a/public_html/templates/user-list-item.tpl b/public_html/templates/user-list-item.tpl new file mode 100644 index 00000000..7300aeed --- /dev/null +++ b/public_html/templates/user-list-item.tpl @@ -0,0 +1,18 @@ +
  • + + <%= user.name %> + +
    +

    + + <%= user.name %> + +

    +
    + Joined: <%= formatRelativeTime(user.registrationTime) %> +
    +
    + Last seen: <%= formatRelativeTime(user.lastLoginTime) %> +
    +
    +
  • diff --git a/public_html/templates/user-list.tpl b/public_html/templates/user-list.tpl index 2d2fba60..9d08fc8f 100644 --- a/public_html/templates/user-list.tpl +++ b/public_html/templates/user-list.tpl @@ -14,28 +14,8 @@ - - -
    +
    +
      +
    +