From 688b5b1281264993a5cb870d52c6c539dc43b2f9 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sun, 5 Oct 2014 10:09:02 +0200 Subject: [PATCH] Added prev/next post controls --- TODO | 4 -- public_html/css/post.css | 19 +++++ public_html/index.html | 2 + public_html/js/Keyboard.js | 5 ++ public_html/js/Pager.js | 26 ++++++- .../js/Presenters/PostListPresenter.js | 2 + public_html/js/Presenters/PostPresenter.js | 46 +++++++++++- .../js/Presenters/UserListPresenter.js | 1 + public_html/js/Router.js | 2 +- .../js/Services/PostsAroundCalculator.js | 70 +++++++++++++++++++ public_html/templates/post-list-item.tpl | 8 ++- public_html/templates/post.tpl | 24 +++++++ 12 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 public_html/js/Services/PostsAroundCalculator.js diff --git a/TODO b/TODO index 8a889f9c..bf4a80b3 100644 --- a/TODO +++ b/TODO @@ -6,10 +6,6 @@ everything related to posts: - better thumbnail loading - single post view - - previous and next post (difficult) - - remember last search - - take care of pages - - add A/D hotkeys - editing - ability to loop video posts diff --git a/public_html/css/post.css b/public_html/css/post.css index b3e1f011..d61c5b9c 100644 --- a/public_html/css/post.css +++ b/public_html/css/post.css @@ -9,6 +9,25 @@ border: 0; } +#post-current-search-wrapper { + text-align: center; +} +#post-current-search { + margin: 0 auto 1em auto; + display: inline-block; +} + +#post-current-search a { + margin: 0 2em; +} +#post-current-search a, +#post-current-search div { + display: inline-block; +} +#post-current-search a:not(.enabled) { + color: silver; +} + #post-view-wrapper #sidebar { line-height: 1.33em; font-size: 90%; diff --git a/public_html/index.html b/public_html/index.html index 3387a340..dbef564a 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -89,6 +89,8 @@ + + diff --git a/public_html/js/Keyboard.js b/public_html/js/Keyboard.js index 11fef3d8..2261ce87 100644 --- a/public_html/js/Keyboard.js +++ b/public_html/js/Keyboard.js @@ -22,10 +22,15 @@ App.Keyboard = function(mousetrap) { mousetrap.reset(); } + function unbind(key) { + mousetrap.unbind(key); + } + return { keydown: keydown, keyup: keyup, reset: reset, + unbind: unbind, }; }; diff --git a/public_html/js/Pager.js b/public_html/js/Pager.js index 398f8026..6062631e 100644 --- a/public_html/js/Pager.js +++ b/public_html/js/Pager.js @@ -9,6 +9,7 @@ App.Pager = function( var pageNumber; var searchParams; var url; + var cache = {}; function init(args) { url = args.url; @@ -55,7 +56,7 @@ App.Pager = function( function retrieve() { return promise.make(function(resolve, reject) { - promise.wait(api.get(url, _.extend({}, searchParams, {page: pageNumber}))) + promise.wait(api.get(url, _.extend({page: pageNumber}, searchParams))) .then(function(response) { var pageSize = response.json.pageSize; var totalRecords = response.json.totalRecords; @@ -71,6 +72,27 @@ App.Pager = function( }); } + function retrieveCached() { + return promise.make(function(resolve, reject) { + var cacheKey = JSON.stringify(_.extend({}, searchParams, {url: url, page: getPage()})); + if (cacheKey in cache) { + resolve.apply(this, cache[cacheKey]); + } else { + promise.wait(retrieve()) + .then(function() { + cache[cacheKey] = arguments; + resolve.apply(this, arguments); + }).fail(function() { + reject.apply(this, arguments); + }); + } + }); + } + + function resetCache() { + cache = {}; + } + function getVisiblePages() { var pages = [1, totalPages || 1]; var pagesAroundCurrent = 2; @@ -100,7 +122,9 @@ App.Pager = function( getSearchParams: getSearchParams, setSearchParams: setSearchParams, retrieve: retrieve, + retrieveCached: retrieveCached, getVisiblePages: getVisiblePages, + resetCache: resetCache, }; }; diff --git a/public_html/js/Presenters/PostListPresenter.js b/public_html/js/Presenters/PostListPresenter.js index 44db2976..abd65084 100644 --- a/public_html/js/Presenters/PostListPresenter.js +++ b/public_html/js/Presenters/PostListPresenter.js @@ -24,6 +24,7 @@ App.Presenters.PostListPresenter = function( topNavigationPresenter.select('posts'); topNavigationPresenter.changeTitle('Posts'); searchArgs = util.parseComplexRouteArgs(args.searchArgs); + searchArgs.page = parseInt(searchArgs.page) || 1; promise.wait( util.promiseTemplate('post-list'), @@ -93,6 +94,7 @@ App.Presenters.PostListPresenter = function( _.each(posts, function(post) { $target.append(jQuery('
  • ' + itemTemplate({ + searchArgs: searchArgs, post: post, }) + '
  • ')); }); diff --git a/public_html/js/Presenters/PostPresenter.js b/public_html/js/Presenters/PostPresenter.js index 1740c215..196b9cf2 100644 --- a/public_html/js/Presenters/PostPresenter.js +++ b/public_html/js/Presenters/PostPresenter.js @@ -11,6 +11,7 @@ App.Presenters.PostPresenter = function( router, keyboard, presenterManager, + postsAroundCalculator, postCommentListPresenter, topNavigationPresenter, messagePresenter) { @@ -23,11 +24,13 @@ App.Presenters.PostPresenter = function( var postContentTemplate; var historyTemplate; + var postNameOrId; + var searchArgs; + var post; var postScore; var postFavorites; var postHistory; - var postNameOrId; var privileges = {}; var editPrivileges = {}; @@ -40,6 +43,7 @@ App.Presenters.PostPresenter = function( function init(args, loaded) { topNavigationPresenter.select('posts'); + postsAroundCalculator.resetCache(); privileges.canDeletePosts = auth.hasPrivilege(auth.privileges.deletePosts); privileges.canFeaturePosts = auth.hasPrivilege(auth.privileges.featurePosts); @@ -76,6 +80,9 @@ App.Presenters.PostPresenter = function( function reinit(args, loaded) { postNameOrId = args.postNameOrId; + searchArgs = util.parseComplexRouteArgs(args.searchArgs); + searchArgs.page = parseInt(searchArgs.page) || 1; + promise.wait(refreshPost()) .then(function() { topNavigationPresenter.changeTitle('@' + post.id); @@ -84,6 +91,39 @@ App.Presenters.PostPresenter = function( }).fail(loaded); } + function attachLinksToPostsAround() { + var searchParams = {query: searchArgs.query, order: searchArgs.order}; + promise.wait(postsAroundCalculator.getLinksToPostsAround(searchParams, searchArgs.page, post.id)) + .then(function(nextPostUrl, prevPostUrl) { + var $prevPost = $el.find('#post-current-search .right a'); + var $nextPost = $el.find('#post-current-search .left a'); + + if (nextPostUrl) { + $nextPost.addClass('enabled'); + $nextPost.attr('href', nextPostUrl); + keyboard.keyup('a', function() { + router.navigate(nextPostUrl); + }); + } else { + $nextPost.removeClass('enabled'); + $nextPost.removeAttr('href'); + keyboard.unbind('a'); + } + + if (prevPostUrl) { + $prevPost.addClass('enabled'); + $prevPost.attr('href', prevPostUrl); + keyboard.keyup('d', function() { + router.navigate(prevPostUrl); + }); + } else { + $prevPost.removeClass('enabled'); + $prevPost.removeAttr('href'); + keyboard.unbind('d'); + } + }); + } + function refreshPost() { return promise.make(function(resolve, reject) { promise.wait(api.get('/posts/' + postNameOrId)) @@ -128,6 +168,8 @@ App.Presenters.PostPresenter = function( presenterManager.initPresenters([ [postCommentListPresenter, _.extend({post: post}, {$target: $el.find('#post-comments-target')})]], function() { }); + + attachLinksToPostsAround(); } function renderSidebar() { @@ -137,6 +179,7 @@ App.Presenters.PostPresenter = function( function renderPostTemplate() { return postTemplate({ + searchArgs: searchArgs, post: post, ownScore: postScore, postFavorites: postFavorites, @@ -370,6 +413,7 @@ App.DI.register('postPresenter', [ 'router', 'keyboard', 'presenterManager', + 'postsAroundCalculator', 'postCommentListPresenter', 'topNavigationPresenter', 'messagePresenter'], diff --git a/public_html/js/Presenters/UserListPresenter.js b/public_html/js/Presenters/UserListPresenter.js index 1dfd073a..5c0d14f7 100644 --- a/public_html/js/Presenters/UserListPresenter.js +++ b/public_html/js/Presenters/UserListPresenter.js @@ -47,6 +47,7 @@ App.Presenters.UserListPresenter = function( var searchArgs = util.parseComplexRouteArgs(args.searchArgs); searchArgs.order = searchArgs.order || 'name,asc'; + searchArgs.page = parseInt(searchArgs.page) || 1; updateActiveOrder(searchArgs.order); pagerPresenter.reinit({ diff --git a/public_html/js/Router.js b/public_html/js/Router.js index 5fda740b..01ab1b12 100644 --- a/public_html/js/Router.js +++ b/public_html/js/Router.js @@ -39,7 +39,7 @@ App.Router = function(pathJs, _, jQuery, promise, util, appState, presenterManag inject('#/users(/:searchArgs)', 'userListPresenter'); inject('#/user/:userName(/:tab)', 'userPresenter'); inject('#/posts(/:searchArgs)', 'postListPresenter'); - inject('#/post(/:postNameOrId)', 'postPresenter'); + inject('#/post(/:postNameOrId)(/:searchArgs)', 'postPresenter'); inject('#/comments(/:searchArgs)', 'globalCommentListPresenter'); inject('#/tags(/:searchArgs)', 'tagListPresenter'); inject('#/help', 'helpPresenter'); diff --git a/public_html/js/Services/PostsAroundCalculator.js b/public_html/js/Services/PostsAroundCalculator.js new file mode 100644 index 00000000..80d0f1b4 --- /dev/null +++ b/public_html/js/Services/PostsAroundCalculator.js @@ -0,0 +1,70 @@ +var App = App || {}; +App.Services = App.Services || {}; + +App.Services.PostsAroundCalculator = function(_, promise, util, pager) { + + pager.init({url: '/posts'}); + + function resetCache() { + pager.resetCache(); + } + + function getLinksToPostsAround(searchParams, page, postId) { + return promise.make(function(resolve, reject) { + pager.setSearchParams(searchParams); + pager.setPage(page); + promise.wait(pager.retrieveCached()) + .then(function(response) { + var postIds = _.pluck(response.entities, 'id'); + var position = _.indexOf(postIds, postId); + + if (position === -1) { + resolve(null, null); + } + + promise.wait( + getLinkToPostAround(postIds, position, page, -1), + getLinkToPostAround(postIds, position, page, 1)) + .then(function(nextPostUrl, prevPostUrl) { + resolve(nextPostUrl, prevPostUrl); + }); + }); + }); + } + + function getLinkToPostAround(postIds, position, page, direction) { + return promise.make(function(resolve, reject) { + if (position + direction >= 0 && position + direction < postIds.length) { + var url = util.compileComplexRouteArgs( + '#/post/' + postIds[position + direction], + _.extend({page: page}, pager.getSearchParams())); + resolve(url); + } else if (page + direction >= 1) { + pager.setPage(page + direction); + promise.wait(pager.retrieveCached()).then(function(response) { + if (response.entities.length) { + var post = direction === - 1 ? + _.last(response.entities) : + _.first(response.entities); + + var url = util.compileComplexRouteArgs( + '#/post/' + post.id, + _.extend({page: page + direction}, pager.getSearchParams())); + resolve(url); + } else { + resolve(null); + } + }); + } else { + resolve(null); + } + }); + } + + return { + resetCache: resetCache, + getLinksToPostsAround: getLinksToPostsAround, + }; +}; + +App.DI.register('postsAroundCalculator', ['_', 'promise', 'util', 'pager'], App.Services.PostsAroundCalculator); diff --git a/public_html/templates/post-list-item.tpl b/public_html/templates/post-list-item.tpl index b5c44144..3d646bfe 100644 --- a/public_html/templates/post-list-item.tpl +++ b/public_html/templates/post-list-item.tpl @@ -1,7 +1,11 @@
    + <% if (typeof(searchArgs) !== 'undefined') { %> + href="#/post/<%= post.id %>/query=<%= searchArgs.query %>;order=<%= searchArgs.order %>;page=<%= searchArgs.page %>" + <% } else { %> + href="#/post/<%= post.id %>" + <% } %> + title="<%= _.map(post.tags, function(tag) { return '#' + tag.name; }).join(', ') %>"> <%= post.idMarkdown %> diff --git a/public_html/templates/post.tpl b/public_html/templates/post.tpl index 015e480c..adfe76c1 100644 --- a/public_html/templates/post.tpl +++ b/public_html/templates/post.tpl @@ -1,5 +1,29 @@ <% var permaLink = (window.location.origin + '/' + window.location.pathname + '/data/posts/' + post.name).replace(/([^:])\/+/g, '$1/') %> +
    +
    + + + + + +
    +
    +