client/comments: add comment adding

This commit is contained in:
rr- 2016-06-12 18:08:50 +02:00
parent b9fc626ace
commit a28b4bdd3e
9 changed files with 333 additions and 193 deletions

View file

@ -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', ' ', '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', ' ', '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

View file

@ -44,32 +44,6 @@
--><% } %><!--
--></header>
<div class='tabs'>
<form>
<div class='tabs-wrapper'>
<div class='preview tab'>
<div class='content-wrapper'><div class='content'><%= ctx.makeMarkdown(ctx.comment.text) %></div></div>
</div>
<div class='edit tab'>
<textarea required minlength=1><%= ctx.comment.text %></textarea>
</div>
</div>
<nav class='buttons'>
<ul>
<li class='preview'><a href='#'>Preview</a></li>
<li class='edit'><a href='#'>Edit</a></li>
</ul>
</nav>
<nav class='actions'>
<input type='submit' class='save' value='Save'/>
<input type='button' class='cancel discourage' value='Cancel'/>
</nav>
</form>
<div class='messages'></div>
</div>
<div class='comment-form-container'></div>
</div>
</div>

View file

@ -0,0 +1,31 @@
<div class='tabs'>
<form>
<div class='tabs-wrapper'><!--
--><div class='preview tab'><!--
--><div class='comment-content-wrapper'><!--
--><div class='comment-content'><!--
--><%= ctx.makeMarkdown(ctx.comment.text) %><!--
--></div><!--
--></div><!--
--></div><!--
--><div class='edit tab'><!--
--><textarea required minlength=1><%= ctx.comment.text %></textarea><!--
--></div><!--
--></div>
<nav class='buttons'>
<ul>
<li class='preview'><a href='#'>Preview</a></li>
<li class='edit'><a href='#'>Edit</a></li>
</ul>
</nav>
<nav class='actions'>
<input type='submit' class='save' value='Save'/>
<input type='button' class='cancel discourage' value='Cancel'/>
</nav>
</form>
<div class='messages'></div>
</div>

View file

@ -46,6 +46,13 @@
<div class='content'>
<div class='post-container'></div>
<div class='comments-container'></div>
<% if (ctx.canListComments) { %>
<div class='comments-container'></div>
<% } %>
<% if (ctx.canCreateComments) { %>
<h2>Add comment</h2>
<div class='comment-form-container'></div>
<% } %>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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