diff options
| -rw-r--r-- | client/css/post-detail-view.styl | 33 | ||||
| -rw-r--r-- | client/css/post-main-view.styl (renamed from client/css/post-view.styl) | 0 | ||||
| -rw-r--r-- | client/html/post_detail.tpl | 12 | ||||
| -rw-r--r-- | client/html/post_edit_sidebar.tpl | 5 | ||||
| -rw-r--r-- | client/html/post_main.tpl (renamed from client/html/post.tpl) | 0 | ||||
| -rw-r--r-- | client/html/post_merge.tpl | 23 | ||||
| -rw-r--r-- | client/html/post_merge_side.tpl | 48 | ||||
| -rw-r--r-- | client/js/controllers/base_post_controller.js | 20 | ||||
| -rw-r--r-- | client/js/controllers/post_detail_controller.js | 86 | ||||
| -rw-r--r-- | client/js/controllers/post_main_controller.js (renamed from client/js/controllers/post_controller.js) | 37 | ||||
| -rw-r--r-- | client/js/controls/post_edit_sidebar_control.js | 19 | ||||
| -rw-r--r-- | client/js/main.js | 3 | ||||
| -rw-r--r-- | client/js/models/post.js | 25 | ||||
| -rw-r--r-- | client/js/views/post_detail_view.js | 80 | ||||
| -rw-r--r-- | client/js/views/post_main_view.js (renamed from client/js/views/post_view.js) | 6 | ||||
| -rw-r--r-- | client/js/views/post_merge_view.js | 129 |
16 files changed, 502 insertions, 24 deletions
diff --git a/client/css/post-detail-view.styl b/client/css/post-detail-view.styl new file mode 100644 index 0000000..32e7497 --- /dev/null +++ b/client/css/post-detail-view.styl @@ -0,0 +1,33 @@ +#post + width: 100% + max-width: 40em + h1 + margin-top: 0 + form + width: 100% + .buttons i + margin-right: 0.5em + .post-merge + .left-post-container + width: 47% + float: left + .right-post-container + width: 47% + float: right + input[type=text] + width: 8em + margin-top: -2px + .post-mirror + margin-bottom: 1em + &:after + display: block + height: 1px + content: ' ' + clear: both + .post-thumbnail .thumbnail + width: 100% + height: 9em + .target-post .thumbnail + margin-right: 0.35em + .target-post, .target-post-content + margin: 1em 0 diff --git a/client/css/post-view.styl b/client/css/post-main-view.styl index 689ec73..689ec73 100644 --- a/client/css/post-view.styl +++ b/client/css/post-main-view.styl diff --git a/client/html/post_detail.tpl b/client/html/post_detail.tpl new file mode 100644 index 0000000..5e04ab2 --- /dev/null +++ b/client/html/post_detail.tpl @@ -0,0 +1,12 @@ +<div class='content-wrapper' id='post'> + <h1>Post #<%- ctx.post.id %></h1> + <nav class='buttons'><!-- + --><ul><!-- + --><li><a href='/post/<%- ctx.post.id %>'><i class='fa fa-reply'></i> Main view</a></li><!-- + --><% if (ctx.canMerge) { %><!-- + --><li data-name='merge'><a href='/post/<%- ctx.post.id %>/merge'>Merge with…</a></li><!-- + --><% } %><!-- + --></ul><!-- + --></nav> + <div class='post-content-holder'></div> +</div> diff --git a/client/html/post_edit_sidebar.tpl b/client/html/post_edit_sidebar.tpl index 9b08891..d36a0e4 100644 --- a/client/html/post_edit_sidebar.tpl +++ b/client/html/post_edit_sidebar.tpl @@ -84,12 +84,15 @@ </section> <% } %> - <% if (ctx.canFeaturePosts || ctx.canDeletePosts) { %> + <% if (ctx.canFeaturePosts || ctx.canDeletePosts || ctx.canMergePosts) { %> <section class='management'> <ul> <% if (ctx.canFeaturePosts) { %> <li><a href class='feature'>Feature this post on main page</a></li> <% } %> + <% if (ctx.canMergePosts) { %> + <li><a href class='merge'>Merge this post with another</a></li> + <% } %> <% if (ctx.canDeletePosts) { %> <li><a href class='delete'>Delete this post</a></li> <% } %> diff --git a/client/html/post.tpl b/client/html/post_main.tpl index 07ade0b..07ade0b 100644 --- a/client/html/post.tpl +++ b/client/html/post_main.tpl diff --git a/client/html/post_merge.tpl b/client/html/post_merge.tpl new file mode 100644 index 0000000..59b3f49 --- /dev/null +++ b/client/html/post_merge.tpl @@ -0,0 +1,23 @@ +<div class='post-merge'> + <form> + <ul> + <li class='post-mirror'> + <div class='left-post-container'></div> + <div class='right-post-container'></div> + </li> + + <li> + <p>Tags, relations, scores, favorites and comments will be + merged. All other properties need to be handled manually.</p> + + <%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge these posts.'}) %> + </li> + </ul> + + <div class='messages'></div> + + <div class='buttons'> + <input type='submit' value='Merge posts'/> + </div> + </form> +</div> diff --git a/client/html/post_merge_side.tpl b/client/html/post_merge_side.tpl new file mode 100644 index 0000000..60cc080 --- /dev/null +++ b/client/html/post_merge_side.tpl @@ -0,0 +1,48 @@ +<% if (ctx.editable) { %> + <p>Post # <input type='text' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>'/></p> +<% } else { %> + <p>Post # <input type='text' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>' readonly/></p> +<% } %> + +<% if (ctx.post) { %> + <div class='post-thumbnail'> + <a rel='external' href='<%- ctx.post.contentUrl %>'> + <%= ctx.makeThumbnail(ctx.post.thumbnailUrl) %> + </a> + </div> + <div class='target-post'> + <%= ctx.makeRadio({ + required: true, + text: 'Merge to this post<br/><small>' + + ctx.makeUserLink(ctx.post.user) + + ', ' + + ctx.makeRelativeTime(ctx.post.creationTime) + + '</small>', + name: 'target-post', + value: ctx.name, + }) %> + </div> + <div class='target-post-content'> + <%= ctx.makeRadio({ + required: true, + text: 'Use this file<br/><small>' + + ctx.makeFileSize(ctx.post.fileSize) + ' ' + + { + 'image/gif': 'GIF', + 'image/jpeg': 'JPEG', + 'image/png': 'PNG', + 'video/webm': 'WEBM', + 'application/x-shockwave-flash': 'SWF', + }[ctx.post.mimeType] + + ' (' + + (ctx.post.canvasWidth ? + `${ctx.post.canvasWidth}x${ctx.post.canvasHeight}` : + '?') + + ')</small>', + name: 'target-post-content', + value: ctx.name, + }) %> + <p> + </p> + </div> +<% } %> diff --git a/client/js/controllers/base_post_controller.js b/client/js/controllers/base_post_controller.js new file mode 100644 index 0000000..b86aea7 --- /dev/null +++ b/client/js/controllers/base_post_controller.js @@ -0,0 +1,20 @@ +'use strict'; + +const api = require('../api.js'); +const topNavigation = require('../models/top_navigation.js'); +const EmptyView = require('../views/empty_view.js'); + +class BasePostController { + constructor(ctx) { + if (!api.hasPrivilege('posts:view')) { + this._view = new EmptyView(); + this._view.showError('You don\'t have privileges to view posts.'); + return; + } + + topNavigation.activate('posts'); + topNavigation.setTitle('Post #' + ctx.parameters.id.toString()); + } +} + +module.exports = BasePostController; diff --git a/client/js/controllers/post_detail_controller.js b/client/js/controllers/post_detail_controller.js new file mode 100644 index 0000000..cdff681 --- /dev/null +++ b/client/js/controllers/post_detail_controller.js @@ -0,0 +1,86 @@ +'use strict'; + +const router = require('../router.js'); +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 PostList = require('../models/post_list.js'); +const PostDetailView = require('../views/post_detail_view.js'); +const BasePostController = require('./base_post_controller.js'); +const EmptyView = require('../views/empty_view.js'); + +class PostDetailController extends BasePostController { + constructor(ctx, section) { + super(ctx); + + Post.get(ctx.parameters.id).then(post => { + this._id = ctx.parameters.id; + post.addEventListener('change', e => this._evtSaved(e, section)); + + this._view = new PostDetailView({ + post: post, + section: section, + canMerge: api.hasPrivilege('posts:merge'), + }); + + this._view.addEventListener('select', e => this._evtSelect(e)); + this._view.addEventListener('merge', e => this._evtMerge(e)); + }, errorMessage => { + this._view = new EmptyView(); + this._view.showError(errorMessage); + }); + } + + showSuccess(message) { + this._view.showSuccess(message); + } + + _evtSelect(e) { + this._view.clearMessages(); + this._view.disableForm(); + Post.get(e.detail.postId).then(post => { + this._view.selectPost(post); + this._view.enableForm(); + }, errorMessage => { + this._view.showError(errorMessage); + this._view.enableForm(); + }); + } + + _evtSaved(e, section) { + misc.disableExitConfirmation(); + if (this._id !== e.detail.post.id) { + router.replace( + '/post/' + e.detail.post.id + '/' + section, null, false); + } + } + + _evtMerge(e) { + this._view.clearMessages(); + this._view.disableForm(); + e.detail.post.merge(e.detail.targetPost.id, e.detail.useOldContent) + .then(() => { + this._view = new PostDetailView({ + post: e.detail.targetPost, + section: 'merge', + canMerge: api.hasPrivilege('posts:merge'), + }); + this._view.showSuccess('Post merged.'); + router.replace( + '/post/' + e.detail.targetPost.id + '/merge', null, false); + }, errorMessage => { + this._view.showError(errorMessage); + this._view.enableForm(); + }); + } +} + +module.exports = router => { + router.enter( + '/post/:id/merge', + (ctx, next) => { + ctx.controller = new PostDetailController(ctx, 'merge'); + }); +}; diff --git a/client/js/controllers/post_controller.js b/client/js/controllers/post_main_controller.js index 384643a..561ecb4 100644 --- a/client/js/controllers/post_controller.js +++ b/client/js/controllers/post_main_controller.js @@ -7,26 +7,19 @@ const settings = require('../models/settings.js'); const Comment = require('../models/comment.js'); const Post = require('../models/post.js'); const PostList = require('../models/post_list.js'); -const topNavigation = require('../models/top_navigation.js'); -const PostView = require('../views/post_view.js'); +const PostMainView = require('../views/post_main_view.js'); +const BasePostController = require('./base_post_controller.js'); const EmptyView = require('../views/empty_view.js'); -class PostController { - constructor(id, editMode, ctx) { - if (!api.hasPrivilege('posts:view')) { - this._view = new EmptyView(); - this._view.showError('You don\'t have privileges to view posts.'); - return; - } - - topNavigation.activate('posts'); - topNavigation.setTitle('Post #' + id.toString()); +class PostMainController extends BasePostController { + constructor(ctx, editMode) { + super(ctx); let parameters = ctx.parameters; Promise.all([ - Post.get(id), + Post.get(ctx.parameters.id), PostList.getAround( - id, this._decorateSearchQuery( + ctx.parameters.id, this._decorateSearchQuery( parameters ? parameters.query : '')), ]).then(responses => { const [post, aroundResponse] = responses; @@ -36,13 +29,13 @@ class PostController { if (parameters.query) { ctx.state.parameters = parameters; const url = editMode ? - '/post/' + id + '/edit' : - '/post/' + id; + '/post/' + ctx.parameters.id + '/edit' : + '/post/' + ctx.parameters.id; router.replace(url, ctx.state, false); } this._post = post; - this._view = new PostView({ + this._view = new PostMainView({ post: post, editMode: editMode, prevPostId: aroundResponse.prev ? aroundResponse.prev.id : null, @@ -72,6 +65,8 @@ class PostController { 'feature', e => this._evtFeaturePost(e)); this._view.sidebarControl.addEventListener( 'delete', e => this._evtDeletePost(e)); + this._view.sidebarControl.addEventListener( + 'merge', e => this._evtMergePost(e)); } if (this._view.commentFormControl) { @@ -128,6 +123,10 @@ class PostController { }); } + _evtMergePost(e) { + router.show('/post/' + e.detail.post.id + '/merge'); + } + _evtDeletePost(e) { this._view.sidebarControl.disableForm(); this._view.sidebarControl.clearMessages(); @@ -262,7 +261,7 @@ module.exports = router => { if (ctx.state.parameters) { Object.assign(ctx.parameters, ctx.state.parameters); } - ctx.controller = new PostController(ctx.parameters.id, true, ctx); + ctx.controller = new PostMainController(ctx, true); }); router.enter( '/post/:id/:parameters(.*)?', @@ -272,6 +271,6 @@ module.exports = router => { if (ctx.state.parameters) { Object.assign(ctx.parameters, ctx.state.parameters); } - ctx.controller = new PostController(ctx.parameters.id, false, ctx); + ctx.controller = new PostMainController(ctx, false); }); }; diff --git a/client/js/controls/post_edit_sidebar_control.js b/client/js/controls/post_edit_sidebar_control.js index 2dc3629..49576a1 100644 --- a/client/js/controls/post_edit_sidebar_control.js +++ b/client/js/controls/post_edit_sidebar_control.js @@ -36,6 +36,7 @@ class PostEditSidebarControl extends events.EventTarget { canCreateAnonymousPosts: api.hasPrivilege('posts:create:anonymous'), canDeletePosts: api.hasPrivilege('posts:delete'), canFeaturePosts: api.hasPrivilege('posts:feature'), + canMergePosts: api.hasPrivilege('posts:merge'), })); new ExpanderControl( @@ -108,6 +109,11 @@ class PostEditSidebarControl extends events.EventTarget { 'click', e => this._evtFeatureClick(e)); } + if (this._mergeLinkNode) { + this._mergeLinkNode.addEventListener( + 'click', e => this._evtMergeClick(e)); + } + if (this._deleteLinkNode) { this._deleteLinkNode.addEventListener( 'click', e => this._evtDeleteClick(e)); @@ -186,6 +192,15 @@ class PostEditSidebarControl extends events.EventTarget { } } + _evtMergeClick(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('merge', { + detail: { + post: this._post, + }, + })); + } + _evtDeleteClick(e) { e.preventDefault(); if (confirm('Are you sure you want to delete this post?')) { @@ -314,6 +329,10 @@ class PostEditSidebarControl extends events.EventTarget { return this._formNode.querySelector('.management .feature'); } + get _mergeLinkNode() { + return this._formNode.querySelector('.management .merge'); + } + get _deleteLinkNode() { return this._formNode.querySelector('.management .delete'); } diff --git a/client/js/main.js b/client/js/main.js index 25b6469..0f324b6 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -34,7 +34,8 @@ controllers.push(require('./controllers/auth_controller.js')); controllers.push(require('./controllers/password_reset_controller.js')); controllers.push(require('./controllers/comments_controller.js')); controllers.push(require('./controllers/snapshots_controller.js')); -controllers.push(require('./controllers/post_controller.js')); +controllers.push(require('./controllers/post_detail_controller.js')); +controllers.push(require('./controllers/post_main_controller.js')); controllers.push(require('./controllers/post_list_controller.js')); controllers.push(require('./controllers/post_upload_controller.js')); controllers.push(require('./controllers/tag_controller.js')); diff --git a/client/js/models/post.js b/client/js/models/post.js index f5f8f00..56a9450 100644 --- a/client/js/models/post.js +++ b/client/js/models/post.js @@ -190,6 +190,31 @@ class Post extends events.EventTarget { }); } + merge(targetId, useOldContent) { + return api.get('/post/' + encodeURIComponent(targetId)) + .then(response => { + return api.post('/post-merge/', { + removeVersion: this._version, + remove: this._id, + mergeToVersion: response.version, + mergeTo: targetId, + replaceContent: useOldContent, + }); + }, response => { + return Promise.reject(response); + }).then(response => { + this._updateFromResponse(response); + this.dispatchEvent(new CustomEvent('change', { + detail: { + post: this, + }, + })); + return Promise.resolve(); + }, response => { + return Promise.reject(response.description); + }); + } + setScore(score) { return api.put('/post/' + this._id + '/score', {score: score}) .then(response => { diff --git a/client/js/views/post_detail_view.js b/client/js/views/post_detail_view.js new file mode 100644 index 0000000..4c19059 --- /dev/null +++ b/client/js/views/post_detail_view.js @@ -0,0 +1,80 @@ +'use strict'; + +const events = require('../events.js'); +const views = require('../util/views.js'); +const PostMergeView = require('./post_merge_view.js'); +const EmptyView = require('../views/empty_view.js'); + +const template = views.getTemplate('post-detail'); + +class PostDetailView extends events.EventTarget { + constructor(ctx) { + super(); + + this._ctx = ctx; + ctx.post.addEventListener('change', e => this._evtChange(e)); + ctx.section = ctx.section || 'summary'; + + this._hostNode = document.getElementById('content-holder'); + this._install(); + } + + _install() { + const ctx = this._ctx; + views.replaceContent(this._hostNode, template(ctx)); + + for (let item of this._hostNode.querySelectorAll('[data-name]')) { + item.classList.toggle( + 'active', item.getAttribute('data-name') === ctx.section); + } + + ctx.hostNode = this._hostNode.querySelector('.post-content-holder'); + if (ctx.section === 'merge') { + if (!this._ctx.canMerge) { + this._view = new EmptyView(); + this._view.showError( + 'You don\'t have privileges to merge posts.'); + } else { + this._view = new PostMergeView(ctx); + events.proxyEvent(this._view, this, 'select'); + events.proxyEvent(this._view, this, 'submit', 'merge'); + } + + } else { + // this._view = new PostSummaryView(ctx); + } + + views.syncScrollPosition(); + } + + clearMessages() { + this._view.clearMessages(); + } + + enableForm() { + this._view.enableForm(); + } + + disableForm() { + this._view.disableForm(); + } + + showSuccess(message) { + this._view.showSuccess(message); + } + + showError(message) { + this._view.showError(message); + } + + selectPost(post) { + this._view.selectPost(post); + } + + _evtChange(e) { + this._ctx.post = e.detail.post; + this._install(this._ctx); + } +} + +module.exports = PostDetailView; diff --git a/client/js/views/post_view.js b/client/js/views/post_main_view.js index 256eb2d..446297d 100644 --- a/client/js/views/post_view.js +++ b/client/js/views/post_main_view.js @@ -13,9 +13,9 @@ const PostEditSidebarControl = const CommentListControl = require('../controls/comment_list_control.js'); const CommentFormControl = require('../controls/comment_form_control.js'); -const template = views.getTemplate('post'); +const template = views.getTemplate('post-main'); -class PostView { +class PostMainView { constructor(ctx) { this._hostNode = document.getElementById('content-holder'); @@ -118,4 +118,4 @@ class PostView { } } -module.exports = PostView; +module.exports = PostMainView; diff --git a/client/js/views/post_merge_view.js b/client/js/views/post_merge_view.js new file mode 100644 index 0000000..fb17527 --- /dev/null +++ b/client/js/views/post_merge_view.js @@ -0,0 +1,129 @@ +'use strict'; + +const config = require('../config.js'); +const events = require('../events.js'); +const views = require('../util/views.js'); + +const KEY_RETURN = 13; +const template = views.getTemplate('post-merge'); +const sideTemplate = views.getTemplate('post-merge-side'); + +class PostMergeView extends events.EventTarget { + constructor(ctx) { + super(); + + this._ctx = ctx; + this._post = ctx.post; + this._hostNode = ctx.hostNode; + + this._leftPost = ctx.post; + this._rightPost = null; + views.replaceContent(this._hostNode, template(this._ctx)); + views.decorateValidator(this._formNode); + + this._refreshLeftSide(); + this._refreshRightSide(); + + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + } + + clearMessages() { + views.clearMessages(this._hostNode); + } + + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + selectPost(post) { + this._rightPost = post; + this._refreshRightSide(); + } + + _refreshLeftSide() { + views.replaceContent( + this._leftSideNode, + sideTemplate(Object.assign({}, this._ctx, { + post: this._leftPost, + name: 'left', + editable: false}))); + } + + _refreshRightSide() { + views.replaceContent( + this._rightSideNode, + sideTemplate(Object.assign({}, this._ctx, { + post: this._rightPost, + name: 'right', + editable: true}))); + + if (this._targetPostFieldNode) { + this._targetPostFieldNode.addEventListener( + 'keydown', e => this._evtTargetPostFieldKeyDown(e)); + } + } + + _evtSubmit(e) { + e.preventDefault(); + const checkedTargetPost = this._formNode.querySelector( + '.target-post :checked').value; + const checkedTargetPostContent = this._formNode.querySelector( + '.target-post-content :checked').value; + this.dispatchEvent(new CustomEvent('submit', { + detail: { + post: checkedTargetPost == 'left' ? + this._rightPost : + this._leftPost, + targetPost: checkedTargetPost == 'left' ? + this._leftPost : + this._rightPost, + useOldContent: checkedTargetPostContent !== checkedTargetPost, + }, + })); + } + + _evtTargetPostFieldKeyDown(e) { + const key = e.which; + if (key !== KEY_RETURN) { + return; + } + e.target.blur(); + e.preventDefault(); + this.dispatchEvent(new CustomEvent('select', { + detail: { + postId: this._targetPostFieldNode.value, + }, + })); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _leftSideNode() { + return this._hostNode.querySelector('.left-post-container'); + } + + get _rightSideNode() { + return this._hostNode.querySelector('.right-post-container'); + } + + get _targetPostFieldNode() { + return this._formNode.querySelector( + '.post-mirror input:not([readonly])[type=text]'); + } +} + +module.exports = PostMergeView; |