diff --git a/TODO b/TODO
index 49a7410d..0fbcbcea 100644
--- a/TODO
+++ b/TODO
@@ -51,11 +51,6 @@ everything related to tags:
- tag edit history
everything related to comments:
- - adding comments
- - listing comments in post view
- - listing comments in comment list view
- - deleting
- - editing
- markdown
- score
diff --git a/public_html/css/comments.css b/public_html/css/comments.css
new file mode 100644
index 00000000..41ac6de7
--- /dev/null
+++ b/public_html/css/comments.css
@@ -0,0 +1,92 @@
+.comment-form {
+ margin: 1em 0 2em;
+}
+
+.comment-form .preview {
+ background: lemonchiffon;
+ padding: 0.5em;
+ margin-bottom: 1em;
+ box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.5) inset;
+ display: none;
+}
+
+.comments ul {
+ list-style-type: none;
+ margin: 1em 0;
+ padding: 0;
+}
+
+.comment {
+ margin: 0 0 1em 0;
+ padding: 0;
+}
+
+.comment .body,
+.comment .avatar {
+ display: inline-block;
+}
+
+.comment .avatar {
+ margin-right: 0.5em;
+ vertical-align: top;
+}
+
+.comment .content {
+ margin-top: 0.25em;
+}
+
+.comment .header {
+ line-height: 16pt;
+ vertical-align: middle;
+}
+.comment .date {
+ color: silver;
+ font-size: 80%;
+ padding-left: 0.5em;
+}
+.comment .header .ops a {
+ color: silver;
+ font-size: 80%;
+}
+.comment .header .ops a:before {
+ content: '[';
+}
+.comment .header .ops a:after {
+ content: ']';
+}
+
+#global-comment-list {
+ text-align: center;
+}
+#global-comment-list .pagination-content {
+ text-align: left;
+}
+#global-comment-list .comments>ul {
+ margin-top: 0;
+}
+#global-comment-list ul.posts {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+#global-comment-list ul.posts>li>*:last-child:after {
+ display: block;
+ content: '';
+ clear: left;
+}
+#global-comment-list ul.posts>li {
+ margin-bottom: 2em;
+}
+#global-comment-list .post {
+ float: left;
+}
+#global-comment-list .comment-add,
+#global-comment-list h1 {
+ display: none;
+}
+#global-comment-list .comments {
+ float: left;
+}
+#global-comment-list .post-small a {
+ margin: 0 1em 0 0;
+}
diff --git a/public_html/css/core.css b/public_html/css/core.css
index fbfe881b..707e82dd 100644
--- a/public_html/css/core.css
+++ b/public_html/css/core.css
@@ -9,6 +9,11 @@ body {
overflow-y: scroll;
}
+h1 {
+ font-weight: normal;
+ font-size: 30px;
+}
+
h2 {
font-variant: small-caps;
font-weight: normal;
diff --git a/public_html/css/post-list.css b/public_html/css/post-list.css
index 2e20f632..37cc1079 100644
--- a/public_html/css/post-list.css
+++ b/public_html/css/post-list.css
@@ -23,32 +23,33 @@
justify-content: center;
}
-#post-list .posts li a {
- display: block;
+.post-small a {
+ display: inline-block;
margin: 0.4em;
border: 1px solid #999;
z-index: 1;
position: relative;
}
-#post-list .posts li img {
+.post-small img {
display: block;
}
-#post-list .posts li a:focus,
-#post-list .posts li a:hover {
+.post-small a:focus,
+.post-small a:hover {
background: #afe;
border-color: #5da;
box-shadow: 0 0 0 2px #5da;
outline: 0;
}
-#post-list .posts li a:focus img,
-#post-list .posts li a:hover img {
+.post-small a:focus img,
+.post-small a:hover img {
opacity: .9;
}
-#post-list .posts li a .info {
+.post-small a .info {
display: none;
+ text-align: center;
position: absolute;
bottom: 0;
left: 0;
@@ -56,16 +57,22 @@
background: #5da;
color: black;
}
-#post-list .posts li a .info li {
+.post-small a .info ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+.post-small a .info li {
display: inline-block;
margin: 0.1em 0.5em;
+ padding: 0;
}
-#post-list .posts li a:focus .info,
-#post-list .posts li a:hover .info {
+.post-small a:focus .info,
+.post-small a:hover .info {
display: block;
}
-#post-list .posts li:not(.post-type-image) a:before {
+.post-small:not(.post-type-image) a:before {
display: block;
content: '';
z-index: 2;
@@ -78,7 +85,7 @@
border-left: 50px solid transparent;
}
-#post-list .posts li:not(.post-type-image) a:after {
+.post-small:not(.post-type-image) a:after {
display: block;
content: '...';
z-index: 3;
@@ -93,13 +100,13 @@
color: white;
font-size: 15px;
}
-#post-list .posts li.post-type-youtube a:after {
+.post-small.post-type-youtube a:after {
font-size: 13px;
content: 'youtube';
}
-#post-list .posts li.post-type-video a:after {
+.post-small.post-type-video a:after {
content: 'video';
}
-#post-list .posts li.post-type-flash a:after {
+.post-small.post-type-flash a:after {
content: 'flash';
}
diff --git a/public_html/css/post.css b/public_html/css/post.css
index 863000db..b3e1f011 100644
--- a/public_html/css/post.css
+++ b/public_html/css/post.css
@@ -16,8 +16,6 @@
#post-view-wrapper #sidebar h1 {
margin-top: 1.5em;
- font-weight: normal;
- font-size: 30px;
}
#post-view-wrapper #sidebar h1:first-of-type {
diff --git a/public_html/index.html b/public_html/index.html
index 377b499d..35801ddc 100644
--- a/public_html/index.html
+++ b/public_html/index.html
@@ -33,6 +33,7 @@
+
@@ -96,7 +97,8 @@
-
+
+
diff --git a/public_html/js/Presenters/CommentListPresenter.js b/public_html/js/Presenters/CommentListPresenter.js
deleted file mode 100644
index 566ce38b..00000000
--- a/public_html/js/Presenters/CommentListPresenter.js
+++ /dev/null
@@ -1,28 +0,0 @@
-var App = App || {};
-App.Presenters = App.Presenters || {};
-
-App.Presenters.CommentListPresenter = function(
- jQuery,
- topNavigationPresenter) {
-
- var $el = jQuery('#content');
-
- function init(args, loaded) {
- topNavigationPresenter.select('comments');
- topNavigationPresenter.changeTitle('Comments');
- render();
- loaded();
- }
-
- function render() {
- $el.html('Comment list placeholder');
- }
-
- return {
- init: init,
- render: render,
- };
-
-};
-
-App.DI.register('commentListPresenter', ['jQuery', 'topNavigationPresenter'], App.Presenters.CommentListPresenter);
diff --git a/public_html/js/Presenters/GlobalCommentListPresenter.js b/public_html/js/Presenters/GlobalCommentListPresenter.js
new file mode 100644
index 00000000..7f4786f2
--- /dev/null
+++ b/public_html/js/Presenters/GlobalCommentListPresenter.js
@@ -0,0 +1,110 @@
+var App = App || {};
+App.Presenters = App.Presenters || {};
+
+App.Presenters.GlobalCommentListPresenter = function(
+ _,
+ jQuery,
+ util,
+ promise,
+ pagerPresenter,
+ topNavigationPresenter) {
+
+ var $el;
+ var listTemplate;
+ var itemTemplate;
+ var postTemplate;
+
+ function init(args, loaded) {
+ $el = jQuery('#content');
+ topNavigationPresenter.select('comments');
+
+ promise.wait(
+ util.promiseTemplate('global-comment-list'),
+ util.promiseTemplate('global-comment-list-item'),
+ util.promiseTemplate('post-list-item'))
+ .then(function(listHtml, listItemHtml, postItemHtml)
+ {
+ listTemplate = _.template(listHtml);
+ itemTemplate = _.template(listItemHtml);
+ postTemplate = _.template(postItemHtml);
+
+ render();
+ loaded();
+
+ pagerPresenter.init({
+ baseUri: '#/comments',
+ backendUri: '/comments',
+ $target: $el.find('.pagination-target'),
+ updateCallback: function(data, clear) {
+ renderPosts(data.entities, clear);
+ },
+ },
+ function() {
+ onArgsChanged(args);
+ });
+ })
+ .fail(function() { console.log(new Error(arguments)); });
+ }
+
+
+ function reinit(args, loaded) {
+ loaded();
+ onArgsChanged(args);
+ }
+
+ function onArgsChanged(args) {
+ var searchArgs = util.parseComplexRouteArgs(args.searchArgs);
+ pagerPresenter.reinit({
+ page: searchArgs.page,
+ searchParams: {
+ query: searchArgs.query,
+ order: searchArgs.order}});
+ }
+
+ function deinit() {
+ pagerPresenter.deinit();
+ }
+
+ function render() {
+ $el.html(listTemplate());
+ }
+
+ function renderPosts(data, clear) {
+ var $target = $el.find('.posts');
+
+ if (clear) {
+ $target.empty();
+ }
+
+ _.each(data, function(data) {
+ var post = data.post;
+ var comments = data.comments;
+
+ var $post = jQuery('
' + itemTemplate({
+ post: post,
+ postTemplate: postTemplate,
+ }) + '');
+
+ var presenter = App.DI.get('postCommentListPresenter');
+
+ presenter.init({
+ post: post,
+ comments: comments,
+ $target: $post.find('.post-comments-target'),
+ }, function() {
+ presenter.render();
+ });
+
+ $target.append($post);
+ });
+ }
+
+ return {
+ init: init,
+ reinit: reinit,
+ deinit: deinit,
+ render: render,
+ };
+};
+
+App.DI.register('globalCommentListPresenter', ['_', 'jQuery', 'util', 'promise', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.GlobalCommentListPresenter);
diff --git a/public_html/js/Presenters/PagerPresenter.js b/public_html/js/Presenters/PagerPresenter.js
index 898f3a25..62a7ec86 100644
--- a/public_html/js/Presenters/PagerPresenter.js
+++ b/public_html/js/Presenters/PagerPresenter.js
@@ -100,9 +100,8 @@ App.Presenters.PagerPresenter = function(
return util.compileComplexRouteArgs(
baseUri,
_.extend(
- {},
- pager.getSearchParams(),
- {page: pager.getPage()}));
+ {page: pager.getPage()},
+ pager.getSearchParams()));
}
function syncUrl() {
diff --git a/public_html/js/Presenters/PostCommentListPresenter.js b/public_html/js/Presenters/PostCommentListPresenter.js
new file mode 100644
index 00000000..a02d2894
--- /dev/null
+++ b/public_html/js/Presenters/PostCommentListPresenter.js
@@ -0,0 +1,194 @@
+var App = App || {};
+App.Presenters = App.Presenters || {};
+
+App.Presenters.PostCommentListPresenter = function(
+ _,
+ jQuery,
+ util,
+ promise,
+ api,
+ auth,
+ topNavigationPresenter,
+ messagePresenter) {
+
+ var $el;
+ var commentListTemplate;
+ var commentListItemTemplate;
+ var commentFormTemplate;
+ var privileges;
+
+ var post;
+ var comments = [];
+
+ function init(args, loaded) {
+ $el = args.$target;
+ post = args.post;
+ comments = args.comments || [];
+
+ privileges = {
+ canListComments: auth.hasPrivilege(auth.privileges.listComments),
+ canAddComments: auth.hasPrivilege(auth.privileges.addComments),
+ editOwnComments: auth.hasPrivilege(auth.privileges.editOwnComments),
+ editAllComments: auth.hasPrivilege(auth.privileges.editAllComments),
+ deleteOwnComments: auth.hasPrivilege(auth.privileges.deleteOwnComments),
+ deleteAllComments: auth.hasPrivilege(auth.privileges.deleteAllComments),
+ };
+
+ promise.wait(
+ util.promiseTemplate('post-comment-list'),
+ util.promiseTemplate('comment-list-item'),
+ util.promiseTemplate('comment-form'),
+ comments.length === 0 ? api.get('/comments/' + args.post.id) : null)
+ .then(function(
+ commentListHtml,
+ commentListItemHtml,
+ commentFormHtml,
+ commentsResponse)
+ {
+ commentListTemplate = _.template(commentListHtml);
+ commentListItemTemplate = _.template(commentListItemHtml);
+ commentFormTemplate = _.template(commentFormHtml);
+ if (commentsResponse) {
+ comments = commentsResponse.json.data;
+ }
+
+ render();
+ loaded();
+ })
+ .fail(function() { console.log(new Error(arguments)); });
+ }
+
+ function render() {
+ $el.html(commentListTemplate(
+ _.extend(
+ {
+ commentListItemTemplate: commentListItemTemplate,
+ commentFormTemplate: commentFormTemplate,
+ formatRelativeTime: util.formatRelativeTime,
+ formatMarkdown: util.formatMarkdown,
+ comments: comments,
+ post: post,
+ },
+ privileges)));
+
+ $el.find('.comment-add form button[type=submit]').click(function(e) { commentFormSubmitted(e, null); });
+ renderComments(comments, true);
+ }
+
+ function renderComments(comments, clear) {
+ var $target = $el.find('.comments');
+ var $targetList = $el.find('ul');
+
+ if (comments.length > 0) {
+ $target.show();
+ } else {
+ $target.hide();
+ }
+
+ if (clear) {
+ $targetList.empty();
+ }
+
+ _.each(comments, function(comment) {
+ renderComment($targetList, comment);
+ });
+ }
+
+ function renderComment($targetList, comment) {
+ var $item = jQuery('' + commentListItemTemplate({
+ comment: comment,
+ formatRelativeTime: util.formatRelativeTime,
+ formatMarkdown: util.formatMarkdown,
+ canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.editOwnComments : privileges.editAllComments,
+ canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.deleteOwnComments : privileges.deleteAllComments,
+ }) + '');
+ $targetList.append($item);
+ $item.find('a.edit').click(function(e) {
+ e.preventDefault();
+ editCommentStart($item, comment);
+ });
+ $item.find('a.delete').click(function(e) {
+ e.preventDefault();
+ deleteComment($item, comment);
+ });
+ }
+
+ function commentFormSubmitted(e, comment) {
+ e.preventDefault();
+ var $button = jQuery(e.target);
+ var $form = $button.parents('form');
+ var sender = $button.val();
+ if (sender === 'preview') {
+ previewComment($form);
+ } else {
+ submitComment($form, comment);
+ }
+ }
+
+ function previewComment($form) {
+ var $preview = $form.find('.preview');
+ $preview.slideUp('fast', function() {
+ $preview.html(util.formatMarkdown($form.find('textarea').val()));
+ $preview.slideDown('fast');
+ });
+ }
+
+ function submitComment($form, commentToEdit) {
+ $form.find('.preview').slideUp();
+ var $textarea = $form.find('textarea');
+
+ var data = {text: $textarea.val()};
+ var p;
+ if (commentToEdit) {
+ p = promise.wait(api.put('/comments/' + commentToEdit.id, data));
+ } else {
+ p = promise.wait(api.post('/comments/' + post.id, data));
+ }
+
+ p.then(function(response) {
+ $textarea.val('');
+ var comment = response.json;
+
+ if (commentToEdit) {
+ $form.slideUp(function() {
+ $form.remove();
+ });
+ comments = _.map(comments, function(c) { return c.id === commentToEdit.id ? comment : c; });
+ } else {
+ comments.push(comment);
+ }
+ renderComments(comments, true);
+ }).fail(function(response) {
+ window.alert(response.json && response.json.error || response);
+ });
+ }
+
+ function editCommentStart($item, comment) {
+ if ($item.find('.comment-form').length > 0) {
+ return;
+ }
+ var $form = jQuery(commentFormTemplate({title: 'Edit comment', text: comment.text}));
+ $item.find('.body').append($form);
+ $item.find('form button[type=submit]').click(function(e) { commentFormSubmitted(e, comment); });
+ }
+
+ function deleteComment($item, comment) {
+ if (!window.confirm('Are you sure you want to delete this comment?')) {
+ return;
+ }
+ promise.wait(api.delete('/comments/' + comment.id))
+ .then(function(response) {
+ comments = _.filter(comments, function(c) { return c.id !== comment.id; });
+ renderComments(comments, true);
+ }).fail(function(response) {
+ window.alert(response.json && response.json.error || response);
+ });
+ }
+
+ return {
+ init: init,
+ render: render,
+ };
+};
+
+App.DI.register('postCommentListPresenter', ['_', 'jQuery', 'util', 'promise', 'api', 'auth', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.PostCommentListPresenter);
diff --git a/public_html/js/Presenters/PostListPresenter.js b/public_html/js/Presenters/PostListPresenter.js
index 8a046775..44db2976 100644
--- a/public_html/js/Presenters/PostListPresenter.js
+++ b/public_html/js/Presenters/PostListPresenter.js
@@ -44,14 +44,17 @@ App.Presenters.PostListPresenter = function(
},
},
function() {
- reinit(args, function() {});
+ onArgsChanged(args);
});
});
}
function reinit(args, loaded) {
loaded();
+ onArgsChanged(args);
+ }
+ function onArgsChanged(args) {
searchArgs = util.parseComplexRouteArgs(args.searchArgs);
pagerPresenter.reinit({
page: searchArgs.page,
@@ -84,18 +87,15 @@ App.Presenters.PostListPresenter = function(
function renderPosts(posts, clear) {
var $target = $el.find('.posts');
- var all = '';
- _.each(posts, function(post) {
- all += itemTemplate({
- post: post,
- });
- });
-
if (clear) {
- $target.html(all);
- } else {
- $target.append(all);
+ $target.empty();
}
+
+ _.each(posts, function(post) {
+ $target.append(jQuery('' + itemTemplate({
+ post: post,
+ }) + ''));
+ });
}
function searchInputKeyPressed(e) {
diff --git a/public_html/js/Presenters/PostPresenter.js b/public_html/js/Presenters/PostPresenter.js
index 44b4eede..1740c215 100644
--- a/public_html/js/Presenters/PostPresenter.js
+++ b/public_html/js/Presenters/PostPresenter.js
@@ -10,6 +10,8 @@ App.Presenters.PostPresenter = function(
auth,
router,
keyboard,
+ presenterManager,
+ postCommentListPresenter,
topNavigationPresenter,
messagePresenter) {
@@ -122,6 +124,10 @@ App.Presenters.PostPresenter = function(
$el.find('.post-edit-wrapper form').submit(editFormSubmitted);
attachSidebarEvents();
+
+ presenterManager.initPresenters([
+ [postCommentListPresenter, _.extend({post: post}, {$target: $el.find('#post-comments-target')})]],
+ function() { });
}
function renderSidebar() {
@@ -354,4 +360,17 @@ App.Presenters.PostPresenter = function(
};
-App.DI.register('postPresenter', ['_', 'jQuery', 'util', 'promise', 'api', 'auth', 'router', 'keyboard', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.PostPresenter);
+App.DI.register('postPresenter', [
+ '_',
+ 'jQuery',
+ 'util',
+ 'promise',
+ 'api',
+ 'auth',
+ 'router',
+ 'keyboard',
+ 'presenterManager',
+ 'postCommentListPresenter',
+ 'topNavigationPresenter',
+ 'messagePresenter'],
+ App.Presenters.PostPresenter);
diff --git a/public_html/js/Presenters/UserListPresenter.js b/public_html/js/Presenters/UserListPresenter.js
index a67f7daa..1dfd073a 100644
--- a/public_html/js/Presenters/UserListPresenter.js
+++ b/public_html/js/Presenters/UserListPresenter.js
@@ -72,19 +72,16 @@ App.Presenters.UserListPresenter = function(
function renderUsers(users, clear) {
var $target = $el.find('.users');
- var all = '';
+ if (clear) {
+ $target.empty();
+ }
+
_.each(users, function(user) {
- all += itemTemplate({
+ $target.append(jQuery('' + itemTemplate({
user: user,
formatRelativeTime: util.formatRelativeTime,
- });
+ }) + ''));
});
-
- if (clear) {
- $target.html(all);
- } else {
- $target.append(all);
- }
}
function orderLinkClicked(e) {
diff --git a/public_html/js/Router.js b/public_html/js/Router.js
index dd2c6ae7..5fda740b 100644
--- a/public_html/js/Router.js
+++ b/public_html/js/Router.js
@@ -40,7 +40,7 @@ App.Router = function(pathJs, _, jQuery, promise, util, appState, presenterManag
inject('#/user/:userName(/:tab)', 'userPresenter');
inject('#/posts(/:searchArgs)', 'postListPresenter');
inject('#/post(/:postNameOrId)', 'postPresenter');
- inject('#/comments(/:searchArgs)', 'commentListPresenter');
+ inject('#/comments(/:searchArgs)', 'globalCommentListPresenter');
inject('#/tags(/:searchArgs)', 'tagListPresenter');
inject('#/help', 'helpPresenter');
setRoot('#/home');
diff --git a/public_html/js/Util.js b/public_html/js/Util.js
index 8cc02e02..b40c341d 100644
--- a/public_html/js/Util.js
+++ b/public_html/js/Util.js
@@ -183,12 +183,18 @@ App.Util = function(_, jQuery, promise) {
});
}
+ function formatMarkdown(text) {
+ //todo
+ return text;
+ }
+
return {
promiseTemplate: promiseTemplate,
parseComplexRouteArgs: parseComplexRouteArgs,
compileComplexRouteArgs: compileComplexRouteArgs,
formatRelativeTime: formatRelativeTime,
formatFileSize: formatFileSize,
+ formatMarkdown: formatMarkdown,
enableExitConfirmation: enableExitConfirmation,
disableExitConfirmation: disableExitConfirmation,
isExitConfirmationEnabled: isExitConfirmationEnabled,
diff --git a/public_html/templates/comment-form.tpl b/public_html/templates/comment-form.tpl
new file mode 100644
index 00000000..a06e69cf
--- /dev/null
+++ b/public_html/templates/comment-form.tpl
@@ -0,0 +1,17 @@
+
diff --git a/public_html/templates/comment-list-item.tpl b/public_html/templates/comment-list-item.tpl
new file mode 100644
index 00000000..35fe3bfe
--- /dev/null
+++ b/public_html/templates/comment-list-item.tpl
@@ -0,0 +1,53 @@
+
diff --git a/public_html/templates/global-comment-list-item.tpl b/public_html/templates/global-comment-list-item.tpl
new file mode 100644
index 00000000..55f9baac
--- /dev/null
+++ b/public_html/templates/global-comment-list-item.tpl
@@ -0,0 +1,8 @@
+
diff --git a/public_html/templates/global-comment-list.tpl b/public_html/templates/global-comment-list.tpl
new file mode 100644
index 00000000..c9a4cabb
--- /dev/null
+++ b/public_html/templates/global-comment-list.tpl
@@ -0,0 +1,6 @@
+
diff --git a/public_html/templates/post-comment-list.tpl b/public_html/templates/post-comment-list.tpl
new file mode 100644
index 00000000..0130400e
--- /dev/null
+++ b/public_html/templates/post-comment-list.tpl
@@ -0,0 +1,13 @@
+<% if (canListComments && comments.length) { %>
+
+<% } %>
+
+<% if (canAddComments) { %>
+
+<% } %>
diff --git a/public_html/templates/post-list-item.tpl b/public_html/templates/post-list-item.tpl
index 1628fc03..0d94a4d8 100644
--- a/public_html/templates/post-list-item.tpl
+++ b/public_html/templates/post-list-item.tpl
@@ -1,4 +1,4 @@
-
+
<% } %>
-
+
diff --git a/public_html/templates/post.tpl b/public_html/templates/post.tpl
index bc2c6d8c..015e480c 100644
--- a/public_html/templates/post.tpl
+++ b/public_html/templates/post.tpl
@@ -230,6 +230,9 @@
<%= postContentTemplate({post: post}) %>
+
+
<% if (privileges.canViewHistory) { %>
<%= historyTemplate({
diff --git a/public_html/templates/user-list-item.tpl b/public_html/templates/user-list-item.tpl
index 91900441..ca36dcfd 100644
--- a/public_html/templates/user-list-item.tpl
+++ b/public_html/templates/user-list-item.tpl
@@ -1,4 +1,4 @@
-
+
@@ -15,4 +15,4 @@
Last seen: <%= formatRelativeTime(user.lastLoginTime) %>
-
+
Comments
++
+