client/general: remove api calls from controls

Introduce some missing models along the way
This commit is contained in:
rr- 2016-06-17 20:25:44 +02:00
parent 54e3099c56
commit a697aba1b0
15 changed files with 769 additions and 312 deletions

View file

@ -1,6 +1,4 @@
<div class='comments'>
<% if (ctx.canListComments && ctx.comments.length) { %>
<ul>
</ul>
<% } %>
<ul>
</ul>
</div>

View file

@ -2,6 +2,7 @@
const api = require('../api.js');
const misc = require('../util/misc.js');
const PostList = require('../models/post_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const CommentsPageView = require('../views/comments_page_view.js');
@ -10,25 +11,62 @@ class CommentsController {
constructor(ctx) {
topNavigation.activate('comments');
const proxy = PageController.createHistoryCacheProxy(
ctx, page => {
const url =
'/posts/?query=sort:comment-date+comment-count-min:1' +
`&page=${page}&pageSize=10&fields=` +
'id,comments,commentCount,thumbnailUrl';
return api.get(url);
});
this._pageController = new PageController({
searchQuery: ctx.searchQuery,
clientUrl: '/comments/' + misc.formatSearchQuery({page: '{page}'}),
requestPage: PageController.createHistoryCacheProxy(
ctx,
page => {
return api.get(
'/posts/?query=sort:comment-date+comment-count-min:1' +
`&page=${page}&pageSize=10&fields=` +
'id,comments,commentCount,thumbnailUrl');
}),
requestPage: page => {
return proxy(page).then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: PostList.fromResponse(response.results)}));
});
},
pageRenderer: pageCtx => {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'),
});
return new CommentsPageView(pageCtx);
const view = new CommentsPageView(pageCtx);
view.addEventListener('change', e => this._evtChange(e));
view.addEventListener('score', e => this._evtScore(e));
view.addEventListener('delete', e => this._evtDelete(e));
return view;
},
});
}
_evtChange(e) {
// TODO: disable form
e.detail.comment.text = e.detail.text;
e.detail.comment.save()
.catch(errorMessage => {
e.detail.target.showError(errorMessage);
// TODO: enable form
});
}
_evtScore(e) {
e.detail.comment.setScore(e.detail.score)
.catch(errorMessage => {
window.alert(errorMessage);
});
}
_evtDelete(e) {
e.detail.comment.delete()
.catch(errorMessage => {
window.alert(errorMessage);
});
}
};
module.exports = router => {

View file

@ -1,7 +1,9 @@
'use strict';
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 topNavigation = require('../models/top_navigation.js');
const PostView = require('../views/post_view.js');
@ -17,6 +19,7 @@ class PostController {
this._decorateSearchQuery('')),
]).then(responses => {
const [post, aroundResponse] = responses;
this._post = post;
this._view = new PostView({
post: post,
editMode: editMode,
@ -26,6 +29,28 @@ class PostController {
canListComments: api.hasPrivilege('comments:list'),
canCreateComments: api.hasPrivilege('comments:create'),
});
if (this._view.sidebarControl) {
this._view.sidebarControl.addEventListener(
'favorite', e => this._evtFavoritePost(e));
this._view.sidebarControl.addEventListener(
'unfavorite', e => this._evtUnfavoritePost(e));
this._view.sidebarControl.addEventListener(
'score', e => this._evtScorePost(e));
}
if (this._view.commentFormControl) {
this._view.commentFormControl.addEventListener(
'change', e => this._evtCommentChange(e));
this._view.commentFormControl.addEventListener(
'submit', e => this._evtCreateComment(e));
}
if (this._view.commentListControl) {
this._view.commentListControl.addEventListener(
'change', e => this._evtUpdateComment(e));
this._view.commentListControl.addEventListener(
'score', e => this._evtScoreComment(e));
this._view.commentListControl.addEventListener(
'delete', e => this._evtDeleteComment(e));
}
}, response => {
this._view = new EmptyView();
this._view.showError(response.description);
@ -45,6 +70,71 @@ class PostController {
}
return text.trim();
}
_evtCommentChange(e) {
misc.enableExitConfirmation();
}
_evtCreateComment(e) {
// TODO: disable form
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
misc.disableExitConfirmation();
}, errorMessage => {
this._view.commentFormControl.showError(errorMessage);
// TODO: enable form
});
}
_evtUpdateComment(e) {
// TODO: disable form
e.detail.comment.text = e.detail.text;
e.detail.comment.save()
.catch(errorMessage => {
e.detail.target.showError(errorMessage);
// TODO: enable form
});
}
_evtScoreComment(e) {
e.detail.comment.setScore(e.detail.score)
.catch(errorMessage => {
window.alert(errorMessage);
});
}
_evtDeleteComment(e) {
e.detail.comment.delete()
.catch(errorMessage => {
window.alert(errorMessage);
});
}
_evtScorePost(e) {
e.detail.post.setScore(e.detail.score)
.catch(errorMessage => {
window.alert(errorMessage);
});
}
_evtFavoritePost(e) {
e.detail.post.addToFavorites()
.catch(errorMessage => {
window.alert(errorMessage);
});
}
_evtUnfavoritePost(e) {
e.detail.post.removeFromFavorites()
.catch(errorMessage => {
window.alert(errorMessage);
});
}
}
module.exports = router => {

View file

@ -1,98 +1,86 @@
'use strict';
const api = require('../api.js');
const events = require('../events.js');
const views = require('../util/views.js');
const CommentFormControl = require('../controls/comment_form_control.js');
class CommentControl {
constructor(hostNode, comment, settings) {
const template = views.getTemplate('comment');
const scoreTemplate = views.getTemplate('score');
class CommentControl extends events.EventTarget {
constructor(hostNode, comment) {
super();
this._hostNode = hostNode;
this._comment = comment;
this._template = views.getTemplate('comment');
this._scoreTemplate = views.getTemplate('score');
this._settings = settings;
this.install();
}
comment.addEventListener('change', e => this._evtChange(e));
comment.addEventListener('changeScore', e => this._evtChangeScore(e));
install() {
const isLoggedIn = api.isLoggedIn(this._comment.user);
const infix = isLoggedIn ? 'own' : 'any';
const sourceNode = this._template({
views.replaceContent(this._hostNode, template({
comment: this._comment,
canViewUsers: api.hasPrivilege('users:view'),
canEditComment: api.hasPrivilege(`comments:edit:${infix}`),
canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
});
}));
if (this._editButtonNode) {
this._editButtonNode.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', 'change');
this._installScore();
}
get _scoreContainerNode() {
return this._hostNode.querySelector('.score-container');
}
get _editButtonNode() {
return this._hostNode.querySelector('.edit');
}
get _deleteButtonNode() {
return this._hostNode.querySelector('.delete');
}
get _upvoteButtonNode() {
return this._hostNode.querySelector('.upvote');
}
get _downvoteButtonNode() {
return this._hostNode.querySelector('.downvote');
}
_installScore() {
views.replaceContent(
sourceNode.querySelector('.score-container'),
this._scoreTemplate({
this._scoreContainerNode,
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');
if (editButton) {
editButton.addEventListener(
'click', e => this._evtEditClick(e));
if (this._upvoteButtonNode) {
this._upvoteButtonNode.addEventListener(
'click', e => this._evtScoreClick(e, 1));
}
if (deleteButton) {
deleteButton.addEventListener(
'click', e => this._evtDeleteClick(e));
if (this._downvoteButtonNode) {
this._downvoteButtonNode.addEventListener(
'click', e => this._evtScoreClick(e, -1));
}
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));
}
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
});
views.replaceContent(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);
});
}
_evtEditClick(e) {
@ -100,20 +88,34 @@ class CommentControl {
this._formControl.enterEditMode();
}
_evtScoreClick(e, score) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('score', {
detail: {
comment: this._comment,
score: this._comment.ownScore === score ? 0 : score,
},
}));
}
_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 => {
if (this._settings.onDelete) {
this._settings.onDelete(this._comment);
}
this._hostNode.parentNode.removeChild(this._hostNode);
}, response => {
window.alert(response.description);
});
this.dispatchEvent(new CustomEvent('delete', {
detail: {
comment: this._comment,
},
}));
}
_evtChange(e) {
this._formControl.exitEditMode();
}
_evtChangeScore(e) {
this._installScore();
}
};

View file

@ -1,19 +1,20 @@
'use strict';
const events = require('../events.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
class CommentFormControl {
constructor(hostNode, comment, settings) {
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._template = views.getTemplate('comment-form');
this._settings = settings;
this.install();
}
this._canCancel = canCancel;
this._minHeight = minHeight || 150;
install() {
const sourceNode = this._template({
const sourceNode = template({
comment: this._comment,
});
@ -30,7 +31,7 @@ class CommentFormControl {
formNode.addEventListener('submit', e => this._evtSaveClick(e));
if (this._settings.canCancel) {
if (this._canCancel) {
cancelButton
.addEventListener('click', e => this._evtCancelClick(e));
} else {
@ -43,7 +44,11 @@ class CommentFormControl {
});
}
textareaNode.addEventListener('change', e => {
misc.enableExitConfirmation();
this.dispatchEvent(new CustomEvent('change', {
detail: {
target: this,
},
}));
this._growTextArea();
});
@ -60,7 +65,6 @@ class CommentFormControl {
exitEditMode() {
this._hostNode.classList.remove('editing');
this._hostNode.querySelector('.tabs-wrapper').style.minHeight = null;
misc.disableExitConfirmation();
views.clearMessages(this._hostNode);
this.setText(this._comment.text);
}
@ -97,11 +101,13 @@ class CommentFormControl {
_evtSaveClick(e) {
e.preventDefault();
if (!this._settings.onSave) {
throw 'No save handler';
}
this._settings.onSave(this._textareaNode.value)
.then(() => { misc.disableExitConfirmation(); });
this.dispatchEvent(new CustomEvent('submit', {
detail: {
target: this,
comment: this._comment,
text: this._textareaNode.value,
},
}));
}
_evtCancelClick(e) {
@ -125,7 +131,7 @@ class CommentFormControl {
_growTextArea() {
this._textareaNode.style.height =
Math.max(
this._settings.minHeight || 0,
this._minHeight || 0,
this._textareaNode.scrollHeight) + 'px';
}
};

View file

@ -1,48 +1,58 @@
'use strict';
const api = require('../api.js');
const events = require('../events.js');
const views = require('../util/views.js');
const CommentControl = require('../controls/comment_control.js');
class CommentListControl {
constructor(hostNode, comments) {
const template = views.getTemplate('comment-list');
class CommentListControl extends events.EventTarget {
constructor(hostNode, comments, reversed) {
super();
this._hostNode = hostNode;
this._comments = comments;
this._template = views.getTemplate('comment-list');
this._commentIdToNode = {};
this.install();
comments.addEventListener('add', e => this._evtAdd(e));
comments.addEventListener('remove', e => this._evtRemove(e));
views.replaceContent(this._hostNode, template());
const commentList = Array.from(comments);
if (reversed) {
commentList.reverse();
}
for (let comment of commentList) {
this._installCommentNode(comment);
}
}
install() {
const sourceNode = this._template({
comments: this._comments,
canListComments: api.hasPrivilege('comments:list'),
});
views.replaceContent(this._hostNode, sourceNode);
this._renderComments();
get _commentListNode() {
return this._hostNode.querySelector('ul');
}
_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, {
onDelete: removedComment => {
for (let [index, comment] of this._comments.entries()) {
if (comment.id === removedComment.id) {
this._comments.splice(index, 1);
}
}
},
});
commentList.appendChild(commentListItemNode);
}
views.replaceContent(this._hostNode.querySelector('ul'), commentList);
_installCommentNode(comment) {
const commentListItemNode = document.createElement('li');
const commentControl = new CommentControl(
commentListItemNode, comment);
events.proxyEvent(commentControl, this, 'change');
events.proxyEvent(commentControl, this, 'score');
events.proxyEvent(commentControl, this, 'delete');
this._commentIdToNode[comment.id] = commentListItemNode;
this._commentListNode.appendChild(commentListItemNode);
}
_uninstallCommentNode(comment) {
const commentListItemNode = this._commentIdToNode[comment.id];
commentListItemNode.parentNode.removeChild(commentListItemNode);
}
_evtAdd(e) {
this._installCommentNode(e.detail.comment);
}
_evtRemove(e) {
this._uninstallCommentNode(e.detail.comment);
}
};

View file

@ -1,22 +1,20 @@
'use strict';
const events = require('../events.js');
const views = require('../util/views.js');
class PostEditSidebarControl {
const template = views.getTemplate('post-edit-sidebar');
class PostEditSidebarControl extends events.EventTarget {
constructor(hostNode, post, postContentControl) {
super();
this._hostNode = hostNode;
this._post = post;
this._postContentControl = postContentControl;
this._template = views.getTemplate('post-edit-sidebar');
this.install();
}
install() {
const sourceNode = this._template({
views.replaceContent(this._hostNode, template({
post: this._post,
});
views.replaceContent(this._hostNode, sourceNode);
}));
}
};

View file

@ -1,93 +1,128 @@
'use strict';
const api = require('../api.js');
const events = require('../events.js');
const tags = require('../tags.js');
const views = require('../util/views.js');
class PostReadonlySidebarControl {
const template = views.getTemplate('post-readonly-sidebar');
const scoreTemplate = views.getTemplate('score');
const favTemplate = views.getTemplate('fav');
class PostReadonlySidebarControl extends events.EventTarget {
constructor(hostNode, post, postContentControl) {
super();
this._hostNode = hostNode;
this._post = post;
this._postContentControl = postContentControl;
this._template = views.getTemplate('post-readonly-sidebar');
this._scoreTemplate = views.getTemplate('score');
this._favTemplate = views.getTemplate('fav');
this.install();
}
post.addEventListener('changeFavorite', e => this._evtChangeFav(e));
post.addEventListener('changeScore', e => this._evtChangeScore(e));
install() {
const sourceNode = this._template({
views.replaceContent(this._hostNode, template({
post: this._post,
getTagCategory: this._getTagCategory,
getTagUsages: this._getTagUsages,
canListPosts: api.hasPrivilege('posts:list'),
canViewTags: api.hasPrivilege('tags:view'),
});
}));
views.replaceContent(
sourceNode.querySelector('.score-container'),
this._scoreTemplate({
score: this._post.score,
ownScore: this._post.ownScore,
canScore: api.hasPrivilege('posts:score'),
}));
this._installFav();
this._installScore();
this._installFitButtons();
this._syncFitButton();
}
get _scoreContainerNode() {
return this._hostNode.querySelector('.score-container');
}
get _favContainerNode() {
return this._hostNode.querySelector('.fav-container');
}
get _upvoteButtonNode() {
return this._hostNode.querySelector('.upvote');
}
get _downvoteButtonNode() {
return this._hostNode.querySelector('.downvote');
}
get _addFavButtonNode() {
return this._hostNode.querySelector('.add-favorite');
}
get _remFavButtonNode() {
return this._hostNode.querySelector('.remove-favorite');
}
get _fitBothButtonNode() {
return this._hostNode.querySelector('.fit-both');
}
get _fitOriginalButtonNode() {
return this._hostNode.querySelector('.fit-original');
}
get _fitWidthButtonNode() {
return this._hostNode.querySelector('.fit-width');
}
get _fitHeightButtonNode() {
return this._hostNode.querySelector('.fit-height');
}
_installFitButtons() {
this._fitBothButtonNode.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitBoth()));
this._fitOriginalButtonNode.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitOriginal()));
this._fitWidthButtonNode.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitWidth()));
this._fitHeightButtonNode.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitHeight()));
}
_installFav() {
views.replaceContent(
sourceNode.querySelector('.fav-container'),
this._favTemplate({
this._favContainerNode,
favTemplate({
favoriteCount: this._post.favoriteCount,
ownFavorite: this._post.ownFavorite,
canFavorite: api.hasPrivilege('posts:favorite'),
}));
const upvoteButton = sourceNode.querySelector('.upvote');
const downvoteButton = sourceNode.querySelector('.downvote');
const addFavButton = sourceNode.querySelector('.add-favorite');
const remFavButton = sourceNode.querySelector('.remove-favorite');
const fitBothButton = sourceNode.querySelector('.fit-both');
const fitOriginalButton = sourceNode.querySelector('.fit-original');
const fitWidthButton = sourceNode.querySelector('.fit-width');
const fitHeightButton = sourceNode.querySelector('.fit-height');
if (upvoteButton) {
upvoteButton.addEventListener(
'click', this._eventRequestProxy(
() => this._setScore(this._post.ownScore === 1 ? 0 : 1)));
if (this._addFavButtonNode) {
this._addFavButtonNode.addEventListener(
'click', e => this._evtAddToFavoritesClick(e));
}
if (downvoteButton) {
downvoteButton.addEventListener(
'click', this._eventRequestProxy(
() => this._setScore(this._post.ownScore === -1 ? 0 : -1)));
if (this._remFavButtonNode) {
this._remFavButtonNode.addEventListener(
'click', e => this._evtRemoveFromFavoritesClick(e));
}
}
if (addFavButton) {
addFavButton.addEventListener(
'click', this._eventRequestProxy(
() => this._addToFavorites()));
_installScore() {
views.replaceContent(
this._scoreContainerNode,
scoreTemplate({
score: this._post.score,
ownScore: this._post.ownScore,
canScore: api.hasPrivilege('posts:score'),
}));
if (this._upvoteButtonNode) {
this._upvoteButtonNode.addEventListener(
'click', e => this._evtScoreClick(e, 1));
}
if (remFavButton) {
remFavButton.addEventListener(
'click', this._eventRequestProxy(
() => this._removeFromFavorites()));
if (this._downvoteButtonNode) {
this._downvoteButtonNode.addEventListener(
'click', e => this._evtScoreClick(e, -1));
}
fitBothButton.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitBoth()));
fitOriginalButton.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitOriginal()));
fitWidthButton.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitWidth()));
fitHeightButton.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitHeight()));
views.replaceContent(this._hostNode, sourceNode);
this._syncFitButton();
}
_eventZoomProxy(func) {
@ -99,15 +134,6 @@ class PostReadonlySidebarControl {
};
}
_eventRequestProxy(promise) {
return e => {
e.preventDefault();
promise().then(() => {
this.install();
});
};
}
_syncFitButton() {
const funcToClassName = {};
funcToClassName[this._postContentControl.fitBoth] = 'fit-both';
@ -134,37 +160,40 @@ class PostReadonlySidebarControl {
return tag ? tag.category : 'unknown';
}
_setScore(score) {
return this._requestAndRefresh(
() => api.put('/post/' + this._post.id + '/score', {score: score}));
_evtAddToFavoritesClick(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('favorite', {
detail: {
post: this._post,
},
}));
}
_addToFavorites() {
return this._requestAndRefresh(
() => api.post('/post/' + this._post.id + '/favorite'));
_evtRemoveFromFavoritesClick(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('unfavorite', {
detail: {
post: this._post,
},
}));
}
_removeFromFavorites() {
return this._requestAndRefresh(
() => api.delete('/post/' + this._post.id + '/favorite'));
_evtScoreClick(e, score) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('score', {
detail: {
post: this._post,
score: this._post.ownScore === score ? 0 : score,
},
}));
}
_requestAndRefresh(requestPromise) {
return new Promise((resolve, reject) => {
requestPromise()
.then(
response => { return api.get('/post/' + this._post.id); },
response => { return Promise.reject(response); })
.then(
response => {
this._post = response;
resolve();
},
response => {
reject();
window.alert(response.description);
});
});
_evtChangeFav(e) {
this._installFav();
}
_evtChangeScore(e) {
this._installScore();
}
};

View file

@ -13,10 +13,22 @@ class EventTarget {
}
};
function proxyEvent(source, target, sourceEventType, targetEventType) {
if (!targetEventType) {
targetEventType = sourceEventType;
}
source.addEventListener(sourceEventType, e => {
target.dispatchEvent(new CustomEvent(targetEventType, {
detail: e.detail,
}));
});
}
module.exports = {
Success: 'success',
Error: 'error',
Info: 'info',
proxyEvent: proxyEvent,
EventTarget: EventTarget,
};

118
client/js/models/comment.js Normal file
View file

@ -0,0 +1,118 @@
'use strict';
const api = require('../api.js');
const events = require('../events.js');
class Comment extends events.EventTarget {
constructor() {
super();
this.commentList = null;
this._id = null;
this._postId = null;
this._text = null;
this._user = null;
this._creationTime = null;
this._lastEditTime = null;
this._score = null;
this._ownScore = null;
}
static create(postId) {
const comment = new Comment();
comment._postId = postId;
return comment;
}
static fromResponse(response) {
const comment = new Comment();
comment._updateFromResponse(response);
return comment;
}
get id() { return this._id; }
get postId() { return this._postId; }
get text() { return this._text; }
get user() { return this._user; }
get creationTime() { return this._creationTime; }
get lastEditTime() { return this._lastEditTime; }
get score() { return this._score; }
get ownScore() { return this._ownScore; }
set text(value) { this._text = value; }
save() {
let promise = null;
if (this._id) {
promise = api.put(
'/comment/' + this._id,
{
text: this._text,
});
} else {
promise = api.post(
'/comments',
{
text: this._text,
postId: this._postId,
});
}
return promise.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
details: {
comment: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
delete() {
return api.delete('/comment/' + this._id)
.then(response => {
if (this.commentList) {
this.commentList.remove(this);
}
this.dispatchEvent(new CustomEvent('delete', {
details: {
comment: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
setScore(score) {
return api.put('/comment/' + this._id + '/score', {score: score})
.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('changeScore', {
details: {
comment: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
_updateFromResponse(response) {
this._id = response.id;
this._postId = response.postId;
this._text = response.text;
this._user = response.user;
this._creationTime = response.creationTime;
this._lastEditTime = response.lastEditTime;
this._score = parseInt(response.score);
this._ownScore = parseInt(response.ownScore);
}
}
module.exports = Comment;

View file

@ -0,0 +1,59 @@
'use strict';
const events = require('../events.js');
const Comment = require('./comment.js');
class CommentList extends events.EventTarget {
constructor(comments) {
super();
this._list = [];
}
static fromResponse(commentsResponse) {
const commentList = new CommentList();
for (let commentResponse of commentsResponse) {
const comment = Comment.fromResponse(commentResponse);
comment.commentList = commentList;
commentList._list.push(comment);
}
return commentList;
}
get comments() {
return [...this._list];
}
add(comment) {
comment.commentList = this;
this._list.push(comment);
this.dispatchEvent(new CustomEvent('add', {
detail: {
comment: comment,
},
}));
}
remove(commentToRemove) {
for (let [index, comment] of this._list.entries()) {
if (comment.id === commentToRemove.id) {
this._list.splice(index, 1);
break;
}
}
this.dispatchEvent(new CustomEvent('remove', {
detail: {
comment: commentToRemove,
},
}));
}
get length() {
return this._list.length;
}
[Symbol.iterator]() {
return this._list[Symbol.iterator]();
}
}
module.exports = CommentList;

View file

@ -2,6 +2,7 @@
const api = require('../api.js');
const events = require('../events.js');
const CommentList = require('./comment_list.js');
class Post extends events.EventTarget {
constructor() {
@ -29,7 +30,22 @@ class Post extends events.EventTarget {
this._ownFavorite = null;
}
// encapsulation - don't let set these casually
static fromResponse(response) {
const post = new Post();
post._updateFromResponse(response);
return post;
}
static get(id) {
return api.get('/post/' + id)
.then(response => {
const post = Post.fromResponse(response);
return Promise.resolve(post);
}, response => {
return Promise.reject(response);
});
}
get id() { return this._id; }
get type() { return this._type; }
get mimeType() { return this._mimeType; }
@ -52,37 +68,97 @@ class Post extends events.EventTarget {
get ownFavorite() { return this._ownFavorite; }
get ownScore() { return this._ownScore; }
static get(id) {
return new Promise((resolve, reject) => {
api.get('/post/' + id)
.then(response => {
const post = new Post();
post._id = response.id;
post._type = response.type;
post._mimeType = response.mimeType;
post._creationTime = response.creationTime;
post._user = response.user;
post._safety = response.safety;
post._contentUrl = response.contentUrl;
post._thumbnailUrl = response.thumbnailUrl;
post._canvasWidth = response.canvasWidth;
post._canvasHeight = response.canvasHeight;
post._fileSize = response.fileSize;
setScore(score) {
return api.put('/post/' + this._id + '/score', {score: score})
.then(response => {
const prevFavorite = this._ownFavorite;
this._updateFromResponse(response);
if (this._ownFavorite !== prevFavorite) {
this.dispatchEvent(new CustomEvent('changeFavorite', {
details: {
post: this,
},
}));
}
this.dispatchEvent(new CustomEvent('changeScore', {
details: {
post: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
post._tags = response.tags;
post._notes = response.notes;
post._comments = response.comments;
post._relations = response.relations;
addToFavorites() {
return api.post('/post/' + this.id + '/favorite')
.then(response => {
const prevScore = this._ownScore;
this._updateFromResponse(response);
if (this._ownScore !== prevScore) {
this.dispatchEvent(new CustomEvent('changeScore', {
details: {
post: this,
},
}));
}
this.dispatchEvent(new CustomEvent('changeFavorite', {
details: {
post: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
post._score = response.score;
post._favoriteCount = response.favoriteCount;
post._ownScore = response.ownScore;
post._ownFavorite = response.ownFavorite;
resolve(post);
}, response => {
reject(response);
});
});
removeFromFavorites() {
return api.delete('/post/' + this.id + '/favorite')
.then(response => {
const prevScore = this._ownScore;
this._updateFromResponse(response);
if (this._ownScore !== prevScore) {
this.dispatchEvent(new CustomEvent('changeScore', {
details: {
post: this,
},
}));
}
this.dispatchEvent(new CustomEvent('changeFavorite', {
details: {
post: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
_updateFromResponse(response) {
this._id = response.id;
this._type = response.type;
this._mimeType = response.mimeType;
this._creationTime = response.creationTime;
this._user = response.user;
this._safety = response.safety;
this._contentUrl = response.contentUrl;
this._thumbnailUrl = response.thumbnailUrl;
this._canvasWidth = response.canvasWidth;
this._canvasHeight = response.canvasHeight;
this._fileSize = response.fileSize;
this._tags = response.tags;
this._notes = response.notes;
this._comments = CommentList.fromResponse(response.comments);
this._relations = response.relations;
this._score = response.score;
this._favoriteCount = response.favoriteCount;
this._ownScore = response.ownScore;
this._ownFavorite = response.ownFavorite;
}
};

View file

@ -0,0 +1,33 @@
'use strict';
const events = require('../events.js');
const Post = require('./post.js');
class PostList extends events.EventTarget {
constructor(posts) {
super();
this._list = [];
}
static fromResponse(postsResponse) {
const postList = new PostList();
for (let postResponse of postsResponse) {
postList._list.push(Post.fromResponse(postResponse));
}
return postList;
}
get posts() {
return [...this._list];
}
get length() {
return this._list.length;
}
[Symbol.iterator]() {
return this._list[Symbol.iterator]();
}
}
module.exports = PostList;

View file

@ -1,24 +1,27 @@
'use strict';
const events = require('../events.js');
const views = require('../util/views.js');
const CommentListControl = require('../controls/comment_list_control.js');
const template = views.getTemplate('comments-page');
class CommentsPageView {
class CommentsPageView extends events.EventTarget {
constructor(ctx) {
super();
this._hostNode = ctx.hostNode;
this._controls = [];
const sourceNode = template(ctx);
for (let post of ctx.results) {
post.comments.sort((a, b) => { return b.id - a.id; });
this._controls.push(
new CommentListControl(
sourceNode.querySelector(
`.comments-container[data-for="${post.id}"]`),
post.comments));
const commentListControl = new CommentListControl(
sourceNode.querySelector(
`.comments-container[data-for="${post.id}"]`),
post.comments,
true);
events.proxyEvent(commentListControl, this, 'change');
events.proxyEvent(commentListControl, this, 'score');
events.proxyEvent(commentListControl, this, 'delete');
}
views.replaceContent(this._hostNode, sourceNode);

View file

@ -1,6 +1,5 @@
'use strict';
const api = require('../api.js');
const router = require('../router.js');
const views = require('../util/views.js');
const keyboard = require('../util/keyboard.js');
@ -52,8 +51,8 @@ class PostView {
ctx.post);
this._installSidebar(ctx);
this._installCommentForm(ctx);
this._installComments(ctx);
this._installCommentForm();
this._installComments(ctx.post.comments);
keyboard.bind('e', () => {
if (ctx.editMode) {
@ -79,49 +78,35 @@ class PostView {
'#content-holder .sidebar-container');
if (ctx.editMode) {
new PostEditSidebarControl(
this.sidebarControl = new PostEditSidebarControl(
sidebarContainerNode, ctx.post, this._postContentControl);
} else {
new PostReadonlySidebarControl(
this.sidebarControl = new PostReadonlySidebarControl(
sidebarContainerNode, ctx.post, this._postContentControl);
}
}
_installCommentForm(ctx) {
_installCommentForm() {
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();
this.commentFormControl = new CommentFormControl(
commentFormContainer, null, false, 150);
this.commentFormControl.enterEditMode();
}
_installComments(ctx) {
_installComments(comments) {
const commentsContainerNode = document.querySelector(
'#content-holder .comments-container');
if (commentsContainerNode) {
new CommentListControl(commentsContainerNode, ctx.post.comments);
if (!commentsContainerNode) {
return;
}
this.commentListControl = new CommentListControl(
commentsContainerNode, comments);
}
}