diff --git a/app/Entity.php b/app/Entity.php index efbbf0eba..df8e4d38b 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -69,14 +69,15 @@ class Entity extends Ownable /** * Get the comments for an entity - * @return \Illuminate\Database\Eloquent\Relations\MorphMany + * @param bool $orderByCreated + * @return MorphMany */ - public function comments() + public function comments($orderByCreated = true) { - return $this->morphMany(Comment::class, 'entity')->orderBy('created_at', 'asc'); + $query = $this->morphMany(Comment::class, 'entity'); + return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query; } - /** * Get the related search terms. * @return \Illuminate\Database\Eloquent\Relations\MorphMany diff --git a/app/Repos/CommentRepo.php b/app/Repos/CommentRepo.php index c3d7468cf..d8c57bdb3 100644 --- a/app/Repos/CommentRepo.php +++ b/app/Repos/CommentRepo.php @@ -80,7 +80,7 @@ class CommentRepo { */ protected function getNextLocalId(Entity $entity) { - $comments = $entity->comments()->orderBy('local_id', 'desc')->first(); + $comments = $entity->comments(false)->orderBy('local_id', 'desc')->first(); if ($comments === null) return 1; return $comments->local_id + 1; } diff --git a/resources/assets/js/components/index.js b/resources/assets/js/components/index.js index d970a581f..b0e8b84aa 100644 --- a/resources/assets/js/components/index.js +++ b/resources/assets/js/components/index.js @@ -16,18 +16,36 @@ let componentMapping = { window.components = {}; let componentNames = Object.keys(componentMapping); +initAll(); -for (let i = 0, len = componentNames.length; i < len; i++) { - let name = componentNames[i]; - let elems = document.querySelectorAll(`[${name}]`); - if (elems.length === 0) continue; +/** + * Initialize components of the given name within the given element. + * @param {String} componentName + * @param {HTMLElement|Document} parentElement + */ +function initComponent(componentName, parentElement) { + let elems = parentElement.querySelectorAll(`[${componentName}]`); + if (elems.length === 0) return; - let component = componentMapping[name]; - if (typeof window.components[name] === "undefined") window.components[name] = []; + let component = componentMapping[componentName]; + if (typeof window.components[componentName] === "undefined") window.components[componentName] = []; for (let j = 0, jLen = elems.length; j < jLen; j++) { - let instance = new component(elems[j]); - if (typeof elems[j].components === 'undefined') elems[j].components = {}; - elems[j].components[name] = instance; - window.components[name].push(instance); + let instance = new component(elems[j]); + if (typeof elems[j].components === 'undefined') elems[j].components = {}; + elems[j].components[componentName] = instance; + window.components[componentName].push(instance); } -} \ No newline at end of file +} + +/** + * Initialize all components found within the given element. + * @param parentElement + */ +function initAll(parentElement) { + if (typeof parentElement === 'undefined') parentElement = document; + for (let i = 0, len = componentNames.length; i < len; i++) { + initComponent(componentNames[i], parentElement); + } +} + +window.components.init = initAll; \ No newline at end of file diff --git a/resources/assets/js/components/page-comments.js b/resources/assets/js/components/page-comments.js index 87cd88905..b5541c348 100644 --- a/resources/assets/js/components/page-comments.js +++ b/resources/assets/js/components/page-comments.js @@ -18,11 +18,18 @@ class PageComments { this.elem.addEventListener('submit', this.updateComment.bind(this)); this.editingComment = null; + this.parentId = null; } handleAction(event) { let actionElem = event.target.closest('[action]'); + if (event.target.matches('a[href^="#"]')) { + let id = event.target.href.split('#')[1]; + console.log(document.querySelector('#' + id)); + window.scrollAndHighlight(document.querySelector('#' + id)); + } if (actionElem === null) return; + event.preventDefault(); let action = actionElem.getAttribute('action'); if (action === 'edit') this.editComment(actionElem.closest('[comment]')); @@ -30,7 +37,8 @@ class PageComments { if (action === 'delete') this.deleteComment(actionElem.closest('[comment]')); if (action === 'addComment') this.showForm(); if (action === 'hideForm') this.hideForm(); - if (action === 'reply') this.setReply(); + if (action === 'reply') this.setReply(actionElem.closest('[comment]')); + if (action === 'remove-reply-to') this.removeReplyTo(); } closeUpdateForm() { @@ -54,7 +62,7 @@ class PageComments { let reqData = { text: text, html: md.render(text), - // parent_id: this.parent_id TODO - Handle replies + parent_id: this.parentId || null, }; // TODO - Loading indicator let commentId = this.editingComment.getAttribute('comment'); @@ -63,6 +71,7 @@ class PageComments { newComment.innerHTML = resp.data; this.editingComment.innerHTML = newComment.children[0].innerHTML; window.$events.emit('success', window.trans('entities.comment_updated_success')); + window.components.init(this.editingComment); this.closeUpdateForm(); this.editingComment = null; }); @@ -71,7 +80,6 @@ class PageComments { deleteComment(commentElem) { let id = commentElem.getAttribute('comment'); // TODO - Loading indicator - // TODO - Confirm dropdown window.$http.delete(window.baseUrl(`/ajax/comment/${id}`)).then(resp => { commentElem.parentNode.removeChild(commentElem); window.$events.emit('success', window.trans('entities.comment_deleted_success')); @@ -86,14 +94,15 @@ class PageComments { let reqData = { text: text, html: md.render(text), - // parent_id: this.parent_id TODO - Handle replies + parent_id: this.parentId || null, }; // TODO - Loading indicator window.$http.post(window.baseUrl(`/ajax/page/${this.pageId}/comment`), reqData).then(resp => { let newComment = document.createElement('div'); newComment.innerHTML = resp.data; - this.container.appendChild(newComment.children[0]); - + let newElem = newComment.children[0]; + this.container.appendChild(newElem); + window.components.init(newElem); window.$events.emit('success', window.trans('entities.comment_created_success')); this.resetForm(); this.updateCount(); @@ -109,13 +118,15 @@ class PageComments { this.formInput.value = ''; this.formContainer.appendChild(this.form); this.hideForm(); + this.removeReplyTo(); } showForm() { this.formContainer.style.display = 'block'; this.formContainer.parentNode.style.display = 'block'; this.elem.querySelector('[comment-add-button]').style.display = 'none'; - this.formInput.focus(); // TODO - Scroll to input on focus + this.formInput.focus(); + window.scrollToElement(this.formInput); } hideForm() { @@ -124,9 +135,18 @@ class PageComments { this.elem.querySelector('[comment-add-button]').style.display = 'block'; } - setReply() { - + setReply(commentElem) { this.showForm(); + this.parentId = Number(commentElem.getAttribute('local-id')); + this.elem.querySelector('[comment-form-reply-to]').style.display = 'block'; + let replyLink = this.elem.querySelector('[comment-form-reply-to] a'); + replyLink.textContent = `#${this.parentId}`; + replyLink.href = `#comment${this.parentId}`; + } + + removeReplyTo() { + this.parentId = null; + this.elem.querySelector('[comment-form-reply-to]').style.display = 'none'; } } diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 37b1b8b7c..b02f6caf3 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -87,12 +87,42 @@ Controllers(ngApp, window.$events); //Global jQuery Config & Extensions +/** + * Scroll the view to a specific element. + * @param {HTMLElement} element + */ +window.scrollToElement = function(element) { + if (!element) return; + let top = element.getBoundingClientRect().top + document.body.scrollTop; + $('html, body').animate({ + scrollTop: top - 60 // Adjust to change final scroll position top margin + }, 300); +}; + +/** + * Scroll and highlight an element. + * @param {HTMLElement} element + */ +window.scrollAndHighlight = function(element) { + if (!element) return; + window.scrollToElement(element); + let color = document.getElementById('custom-styles').getAttribute('data-color-light'); + let initColor = window.getComputedStyle(element).getPropertyValue('background-color'); + element.style.backgroundColor = color; + setTimeout(() => { + element.classList.add('selectFade'); + element.style.backgroundColor = initColor; + }, 10); + setTimeout(() => { + element.classList.remove('selectFade'); + element.style.backgroundColor = ''; + }, 3000); +}; + // Smooth scrolling jQuery.fn.smoothScrollTo = function () { if (this.length === 0) return; - $('html, body').animate({ - scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin - }, 300); // Adjust to change animations speed (ms) + window.scrollToElement(this[0]); return this; }; diff --git a/resources/assets/js/pages/page-show.js b/resources/assets/js/pages/page-show.js index 832ec4b36..14437cd68 100644 --- a/resources/assets/js/pages/page-show.js +++ b/resources/assets/js/pages/page-show.js @@ -81,15 +81,7 @@ let setupPageShow = window.setupPageShow = function (pageId) { let idElem = document.getElementById(text); $('.page-content [data-highlighted]').attr('data-highlighted', '').css('background-color', ''); if (idElem !== null) { - let $idElem = $(idElem); - let color = $('#custom-styles').attr('data-color-light'); - $idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo(); - setTimeout(() => { - $idElem.addClass('anim').addClass('selectFade').css('background-color', ''); - setTimeout(() => { - $idElem.removeClass('selectFade'); - }, 3000); - }, 100); + window.scrollAndHighlight(idElem); } else { $('.page-content').find(':contains("' + text + '")').smoothScrollTo(); } @@ -158,9 +150,6 @@ let setupPageShow = window.setupPageShow = function (pageId) { unstickTree(); } }); - - // in order to call from other places. - window.setupPageShow.goToText = goToText; }; module.exports = setupPageShow; \ No newline at end of file diff --git a/resources/assets/js/translations.js b/resources/assets/js/translations.js index 99c6b4321..70ebfc255 100644 --- a/resources/assets/js/translations.js +++ b/resources/assets/js/translations.js @@ -44,7 +44,6 @@ class Translator { // Parse exact matches let exactMatches = t.match(exactCountRegex); - console.log(exactMatches); if (exactMatches !== null && Number(exactMatches[1]) === count) { result = t.replace(exactCountRegex, '').trim(); break; diff --git a/resources/assets/sass/_animations.scss b/resources/assets/sass/_animations.scss index 015a23ab1..c03553d15 100644 --- a/resources/assets/sass/_animations.scss +++ b/resources/assets/sass/_animations.scss @@ -91,6 +91,6 @@ animation-timing-function: cubic-bezier(.62, .28, .23, .99); } -.anim.selectFade { +.selectFade { transition: background-color ease-in-out 3000ms; } \ No newline at end of file diff --git a/resources/assets/sass/_comments.scss b/resources/assets/sass/_comments.scss index 5fcd49d6e..5fb7e338b 100644 --- a/resources/assets/sass/_comments.scss +++ b/resources/assets/sass/_comments.scss @@ -8,24 +8,29 @@ .content p { margin-bottom: 1em; } + .reply-row { + padding: $-xs $-s; + } } .comment-box .header { padding: $-xs $-s; background-color: #f8f8f8; border-bottom: 1px solid #DDD; - img, a, span { - display: inline-block; - vertical-align: top; - } - a, span { - padding: $-xxs 0 $-xxs 0; - line-height: 1.6; - } - a { color: #666; } - span { - color: #888; - padding-left: $-xxs; + .meta { + img, a, span { + display: inline-block; + vertical-align: top; + } + a, span { + padding: $-xxs 0 $-xxs 0; + line-height: 1.6; + } + a { color: #666; } + span { + color: #888; + padding-left: $-xxs; + } } .text-muted { color: #999; diff --git a/resources/assets/sass/_grid.scss b/resources/assets/sass/_grid.scss index d24ffcfaf..367946f90 100644 --- a/resources/assets/sass/_grid.scss +++ b/resources/assets/sass/_grid.scss @@ -157,6 +157,10 @@ div[class^="col-"] img { &.small { max-width: 840px; } + &.nopad { + padding-left: 0; + padding-right: 0; + } } .row { diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index 2dd4732f2..d30d4d4a2 100644 --- a/resources/assets/sass/_lists.scss +++ b/resources/assets/sass/_lists.scss @@ -352,6 +352,7 @@ ul.pagination { } li.padded { padding: $-xs $-m; + line-height: 1.2; } a { display: block; diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index cfbd1a991..e63981049 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -251,7 +251,8 @@ return [ 'comment_deleted_success' => 'Comment deleted', 'comment_created_success' => 'Comment added', 'comment_updated_success' => 'Comment updated', - 'comment_delete_confirm' => 'This will remove the contents of the comment. Are you sure you want to delete this comment?', + 'comment_delete_confirm' => 'Are you sure you want to delete this comment?', + 'comment_in_reply_to' => 'In reply to :commentId', 'comment_create' => 'Created' ]; \ No newline at end of file diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index dcf5faf1e..e32de68f2 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -1,4 +1,4 @@ -<div class="comment-box" comment="{{ $comment->id }}" id="comment{{$comment->local_id}}"> +<div class="comment-box" comment="{{ $comment->id }}" local-id="{{$comment->local_id}}" parent-id="{{$comment->parent_id || ''}}" id="comment{{$comment->local_id}}"> <div class="header"> <div class="float right actions"> @@ -9,26 +9,43 @@ <button type="button" class="text-button" action="reply" title="{{ trans('common.reply') }}"><i class="zmdi zmdi-mail-reply-all"></i></button> @endif @if(userCan('comment-delete', $comment)) - <button type="button" class="text-button" action="delete" title="{{ trans('common.delete') }}"><i class="zmdi zmdi-delete"></i></button> + + <div dropdown class="dropdown-container"> + <button type="button" dropdown-toggle class="text-button" title="{{ trans('common.delete') }}"><i class="zmdi zmdi-delete"></i></button> + <ul> + <li class="padded"><small class="text-muted">{{trans('entities.comment_delete_confirm')}}</small></li> + <li><a action="delete" class="text-button neg" ><i class="zmdi zmdi-delete"></i>{{ trans('common.delete') }}</a></li> + </ul> + </div> @endif </div> - <a href="#comment{{$comment->local_id}}" class="text-muted">#{{$comment->local_id}}</a> - - <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar" alt="{{ $comment->createdBy->name }}"> - - <a href="{{ $comment->createdBy->getProfileUrl() }}">{{ $comment->createdBy->name }}</a> - {{--TODO - Account for deleted user--}} - <span title="{{ $comment->created_at }}"> + <div class="meta"> + <a href="#comment{{$comment->local_id}}" class="text-muted">#{{$comment->local_id}}</a> + + <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar" alt="{{ $comment->createdBy->name }}"> + + <a href="{{ $comment->createdBy->getProfileUrl() }}">{{ $comment->createdBy->name }}</a> + {{--TODO - Account for deleted user--}} + <span title="{{ $comment->created_at }}"> {{ trans('entities.comment_created', ['createDiff' => $comment->created]) }} </span> - @if($comment->isUpdated()) - <span title="{{ $comment->updated_at }}"> + @if($comment->isUpdated()) + <span title="{{ $comment->updated_at }}"> • - {{ trans('entities.comment_updated', ['updateDiff' => $comment->updated, 'username' => $comment->updatedBy->name]) }} + {{ trans('entities.comment_updated', ['updateDiff' => $comment->updated, 'username' => $comment->updatedBy->name]) }} </span> - @endif + @endif + </div> + </div> + + @if ($comment->parent_id) + <div class="reply-row primary-background-light text-muted"> + {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href="#comment'.$comment->parent_id.'">#'.$comment->parent_id.'</a>']) !!} + </div> + @endif + <div comment-content class="content"> {!! $comment->html !!} </div> diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php index cf2affc15..a0fd518f6 100644 --- a/resources/views/comments/comments.blade.php +++ b/resources/views/comments/comments.blade.php @@ -12,6 +12,10 @@ <div class="comment-box" comment-box style="display:none;"> <div class="header"><i class="zmdi zmdi-comment"></i> {{ trans('entities.comment_new') }}</div> + <div comment-form-reply-to class="reply-row primary-background-light text-muted" style="display: none;"> + <button class="text-button float right" action="remove-reply-to">{{ trans('common.remove') }}</button> + {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href=""></a>']) !!} + </div> <div class="content" comment-form-container> <form novalidate> <div class="form-group"> diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index 51c24cbd7..d7edd2fff 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -148,7 +148,7 @@ </div> - <div class="container small"> + <div class="container small nopad"> @include('comments/comments', ['page' => $page]) </div> @stop