diff --git a/client/css/comment-control.styl b/client/css/comment-control.styl index d69c64a9..d9b58973 100644 --- a/client/css/comment-control.styl +++ b/client/css/comment-control.styl @@ -1,51 +1,8 @@ @import colors +$comment-header-background-color = $top-navigation-color +$comment-border-color = #DDD -.comment-form-container - &:not(.editing) - .tabs nav - display: none - .tabs .edit.tab - display: none - .comment-content - margin-left: 0.5em - &.editing - .tab:not(.active) - display: none - .tabs-wrapper - background: $active-tab-background-color - padding: 0.3em - .tab-wrapper[data-tab='preview'] - background: $window-color - .tab.preview - padding: 1em - .tab.edit - textarea - resize: vertical - width: 100% - max-height: 80vh - box-sizing: padding-box - vertical-align: top /* ghost margin on chrome */ - - form - width: auto - margin: 0 - &:after - display: block - height: 1px - content: ' ' - clear: both - - nav - vertical-align: middle !important - &.buttons - margin: 0 0.3em 0.5em 0 !important - float: left - &.actions - float: left - margin: 0.3em 0 0.5em 0 !important - - -.comment +.comment-container margin: 0 0 1em 0 padding: 0 display: -webkit-flex @@ -63,25 +20,67 @@ a display: inline-block - .body + nav:not(.active), .tab:not(.active) + display: none + + .comment + border: 1px solid $comment-border-color flex-grow: 1 + header white-space: nowrap - line-height: 16pt + font-size: 95% vertical-align: middle - margin-bottom: 0.5em - background: $top-navigation-color - padding: 0.2em 0.5em + position: relative + background: $comment-header-background-color + border-bottom: 1px solid $comment-border-color - .nickname, .date, .score-container, .edit + nav.edit + padding: 0.33em 1em 0 1em + ul + list-style-type: none + margin: 0 + padding: 0 + li + display: inline-block + margin: 0 0 -1px 0 + a + padding: 0.25em 1em + &.active + background: $window-color + border: 1px solid $comment-border-color + border-bottom: none + + nav.readonly + padding: 0.33em 1em + + &:before + position: absolute + display: block + content: ' ' + width: 0 + height: 0 + left: -1.5em + top: calc(50% - 0.75em) + border: 0.75em solid transparent + border-right: 0.75em solid darken($comment-border-color, 10%) + + &:after + position: absolute + display: block + content: ' ' + width: 0 + height: 0 + left: calc(-1.5em + 1px) + top: calc(50% - 0.75em) + border: 0.75em solid transparent + border-right: 0.75em solid $comment-header-background-color + + .date, .score-container, .edit margin-right: 2em - .date, .score-container, .edit, .delete - font-size: 95% .edit, .delete, .score-container a, .nickname a &:not(.inactive) color: mix($main-color, $inactive-tab-text-color) - .edit, .delete - font-size: 80% i margin-right: 0.3em @@ -96,6 +95,19 @@ display: inline-block width: 2em + .body + width: auto + margin: 1em + + .keep-height + position: relative + textarea + position: absolute + width: 100% + height: 100% + .tab.edit + min-height: 150px + .messages margin: 1em 0 @@ -118,9 +130,6 @@ white-space: pre word-wrap: normal - p:first-child - margin-top: 0 - .spoiler background: #eee color: #eee @@ -140,5 +149,5 @@ background: #fafafa color: #444 - blockquote :last-child - margin-bottom: 0 + :last-child + margin-bottom: 0 diff --git a/client/css/comment-list-control.styl b/client/css/comment-list-control.styl index bba8a06b..63459d64 100644 --- a/client/css/comment-list-control.styl +++ b/client/css/comment-list-control.styl @@ -1,4 +1,4 @@ .comments>ul list-style-type: none - margin: 0 0 2em 0 + margin: 0 padding: 0 diff --git a/client/html/comment.tpl b/client/html/comment.tpl index 0002d633..b3b961a0 100644 --- a/client/html/comment.tpl +++ b/client/html/comment.tpl @@ -1,57 +1,85 @@ -
+
-
-
<% - %><% - %><% if (ctx.comment.user && ctx.comment.user.name && ctx.canViewUsers) { %><% - %><% - %><% } %><% +
+
+ - %><%- ctx.comment.user ? ctx.comment.user.name : 'Deleted user' %><% + <% %>
-
+
+
+
+
+ <%= ctx.makeMarkdown(ctx.comment ? ctx.comment.text : '') %> +
+
+ +
+ +
+
+ +
+
diff --git a/client/html/comment_form.tpl b/client/html/comment_form.tpl deleted file mode 100644 index e5839ed3..00000000 --- a/client/html/comment_form.tpl +++ /dev/null @@ -1,31 +0,0 @@ -
-
-
<% - %>
<% - %>
<% - %>
<% - %><%= ctx.makeMarkdown(ctx.comment.text) %><% - %>
<% - %>
<% - - %>
<% - %><% - %>
<% - %>
<% - %>
- - - - -
- -
-
diff --git a/client/js/controllers/post_detail_controller.js b/client/js/controllers/post_detail_controller.js index cdff681c..8b64aee1 100644 --- a/client/js/controllers/post_detail_controller.js +++ b/client/js/controllers/post_detail_controller.js @@ -4,7 +4,6 @@ const router = require('../router.js'); const api = require('../api.js'); const misc = require('../util/misc.js'); const settings = require('../models/settings.js'); -const Comment = require('../models/comment.js'); const Post = require('../models/post.js'); const PostList = require('../models/post_list.js'); const PostDetailView = require('../views/post_detail_view.js'); diff --git a/client/js/controllers/post_main_controller.js b/client/js/controllers/post_main_controller.js index 561ecb46..fb280e46 100644 --- a/client/js/controllers/post_main_controller.js +++ b/client/js/controllers/post_main_controller.js @@ -69,10 +69,10 @@ class PostMainController extends BasePostController { 'merge', e => this._evtMergePost(e)); } - if (this._view.commentFormControl) { - this._view.commentFormControl.addEventListener( + if (this._view.commentControl) { + this._view.commentControl.addEventListener( 'change', e => this._evtCommentChange(e)); - this._view.commentFormControl.addEventListener( + this._view.commentControl.addEventListener( 'submit', e => this._evtCreateComment(e)); } @@ -183,18 +183,18 @@ class PostMainController extends BasePostController { } _evtCreateComment(e) { - // TODO: disable form + this._view.commentControl.disableForm(); const comment = Comment.create(this._post.id); comment.text = e.detail.text; comment.save() .then(() => { this._post.comments.add(comment); - this._view.commentFormControl.setText(''); - // TODO: enable form + this._view.commentControl.exitEditMode(); + this._view.commentControl.enableForm(); misc.disableExitConfirmation(); }, errorMessage => { - this._view.commentFormControl.showError(errorMessage); - // TODO: enable form + this._view.commentControl.showError(errorMessage); + this._view.commentControl.enableForm(); }); } diff --git a/client/js/controls/comment_control.js b/client/js/controls/comment_control.js index 63216e76..849dd4df 100644 --- a/client/js/controls/comment_control.js +++ b/client/js/controls/comment_control.js @@ -1,55 +1,87 @@ 'use strict'; const api = require('../api.js'); +const misc = require('../util/misc.js'); const events = require('../events.js'); const views = require('../util/views.js'); -const CommentFormControl = require('../controls/comment_form_control.js'); const template = views.getTemplate('comment'); const scoreTemplate = views.getTemplate('score'); class CommentControl extends events.EventTarget { - constructor(hostNode, comment) { + constructor(hostNode, comment, onlyEditing) { super(); this._hostNode = hostNode; this._comment = comment; + this._onlyEditing = onlyEditing; - comment.addEventListener('change', e => this._evtChange(e)); - comment.addEventListener('changeScore', e => this._evtChangeScore(e)); + if (comment) { + comment.addEventListener( + 'change', e => this._evtChange(e)); + comment.addEventListener( + 'changeScore', e => this._evtChangeScore(e)); + } - const isLoggedIn = api.isLoggedIn(this._comment.user); + const isLoggedIn = comment && api.isLoggedIn(comment.user); const infix = isLoggedIn ? 'own' : 'any'; views.replaceContent(this._hostNode, template({ - comment: this._comment, + comment: comment, + user: comment ? comment.user : api.user, canViewUsers: api.hasPrivilege('users:view'), canEditComment: api.hasPrivilege(`comments:edit:${infix}`), canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`), + onlyEditing: onlyEditing, })); - if (this._editButtonNode) { - this._editButtonNode.addEventListener( - 'click', e => this._evtEditClick(e)); + if (this._editButtonNodes) { + for (let node of this._editButtonNodes) { + node.addEventListener('click', e => this._evtEditClick(e)); + } } if (this._deleteButtonNode) { this._deleteButtonNode.addEventListener( 'click', e => this._evtDeleteClick(e)); } - this._formControl = new CommentFormControl( - this._hostNode.querySelector('.comment-form-container'), - this._comment, - true); - events.proxyEvent(this._formControl, this, 'submit'); + if (this._previewEditingButtonNode) { + this._previewEditingButtonNode.addEventListener( + 'click', e => this._evtPreviewEditingClick(e)); + } + + if (this._saveChangesButtonNode) { + this._saveChangesButtonNode.addEventListener( + 'click', e => this._evtSaveChangesClick(e)); + } + + if (this._cancelEditingButtonNode) { + this._cancelEditingButtonNode.addEventListener( + 'click', e => this._evtCancelEditingClick(e)); + } this._installScore(); + if (onlyEditing) { + this._selectNav('edit'); + this._selectTab('edit'); + } else { + this._selectNav('readonly'); + this._selectTab('preview'); + } + } + + get _formNode() { + return this._hostNode.querySelector('form'); } get _scoreContainerNode() { return this._hostNode.querySelector('.score-container'); } - get _editButtonNode() { - return this._hostNode.querySelector('.edit'); + get _editButtonNodes() { + return this._hostNode.querySelectorAll('li.edit>a, a.edit'); + } + + get _previewEditingButtonNode() { + return this._hostNode.querySelector('li.preview>a'); } get _deleteButtonNode() { @@ -64,12 +96,32 @@ class CommentControl extends events.EventTarget { return this._hostNode.querySelector('.downvote'); } + get _saveChangesButtonNode() { + return this._hostNode.querySelector('.save-changes'); + } + + get _cancelEditingButtonNode() { + return this._hostNode.querySelector('.cancel-editing'); + } + + get _textareaNode() { + return this._hostNode.querySelector('.tab.edit textarea'); + } + + get _contentNode() { + return this._hostNode.querySelector('.tab.preview .comment-content'); + } + + get _heightKeeperNode() { + return this._hostNode.querySelector('.keep-height'); + } + _installScore() { views.replaceContent( this._scoreContainerNode, scoreTemplate({ - score: this._comment.score, - ownScore: this._comment.ownScore, + score: this._comment ? this._comment.score : 0, + ownScore: this._comment ? this._comment.ownScore : 0, canScore: api.hasPrivilege('comments:score'), })); @@ -83,9 +135,40 @@ class CommentControl extends events.EventTarget { } } + enterEditMode() { + this._selectNav('edit'); + this._selectTab('edit'); + } + + exitEditMode() { + if (this._onlyEditing) { + this._selectNav('edit'); + this._selectTab('edit'); + this._setText(''); + } else { + this._selectNav('readonly'); + this._selectTab('preview'); + this._setText(this._comment.text); + } + this._forgetHeight(); + views.clearMessages(this._hostNode); + } + + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + showError(message) { + views.showError(this._hostNode, message); + } + _evtEditClick(e) { e.preventDefault(); - this._formControl.enterEditMode(); + this.enterEditMode(); } _evtScoreClick(e, score) { @@ -114,12 +197,69 @@ class CommentControl extends events.EventTarget { } _evtChange(e) { - this._formControl.exitEditMode(); + this.exitEditMode(); } _evtChangeScore(e) { this._installScore(); } + + _evtPreviewEditingClick(e) { + e.preventDefault(); + this._contentNode.innerHTML = + misc.formatMarkdown(this._textareaNode.value); + this._selectTab('edit'); + this._selectTab('preview'); + } + + _evtEditClick(e) { + e.preventDefault(); + this.enterEditMode(); + } + + _evtSaveChangesClick(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + target: this, + comment: this._comment, + text: this._textareaNode.value, + }, + })); + } + + _evtCancelEditingClick(e) { + e.preventDefault(); + this.exitEditMode(); + } + + _setText(text) { + this._textareaNode.value = text; + this._contentNode.innerHTML = misc.formatMarkdown(text); + } + + _selectNav(modeName) { + for (let node of this._hostNode.querySelectorAll('nav')) { + node.classList.toggle('active', node.classList.contains(modeName)); + } + } + + _selectTab(tabName) { + this._ensureHeight(); + + for (let node of this._hostNode.querySelectorAll('.tab, .tabs li')) { + node.classList.toggle('active', node.classList.contains(tabName)); + } + } + + _ensureHeight() { + this._heightKeeperNode.style.minHeight = + this._heightKeeperNode.getBoundingClientRect().height + 'px'; + } + + _forgetHeight() { + this._heightKeeperNode.style.minHeight = null; + } }; module.exports = CommentControl; diff --git a/client/js/controls/comment_form_control.js b/client/js/controls/comment_form_control.js deleted file mode 100644 index 8a611c53..00000000 --- a/client/js/controls/comment_form_control.js +++ /dev/null @@ -1,141 +0,0 @@ -'use strict'; - -const events = require('../events.js'); -const misc = require('../util/misc.js'); -const views = require('../util/views.js'); - -const template = views.getTemplate('comment-form'); - -class CommentFormControl extends events.EventTarget { - constructor(hostNode, comment, canCancel, minHeight) { - super(); - this._hostNode = hostNode; - this._comment = comment || {text: ''}; - this._canCancel = canCancel; - this._minHeight = minHeight || 150; - - const sourceNode = 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._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 => { - this.dispatchEvent(new CustomEvent('change', { - detail: { - target: this, - }, - })); - this._growTextArea(); - }); - - views.replaceContent(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('.tab-wrapper').style.minHeight = null; - views.clearMessages(this._hostNode); - this.setText(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(); - this.dispatchEvent(new CustomEvent('submit', { - detail: { - target: this, - comment: this._comment, - text: this._textareaNode.value, - }, - })); - } - - _evtCancelClick(e) { - e.preventDefault(); - this.exitEditMode(); - } - - _selectTab(tabName) { - this._freezeTabHeights(); - const tabWrapperNode = this._hostNode.querySelector('.tab-wrapper'); - tabWrapperNode.setAttribute('data-tab', tabName); - for (let tab of this._hostNode.querySelectorAll('.tab, .buttons li')) { - tab.classList.toggle('active', tab.classList.contains(tabName)); - } - } - - _freezeTabHeights() { - const tabsNode = this._hostNode.querySelector('.tab-wrapper'); - const tabsHeight = tabsNode.getBoundingClientRect().height; - tabsNode.style.minHeight = tabsHeight + 'px'; - } - - _growTextArea() { - this._textareaNode.style.height = - Math.max( - this._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 e09fffc2..ddef71f3 100644 --- a/client/js/controls/comment_list_control.js +++ b/client/js/controls/comment_list_control.js @@ -34,7 +34,7 @@ class CommentListControl extends events.EventTarget { _installCommentNode(comment) { const commentListItemNode = document.createElement('li'); const commentControl = new CommentControl( - commentListItemNode, comment); + commentListItemNode, comment, false); events.proxyEvent(commentControl, this, 'submit'); events.proxyEvent(commentControl, this, 'score'); events.proxyEvent(commentControl, this, 'delete'); diff --git a/client/js/models/comment.js b/client/js/models/comment.js index 2ddc3340..6407597b 100644 --- a/client/js/models/comment.js +++ b/client/js/models/comment.js @@ -23,7 +23,7 @@ class Comment extends events.EventTarget { get id() { return this._id; } get postId() { return this._postId; } - get text() { return this._text; } + get text() { return this._text || ''; } get user() { return this._user; } get creationTime() { return this._creationTime; } get lastEditTime() { return this._lastEditTime; } diff --git a/client/js/views/post_main_view.js b/client/js/views/post_main_view.js index 446297d8..7c115dbd 100644 --- a/client/js/views/post_main_view.js +++ b/client/js/views/post_main_view.js @@ -10,8 +10,8 @@ const PostReadonlySidebarControl = require('../controls/post_readonly_sidebar_control.js'); const PostEditSidebarControl = require('../controls/post_edit_sidebar_control.js'); +const CommentControl = require('../controls/comment_control.js'); const CommentListControl = require('../controls/comment_list_control.js'); -const CommentFormControl = require('../controls/comment_form_control.js'); const template = views.getTemplate('post-main'); @@ -101,9 +101,8 @@ class PostMainView { return; } - this.commentFormControl = new CommentFormControl( - commentFormContainer, null, false, 150); - this.commentFormControl.enterEditMode(); + this.commentControl = new CommentControl( + commentFormContainer, null, true); } _installComments(comments) {