client/comments: add comment list view for post
This commit is contained in:
parent
0908323290
commit
7e8a9a0948
18 changed files with 581 additions and 59 deletions
|
@ -1,4 +1,5 @@
|
||||||
$main-color = #24AADD
|
$main-color = #24AADD
|
||||||
|
$window-color = white
|
||||||
$top-nav-color = #F5F5F5
|
$top-nav-color = #F5F5F5
|
||||||
$text-color = #111
|
$text-color = #111
|
||||||
$inactive-link-color = #888
|
$inactive-link-color = #888
|
||||||
|
|
136
client/css/comments.styl
Normal file
136
client/css/comments.styl
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
@import colors
|
||||||
|
|
||||||
|
.comments>ul
|
||||||
|
list-style-type: none
|
||||||
|
margin: 1em 0
|
||||||
|
padding: 0
|
||||||
|
|
||||||
|
.comment
|
||||||
|
margin: 0 0 1em 0
|
||||||
|
padding: 0
|
||||||
|
display: -webkit-flex
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
&:not(.editing)
|
||||||
|
.tabs nav
|
||||||
|
display: none
|
||||||
|
.tabs .edit.tab
|
||||||
|
display: none
|
||||||
|
&.editing
|
||||||
|
.tab:not(.active)
|
||||||
|
display: none
|
||||||
|
.tabs-wrapper
|
||||||
|
background: $active-tab-background-color
|
||||||
|
.tab
|
||||||
|
padding: 1em
|
||||||
|
.content-wrapper
|
||||||
|
background: $window-color
|
||||||
|
overflow: hidden
|
||||||
|
.content
|
||||||
|
margin: 1em
|
||||||
|
textarea
|
||||||
|
resize: vertical
|
||||||
|
width: 100%
|
||||||
|
max-height: 80vh
|
||||||
|
box-sizing: padding-box
|
||||||
|
|
||||||
|
.avatar
|
||||||
|
margin-right: 1em
|
||||||
|
-webkit-flex-shrink: 0
|
||||||
|
flex-shrink: 0
|
||||||
|
vertical-align: top
|
||||||
|
|
||||||
|
.thumbnail
|
||||||
|
width: 40px
|
||||||
|
height: 40px
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
.body
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
header
|
||||||
|
line-height: 16pt
|
||||||
|
vertical-align: middle
|
||||||
|
margin-bottom: 0.5em
|
||||||
|
background: $top-nav-color
|
||||||
|
padding: 0.2em 0.5em
|
||||||
|
|
||||||
|
.date, .score-container, .edit, .delete
|
||||||
|
margin-left: 2em
|
||||||
|
font-size: 95%
|
||||||
|
.edit, .delete, .score-container a, .nickname a
|
||||||
|
color: mix($main-color, $inactive-tab-text-color)
|
||||||
|
.edit, .delete
|
||||||
|
font-size: 80%
|
||||||
|
|
||||||
|
i
|
||||||
|
margin-right: 0.3em
|
||||||
|
.downvote i
|
||||||
|
text-align: right
|
||||||
|
.upvote i
|
||||||
|
display: inline-block
|
||||||
|
width: 1em
|
||||||
|
margin: 0
|
||||||
|
.value
|
||||||
|
text-align: center
|
||||||
|
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
|
||||||
|
|
||||||
|
.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
|
|
@ -270,6 +270,11 @@ input[type=submit]
|
||||||
background-color: $button-disabled-background-color
|
background-color: $button-disabled-background-color
|
||||||
color: $button-disabled-text-color
|
color: $button-disabled-text-color
|
||||||
|
|
||||||
|
&.discourage
|
||||||
|
border-color: transparent
|
||||||
|
background-color: transparent
|
||||||
|
color: $button-disabled-text-color
|
||||||
|
|
||||||
&:focus
|
&:focus
|
||||||
border: 2px solid $text-color
|
border: 2px solid $text-color
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ body
|
||||||
min-height: 100%
|
min-height: 100%
|
||||||
|
|
||||||
body
|
body
|
||||||
|
background: $window-color
|
||||||
overflow-y: scroll
|
overflow-y: scroll
|
||||||
margin: 0
|
margin: 0
|
||||||
color: $text-color
|
color: $text-color
|
||||||
|
|
|
@ -65,7 +65,7 @@ $safety-unsafe = #F3985F
|
||||||
|
|
||||||
.social
|
.social
|
||||||
margin-top: 1em
|
margin-top: 1em
|
||||||
.score
|
.score-container
|
||||||
float: left
|
float: left
|
||||||
margin-right: 3em
|
margin-right: 3em
|
||||||
.downvote i
|
.downvote i
|
||||||
|
|
75
client/html/comment.tpl
Normal file
75
client/html/comment.tpl
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<div class='comment'>
|
||||||
|
<div class='avatar'>
|
||||||
|
<% if (ctx.comment.user.name && ctx.canViewUsers) { %>
|
||||||
|
<a href='/user/<%= ctx.comment.user.name %>'>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<%= ctx.makeThumbnail(ctx.comment.user.avatarUrl) %>
|
||||||
|
|
||||||
|
<% if (ctx.comment.user.name && ctx.canViewUsers) { %>
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='body'>
|
||||||
|
<header><!--
|
||||||
|
--><span class='nickname'><!--
|
||||||
|
--><% if (ctx.comment.user.name && ctx.canViewUsers) { %><!--
|
||||||
|
--><a href='/user/<%= ctx.comment.user.name %>'><!--
|
||||||
|
--><% } %><!--
|
||||||
|
|
||||||
|
--><%= ctx.comment.user.name %><!--
|
||||||
|
|
||||||
|
--><% if (ctx.comment.user.name && ctx.canViewUsers) { %><!--
|
||||||
|
--></a><!--
|
||||||
|
--><% } %><!--
|
||||||
|
--></span><!--
|
||||||
|
|
||||||
|
--><span class='date'><!--
|
||||||
|
--><%= ctx.makeRelativeTime(ctx.comment.creationTime) %><!--
|
||||||
|
--></span><!--
|
||||||
|
|
||||||
|
--><span class='score-container'></span><!--
|
||||||
|
|
||||||
|
--><% if (ctx.canEditComment) { %><!--
|
||||||
|
--><a class='edit' href='#'><!--
|
||||||
|
--><i class='fa fa-pencil'></i> edit<!--
|
||||||
|
--></a><!--
|
||||||
|
--><% } %><!--
|
||||||
|
|
||||||
|
--><% if (ctx.canDeleteComment) { %><!--
|
||||||
|
--><a class='delete' href='#'><!--
|
||||||
|
--><i class='fa fa-remove'></i> delete<!--
|
||||||
|
--></a><!--
|
||||||
|
--><% } %><!--
|
||||||
|
--></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>
|
||||||
|
</div>
|
6
client/html/comment_list.tpl
Normal file
6
client/html/comment_list.tpl
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<div class='comments'>
|
||||||
|
<% if (ctx.canListComments && ctx.comments.length) { %>
|
||||||
|
<ul>
|
||||||
|
</ul>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
15
client/html/fav.tpl
Normal file
15
client/html/fav.tpl
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<% if (ctx.canFavorite) { %>
|
||||||
|
<% if (ctx.ownFavorite) { %>
|
||||||
|
<a class='remove-favorite' href='#'>
|
||||||
|
<i class='fa fa-heart'></i>
|
||||||
|
<% } else { %>
|
||||||
|
<a class='add-favorite' href='#'>
|
||||||
|
<i class='fa fa-heart-o'></i>
|
||||||
|
<% } %>
|
||||||
|
<% } else { %>
|
||||||
|
<a class='add-favorite inactive'>
|
||||||
|
<i class='fa fa-heart-o'></i>
|
||||||
|
<% } %>
|
||||||
|
<span class='vim-nav-hint'>add to favorites</span>
|
||||||
|
</a>
|
||||||
|
<span class='value'><%= ctx.favoriteCount %></span>
|
|
@ -46,8 +46,6 @@
|
||||||
<div class='content'>
|
<div class='content'>
|
||||||
<div class='post-container'></div>
|
<div class='post-container'></div>
|
||||||
|
|
||||||
<section class='comments'>
|
<div class='comments-container'></div>
|
||||||
<!-- TODO: comments -->
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -38,53 +38,9 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class='social'>
|
<section class='social'>
|
||||||
<div class='score'>
|
<div class='score-container'></div>
|
||||||
<% if (ctx.canScorePosts) { %>
|
|
||||||
<a class='upvote' href='#'>
|
|
||||||
<% if (ctx.post.ownScore == 1) { %>
|
|
||||||
<i class='fa fa-thumbs-up'></i>
|
|
||||||
<% } else { %>
|
|
||||||
<i class='fa fa-thumbs-o-up'></i>
|
|
||||||
<% } %>
|
|
||||||
<span class='vim-nav-hint'>upvote</span>
|
|
||||||
<span class='vim-nav-hint'>like</span>
|
|
||||||
</a>
|
|
||||||
<% } else { %>
|
|
||||||
<a class='upvote inactive'>
|
|
||||||
<i class='fa fa-thumbs-o-up'></i>
|
|
||||||
</a>
|
|
||||||
<% } %>
|
|
||||||
<span class='value'><%= ctx.post.score %></span>
|
|
||||||
<% if (ctx.canScorePosts) { %>
|
|
||||||
<a class='downvote' href='#'>
|
|
||||||
<% if (ctx.post.ownScore == -1) { %>
|
|
||||||
<i class='fa fa-thumbs-down'></i>
|
|
||||||
<% } else { %>
|
|
||||||
<i class='fa fa-thumbs-o-down'></i>
|
|
||||||
<% } %>
|
|
||||||
<span class='vim-nav-hint'>downvote</span>
|
|
||||||
<span class='vim-nav-hint'>dislike</span>
|
|
||||||
</a>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='fav'>
|
<div class='fav-container'></div>
|
||||||
<% if (ctx.canFavoritePosts) { %>
|
|
||||||
<% if (ctx.post.ownFavorite) { %>
|
|
||||||
<a class='remove-favorite' href='#'>
|
|
||||||
<i class='fa fa-heart'></i>
|
|
||||||
<% } else { %>
|
|
||||||
<a class='add-favorite' href='#'>
|
|
||||||
<i class='fa fa-heart-o'></i>
|
|
||||||
<% } %>
|
|
||||||
<% } else { %>
|
|
||||||
<a class='add-favorite inactive'>
|
|
||||||
<i class='fa fa-heart-o'></i>
|
|
||||||
<% } %>
|
|
||||||
<span class='vim-nav-hint'>add to favorites</span>
|
|
||||||
</a>
|
|
||||||
<span class='value'><%= ctx.post.favoriteCount %></span>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
|
27
client/html/score.tpl
Normal file
27
client/html/score.tpl
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<% if (ctx.canScore) { %>
|
||||||
|
<a class='upvote' href='#'>
|
||||||
|
<% if (ctx.ownScore == 1) { %>
|
||||||
|
<i class='fa fa-thumbs-up'></i>
|
||||||
|
<% } else { %>
|
||||||
|
<i class='fa fa-thumbs-o-up'></i>
|
||||||
|
<% } %>
|
||||||
|
<span class='vim-nav-hint'>upvote</span>
|
||||||
|
<span class='vim-nav-hint'>like</span>
|
||||||
|
</a>
|
||||||
|
<% } else { %>
|
||||||
|
<a class='upvote inactive'>
|
||||||
|
<i class='fa fa-thumbs-o-up'></i>
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
<span class='value'><%= ctx.score %></span>
|
||||||
|
<% if (ctx.canScore) { %>
|
||||||
|
<a class='downvote' href='#'>
|
||||||
|
<% if (ctx.ownScore == -1) { %>
|
||||||
|
<i class='fa fa-thumbs-down'></i>
|
||||||
|
<% } else { %>
|
||||||
|
<i class='fa fa-thumbs-o-down'></i>
|
||||||
|
<% } %>
|
||||||
|
<span class='vim-nav-hint'>downvote</span>
|
||||||
|
<span class='vim-nav-hint'>dislike</span>
|
||||||
|
</a>
|
||||||
|
<% } %>
|
185
client/js/controls/comment_control.js
Normal file
185
client/js/controls/comment_control.js
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const api = require('../api.js');
|
||||||
|
const misc = require('../util/misc.js');
|
||||||
|
const views = require('../util/views.js');
|
||||||
|
|
||||||
|
class CommentControl {
|
||||||
|
constructor(hostNode, comment) {
|
||||||
|
this._hostNode = hostNode;
|
||||||
|
this._comment = comment;
|
||||||
|
this._template = views.getTemplate('comment');
|
||||||
|
this._scoreTemplate = views.getTemplate('score');
|
||||||
|
|
||||||
|
this.install();
|
||||||
|
}
|
||||||
|
|
||||||
|
install() {
|
||||||
|
const isLoggedIn = api.isLoggedIn(this._comment.user);
|
||||||
|
const infix = isLoggedIn ? 'own' : 'any';
|
||||||
|
const sourceNode = this._template({
|
||||||
|
comment: this._comment,
|
||||||
|
canViewUsers: api.hasPrivilege('users:view'),
|
||||||
|
canEditComment: api.hasPrivilege(`comments:edit:${infix}`),
|
||||||
|
canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
views.showView(
|
||||||
|
sourceNode.querySelector('.score-container'),
|
||||||
|
this._scoreTemplate({
|
||||||
|
score: this._comment.score,
|
||||||
|
ownScore: this._comment.ownScore,
|
||||||
|
canScore: api.hasPrivilege('comments:score'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const editButton = sourceNode.querySelector('.edit');
|
||||||
|
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(
|
||||||
|
'click', e => this._evtEditClick(e));
|
||||||
|
}
|
||||||
|
if (deleteButton) {
|
||||||
|
deleteButton.addEventListener(
|
||||||
|
'click', e => this._evtDeleteClick(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upvoteButton) {
|
||||||
|
upvoteButton.addEventListener(
|
||||||
|
'click',
|
||||||
|
e => this._evtScoreClick(
|
||||||
|
e, () => this._comment.ownScore === 1 ? 0 : 1));
|
||||||
|
}
|
||||||
|
if (downvoteButton) {
|
||||||
|
downvoteButton.addEventListener(
|
||||||
|
'click',
|
||||||
|
e => this._evtScoreClick(
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
textareaNode.addEventListener('change', e => { this._growTextArea(); });
|
||||||
|
|
||||||
|
views.showView(this._hostNode, sourceNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtScoreClick(e, scoreGetter) {
|
||||||
|
e.preventDefault();
|
||||||
|
api.put(
|
||||||
|
'/comment/' + this._comment.id + '/score',
|
||||||
|
{score: scoreGetter()})
|
||||||
|
.then(
|
||||||
|
response => {
|
||||||
|
this._comment.score = parseInt(response.score);
|
||||||
|
this._comment.ownScore = parseInt(response.ownScore);
|
||||||
|
this.install();
|
||||||
|
}, response => {
|
||||||
|
window.alert(response.description);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtDeleteClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!window.confirm('Are you sure you want to delete this comment?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api.delete('/comment/' + this._comment.id)
|
||||||
|
.then(response => {
|
||||||
|
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;
|
41
client/js/controls/comment_list_control.js
Normal file
41
client/js/controls/comment_list_control.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const api = require('../api.js');
|
||||||
|
const views = require('../util/views.js');
|
||||||
|
const CommentControl = require('../controls/comment_control.js');
|
||||||
|
|
||||||
|
class CommentListControl {
|
||||||
|
constructor(hostNode, comments) {
|
||||||
|
this._hostNode = hostNode;
|
||||||
|
this._comments = comments;
|
||||||
|
this._template = views.getTemplate('comment-list');
|
||||||
|
|
||||||
|
this.install();
|
||||||
|
}
|
||||||
|
|
||||||
|
install() {
|
||||||
|
const sourceNode = this._template({
|
||||||
|
comments: this._comments,
|
||||||
|
canListComments: api.hasPrivilege('comments:list'),
|
||||||
|
});
|
||||||
|
|
||||||
|
views.showView(this._hostNode, sourceNode);
|
||||||
|
|
||||||
|
this._renderComments();
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderComments() {
|
||||||
|
if (!this._comments.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const commentList = new DocumentFragment();
|
||||||
|
for (let comment of this._comments) {
|
||||||
|
const commentListItemNode = document.createElement('li');
|
||||||
|
new CommentControl(commentListItemNode, comment);
|
||||||
|
commentList.appendChild(commentListItemNode);
|
||||||
|
}
|
||||||
|
views.showView(this._hostNode.querySelector('ul'), commentList);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = CommentListControl;
|
|
@ -10,6 +10,8 @@ class PostReadonlySidebarControl {
|
||||||
this._post = post;
|
this._post = post;
|
||||||
this._postContentControl = postContentControl;
|
this._postContentControl = postContentControl;
|
||||||
this._template = views.getTemplate('post-readonly-sidebar');
|
this._template = views.getTemplate('post-readonly-sidebar');
|
||||||
|
this._scoreTemplate = views.getTemplate('score');
|
||||||
|
this._favTemplate = views.getTemplate('fav');
|
||||||
|
|
||||||
this.install();
|
this.install();
|
||||||
}
|
}
|
||||||
|
@ -20,10 +22,25 @@ class PostReadonlySidebarControl {
|
||||||
getTagCategory: this._getTagCategory,
|
getTagCategory: this._getTagCategory,
|
||||||
getTagUsages: this._getTagUsages,
|
getTagUsages: this._getTagUsages,
|
||||||
canListPosts: api.hasPrivilege('posts:list'),
|
canListPosts: api.hasPrivilege('posts:list'),
|
||||||
canScorePosts: api.hasPrivilege('posts:score'),
|
|
||||||
canFavoritePosts: api.hasPrivilege('posts:favorite'),
|
|
||||||
canViewTags: api.hasPrivilege('tags:view'),
|
canViewTags: api.hasPrivilege('tags:view'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
views.showView(
|
||||||
|
sourceNode.querySelector('.score-container'),
|
||||||
|
this._scoreTemplate({
|
||||||
|
score: this._post.score,
|
||||||
|
ownScore: this._post.ownScore,
|
||||||
|
canScore: api.hasPrivilege('posts:score'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
views.showView(
|
||||||
|
sourceNode.querySelector('.fav-container'),
|
||||||
|
this._favTemplate({
|
||||||
|
favoriteCount: this._post.favoriteCount,
|
||||||
|
ownFavorite: this._post.ownFavorite,
|
||||||
|
canFavorite: api.hasPrivilege('posts:favorite'),
|
||||||
|
}));
|
||||||
|
|
||||||
const upvoteButton = sourceNode.querySelector('.upvote');
|
const upvoteButton = sourceNode.querySelector('.upvote');
|
||||||
const downvoteButton = sourceNode.querySelector('.downvote')
|
const downvoteButton = sourceNode.querySelector('.downvote')
|
||||||
const addFavButton = sourceNode.querySelector('.add-favorite')
|
const addFavButton = sourceNode.querySelector('.add-favorite')
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
require('./util/polyfill.js');
|
require('./util/polyfill.js');
|
||||||
|
const misc = require('./util/misc.js');
|
||||||
|
|
||||||
const page = require('page');
|
const page = require('page');
|
||||||
const origPushState = page.Context.prototype.pushState;
|
const origPushState = page.Context.prototype.pushState;
|
||||||
|
@ -9,6 +10,20 @@ page.Context.prototype.pushState = function() {
|
||||||
origPushState.call(this);
|
origPushState.call(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
page.cancel = function(ctx) {
|
||||||
|
prevContext = ctx;
|
||||||
|
ctx.pushState();
|
||||||
|
};
|
||||||
|
|
||||||
|
page.exit((ctx, next) => {
|
||||||
|
views.unlistenToMessages();
|
||||||
|
if (misc.confirmPageExit()) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
page.cancel(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const mousetrap = require('mousetrap');
|
const mousetrap = require('mousetrap');
|
||||||
page(/.*/, (ctx, next) => {
|
page(/.*/, (ctx, next) => {
|
||||||
mousetrap.reset();
|
mousetrap.reset();
|
||||||
|
@ -34,11 +49,6 @@ for (let controller of controllers) {
|
||||||
controller.registerRoutes();
|
controller.registerRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
page.exit((ctx, next) => {
|
|
||||||
views.unlistenToMessages();
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
const api = require('./api.js');
|
const api = require('./api.js');
|
||||||
Promise.all([tags.refreshExport(), api.loginFromCookies()])
|
Promise.all([tags.refreshExport(), api.loginFromCookies()])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
@ -199,6 +199,27 @@ function unindent(callSite, ...args) {
|
||||||
return format(output);
|
return format(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enableExitConfirmation() {
|
||||||
|
window.onbeforeunload = e => {
|
||||||
|
return 'Are you sure you want to leave? ' +
|
||||||
|
'Data you have entered may not be saved.';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableExitConfirmation() {
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmPageExit() {
|
||||||
|
if (!window.onbeforeunload) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (window.confirm(window.onbeforeunload())) {
|
||||||
|
disableExitConfirmation();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
range: range,
|
range: range,
|
||||||
formatSearchQuery: formatSearchQuery,
|
formatSearchQuery: formatSearchQuery,
|
||||||
|
@ -208,4 +229,7 @@ module.exports = {
|
||||||
formatFileSize: formatFileSize,
|
formatFileSize: formatFileSize,
|
||||||
formatMarkdown: formatMarkdown,
|
formatMarkdown: formatMarkdown,
|
||||||
unindent: unindent,
|
unindent: unindent,
|
||||||
|
enableExitConfirmation: enableExitConfirmation,
|
||||||
|
disableExitConfirmation: disableExitConfirmation,
|
||||||
|
confirmPageExit: confirmPageExit,
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,6 +29,10 @@ function makeFileSize(fileSize) {
|
||||||
return misc.formatFileSize(fileSize);
|
return misc.formatFileSize(fileSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeMarkdown(text) {
|
||||||
|
return misc.formatMarkdown(text);
|
||||||
|
}
|
||||||
|
|
||||||
function makeRelativeTime(time) {
|
function makeRelativeTime(time) {
|
||||||
return makeNonVoidElement(
|
return makeNonVoidElement(
|
||||||
'time',
|
'time',
|
||||||
|
@ -202,7 +206,7 @@ function makeVoidElement(name, attributes) {
|
||||||
return `<${_serializeElement(name, attributes)}/>`;
|
return `<${_serializeElement(name, attributes)}/>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _messageHandler(target, message, className) {
|
function showMessage(target, message, className) {
|
||||||
if (!message) {
|
if (!message) {
|
||||||
message = 'Unknown message';
|
message = 'Unknown message';
|
||||||
}
|
}
|
||||||
|
@ -222,6 +226,18 @@ function _messageHandler(target, message, className) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showError(target, message) {
|
||||||
|
return showMessage(target, message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccess(target, message) {
|
||||||
|
return showMessage(target, message, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showInfo(target, message) {
|
||||||
|
return showMessage(target, message, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
function unlistenToMessages() {
|
function unlistenToMessages() {
|
||||||
events.unlisten(events.Success);
|
events.unlisten(events.Success);
|
||||||
events.unlisten(events.Error);
|
events.unlisten(events.Error);
|
||||||
|
@ -234,7 +250,7 @@ function listenToMessages(target) {
|
||||||
events.listen(
|
events.listen(
|
||||||
eventType,
|
eventType,
|
||||||
msg => {
|
msg => {
|
||||||
return _messageHandler(target, msg, className);
|
return showMessage(target, msg, className);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
listen(events.Success, 'success');
|
listen(events.Success, 'success');
|
||||||
|
@ -269,6 +285,7 @@ function getTemplate(templatePath) {
|
||||||
Object.assign(ctx, {
|
Object.assign(ctx, {
|
||||||
makeRelativeTime: makeRelativeTime,
|
makeRelativeTime: makeRelativeTime,
|
||||||
makeFileSize: makeFileSize,
|
makeFileSize: makeFileSize,
|
||||||
|
makeMarkdown: makeMarkdown,
|
||||||
makeThumbnail: makeThumbnail,
|
makeThumbnail: makeThumbnail,
|
||||||
makeRadio: makeRadio,
|
makeRadio: makeRadio,
|
||||||
makeCheckbox: makeCheckbox,
|
makeCheckbox: makeCheckbox,
|
||||||
|
@ -420,4 +437,7 @@ module.exports = {
|
||||||
slideDown: slideDown,
|
slideDown: slideDown,
|
||||||
slideUp: slideUp,
|
slideUp: slideUp,
|
||||||
monitorNodeRemoval: monitorNodeRemoval,
|
monitorNodeRemoval: monitorNodeRemoval,
|
||||||
|
showError: showError,
|
||||||
|
showSuccess: showSuccess,
|
||||||
|
showInfo: showInfo,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,7 @@ const PostReadonlySidebarControl
|
||||||
= require('../controls/post_readonly_sidebar_control.js');
|
= require('../controls/post_readonly_sidebar_control.js');
|
||||||
const PostEditSidebarControl
|
const PostEditSidebarControl
|
||||||
= require('../controls/post_edit_sidebar_control.js');
|
= require('../controls/post_edit_sidebar_control.js');
|
||||||
|
const CommentListControl = require('../controls/comment_list_control.js');
|
||||||
|
|
||||||
class PostView {
|
class PostView {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -63,6 +64,10 @@ class PostView {
|
||||||
this._postContentControl);
|
this._postContentControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new CommentListControl(
|
||||||
|
postViewNode.querySelector('.comments-container'),
|
||||||
|
ctx.post.comments);
|
||||||
|
|
||||||
keyboard.bind('e', () => {
|
keyboard.bind('e', () => {
|
||||||
if (ctx.editMode) {
|
if (ctx.editMode) {
|
||||||
page.show('/post/' + ctx.post.id);
|
page.show('/post/' + ctx.post.id);
|
||||||
|
|
Reference in a new issue