diff --git a/client/css/comments.styl b/client/css/comments.styl
index dafdaa9..c1583b7 100644
--- a/client/css/comments.styl
+++ b/client/css/comments.styl
@@ -5,18 +5,15 @@
margin: 0 0 2em 0
padding: 0
-.comment
- margin: 0 0 1em 0
- padding: 0
- display: -webkit-flex
- display: flex
+
+.comment-form-container
&:not(.editing)
.tabs nav
display: none
.tabs .edit.tab
display: none
- .content
+ .comment-content
margin-left: 0.5em
&.editing
.tab:not(.active)
@@ -25,10 +22,10 @@
background: $active-tab-background-color
.tab
padding: 1em
- .content-wrapper
+ .comment-content-wrapper
background: $window-color
overflow: hidden
- .content
+ .comment-content
margin: 1em
textarea
resize: vertical
@@ -36,6 +33,27 @@
max-height: 80vh
box-sizing: padding-box
+ form
+ width: auto
+ margin: 0
+
+ nav
+ vertical-align: middle !important
+ margin: 0 0.3em 0.5em 0 !important
+ &.buttons
+ float: left
+ &.actions
+ float: left
+ margin-top: 0.3em !important
+
+
+
+.comment
+ margin: 0 0 1em 0
+ padding: 0
+ display: -webkit-flex
+ display: flex
+
.avatar
margin-right: 1em
-webkit-flex-shrink: 0
@@ -80,64 +98,55 @@
display: inline-block
width: 2em
- form
- width: auto
- margin: 0
-
- nav
- vertical-align: middle
- margin: 0 0.8em 0.5em 0
- &.buttons
- float: left
- &.actions
- float: left
- margin-top: 0.3em
-
.messages
margin: 1em 0
- .content
- ul
- list-style-position: inside
- margin: 1em 0
- padding: 0
- .sjis
- font-family: 'MS PGothic', 'MS Pゴシック', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif
- background: #fbfbfb
- color: #111
- font-size: 12pt
- line-height: 1
- margin: 0
- padding: 4px
- overflow: auto
- white-space: pre
- word-wrap: normal
- p:first-child
- margin-top: 0
+.comment-content
+ ul
+ list-style-position: inside
+ margin: 1em 0
+ padding: 0
- .spoiler
- background: #eee
- color: #eee
- &:hover
- color: dimgray
- &:before
- content: '['
- color: #000
- &:after
- content: ']'
- color: #000
+ .sjis
+ font-family: 'MS PGothic', 'MS Pゴシック', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif
+ background: #fbfbfb
+ color: #111
+ font-size: 12pt
+ line-height: 1
+ margin: 0
+ padding: 4px
+ overflow: auto
+ white-space: pre
+ word-wrap: normal
+
+ p:first-child
+ margin-top: 0
+
+ .spoiler
+ background: #eee
+ color: #eee
+ &:hover
+ color: dimgray
+ &:before
+ content: '['
+ color: #000
+ &:after
+ content: ']'
+ color: #000
+
+ blockquote
+ border-left: 3px solid #eee
+ margin-left: 0
+ padding: 0.3em 0.3em 0.3em 0.7em
+ background: #fafafa
+ color: #444
+
+ blockquote :last-child
+ margin-bottom: 0
- blockquote
- border-left: 3px solid #eee
- margin-left: 0
- padding: 0.3em 0.3em 0.3em 0.7em
- background: #fafafa
- color: #444
- blockquote :last-child
- margin-bottom: 0
.global-comment-list
text-align: left
diff --git a/client/html/comment.tpl b/client/html/comment.tpl
index aac6346..32eba69 100644
--- a/client/html/comment.tpl
+++ b/client/html/comment.tpl
@@ -44,32 +44,6 @@
--><% } %>
-
+
diff --git a/client/html/comment_form.tpl b/client/html/comment_form.tpl
new file mode 100644
index 0000000..ec35386
--- /dev/null
+++ b/client/html/comment_form.tpl
@@ -0,0 +1,31 @@
+
diff --git a/client/html/post.tpl b/client/html/post.tpl
index 39bf9d6..d6d3030 100644
--- a/client/html/post.tpl
+++ b/client/html/post.tpl
@@ -46,6 +46,13 @@
-
+ <% if (ctx.canListComments) { %>
+
+ <% } %>
+
+ <% if (ctx.canCreateComments) { %>
+
Add comment
+
+ <% } %>
diff --git a/client/js/controllers/posts_controller.js b/client/js/controllers/posts_controller.js
index 2765f9f..b22b7be 100644
--- a/client/js/controllers/posts_controller.js
+++ b/client/js/controllers/posts_controller.js
@@ -74,6 +74,8 @@ class PostsController {
nextPostId: aroundResponse.next ? aroundResponse.next.id : null,
prevPostId: aroundResponse.prev ? aroundResponse.prev.id : null,
canEditPosts: api.hasPrivilege('posts:edit'),
+ canListComments: api.hasPrivilege('comments:list'),
+ canCreateComments: api.hasPrivilege('comments:create'),
});
}, response => {
this._emptyView.render();
diff --git a/client/js/controls/comment_control.js b/client/js/controls/comment_control.js
index 5ca252a..5e283d4 100644
--- a/client/js/controls/comment_control.js
+++ b/client/js/controls/comment_control.js
@@ -1,15 +1,16 @@
'use strict';
const api = require('../api.js');
-const misc = require('../util/misc.js');
const views = require('../util/views.js');
+const CommentFormControl = require('../controls/comment_form_control.js');
class CommentControl {
- constructor(hostNode, comment) {
+ constructor(hostNode, comment, settings) {
this._hostNode = hostNode;
this._comment = comment;
this._template = views.getTemplate('comment');
this._scoreTemplate = views.getTemplate('score');
+ this._settings = settings;
this.install();
}
@@ -36,11 +37,6 @@ class CommentControl {
const deleteButton = sourceNode.querySelector('.delete');
const upvoteButton = sourceNode.querySelector('.upvote');
const downvoteButton = sourceNode.querySelector('.downvote');
- const previewTabButton = sourceNode.querySelector('.buttons .preview');
- const editTabButton = sourceNode.querySelector('.buttons .edit');
- const formNode = sourceNode.querySelector('form');
- const cancelButton = sourceNode.querySelector('.cancel');
- const textareaNode = sourceNode.querySelector('form textarea');
if (editButton) {
editButton.addEventListener(
@@ -64,20 +60,22 @@ class CommentControl {
e, () => this._comment.ownScore === -1 ? 0 : -1));
}
- previewTabButton.addEventListener(
- 'click', e => this._evtPreviewClick(e));
- editTabButton.addEventListener(
- 'click', e => this._evtEditClick(e));
-
- formNode.addEventListener('submit', e => this._evtSaveClick(e));
- cancelButton.addEventListener('click', e => this._evtCancelClick(e));
-
- for (let event of ['cut', 'paste', 'drop', 'keydown']) {
- textareaNode.addEventListener(event, e => {
- window.setTimeout(() => this._growTextArea(), 0);
+ this._formControl = new CommentFormControl(
+ sourceNode.querySelector('.comment-form-container'),
+ this._comment,
+ {
+ onSave: text => {
+ return api.put('/comment/' + this._comment.id, {
+ text: text,
+ }).then(response => {
+ this._comment = response;
+ this.install();
+ }, response => {
+ this._formControl.showError(response.description);
+ });
+ },
+ canCancel: true
});
- }
- textareaNode.addEventListener('change', e => { this._growTextArea(); });
views.showView(this._hostNode, sourceNode);
}
@@ -97,6 +95,11 @@ class CommentControl {
});
}
+ _evtEditClick(e) {
+ e.preventDefault();
+ this._formControl.enterEditMode();
+ }
+
_evtDeleteClick(e) {
e.preventDefault();
if (!window.confirm('Are you sure you want to delete this comment?')) {
@@ -104,82 +107,14 @@ class CommentControl {
}
api.delete('/comment/' + this._comment.id)
.then(response => {
+ if (this._settings.onDelete) {
+ this._settings.onDelete(this._comment);
+ }
this._hostNode.parentNode.removeChild(this._hostNode);
}, response => {
window.alert(response.description);
});
}
-
- _evtSaveClick(e) {
- e.preventDefault();
- api.put('/comment/' + this._comment.id, {
- text: this._hostNode.querySelector('.edit.tab textarea').value,
- }).then(response => {
- this._comment = response;
- this.install();
- }, response => {
- this._showError(response.description);
- });
- }
-
- _evtPreviewClick(e) {
- e.preventDefault();
- this._hostNode.querySelector('.preview.tab .content').innerHTML
- = misc.formatMarkdown(
- this._hostNode.querySelector('.edit.tab textarea').value);
- this._freezeTabHeights();
- this._selectTab('preview');
- }
-
- _evtEditClick(e) {
- e.preventDefault();
- this._freezeTabHeights();
- this._enterEditMode();
- this._selectTab('edit');
- this._growTextArea();
- }
-
- _evtCancelClick(e) {
- e.preventDefault();
- this._exitEditMode();
- this._hostNode.querySelector('.edit.tab textarea').value
- = this._comment.text;
- }
-
- _enterEditMode() {
- this._hostNode.querySelector('.comment').classList.add('editing');
- misc.enableExitConfirmation();
- }
-
- _exitEditMode() {
- this._hostNode.querySelector('.comment').classList.remove('editing');
- this._hostNode.querySelector('.tabs-wrapper').style.minHeight = null;
- misc.disableExitConfirmation();
- views.clearMessages(this._hostNode);
- }
-
- _selectTab(tabName) {
- this._freezeTabHeights();
- for (let tab of this._hostNode.querySelectorAll('.tab, .buttons li')) {
- tab.classList.toggle('active', tab.classList.contains(tabName));
- }
- }
-
- _freezeTabHeights() {
- const tabsNode = this._hostNode.querySelector('.tabs-wrapper');
- const tabsHeight = tabsNode.getBoundingClientRect().height;
- tabsNode.style.minHeight = tabsHeight + 'px';
- }
-
- _growTextArea() {
- const previewNode = this._hostNode.querySelector('.content');
- const textareaNode = this._hostNode.querySelector('textarea');
- textareaNode.style.height = textareaNode.scrollHeight + 'px';
- }
-
- _showError(message) {
- views.showError(this._hostNode, message);
- }
};
module.exports = CommentControl;
diff --git a/client/js/controls/comment_form_control.js b/client/js/controls/comment_form_control.js
new file mode 100644
index 0000000..94e6fdc
--- /dev/null
+++ b/client/js/controls/comment_form_control.js
@@ -0,0 +1,134 @@
+'use strict';
+
+const misc = require('../util/misc.js');
+const views = require('../util/views.js');
+
+class CommentFormControl {
+ constructor(hostNode, comment, settings) {
+ this._hostNode = hostNode;
+ this._comment = comment || {text: ''};
+ this._template = views.getTemplate('comment-form');
+ this._settings = settings;
+ this.install();
+ }
+
+ install() {
+ const sourceNode = this._template({
+ comment: this._comment,
+ });
+
+ const previewTabButton = sourceNode.querySelector('.buttons .preview');
+ const editTabButton = sourceNode.querySelector('.buttons .edit');
+ const formNode = sourceNode.querySelector('form');
+ const cancelButton = sourceNode.querySelector('.cancel');
+ const textareaNode = sourceNode.querySelector('form textarea');
+
+ previewTabButton.addEventListener(
+ 'click', e => this._evtPreviewClick(e));
+ editTabButton.addEventListener(
+ 'click', e => this._evtEditClick(e));
+
+ formNode.addEventListener('submit', e => this._evtSaveClick(e));
+
+ if (this._settings.canCancel) {
+ cancelButton
+ .addEventListener('click', e => this._evtCancelClick(e));
+ } else {
+ cancelButton.style.display = 'none';
+ }
+
+ for (let event of ['cut', 'paste', 'drop', 'keydown']) {
+ textareaNode.addEventListener(event, e => {
+ window.setTimeout(() => this._growTextArea(), 0);
+ });
+ }
+ textareaNode.addEventListener('change', e => {
+ misc.enableExitConfirmation();
+ this._growTextArea();
+ });
+
+ views.showView(this._hostNode, sourceNode);
+ }
+
+ enterEditMode() {
+ this._freezeTabHeights();
+ this._hostNode.classList.add('editing');
+ this._selectTab('edit');
+ this._growTextArea();
+ }
+
+ exitEditMode() {
+ this._hostNode.classList.remove('editing');
+ this._hostNode.querySelector('.tabs-wrapper').style.minHeight = null;
+ misc.disableExitConfirmation();
+ views.clearMessages(this._hostNode);
+ this._hostNode.querySelector('.edit.tab textarea').value
+ = this._comment.text;
+ }
+
+ get _textareaNode() {
+ return this._hostNode.querySelector('.edit.tab textarea');
+ }
+
+ get _contentNode() {
+ return this._hostNode.querySelector('.preview.tab .comment-content');
+ }
+
+ setText(text) {
+ this._textareaNode.value = text;
+ this._contentNode.innerHTML = misc.formatMarkdown(text);
+ }
+
+ showError(message) {
+ views.showError(this._hostNode, message);
+ }
+
+ _evtPreviewClick(e) {
+ e.preventDefault();
+ this._contentNode.innerHTML
+ = misc.formatMarkdown(this._textareaNode.value);
+ this._freezeTabHeights();
+ this._selectTab('preview');
+ }
+
+ _evtEditClick(e) {
+ e.preventDefault();
+ this.enterEditMode();
+ }
+
+ _evtSaveClick(e) {
+ e.preventDefault();
+ if (!this._settings.onSave) {
+ throw 'No save handler';
+ }
+ this._settings.onSave(this._textareaNode.value)
+ .then(() => { misc.disableExitConfirmation(); });
+ }
+
+ _evtCancelClick(e) {
+ e.preventDefault();
+ this.exitEditMode();
+ }
+
+ _selectTab(tabName) {
+ this._freezeTabHeights();
+ for (let tab of this._hostNode.querySelectorAll('.tab, .buttons li')) {
+ tab.classList.toggle('active', tab.classList.contains(tabName));
+ }
+ }
+
+ _freezeTabHeights() {
+ const tabsNode = this._hostNode.querySelector('.tabs-wrapper');
+ const tabsHeight = tabsNode.getBoundingClientRect().height;
+ tabsNode.style.minHeight = tabsHeight + 'px';
+ }
+
+ _growTextArea() {
+ this._textareaNode.style.height
+ = Math.max(
+ this._settings.minHeight || 0,
+ this._textareaNode.scrollHeight) + 'px';
+ }
+};
+
+module.exports = CommentFormControl;
diff --git a/client/js/controls/comment_list_control.js b/client/js/controls/comment_list_control.js
index d78a6ad..91b13f8 100644
--- a/client/js/controls/comment_list_control.js
+++ b/client/js/controls/comment_list_control.js
@@ -31,7 +31,15 @@ class CommentListControl {
const commentList = new DocumentFragment();
for (let comment of this._comments) {
const commentListItemNode = document.createElement('li');
- new CommentControl(commentListItemNode, comment);
+ new CommentControl(commentListItemNode, comment, {
+ onDelete: removedComment => {
+ for (let [index, comment] of this._comments.entries()) {
+ if (comment.id === removedComment.id) {
+ this._comments.splice(index, 1);
+ }
+ }
+ },
+ });
commentList.appendChild(commentListItemNode);
}
views.showView(this._hostNode.querySelector('ul'), commentList);
diff --git a/client/js/views/post_view.js b/client/js/views/post_view.js
index 0c7b541..0968682 100644
--- a/client/js/views/post_view.js
+++ b/client/js/views/post_view.js
@@ -1,5 +1,6 @@
'use strict';
+const api = require('../api.js');
const views = require('../util/views.js');
const keyboard = require('../util/keyboard.js');
const page = require('page');
@@ -11,6 +12,7 @@ const PostReadonlySidebarControl
const PostEditSidebarControl
= require('../controls/post_edit_sidebar_control.js');
const CommentListControl = require('../controls/comment_list_control.js');
+const CommentFormControl = require('../controls/comment_form_control.js');
class PostView {
constructor() {
@@ -52,21 +54,9 @@ class PostView {
postContainerNode.querySelector('.post-overlay'),
ctx.post);
- if (ctx.editMode) {
- new PostEditSidebarControl(
- postViewNode.querySelector('.sidebar-container'),
- ctx.post,
- this._postContentControl);
- } else {
- new PostReadonlySidebarControl(
- postViewNode.querySelector('.sidebar-container'),
- ctx.post,
- this._postContentControl);
- }
-
- new CommentListControl(
- postViewNode.querySelector('.comments-container'),
- ctx.post.comments);
+ this._installSidebar(ctx);
+ this._installCommentForm(ctx);
+ this._installComments(ctx);
keyboard.bind('e', () => {
if (ctx.editMode) {
@@ -86,6 +76,56 @@ class PostView {
}
});
}
+
+ _installSidebar(ctx) {
+ const sidebarContainerNode = document.querySelector(
+ '#content-holder .sidebar-container');
+
+ if (ctx.editMode) {
+ new PostEditSidebarControl(
+ sidebarContainerNode, ctx.post, this._postContentControl);
+ } else {
+ new PostReadonlySidebarControl(
+ sidebarContainerNode, ctx.post, this._postContentControl);
+ }
+ }
+
+ _installCommentForm(ctx) {
+ const commentFormContainer = document.querySelector(
+ '#content-holder .comment-form-container');
+ if (!commentFormContainer) {
+ return;
+ }
+
+ this._formControl = new CommentFormControl(
+ commentFormContainer,
+ null,
+ {
+ onSave: text => {
+ return api.post('/comments', {
+ postId: ctx.post.id,
+ text: text,
+ }).then(response => {
+ ctx.post.comments.push(response);
+ this._formControl.setText('');
+ this._installComments(ctx);
+ }, response => {
+ this._formControl.showError(response.description);
+ });
+ },
+ canCancel: false,
+ minHeight: 150,
+ });
+ this._formControl.enterEditMode();
+ }
+
+ _installComments(ctx) {
+ const commentsContainerNode = document.querySelector(
+ '#content-holder .comments-container');
+ if (commentsContainerNode) {
+ new CommentListControl(commentsContainerNode, ctx.post.comments);
+ }
+ }
}
module.exports = PostView;