diff --git a/client/html/comment_list.tpl b/client/html/comment_list.tpl
index 5e334585..91339cd3 100644
--- a/client/html/comment_list.tpl
+++ b/client/html/comment_list.tpl
@@ -1,6 +1,4 @@
diff --git a/client/js/controllers/comments_controller.js b/client/js/controllers/comments_controller.js
index b3325112..a7fedad7 100644
--- a/client/js/controllers/comments_controller.js
+++ b/client/js/controllers/comments_controller.js
@@ -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 => {
diff --git a/client/js/controllers/post_controller.js b/client/js/controllers/post_controller.js
index 0a63e5e8..a6b2430f 100644
--- a/client/js/controllers/post_controller.js
+++ b/client/js/controllers/post_controller.js
@@ -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 => {
diff --git a/client/js/controls/comment_control.js b/client/js/controls/comment_control.js
index 4ee3e51f..b9268706 100644
--- a/client/js/controls/comment_control.js
+++ b/client/js/controls/comment_control.js
@@ -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();
}
};
diff --git a/client/js/controls/comment_form_control.js b/client/js/controls/comment_form_control.js
index 7d488aaa..bef30ac0 100644
--- a/client/js/controls/comment_form_control.js
+++ b/client/js/controls/comment_form_control.js
@@ -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';
}
};
diff --git a/client/js/controls/comment_list_control.js b/client/js/controls/comment_list_control.js
index f38ab490..33888e0e 100644
--- a/client/js/controls/comment_list_control.js
+++ b/client/js/controls/comment_list_control.js
@@ -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);
}
};
diff --git a/client/js/controls/post_edit_sidebar_control.js b/client/js/controls/post_edit_sidebar_control.js
index 798df582..e2762375 100644
--- a/client/js/controls/post_edit_sidebar_control.js
+++ b/client/js/controls/post_edit_sidebar_control.js
@@ -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);
+ }));
}
};
diff --git a/client/js/controls/post_readonly_sidebar_control.js b/client/js/controls/post_readonly_sidebar_control.js
index f15ed2b5..c163ad6b 100644
--- a/client/js/controls/post_readonly_sidebar_control.js
+++ b/client/js/controls/post_readonly_sidebar_control.js
@@ -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();
}
};
diff --git a/client/js/events.js b/client/js/events.js
index 4ac6c735..ed8ee523 100644
--- a/client/js/events.js
+++ b/client/js/events.js
@@ -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,
};
diff --git a/client/js/models/comment.js b/client/js/models/comment.js
new file mode 100644
index 00000000..63332652
--- /dev/null
+++ b/client/js/models/comment.js
@@ -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;
diff --git a/client/js/models/comment_list.js b/client/js/models/comment_list.js
new file mode 100644
index 00000000..8f4a0440
--- /dev/null
+++ b/client/js/models/comment_list.js
@@ -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;
diff --git a/client/js/models/post.js b/client/js/models/post.js
index 702f935a..80963ec5 100644
--- a/client/js/models/post.js
+++ b/client/js/models/post.js
@@ -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;
}
};
diff --git a/client/js/models/post_list.js b/client/js/models/post_list.js
new file mode 100644
index 00000000..4595239a
--- /dev/null
+++ b/client/js/models/post_list.js
@@ -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;
diff --git a/client/js/views/comments_page_view.js b/client/js/views/comments_page_view.js
index f9aa62f6..60a15c14 100644
--- a/client/js/views/comments_page_view.js
+++ b/client/js/views/comments_page_view.js
@@ -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);
diff --git a/client/js/views/post_view.js b/client/js/views/post_view.js
index 6cde2281..0f5d6e56 100644
--- a/client/js/views/post_view.js
+++ b/client/js/views/post_view.js
@@ -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);
}
}
-
- <% } %> ++