Added prev/next post controls

This commit is contained in:
Marcin Kurczewski 2014-10-05 10:09:02 +02:00
parent 33c1d99583
commit 688b5b1281
12 changed files with 200 additions and 9 deletions

4
TODO
View file

@ -6,10 +6,6 @@ everything related to posts:
- better thumbnail loading - better thumbnail loading
- single post view - single post view
- previous and next post (difficult)
- remember last search
- take care of pages
- add A/D hotkeys
- editing - editing
- ability to loop video posts - ability to loop video posts

View file

@ -9,6 +9,25 @@
border: 0; 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 { #post-view-wrapper #sidebar {
line-height: 1.33em; line-height: 1.33em;
font-size: 90%; font-size: 90%;

View file

@ -89,6 +89,8 @@
<script type="text/javascript" src="/js/Controls/TagInput.js"></script> <script type="text/javascript" src="/js/Controls/TagInput.js"></script>
<script type="text/javascript" src="/js/PresenterManager.js"></script> <script type="text/javascript" src="/js/PresenterManager.js"></script>
<script type="text/javascript" src="/js/Services/PostsAroundCalculator.js"></script>
<script type="text/javascript" src="/js/Presenters/TopNavigationPresenter.js"></script> <script type="text/javascript" src="/js/Presenters/TopNavigationPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/PagerPresenter.js"></script> <script type="text/javascript" src="/js/Presenters/PagerPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/MessagePresenter.js"></script> <script type="text/javascript" src="/js/Presenters/MessagePresenter.js"></script>

View file

@ -22,10 +22,15 @@ App.Keyboard = function(mousetrap) {
mousetrap.reset(); mousetrap.reset();
} }
function unbind(key) {
mousetrap.unbind(key);
}
return { return {
keydown: keydown, keydown: keydown,
keyup: keyup, keyup: keyup,
reset: reset, reset: reset,
unbind: unbind,
}; };
}; };

View file

@ -9,6 +9,7 @@ App.Pager = function(
var pageNumber; var pageNumber;
var searchParams; var searchParams;
var url; var url;
var cache = {};
function init(args) { function init(args) {
url = args.url; url = args.url;
@ -55,7 +56,7 @@ App.Pager = function(
function retrieve() { function retrieve() {
return promise.make(function(resolve, reject) { 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) { .then(function(response) {
var pageSize = response.json.pageSize; var pageSize = response.json.pageSize;
var totalRecords = response.json.totalRecords; 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() { function getVisiblePages() {
var pages = [1, totalPages || 1]; var pages = [1, totalPages || 1];
var pagesAroundCurrent = 2; var pagesAroundCurrent = 2;
@ -100,7 +122,9 @@ App.Pager = function(
getSearchParams: getSearchParams, getSearchParams: getSearchParams,
setSearchParams: setSearchParams, setSearchParams: setSearchParams,
retrieve: retrieve, retrieve: retrieve,
retrieveCached: retrieveCached,
getVisiblePages: getVisiblePages, getVisiblePages: getVisiblePages,
resetCache: resetCache,
}; };
}; };

View file

@ -24,6 +24,7 @@ App.Presenters.PostListPresenter = function(
topNavigationPresenter.select('posts'); topNavigationPresenter.select('posts');
topNavigationPresenter.changeTitle('Posts'); topNavigationPresenter.changeTitle('Posts');
searchArgs = util.parseComplexRouteArgs(args.searchArgs); searchArgs = util.parseComplexRouteArgs(args.searchArgs);
searchArgs.page = parseInt(searchArgs.page) || 1;
promise.wait( promise.wait(
util.promiseTemplate('post-list'), util.promiseTemplate('post-list'),
@ -93,6 +94,7 @@ App.Presenters.PostListPresenter = function(
_.each(posts, function(post) { _.each(posts, function(post) {
$target.append(jQuery('<li>' + itemTemplate({ $target.append(jQuery('<li>' + itemTemplate({
searchArgs: searchArgs,
post: post, post: post,
}) + '</li>')); }) + '</li>'));
}); });

View file

@ -11,6 +11,7 @@ App.Presenters.PostPresenter = function(
router, router,
keyboard, keyboard,
presenterManager, presenterManager,
postsAroundCalculator,
postCommentListPresenter, postCommentListPresenter,
topNavigationPresenter, topNavigationPresenter,
messagePresenter) { messagePresenter) {
@ -23,11 +24,13 @@ App.Presenters.PostPresenter = function(
var postContentTemplate; var postContentTemplate;
var historyTemplate; var historyTemplate;
var postNameOrId;
var searchArgs;
var post; var post;
var postScore; var postScore;
var postFavorites; var postFavorites;
var postHistory; var postHistory;
var postNameOrId;
var privileges = {}; var privileges = {};
var editPrivileges = {}; var editPrivileges = {};
@ -40,6 +43,7 @@ App.Presenters.PostPresenter = function(
function init(args, loaded) { function init(args, loaded) {
topNavigationPresenter.select('posts'); topNavigationPresenter.select('posts');
postsAroundCalculator.resetCache();
privileges.canDeletePosts = auth.hasPrivilege(auth.privileges.deletePosts); privileges.canDeletePosts = auth.hasPrivilege(auth.privileges.deletePosts);
privileges.canFeaturePosts = auth.hasPrivilege(auth.privileges.featurePosts); privileges.canFeaturePosts = auth.hasPrivilege(auth.privileges.featurePosts);
@ -76,6 +80,9 @@ App.Presenters.PostPresenter = function(
function reinit(args, loaded) { function reinit(args, loaded) {
postNameOrId = args.postNameOrId; postNameOrId = args.postNameOrId;
searchArgs = util.parseComplexRouteArgs(args.searchArgs);
searchArgs.page = parseInt(searchArgs.page) || 1;
promise.wait(refreshPost()) promise.wait(refreshPost())
.then(function() { .then(function() {
topNavigationPresenter.changeTitle('@' + post.id); topNavigationPresenter.changeTitle('@' + post.id);
@ -84,6 +91,39 @@ App.Presenters.PostPresenter = function(
}).fail(loaded); }).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() { function refreshPost() {
return promise.make(function(resolve, reject) { return promise.make(function(resolve, reject) {
promise.wait(api.get('/posts/' + postNameOrId)) promise.wait(api.get('/posts/' + postNameOrId))
@ -128,6 +168,8 @@ App.Presenters.PostPresenter = function(
presenterManager.initPresenters([ presenterManager.initPresenters([
[postCommentListPresenter, _.extend({post: post}, {$target: $el.find('#post-comments-target')})]], [postCommentListPresenter, _.extend({post: post}, {$target: $el.find('#post-comments-target')})]],
function() { }); function() { });
attachLinksToPostsAround();
} }
function renderSidebar() { function renderSidebar() {
@ -137,6 +179,7 @@ App.Presenters.PostPresenter = function(
function renderPostTemplate() { function renderPostTemplate() {
return postTemplate({ return postTemplate({
searchArgs: searchArgs,
post: post, post: post,
ownScore: postScore, ownScore: postScore,
postFavorites: postFavorites, postFavorites: postFavorites,
@ -370,6 +413,7 @@ App.DI.register('postPresenter', [
'router', 'router',
'keyboard', 'keyboard',
'presenterManager', 'presenterManager',
'postsAroundCalculator',
'postCommentListPresenter', 'postCommentListPresenter',
'topNavigationPresenter', 'topNavigationPresenter',
'messagePresenter'], 'messagePresenter'],

View file

@ -47,6 +47,7 @@ App.Presenters.UserListPresenter = function(
var searchArgs = util.parseComplexRouteArgs(args.searchArgs); var searchArgs = util.parseComplexRouteArgs(args.searchArgs);
searchArgs.order = searchArgs.order || 'name,asc'; searchArgs.order = searchArgs.order || 'name,asc';
searchArgs.page = parseInt(searchArgs.page) || 1;
updateActiveOrder(searchArgs.order); updateActiveOrder(searchArgs.order);
pagerPresenter.reinit({ pagerPresenter.reinit({

View file

@ -39,7 +39,7 @@ App.Router = function(pathJs, _, jQuery, promise, util, appState, presenterManag
inject('#/users(/:searchArgs)', 'userListPresenter'); inject('#/users(/:searchArgs)', 'userListPresenter');
inject('#/user/:userName(/:tab)', 'userPresenter'); inject('#/user/:userName(/:tab)', 'userPresenter');
inject('#/posts(/:searchArgs)', 'postListPresenter'); inject('#/posts(/:searchArgs)', 'postListPresenter');
inject('#/post(/:postNameOrId)', 'postPresenter'); inject('#/post(/:postNameOrId)(/:searchArgs)', 'postPresenter');
inject('#/comments(/:searchArgs)', 'globalCommentListPresenter'); inject('#/comments(/:searchArgs)', 'globalCommentListPresenter');
inject('#/tags(/:searchArgs)', 'tagListPresenter'); inject('#/tags(/:searchArgs)', 'tagListPresenter');
inject('#/help', 'helpPresenter'); inject('#/help', 'helpPresenter');

View file

@ -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);

View file

@ -1,7 +1,11 @@
<div class="post-small post-type-<%= post.contentType %> "> <div class="post-small post-type-<%= post.contentType %> ">
<a class="link" <a class="link"
title="<%= _.map(post.tags, function(tag) { return '#' + tag.name; }).join(', ') %>" <% if (typeof(searchArgs) !== 'undefined') { %>
href="#/post/<%= post.id %>"> 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(', ') %>">
<img class="thumb" src="/data/thumbnails/160x160/posts/<%= post.name %>" alt="<%= post.idMarkdown %>"/> <img class="thumb" src="/data/thumbnails/160x160/posts/<%= post.name %>" alt="<%= post.idMarkdown %>"/>

View file

@ -1,5 +1,29 @@
<% var permaLink = (window.location.origin + '/' + window.location.pathname + '/data/posts/' + post.name).replace(/([^:])\/+/g, '$1/') %> <% var permaLink = (window.location.origin + '/' + window.location.pathname + '/data/posts/' + post.name).replace(/([^:])\/+/g, '$1/') %>
<div id="post-current-search-wrapper">
<div id="post-current-search">
<div class="left">
<a class="enabled">
<i class="fa fa-chevron-left"></i>
Next
</a>
</div>
<div class="search">
<a href="#/posts/query=<%= searchArgs.query %>;order=<%= searchArgs.order %>">
Current search: <%= searchArgs.query || '-' %>
</a>
</div>
<div class="right">
<a class="enabled">
Previous
<i class="fa fa-chevron-right"></i>
</a>
</div>
</div>
</div>
<div id="post-view-wrapper"> <div id="post-view-wrapper">
<div id="sidebar"> <div id="sidebar">
<ul class="essential"> <ul class="essential">