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'> <div class='comments'>
<% if (ctx.canListComments && ctx.comments.length) { %> <ul>
<ul> </ul>
</ul>
<% } %>
</div> </div>

View File

@ -2,6 +2,7 @@
const api = require('../api.js'); const api = require('../api.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const PostList = require('../models/post_list.js');
const topNavigation = require('../models/top_navigation.js'); const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js'); const PageController = require('../controllers/page_controller.js');
const CommentsPageView = require('../views/comments_page_view.js'); const CommentsPageView = require('../views/comments_page_view.js');
@ -10,25 +11,62 @@ class CommentsController {
constructor(ctx) { constructor(ctx) {
topNavigation.activate('comments'); 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({ this._pageController = new PageController({
searchQuery: ctx.searchQuery, searchQuery: ctx.searchQuery,
clientUrl: '/comments/' + misc.formatSearchQuery({page: '{page}'}), clientUrl: '/comments/' + misc.formatSearchQuery({page: '{page}'}),
requestPage: PageController.createHistoryCacheProxy( requestPage: page => {
ctx, return proxy(page).then(response => {
page => { return Promise.resolve(Object.assign(
return api.get( {},
'/posts/?query=sort:comment-date+comment-count-min:1' + response,
`&page=${page}&pageSize=10&fields=` + {results: PostList.fromResponse(response.results)}));
'id,comments,commentCount,thumbnailUrl'); });
}), },
pageRenderer: pageCtx => { pageRenderer: pageCtx => {
Object.assign(pageCtx, { Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'), 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 => { module.exports = router => {

View File

@ -1,7 +1,9 @@
'use strict'; 'use strict';
const api = require('../api.js'); const api = require('../api.js');
const misc = require('../util/misc.js');
const settings = require('../models/settings.js'); const settings = require('../models/settings.js');
const Comment = require('../models/comment.js');
const Post = require('../models/post.js'); const Post = require('../models/post.js');
const topNavigation = require('../models/top_navigation.js'); const topNavigation = require('../models/top_navigation.js');
const PostView = require('../views/post_view.js'); const PostView = require('../views/post_view.js');
@ -17,6 +19,7 @@ class PostController {
this._decorateSearchQuery('')), this._decorateSearchQuery('')),
]).then(responses => { ]).then(responses => {
const [post, aroundResponse] = responses; const [post, aroundResponse] = responses;
this._post = post;
this._view = new PostView({ this._view = new PostView({
post: post, post: post,
editMode: editMode, editMode: editMode,
@ -26,6 +29,28 @@ class PostController {
canListComments: api.hasPrivilege('comments:list'), canListComments: api.hasPrivilege('comments:list'),
canCreateComments: api.hasPrivilege('comments:create'), 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 => { }, response => {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError(response.description); this._view.showError(response.description);
@ -45,6 +70,71 @@ class PostController {
} }
return text.trim(); 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 => { module.exports = router => {

View File

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

View File

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

View File

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

View File

@ -1,22 +1,20 @@
'use strict'; 'use strict';
const events = require('../events.js');
const views = require('../util/views.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) { constructor(hostNode, post, postContentControl) {
super();
this._hostNode = hostNode; this._hostNode = hostNode;
this._post = post; this._post = post;
this._postContentControl = postContentControl; this._postContentControl = postContentControl;
this._template = views.getTemplate('post-edit-sidebar');
this.install(); views.replaceContent(this._hostNode, template({
}
install() {
const sourceNode = this._template({
post: this._post, post: this._post,
}); }));
views.replaceContent(this._hostNode, sourceNode);
} }
}; };

View File

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

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 = { module.exports = {
Success: 'success', Success: 'success',
Error: 'error', Error: 'error',
Info: 'info', Info: 'info',
proxyEvent: proxyEvent,
EventTarget: EventTarget, 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 api = require('../api.js');
const events = require('../events.js'); const events = require('../events.js');
const CommentList = require('./comment_list.js');
class Post extends events.EventTarget { class Post extends events.EventTarget {
constructor() { constructor() {
@ -29,7 +30,22 @@ class Post extends events.EventTarget {
this._ownFavorite = null; 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 id() { return this._id; }
get type() { return this._type; } get type() { return this._type; }
get mimeType() { return this._mimeType; } get mimeType() { return this._mimeType; }
@ -52,37 +68,97 @@ class Post extends events.EventTarget {
get ownFavorite() { return this._ownFavorite; } get ownFavorite() { return this._ownFavorite; }
get ownScore() { return this._ownScore; } get ownScore() { return this._ownScore; }
static get(id) { setScore(score) {
return new Promise((resolve, reject) => { return api.put('/post/' + this._id + '/score', {score: score})
api.get('/post/' + id) .then(response => {
.then(response => { const prevFavorite = this._ownFavorite;
const post = new Post(); this._updateFromResponse(response);
post._id = response.id; if (this._ownFavorite !== prevFavorite) {
post._type = response.type; this.dispatchEvent(new CustomEvent('changeFavorite', {
post._mimeType = response.mimeType; details: {
post._creationTime = response.creationTime; post: this,
post._user = response.user; },
post._safety = response.safety; }));
post._contentUrl = response.contentUrl; }
post._thumbnailUrl = response.thumbnailUrl; this.dispatchEvent(new CustomEvent('changeScore', {
post._canvasWidth = response.canvasWidth; details: {
post._canvasHeight = response.canvasHeight; post: this,
post._fileSize = response.fileSize; },
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
post._tags = response.tags; addToFavorites() {
post._notes = response.notes; return api.post('/post/' + this.id + '/favorite')
post._comments = response.comments; .then(response => {
post._relations = response.relations; 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; removeFromFavorites() {
post._favoriteCount = response.favoriteCount; return api.delete('/post/' + this.id + '/favorite')
post._ownScore = response.ownScore; .then(response => {
post._ownFavorite = response.ownFavorite; const prevScore = this._ownScore;
resolve(post); this._updateFromResponse(response);
}, response => { if (this._ownScore !== prevScore) {
reject(response); 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'; 'use strict';
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const CommentListControl = require('../controls/comment_list_control.js'); const CommentListControl = require('../controls/comment_list_control.js');
const template = views.getTemplate('comments-page'); const template = views.getTemplate('comments-page');
class CommentsPageView { class CommentsPageView extends events.EventTarget {
constructor(ctx) { constructor(ctx) {
super();
this._hostNode = ctx.hostNode; this._hostNode = ctx.hostNode;
this._controls = [];
const sourceNode = template(ctx); const sourceNode = template(ctx);
for (let post of ctx.results) { for (let post of ctx.results) {
post.comments.sort((a, b) => { return b.id - a.id; }); const commentListControl = new CommentListControl(
this._controls.push( sourceNode.querySelector(
new CommentListControl( `.comments-container[data-for="${post.id}"]`),
sourceNode.querySelector( post.comments,
`.comments-container[data-for="${post.id}"]`), true);
post.comments)); events.proxyEvent(commentListControl, this, 'change');
events.proxyEvent(commentListControl, this, 'score');
events.proxyEvent(commentListControl, this, 'delete');
} }
views.replaceContent(this._hostNode, sourceNode); views.replaceContent(this._hostNode, sourceNode);

View File

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