+ <%= ctx.makeRadio({
+ required: true,
+ text: 'Merge to this post
' +
+ ctx.makeUserLink(ctx.post.user) +
+ ', ' +
+ ctx.makeRelativeTime(ctx.post.creationTime) +
+ '',
+ name: 'target-post',
+ value: ctx.name,
+ }) %>
+
+
+ <%= ctx.makeRadio({
+ required: true,
+ text: 'Use this file
' +
+ 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}` :
+ '?') +
+ ')',
+ name: 'target-post-content',
+ value: ctx.name,
+ }) %>
+
+
+
+<% } %>
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
similarity index 91%
rename from client/js/controllers/post_controller.js
rename to 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
similarity index 97%
rename from client/js/views/post_view.js
rename to 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;