diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 8b37379fa..32ff76fa1 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -144,226 +144,4 @@ module.exports = function (ngApp, events) { }; }]); - - // Controller used to reply to and add new comments - ngApp.controller('CommentReplyController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { - const MarkdownIt = require("markdown-it"); - const md = new MarkdownIt({html: true}); - let vm = this; - - vm.saveComment = function () { - let pageId = $scope.comment.pageId || $scope.pageId; - let comment = $scope.comment.text; - if (!comment) { - return events.emit('warning', trans('errors.empty_comment')); - } - let commentHTML = md.render($scope.comment.text); - let serviceUrl = `/ajax/page/${pageId}/comment/`; - let httpMethod = 'post'; - let reqObj = { - text: comment, - html: commentHTML - }; - - if ($scope.isEdit === true) { - // this will be set when editing the comment. - serviceUrl = `/ajax/page/${pageId}/comment/${$scope.comment.id}`; - httpMethod = 'put'; - } else if ($scope.isReply === true) { - // if its reply, get the parent comment id - reqObj.parent_id = $scope.parentId; - } - $http[httpMethod](window.baseUrl(serviceUrl), reqObj).then(resp => { - if (!isCommentOpSuccess(resp)) { - return; - } - // hide the comments first, and then retrigger the refresh - if ($scope.isEdit) { - updateComment($scope.comment, resp.data); - $scope.$emit('evt.comment-success', $scope.comment.id); - } else { - $scope.comment.text = ''; - if ($scope.isReply === true && $scope.parent.sub_comments) { - $scope.parent.sub_comments.push(resp.data.comment); - } else { - $scope.$emit('evt.new-comment', resp.data.comment); - } - $scope.$emit('evt.comment-success', null, true); - } - $scope.comment.is_hidden = true; - $timeout(function() { - $scope.comment.is_hidden = false; - }); - - events.emit('success', trans(resp.data.message)); - - }, checkError); - - }; - - function checkError(response) { - let msg = null; - if (isCommentOpSuccess(response)) { - // all good - return; - } else if (response.data) { - msg = response.data.message; - } else { - msg = trans('errors.comment_add'); - } - if (msg) { - events.emit('success', msg); - } - } - }]); - - // Controller used to delete comments - ngApp.controller('CommentDeleteController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { - let vm = this; - - vm.delete = function(comment) { - $http.delete(window.baseUrl(`/ajax/comment/${comment.id}`)).then(resp => { - if (!isCommentOpSuccess(resp)) { - return; - } - updateComment(comment, resp.data, $timeout, true); - }, function (resp) { - if (isCommentOpSuccess(resp)) { - events.emit('success', trans('entities.comment_deleted')); - } else { - events.emit('error', trans('error.comment_delete')); - } - }); - }; - }]); - - // Controller used to fetch all comments for a page - ngApp.controller('CommentListController', ['$scope', '$http', '$timeout', '$location', function ($scope, $http, $timeout, $location) { - let vm = this; - $scope.errors = {}; - // keep track of comment levels - $scope.level = 1; - vm.totalCommentsStr = trans('entities.comments_loading'); - vm.permissions = {}; - vm.trans = window.trans; - - $scope.$on('evt.new-comment', function (event, comment) { - // add the comment to the comment list. - vm.comments.push(comment); - ++vm.totalComments; - setTotalCommentMsg(); - event.stopPropagation(); - event.preventDefault(); - }); - - vm.canEditDelete = function (comment, prop) { - if (!comment.active) { - return false; - } - let propAll = prop + '_all'; - let propOwn = prop + '_own'; - - if (vm.permissions[propAll]) { - return true; - } - - if (vm.permissions[propOwn] && comment.created_by.id === vm.current_user_id) { - return true; - } - - return false; - }; - - vm.canComment = function () { - return vm.permissions.comment_create; - }; - - // check if there are is any direct linking - let linkedCommentId = $location.search().cm; - - $timeout(function() { - $http.get(window.baseUrl(`/ajax/page/${$scope.pageId}/comments/`)).then(resp => { - if (!isCommentOpSuccess(resp)) { - // just show that no comments are available. - vm.totalComments = 0; - setTotalCommentMsg(); - return; - } - vm.comments = resp.data.comments; - vm.totalComments = +resp.data.total; - vm.permissions = resp.data.permissions; - vm.current_user_id = resp.data.user_id; - setTotalCommentMsg(); - if (!linkedCommentId) { - return; - } - $timeout(function() { - // wait for the UI to render. - focusLinkedComment(linkedCommentId); - }); - }, checkError); - }); - - function setTotalCommentMsg () { - if (vm.totalComments === 0) { - vm.totalCommentsStr = trans('entities.no_comments'); - } else if (vm.totalComments === 1) { - vm.totalCommentsStr = trans('entities.one_comment'); - } else { - vm.totalCommentsStr = trans('entities.x_comments', { - numComments: vm.totalComments - }); - } - } - - function focusLinkedComment(linkedCommentId) { - let comment = angular.element('#' + linkedCommentId); - if (comment.length === 0) { - return; - } - - window.setupPageShow.goToText(linkedCommentId); - } - - function checkError(response) { - let msg = null; - if (isCommentOpSuccess(response)) { - // all good - return; - } else if (response.data) { - msg = response.data.message; - } else { - msg = trans('errors.comment_list'); - } - if (msg) { - events.emit('success', msg); - } - } - }]); - - function updateComment(comment, resp, $timeout, isDelete) { - comment.text = resp.comment.text; - comment.updated = resp.comment.updated; - comment.updated_by = resp.comment.updated_by; - comment.active = resp.comment.active; - if (isDelete && !resp.comment.active) { - comment.html = trans('entities.comment_deleted'); - } else { - comment.html = resp.comment.html; - } - if (!$timeout) { - return; - } - comment.is_hidden = true; - $timeout(function() { - comment.is_hidden = false; - }); - } - - function isCommentOpSuccess(resp) { - if (resp && resp.data && resp.data.status === 'success') { - return true; - } - return false; - } }; diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 8813eb881..1f28673e1 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -386,128 +386,4 @@ module.exports = function (ngApp, events) { } } }]); - - ngApp.directive('commentReply', [function () { - return { - restrict: 'E', - templateUrl: 'comment-reply.html', - scope: { - pageId: '=', - parentId: '=', - parent: '=' - }, - link: function (scope, element) { - scope.isReply = true; - element.find('textarea').focus(); - scope.$on('evt.comment-success', function (event) { - // no need for the event to do anything more. - event.stopPropagation(); - event.preventDefault(); - scope.closeBox(); - }); - - scope.closeBox = function () { - element.remove(); - scope.$destroy(); - }; - } - }; - }]); - - ngApp.directive('commentEdit', [function () { - return { - restrict: 'E', - templateUrl: 'comment-reply.html', - scope: { - comment: '=' - }, - link: function (scope, element) { - scope.isEdit = true; - element.find('textarea').focus(); - scope.$on('evt.comment-success', function (event, commentId) { - // no need for the event to do anything more. - event.stopPropagation(); - event.preventDefault(); - if (commentId === scope.comment.id && !scope.isNew) { - scope.closeBox(); - } - }); - - scope.closeBox = function () { - element.remove(); - scope.$destroy(); - }; - } - }; - }]); - - - ngApp.directive('commentReplyLink', ['$document', '$compile', function ($document, $compile) { - return { - scope: { - comment: '=' - }, - link: function (scope, element, attr) { - element.on('$destroy', function () { - element.off('click'); - scope.$destroy(); - }); - - element.on('click', function (e) { - e.preventDefault(); - var $container = element.parents('.comment-actions').first(); - if (!$container.length) { - console.error('commentReplyLink directive should be placed inside a container with class comment-box!'); - return; - } - if (attr.noCommentReplyDupe) { - removeDupe(); - } - - compileHtml($container, scope, attr.isReply === 'true'); - }); - } - }; - - function compileHtml($container, scope, isReply) { - let lnkFunc = null; - if (isReply) { - lnkFunc = $compile(''); - } else { - lnkFunc = $compile(''); - } - var compiledHTML = lnkFunc(scope); - $container.append(compiledHTML); - } - - function removeDupe() { - let $existingElement = $document.find('.comments-list comment-reply, .comments-list comment-edit'); - if (!$existingElement.length) { - return; - } - - $existingElement.remove(); - } - }]); - - ngApp.directive('commentDeleteLink', ['$window', function ($window) { - return { - controller: 'CommentDeleteController', - scope: { - comment: '=' - }, - link: function (scope, element, attr, ctrl) { - - element.on('click', function(e) { - e.preventDefault(); - var resp = $window.confirm(trans('entities.comment_delete_confirm')); - if (!resp) { - return; - } - - ctrl.delete(scope.comment); - }); - } - }; - }]); }; diff --git a/resources/assets/js/vues/components/comments/comment-reply.js b/resources/assets/js/vues/components/comments/comment-reply.js new file mode 100644 index 000000000..0f65fc237 --- /dev/null +++ b/resources/assets/js/vues/components/comments/comment-reply.js @@ -0,0 +1,113 @@ +const MarkdownIt = require("markdown-it"); +const md = new MarkdownIt({ html: true }); + +var template = ` +
+
+ + + + +
+
+`; + +const props = { + pageId: {}, + commentObj: {}, + isReply: { + default: false, + type: Boolean + }, isEdit: { + default: false, + type: Boolean + } +}; + +function data() { + let comment = { + text: '' + }; + + if (this.isReply) { + comment.page_id = this.commentObj.page_id; + comment.id = this.commentObj.id; + } else if (this.isEdit) { + comment = this.commentObj; + } + + return { + comment: comment, + trans: trans + }; +} + +const methods = { + saveComment: function (event) { + let pageId = this.comment.page_id || this.pageId; + let commentText = this.comment.text; + if (!commentText) { + return this.$events.emit('error', trans('errors.empty_comment')) + } + let commentHTML = md.render(commentText); + let serviceUrl = `/ajax/page/${pageId}/comment/`; + let httpMethod = 'post'; + let reqObj = { + text: commentText, + html: commentHTML + }; + + if (this.isEdit === true) { + // this will be set when editing the comment. + serviceUrl = `/ajax/page/${pageId}/comment/${this.comment.id}`; + httpMethod = 'put'; + } else if (this.isReply === true) { + // if its reply, get the parent comment id + reqObj.parent_id = this.comment.id; + } + $http[httpMethod](window.baseUrl(serviceUrl), reqObj).then(resp => { + if (!isCommentOpSuccess(resp)) { + this.$events.emit('error', getErrorMsg(resp)); + return; + } + // hide the comments first, and then retrigger the refresh + if (this.isEdit) { + this.$emit('comment-edited', event, resp.data.comment); + } else { + this.comment.text = ''; + this.$emit('comment-added', event); + if (this.isReply === true) { + this.$emit('comment-replied', event, resp.data.comment); + } else { + this.$parent.$emit('new-comment', event, resp.data.comment); + } + } + this.$events.emit('success', resp.data.message); + }).catch(err => { + this.$events.emit('error', trans('errors.comment_add')) + }); + }, + closeBox: function (event) { + this.$emit('editor-removed', event); + } +}; + +const computed = {}; + +function isCommentOpSuccess(resp) { + if (resp && resp.data && resp.data.status === 'success') { + return true; + } + return false; +} + +function getErrorMsg(response) { + if (response.data) { + return response.data.message; + } else { + return trans('errors.comment_add'); + } +} + +module.exports = { name: 'comment-reply', template, data, props, methods, computed }; + diff --git a/resources/assets/js/vues/components/comments/comment.js b/resources/assets/js/vues/components/comments/comment.js new file mode 100644 index 000000000..419c0a5fa --- /dev/null +++ b/resources/assets/js/vues/components/comments/comment.js @@ -0,0 +1,174 @@ +const commentReply = require('./comment-reply'); + +const template = ` +
+
+
+ user avatar +
+
+ +
+ +
+
+ {{ trans('entities.comment_deleted') }} +
+
+ +
+
+ + +
+ + +
+
+
+`; + +const props = ['initialComment', 'index', 'level', 'permissions', 'currentUserId']; + +function data() { + return { + trans: trans, + comments: [], + showEditor: false, + comment: this.initialComment, + nextLevel: this.level + 1 + }; +} + +const methods = { + deleteComment: function () { + var resp = window.confirm(trans('entities.comment_delete_confirm')); + if (!resp) { + return; + } + this.$http.delete(window.baseUrl(`/ajax/comment/${this.comment.id}`)).then(resp => { + if (!isCommentOpSuccess(resp)) { + this.$events.emit('error', trans('error.comment_delete')); + return; + } + this.$events.emit('success', trans('entities.comment_deleted')); + this.comment = resp.data.comment; + }).catch(err => { + this.$events.emit('error', trans('error.comment_delete')); + }); + }, + replyComment: function () { + this.toggleEditor(false); + }, + editComment: function () { + this.toggleEditor(true); + }, + hideComment: function () { + this.showEditor = false; + }, + toggleEditor: function (isEdit) { + this.showEditor = false; + this.isEdit = isEdit; + this.isReply = !isEdit; + this.showEditor = true; + }, + commentReplied: function (event, comment) { + this.comments.push(comment); + this.showEditor = false; + }, + commentEdited: function (event, comment) { + this.comment = comment; + this.showEditor = false; + }, + commentAdded: function (event, comment) { + // this is to handle non-parent child relationship + // we want to make it go up. + this.$emit('comment-added', event); + }, + canEditOrDelete: function (prop) { + if (!this.comment.active) { + return false; + } + + if (!this.permissions) { + return false; + } + + let propAll = 'comment_' + prop + '_all'; + let propOwn = 'comment_' + prop + '_own'; + + if (this.permissions[propAll]) { + return true; + } + + if (this.permissions[propOwn] && this.comment.created_by.id === this.currentUserId) { + return true; + } + + return false; + }, + canComment: function () { + if (!this.permissions) { + return false; + } + return this.permissions.comment_create === true; + } +}; + +const computed = { + commentId: function () { + return `comment-${this.comment.page_id}-${this.comment.id}`; + }, + commentHref: function () { + return `#?cm=${this.commentId}`; + } +}; + +function mounted() { + if (this.comment.sub_comments && this.comment.sub_comments.length) { + // set this so that we can render the next set of sub comments. + this.comments = this.comment.sub_comments; + } +} + +function isCommentOpSuccess(resp) { + if (resp && resp.data && resp.data.status === 'success') { + return true; + } + return false; +} + +module.exports = { + name: 'comment', + template, data, props, methods, computed, mounted, components: { + commentReply + } +}; + diff --git a/resources/assets/js/vues/page-comments.js b/resources/assets/js/vues/page-comments.js new file mode 100644 index 000000000..e42cdbf9c --- /dev/null +++ b/resources/assets/js/vues/page-comments.js @@ -0,0 +1,117 @@ +const comment = require('./components/comments/comment'); +const commentReply = require('./components/comments/comment-reply'); + +let data = { + totalCommentsStr: trans('entities.comments_loading'), + comments: [], + permissions: null, + currentUserId: null, + trans: trans, + commentCount: 0 +}; + +let methods = { + commentAdded: function () { + ++this.totalComments; + } +} + +let computed = { + totalComments: { + get: function () { + return this.commentCount; + }, + set: function (value) { + this.commentCount = value; + if (value === 0) { + this.totalCommentsStr = trans('entities.no_comments'); + } else if (value === 1) { + this.totalCommentsStr = trans('entities.one_comment'); + } else { + this.totalCommentsStr = trans('entities.x_comments', { + numComments: value + }); + } + } + }, + canComment: function () { + if (!this.permissions) { + return false; + } + return this.permissions.comment_create === true; + } +} + +function mounted() { + this.pageId = Number(this.$el.getAttribute('page-id')); + let linkedCommentId = getUrlParameter('cm'); + this.$http.get(window.baseUrl(`/ajax/page/${this.pageId}/comments/`)).then(resp => { + if (!isCommentOpSuccess(resp)) { + // just show that no comments are available. + vm.totalComments = 0; + this.$events.emit('error', getErrorMsg(resp)); + return; + } + this.comments = resp.data.comments; + this.totalComments = +resp.data.total; + this.permissions = resp.data.permissions; + this.currentUserId = resp.data.user_id; + if (!linkedCommentId) { + return; + } + + // adding a setTimeout to give the comment list some time to render + // before focusing the comment. + setTimeout(function() { + focusLinkedComment(linkedCommentId); + }); + }).catch(err => { + this.$events.emit('error', trans('errors.comment_list')); + }); +} + +function isCommentOpSuccess(resp) { + if (resp && resp.data && resp.data.status === 'success') { + return true; + } + return false; +} + +function getErrorMsg(response) { + if (response.data) { + return response.data.message; + } else { + return trans('errors.comment_add'); + } +} + +function created() { + this.$on('new-comment', function (event, comment) { + this.comments.push(comment); + }) +} + +function beforeDestroy() { + this.$off('new-comment'); +} + +function getUrlParameter(name) { + name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); + var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); + var results = regex.exec(location.hash); + return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); +} + +function focusLinkedComment(linkedCommentId) { + let comment = document.getElementById(linkedCommentId); + if (comment && comment.length !== 0) { + window.setupPageShow.goToText(linkedCommentId); + } +} + +module.exports = { + data, methods, mounted, computed, components: { + comment, commentReply + }, + created, beforeDestroy +}; \ No newline at end of file diff --git a/resources/assets/js/vues/vues.js b/resources/assets/js/vues/vues.js index a70d32009..d4c4c4574 100644 --- a/resources/assets/js/vues/vues.js +++ b/resources/assets/js/vues/vues.js @@ -11,6 +11,7 @@ let vueMapping = { 'image-manager': require('./image-manager'), 'tag-manager': require('./tag-manager'), 'attachment-manager': require('./attachment-manager'), + 'page-comments': require('./page-comments') }; window.vues = {}; diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php index 71bcd1f9a..09158caac 100644 --- a/resources/lang/en/errors.php +++ b/resources/lang/en/errors.php @@ -63,7 +63,7 @@ return [ // Comments 'comment_list' => 'An error occurred while fetching the comments.', 'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.', - 'comment_add' => 'An error occurred while adding the comment.', + 'comment_add' => 'An error occurred while adding / updating the comment.', 'comment_delete' => 'An error occurred while deleting the comment.', 'empty_comment' => 'Cannot add an empty comment.', diff --git a/resources/views/comments/comment-reply.blade.php b/resources/views/comments/comment-reply.blade.php deleted file mode 100644 index 02535341c..000000000 --- a/resources/views/comments/comment-reply.blade.php +++ /dev/null @@ -1,12 +0,0 @@ -
-
- - - - -
-
- -@if($errors->has('markdown')) -
{{ $errors->first('markdown') }}
-@endif \ No newline at end of file diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php index ffa75cfed..fcf284b26 100644 --- a/resources/views/comments/comments.blade.php +++ b/resources/views/comments/comments.blade.php @@ -1,18 +1,11 @@ - - -
-

@{{vm.totalCommentsStr}}

-
-
-
- -
-
-
- @include('comments/comment-reply', ['pageId' => $pageId]) -
+
+

@{{totalCommentsStr}}

+
+ +
+ + +
\ No newline at end of file diff --git a/resources/views/comments/list-item.blade.php b/resources/views/comments/list-item.blade.php deleted file mode 100644 index f274d2ed2..000000000 --- a/resources/views/comments/list-item.blade.php +++ /dev/null @@ -1,30 +0,0 @@ -
-
- user avatar -
-
- -
- -
-
- {{ trans('entities.comment_deleted') }} -
-
- -
-
-
-
-
-
-
\ No newline at end of file