From a697aba1b071bf8a4b2d4c26fd92f2655b77bd03 Mon Sep 17 00:00:00 2001 From: rr- Date: Fri, 17 Jun 2016 20:25:44 +0200 Subject: [PATCH] client/general: remove api calls from controls Introduce some missing models along the way --- client/html/comment_list.tpl | 6 +- client/js/controllers/comments_controller.js | 56 ++++- client/js/controllers/post_controller.js | 90 ++++++++ client/js/controls/comment_control.js | 160 ++++++------- client/js/controls/comment_form_control.js | 40 ++-- client/js/controls/comment_list_control.js | 76 +++--- .../js/controls/post_edit_sidebar_control.js | 16 +- .../controls/post_readonly_sidebar_control.js | 217 ++++++++++-------- client/js/events.js | 12 + client/js/models/comment.js | 118 ++++++++++ client/js/models/comment_list.js | 59 +++++ client/js/models/post.js | 136 ++++++++--- client/js/models/post_list.js | 33 +++ client/js/views/comments_page_view.js | 19 +- client/js/views/post_view.js | 43 ++-- 15 files changed, 769 insertions(+), 312 deletions(-) create mode 100644 client/js/models/comment.js create mode 100644 client/js/models/comment_list.js create mode 100644 client/js/models/post_list.js 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 @@
- <% if (ctx.canListComments && ctx.comments.length) { %> - - <% } %> +
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); } }