From 4b9f6beb376fc6b0c31af77d96ff0842bcdd53bc Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Wed, 7 Jun 2023 13:24:49 +0100 Subject: [PATCH 1/5] Comments: Updated to show as nested threads Initial functional implementation, a lot of tweaking and adapting to be done. --- app/Activity/CommentRepo.php | 24 +---- app/Activity/Tools/CommentTree.php | 102 ++++++++++++++++++ app/Entities/Controllers/PageController.php | 12 +-- resources/sass/_components.scss | 7 ++ .../views/comments/comment-branch.blade.php | 17 +++ resources/views/comments/comment.blade.php | 2 +- resources/views/comments/comments.blade.php | 10 +- resources/views/pages/show.blade.php | 4 +- 8 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 app/Activity/Tools/CommentTree.php create mode 100644 resources/views/comments/comment-branch.blade.php diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index f16767fcf..2aabab79d 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -7,27 +7,14 @@ use BookStack\Entities\Models\Entity; use BookStack\Facades\Activity as ActivityService; use League\CommonMark\CommonMarkConverter; -/** - * Class CommentRepo. - */ class CommentRepo { - /** - * @var Comment - */ - protected $comment; - - public function __construct(Comment $comment) - { - $this->comment = $comment; - } - /** * Get a comment by ID. */ public function getById(int $id): Comment { - return $this->comment->newQuery()->findOrFail($id); + return Comment::query()->findOrFail($id); } /** @@ -36,7 +23,7 @@ class CommentRepo public function create(Entity $entity, string $text, ?int $parent_id): Comment { $userId = user()->id; - $comment = $this->comment->newInstance(); + $comment = new Comment(); $comment->text = $text; $comment->html = $this->commentToHtml($text); @@ -83,7 +70,7 @@ class CommentRepo 'allow_unsafe_links' => false, ]); - return $converter->convertToHtml($commentText); + return $converter->convert($commentText); } /** @@ -91,9 +78,8 @@ class CommentRepo */ protected function getNextLocalId(Entity $entity): int { - /** @var Comment $comment */ - $comment = $entity->comments(false)->orderBy('local_id', 'desc')->first(); + $currentMaxId = $entity->comments()->max('local_id'); - return ($comment->local_id ?? 0) + 1; + return $currentMaxId + 1; } } diff --git a/app/Activity/Tools/CommentTree.php b/app/Activity/Tools/CommentTree.php new file mode 100644 index 000000000..559edccf3 --- /dev/null +++ b/app/Activity/Tools/CommentTree.php @@ -0,0 +1,102 @@ +<?php + +namespace BookStack\Activity\Tools; + +use BookStack\Activity\Models\Comment; +use BookStack\Entities\Models\Page; + +class CommentTree +{ + /** + * The built nested tree structure array. + * @var array{comment: Comment, depth: int, children: array}[] + */ + protected array $tree; + protected array $comments; + + public function __construct( + protected Page $page + ) { + $this->comments = $this->loadComments(); + $this->tree = $this->createTree($this->comments); + } + + public function enabled(): bool + { + return !setting('app-disable-comments'); + } + + public function empty(): bool + { + return count($this->tree) === 0; + } + + public function count(): int + { + return count($this->comments); + } + + public function get(): array + { + return $this->tree; + } + + /** + * @param Comment[] $comments + */ + protected function createTree(array $comments): array + { + $byId = []; + foreach ($comments as $comment) { + $byId[$comment->local_id] = $comment; + } + + $childMap = []; + foreach ($comments as $comment) { + $parent = $comment->parent_id; + if (is_null($parent) || !isset($byId[$parent])) { + $parent = 0; + } + + if (!isset($childMap[$parent])) { + $childMap[$parent] = []; + } + $childMap[$parent][] = $comment->local_id; + } + + $tree = []; + foreach ($childMap[0] as $childId) { + $tree[] = $this->createTreeForId($childId, 0, $byId, $childMap); + } + + return $tree; + } + + protected function createTreeForId(int $id, int $depth, array &$byId, array &$childMap): array + { + $childIds = $childMap[$id] ?? []; + $children = []; + + foreach ($childIds as $childId) { + $children[] = $this->createTreeForId($childId, $depth + 1, $byId, $childMap); + } + + return [ + 'comment' => $byId[$id], + 'depth' => $depth, + 'children' => $children, + ]; + } + + protected function loadComments(): array + { + if (!$this->enabled()) { + return []; + } + + return $this->page->comments() + ->with('createdBy') + ->get() + ->all(); + } +} diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php index a6ef68dd7..e0444ecd2 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -3,6 +3,7 @@ namespace BookStack\Entities\Controllers; use BookStack\Activity\Models\View; +use BookStack\Activity\Tools\CommentTree; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Tools\BookContents; @@ -140,15 +141,10 @@ class PageController extends Controller $pageContent = (new PageContent($page)); $page->html = $pageContent->render(); - $sidebarTree = (new BookContents($page->book))->getTree(); $pageNav = $pageContent->getNavigation($page->html); - // Check if page comments are enabled - $commentsEnabled = !setting('app-disable-comments'); - if ($commentsEnabled) { - $page->load(['comments.createdBy']); - } - + $sidebarTree = (new BookContents($page->book))->getTree(); + $commentTree = (new CommentTree($page)); $nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree); View::incrementFor($page); @@ -159,7 +155,7 @@ class PageController extends Controller 'book' => $page->book, 'current' => $page, 'sidebarTree' => $sidebarTree, - 'commentsEnabled' => $commentsEnabled, + 'commentTree' => $commentTree, 'pageNav' => $pageNav, 'next' => $nextPreviousLocator->getNext(), 'previous' => $nextPreviousLocator->getPrevious(), diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 1521e6eaa..bd85bb99f 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -704,6 +704,13 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } } +.comment-thread-indicator { + border-inline-start: 3px dotted #DDD; + @include lightDark(border-color, #DDD, #444); + margin-inline-start: $-xs; + width: $-l; +} + #tag-manager .drag-card { max-width: 500px; } diff --git a/resources/views/comments/comment-branch.blade.php b/resources/views/comments/comment-branch.blade.php new file mode 100644 index 000000000..d64dd4ade --- /dev/null +++ b/resources/views/comments/comment-branch.blade.php @@ -0,0 +1,17 @@ +<div> + <div class="mb-m"> + @include('comments.comment', ['comment' => $branch['comment']]) + </div> + @if(count($branch['children']) > 0) + <div class="flex-container-row"> + <div class="pb-m"> + <div class="comment-thread-indicator fill-height"></div> + </div> + <div class="flex"> + @foreach($branch['children'] as $childBranch) + @include('comments.comment-branch', ['branch' => $childBranch]) + @endforeach + </div> + </div> + @endif +</div> \ No newline at end of file diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 6189c65d4..093e5a899 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -1,4 +1,4 @@ -<div class="comment-box mb-m" comment="{{ $comment->id }}" local-id="{{$comment->local_id}}" parent-id="{{$comment->parent_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 p-s"> <div class="grid half left-focus no-gap v-center"> <div class="meta text-muted text-small"> diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php index 140d0d027..f50e3a218 100644 --- a/resources/views/comments/comments.blade.php +++ b/resources/views/comments/comments.blade.php @@ -8,8 +8,8 @@ aria-label="{{ trans('entities.comments') }}"> <div refs="page-comments@commentCountBar" class="grid half left-focus v-center no-row-gap"> - <h5 comments-title>{{ trans_choice('entities.comment_count', count($page->comments), ['count' => count($page->comments)]) }}</h5> - @if (count($page->comments) === 0 && userCan('comment-create-all')) + <h5 comments-title>{{ trans_choice('entities.comment_count', $commentTree->count(), ['count' => $commentTree->count()]) }}</h5> + @if ($commentTree->empty() && userCan('comment-create-all')) <div class="text-m-right" refs="page-comments@addButtonContainer"> <button type="button" action="addComment" class="button outline">{{ trans('entities.comment_add') }}</button> @@ -18,15 +18,15 @@ </div> <div refs="page-comments@commentContainer" class="comment-container"> - @foreach($page->comments as $comment) - @include('comments.comment', ['comment' => $comment]) + @foreach($commentTree->get() as $branch) + @include('comments.comment-branch', ['branch' => $branch]) @endforeach </div> @if(userCan('comment-create-all')) @include('comments.create') - @if (count($page->comments) > 0) + @if (!$commentTree->empty()) <div refs="page-comments@addButtonContainer" class="text-right"> <button type="button" action="addComment" class="button outline">{{ trans('entities.comment_add') }}</button> diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index 2cbc7fe47..fa6b1a2cd 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -27,7 +27,7 @@ @include('entities.sibling-navigation', ['next' => $next, 'previous' => $previous]) - @if ($commentsEnabled) + @if ($commentTree->enabled()) @if(($previous || $next)) <div class="px-xl"> <hr class="darker"> @@ -35,7 +35,7 @@ @endif <div class="px-xl comments-container mb-l print-hidden"> - @include('comments.comments', ['page' => $page]) + @include('comments.comments', ['commentTree' => $commentTree, 'page' => $page]) <div class="clearfix"></div> </div> @endif From 154924cc0c19546599bbe1953744f9ab1162b263 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Wed, 7 Jun 2023 17:47:37 +0100 Subject: [PATCH 2/5] Comments: updated component and split out code Split out comment component code so single-comment actions (delete, edit) are handled within their own compontent. Modernised existing component code. --- lang/en/entities.php | 2 - resources/js/components/index.js | 1 + resources/js/components/page-comment.js | 85 ++++++++++ resources/js/components/page-comments.js | 159 +++++------------- .../views/comments/comment-branch.blade.php | 2 +- resources/views/comments/comment.blade.php | 43 +++-- resources/views/comments/comments.blade.php | 14 +- resources/views/comments/create.blade.php | 20 +-- 8 files changed, 167 insertions(+), 159 deletions(-) create mode 100644 resources/js/components/page-comment.js diff --git a/lang/en/entities.php b/lang/en/entities.php index 501fc9f2a..8499bb30f 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -362,8 +362,6 @@ return [ 'comment_placeholder' => 'Leave a comment here', 'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments', 'comment_save' => 'Save Comment', - 'comment_saving' => 'Saving comment...', - 'comment_deleting' => 'Deleting comment...', 'comment_new' => 'New Comment', 'comment_created' => 'commented :createDiff', 'comment_updated' => 'Updated :updateDiff by :username', diff --git a/resources/js/components/index.js b/resources/js/components/index.js index 803714e62..a56f18a5a 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -34,6 +34,7 @@ export {MarkdownEditor} from './markdown-editor'; export {NewUserPassword} from './new-user-password'; export {Notification} from './notification'; export {OptionalInput} from './optional-input'; +export {PageComment} from './page-comment'; export {PageComments} from './page-comments'; export {PageDisplay} from './page-display'; export {PageEditor} from './page-editor'; diff --git a/resources/js/components/page-comment.js b/resources/js/components/page-comment.js new file mode 100644 index 000000000..ff86297e2 --- /dev/null +++ b/resources/js/components/page-comment.js @@ -0,0 +1,85 @@ +import {Component} from './component'; +import {getLoading, htmlToDom} from '../services/dom'; + +export class PageComment extends Component { + + setup() { + // Options + this.commentId = this.$opts.commentId; + this.commentLocalId = this.$opts.commentLocalId; + this.commentParentId = this.$opts.commentParentId; + this.deletedText = this.$opts.deletedText; + this.updatedText = this.$opts.updatedText; + + // Element References + this.container = this.$el; + this.contentContainer = this.$refs.contentContainer; + this.form = this.$refs.form; + this.formCancel = this.$refs.formCancel; + this.editButton = this.$refs.editButton; + this.deleteButton = this.$refs.deleteButton; + this.replyButton = this.$refs.replyButton; + this.input = this.$refs.input; + + this.setupListeners(); + } + + setupListeners() { + this.replyButton.addEventListener('click', () => this.$emit('reply', {id: this.commentLocalId})); + this.editButton.addEventListener('click', this.startEdit.bind(this)); + this.deleteButton.addEventListener('click', this.delete.bind(this)); + this.form.addEventListener('submit', this.update.bind(this)); + this.formCancel.addEventListener('click', () => this.toggleEditMode(false)); + } + + toggleEditMode(show) { + this.contentContainer.toggleAttribute('hidden', show); + this.form.toggleAttribute('hidden', !show); + } + + startEdit() { + this.toggleEditMode(true); + const lineCount = this.$refs.input.value.split('\n').length; + this.$refs.input.style.height = `${(lineCount * 20) + 40}px`; + } + + async update(event) { + event.preventDefault(); + const loading = this.showLoading(); + this.form.toggleAttribute('hidden', true); + + const reqData = { + text: this.input.value, + parent_id: this.parentId || null, + }; + + try { + const resp = await window.$http.put(`/comment/${this.commentId}`, reqData); + const newComment = htmlToDom(resp.data); + this.container.replaceWith(newComment); + window.$events.success(this.updatedText); + } catch (err) { + console.error(err); + window.$events.showValidationErrors(err); + this.form.toggleAttribute('hidden', false); + loading.remove(); + } + } + + async delete() { + this.showLoading(); + + await window.$http.delete(`/comment/${this.commentId}`); + this.container.closest('.comment-branch').remove(); + window.$events.success(this.deletedText); + this.$emit('delete'); + } + + showLoading() { + const loading = getLoading(); + loading.classList.add('px-l'); + this.container.append(loading); + return loading; + } + +} diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js index 0ac9d0572..9dc529963 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.js @@ -1,6 +1,5 @@ -import {scrollAndHighlightElement} from '../services/util'; import {Component} from './component'; -import {htmlToDom} from '../services/dom'; +import {getLoading, htmlToDom} from '../services/dom'; export class PageComments extends Component { @@ -10,166 +9,110 @@ export class PageComments extends Component { // Element references this.container = this.$refs.commentContainer; - this.formContainer = this.$refs.formContainer; this.commentCountBar = this.$refs.commentCountBar; + this.commentsTitle = this.$refs.commentsTitle; this.addButtonContainer = this.$refs.addButtonContainer; this.replyToRow = this.$refs.replyToRow; + this.formContainer = this.$refs.formContainer; + this.form = this.$refs.form; + this.formInput = this.$refs.formInput; + this.addCommentButton = this.$refs.addCommentButton; + this.hideFormButton = this.$refs.hideFormButton; + this.removeReplyToButton = this.$refs.removeReplyToButton; // Translations - this.updatedText = this.$opts.updatedText; - this.deletedText = this.$opts.deletedText; this.createdText = this.$opts.createdText; this.countText = this.$opts.countText; // Internal State - this.editingComment = null; this.parentId = null; - if (this.formContainer) { - this.form = this.formContainer.querySelector('form'); - this.formInput = this.form.querySelector('textarea'); - this.form.addEventListener('submit', this.saveComment.bind(this)); - } - - this.elem.addEventListener('click', this.handleAction.bind(this)); - this.elem.addEventListener('submit', this.updateComment.bind(this)); + this.setupListeners(); } - handleAction(event) { - const actionElem = event.target.closest('[action]'); + setupListeners() { + this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this)); + this.hideFormButton.addEventListener('click', this.hideForm.bind(this)); + this.addCommentButton.addEventListener('click', this.showForm.bind(this)); - if (event.target.matches('a[href^="#"]')) { - const id = event.target.href.split('#')[1]; - scrollAndHighlightElement(document.querySelector(`#${id}`)); - } - - if (actionElem === null) return; - event.preventDefault(); - - const action = actionElem.getAttribute('action'); - const comment = actionElem.closest('[comment]'); - if (action === 'edit') this.editComment(comment); - if (action === 'closeUpdateForm') this.closeUpdateForm(); - if (action === 'delete') this.deleteComment(comment); - if (action === 'addComment') this.showForm(); - if (action === 'hideForm') this.hideForm(); - if (action === 'reply') this.setReply(comment); - if (action === 'remove-reply-to') this.removeReplyTo(); - } - - closeUpdateForm() { - if (!this.editingComment) return; - this.editingComment.querySelector('[comment-content]').style.display = 'block'; - this.editingComment.querySelector('[comment-edit-container]').style.display = 'none'; - } - - editComment(commentElem) { - this.hideForm(); - if (this.editingComment) this.closeUpdateForm(); - commentElem.querySelector('[comment-content]').style.display = 'none'; - commentElem.querySelector('[comment-edit-container]').style.display = 'block'; - const textArea = commentElem.querySelector('[comment-edit-container] textarea'); - const lineCount = textArea.value.split('\n').length; - textArea.style.height = `${(lineCount * 20) + 40}px`; - this.editingComment = commentElem; - } - - updateComment(event) { - const form = event.target; - event.preventDefault(); - const text = form.querySelector('textarea').value; - const reqData = { - text, - parent_id: this.parentId || null, - }; - this.showLoading(form); - const commentId = this.editingComment.getAttribute('comment'); - window.$http.put(`/comment/${commentId}`, reqData).then(resp => { - const newComment = document.createElement('div'); - newComment.innerHTML = resp.data; - this.editingComment.innerHTML = newComment.children[0].innerHTML; - window.$events.success(this.updatedText); - window.$components.init(this.editingComment); - this.closeUpdateForm(); - this.editingComment = null; - }).catch(window.$events.showValidationErrors).then(() => { - this.hideLoading(form); - }); - } - - deleteComment(commentElem) { - const id = commentElem.getAttribute('comment'); - this.showLoading(commentElem.querySelector('[comment-content]')); - window.$http.delete(`/comment/${id}`).then(() => { - commentElem.parentNode.removeChild(commentElem); - window.$events.success(this.deletedText); + this.elem.addEventListener('page-comment-delete', () => { this.updateCount(); this.hideForm(); }); + + this.elem.addEventListener('page-comment-reply', event => { + this.setReply(event.detail.id); + }); + + if (this.form) { + this.form.addEventListener('submit', this.saveComment.bind(this)); + } } saveComment(event) { event.preventDefault(); event.stopPropagation(); + + const loading = getLoading(); + loading.classList.add('px-l'); + this.form.after(loading); + this.form.toggleAttribute('hidden', true); + const text = this.formInput.value; const reqData = { text, parent_id: this.parentId || null, }; - this.showLoading(this.form); + window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => { const newElem = htmlToDom(resp.data); this.container.appendChild(newElem); - window.$components.init(newElem); window.$events.success(this.createdText); this.resetForm(); this.updateCount(); }).catch(err => { + this.form.toggleAttribute('hidden', false); window.$events.showValidationErrors(err); - this.hideLoading(this.form); }); + + loading.remove(); } updateCount() { - const count = this.container.children.length; - this.elem.querySelector('[comments-title]').textContent = window.trans_plural(this.countText, count, {count}); + const count = this.getCommentCount(); + this.commentsTitle.textContent = window.trans_plural(this.countText, count, {count}); } resetForm() { this.formInput.value = ''; - this.formContainer.appendChild(this.form); this.hideForm(); this.removeReplyTo(); - this.hideLoading(this.form); } showForm() { - this.formContainer.style.display = 'block'; - this.formContainer.parentNode.style.display = 'block'; - this.addButtonContainer.style.display = 'none'; + this.formContainer.toggleAttribute('hidden', false); + this.addButtonContainer.toggleAttribute('hidden', true); this.formInput.focus(); - this.formInput.scrollIntoView({behavior: 'smooth'}); } hideForm() { - this.formContainer.style.display = 'none'; - this.formContainer.parentNode.style.display = 'none'; + this.formContainer.toggleAttribute('hidden', true); if (this.getCommentCount() > 0) { this.elem.appendChild(this.addButtonContainer); } else { this.commentCountBar.appendChild(this.addButtonContainer); } - this.addButtonContainer.style.display = 'block'; + this.addButtonContainer.toggleAttribute('hidden', false); } getCommentCount() { - return this.elem.querySelectorAll('.comment-box[comment]').length; + return this.container.querySelectorAll('[compontent="page-comment"]').length; } - setReply(commentElem) { + setReply(commentLocalId) { this.showForm(); - this.parentId = Number(commentElem.getAttribute('local-id')); - this.replyToRow.style.display = 'block'; + this.parentId = commentLocalId; + this.replyToRow.toggleAttribute('hidden', false); const replyLink = this.replyToRow.querySelector('a'); replyLink.textContent = `#${this.parentId}`; replyLink.href = `#comment${this.parentId}`; @@ -177,23 +120,7 @@ export class PageComments extends Component { removeReplyTo() { this.parentId = null; - this.replyToRow.style.display = 'none'; - } - - showLoading(formElem) { - const groups = formElem.querySelectorAll('.form-group'); - for (const group of groups) { - group.style.display = 'none'; - } - formElem.querySelector('.form-group.loading').style.display = 'block'; - } - - hideLoading(formElem) { - const groups = formElem.querySelectorAll('.form-group'); - for (const group of groups) { - group.style.display = 'block'; - } - formElem.querySelector('.form-group.loading').style.display = 'none'; + this.replyToRow.toggleAttribute('hidden', true); } } diff --git a/resources/views/comments/comment-branch.blade.php b/resources/views/comments/comment-branch.blade.php index d64dd4ade..69c967cd2 100644 --- a/resources/views/comments/comment-branch.blade.php +++ b/resources/views/comments/comment-branch.blade.php @@ -1,4 +1,4 @@ -<div> +<div class="comment-branch"> <div class="mb-m"> @include('comments.comment', ['comment' => $branch['comment']]) </div> diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 093e5a899..ce0d59473 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -1,4 +1,11 @@ -<div class="comment-box" comment="{{ $comment->id }}" local-id="{{$comment->local_id}}" parent-id="{{$comment->parent_id}}" id="comment{{$comment->local_id}}"> +<div component="page-comment" + option:page-comment:comment-id="{{ $comment->id }}" + option:page-comment:comment-local-id="{{ $comment->local_id }}" + option:page-comment:comment-parent-id="{{ $comment->parent_id }}" + option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}" + option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}" + id="comment{{$comment->local_id}}" + class="comment-box"> <div class="header p-s"> <div class="grid half left-focus no-gap v-center"> <div class="meta text-muted text-small"> @@ -21,10 +28,10 @@ </div> <div class="actions text-right"> @if(userCan('comment-update', $comment)) - <button type="button" class="text-button" action="edit" aria-label="{{ trans('common.edit') }}" title="{{ trans('common.edit') }}">@icon('edit')</button> + <button refs="page-comment@edit-button" type="button" class="text-button" aria-label="{{ trans('common.edit') }}" title="{{ trans('common.edit') }}">@icon('edit')</button> @endif @if(userCan('comment-create-all')) - <button type="button" class="text-button" action="reply" aria-label="{{ trans('common.reply') }}" title="{{ trans('common.reply') }}">@icon('reply')</button> + <button refs="page-comment@reply-button" type="button" class="text-button" aria-label="{{ trans('common.reply') }}" title="{{ trans('common.reply') }}">@icon('reply')</button> @endif @if(userCan('comment-delete', $comment)) <div component="dropdown" class="dropdown-container"> @@ -32,7 +39,7 @@ <ul refs="dropdown@menu" class="dropdown-menu" role="menu"> <li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li> <li> - <button action="delete" type="button" class="text-button text-neg icon-item"> + <button refs="page-comment@delete-button" type="button" class="text-button text-neg icon-item"> @icon('delete') <div>{{ trans('common.delete') }}</div> </button> @@ -51,28 +58,20 @@ </div> @endif - <div comment-content class="content px-s pb-s"> - <div class="form-group loading" style="display: none;"> - @include('common.loading-icon', ['text' => trans('entities.comment_deleting')]) - </div> + <div refs="page-comment@content-container" class="content px-s pb-s"> {!! $comment->html !!} </div> @if(userCan('comment-update', $comment)) - <div comment-edit-container style="display: none;" class="content px-s"> - <form novalidate> - <div class="form-group description-input"> - <textarea name="markdown" rows="3" placeholder="{{ trans('entities.comment_placeholder') }}">{{ $comment->text }}</textarea> - </div> - <div class="form-group text-right"> - <button type="button" class="button outline" action="closeUpdateForm">{{ trans('common.cancel') }}</button> - <button type="submit" class="button">{{ trans('entities.comment_save') }}</button> - </div> - <div class="form-group loading" style="display: none;"> - @include('common.loading-icon', ['text' => trans('entities.comment_saving')]) - </div> - </form> - </div> + <form novalidate refs="page-comment@form" hidden class="content px-s block"> + <div class="form-group description-input"> + <textarea refs="page-comment@input" name="markdown" rows="3" placeholder="{{ trans('entities.comment_placeholder') }}">{{ $comment->text }}</textarea> + </div> + <div class="form-group text-right"> + <button type="button" class="button outline" refs="page-comment@form-cancel">{{ trans('common.cancel') }}</button> + <button type="submit" class="button">{{ trans('entities.comment_save') }}</button> + </div> + </form> @endif </div> \ No newline at end of file diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php index f50e3a218..b79f0fd45 100644 --- a/resources/views/comments/comments.blade.php +++ b/resources/views/comments/comments.blade.php @@ -1,17 +1,16 @@ <section component="page-comments" option:page-comments:page-id="{{ $page->id }}" - option:page-comments:updated-text="{{ trans('entities.comment_updated_success') }}" - option:page-comments:deleted-text="{{ trans('entities.comment_deleted_success') }}" option:page-comments:created-text="{{ trans('entities.comment_created_success') }}" option:page-comments:count-text="{{ trans('entities.comment_count') }}" class="comments-list" aria-label="{{ trans('entities.comments') }}"> - <div refs="page-comments@commentCountBar" class="grid half left-focus v-center no-row-gap"> - <h5 comments-title>{{ trans_choice('entities.comment_count', $commentTree->count(), ['count' => $commentTree->count()]) }}</h5> + <div refs="page-comments@comment-count-bar" class="grid half left-focus v-center no-row-gap"> + <h5 refs="page-comments@comments-title">{{ trans_choice('entities.comment_count', $commentTree->count(), ['count' => $commentTree->count()]) }}</h5> @if ($commentTree->empty() && userCan('comment-create-all')) - <div class="text-m-right" refs="page-comments@addButtonContainer"> - <button type="button" action="addComment" + <div class="text-m-right" refs="page-comments@add-button-container"> + <button type="button" + refs="page-comments@add-comment-button" class="button outline">{{ trans('entities.comment_add') }}</button> </div> @endif @@ -28,7 +27,8 @@ @if (!$commentTree->empty()) <div refs="page-comments@addButtonContainer" class="text-right"> - <button type="button" action="addComment" + <button type="button" + refs="page-comments@add-comment-button" class="button outline">{{ trans('entities.comment_add') }}</button> </div> @endif diff --git a/resources/views/comments/create.blade.php b/resources/views/comments/create.blade.php index a5a84b004..5f9f6d449 100644 --- a/resources/views/comments/create.blade.php +++ b/resources/views/comments/create.blade.php @@ -1,31 +1,29 @@ -<div class="comment-box" style="display:none;"> +<div refs="page-comments@form-container" hidden class="comment-box"> <div class="header p-s">{{ trans('entities.comment_new') }}</div> - <div refs="page-comments@replyToRow" class="reply-row primary-background-light text-muted px-s py-xs mb-s" style="display: none;"> + <div refs="page-comments@reply-to-row" hidden class="primary-background-light text-muted px-s py-xs mb-s"> <div class="grid left-focus v-center"> <div> {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href=""></a>']) !!} </div> <div class="text-right"> - <button class="text-button" action="remove-reply-to">{{ trans('common.remove') }}</button> + <button refs="page-comments@remove-reply-to-button" class="text-button">{{ trans('common.remove') }}</button> </div> </div> </div> - <div refs="page-comments@formContainer" class="content px-s"> - <form novalidate> + <div class="content px-s"> + <form refs="page-comments@form" novalidate> <div class="form-group description-input"> - <textarea name="markdown" rows="3" - placeholder="{{ trans('entities.comment_placeholder') }}"></textarea> + <textarea refs="page-comments@form-input" name="markdown" + rows="3" + placeholder="{{ trans('entities.comment_placeholder') }}"></textarea> </div> <div class="form-group text-right"> <button type="button" class="button outline" - action="hideForm">{{ trans('common.cancel') }}</button> + refs="page-comments@hide-form-button">{{ trans('common.cancel') }}</button> <button type="submit" class="button">{{ trans('entities.comment_save') }}</button> </div> - <div class="form-group loading" style="display: none;"> - @include('common.loading-icon', ['text' => trans('entities.comment_saving')]) - </div> </form> </div> From 3b46b92bb9751fe684d3c3f1cae4b942ee44b36f Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Thu, 8 Jun 2023 15:03:38 +0100 Subject: [PATCH 3/5] Comments: Updated to show form in expected location Includes a change of create response to use a branch as a template. --- .../Controllers/CommentController.php | 15 ++++--- resources/js/components/page-comment.js | 5 ++- resources/js/components/page-comments.js | 25 +++++++---- resources/sass/_components.scss | 4 ++ .../views/comments/comment-branch.blade.php | 20 ++++----- resources/views/comments/comment.blade.php | 6 +-- resources/views/comments/create.blade.php | 44 ++++++++++--------- 7 files changed, 68 insertions(+), 51 deletions(-) diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index b198d2d56..9e7491fd7 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -10,11 +10,9 @@ use Illuminate\Validation\ValidationException; class CommentController extends Controller { - protected $commentRepo; - - public function __construct(CommentRepo $commentRepo) - { - $this->commentRepo = $commentRepo; + public function __construct( + protected CommentRepo $commentRepo + ) { } /** @@ -43,7 +41,12 @@ class CommentController extends Controller $this->checkPermission('comment-create-all'); $comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id')); - return view('comments.comment', ['comment' => $comment]); + return view('comments.comment-branch', [ + 'branch' => [ + 'comment' => $comment, + 'children' => [], + ] + ]); } /** diff --git a/resources/js/components/page-comment.js b/resources/js/components/page-comment.js index ff86297e2..43159b42c 100644 --- a/resources/js/components/page-comment.js +++ b/resources/js/components/page-comment.js @@ -25,7 +25,10 @@ export class PageComment extends Component { } setupListeners() { - this.replyButton.addEventListener('click', () => this.$emit('reply', {id: this.commentLocalId})); + this.replyButton.addEventListener('click', () => this.$emit('reply', { + id: this.commentLocalId, + element: this.container, + })); this.editButton.addEventListener('click', this.startEdit.bind(this)); this.deleteButton.addEventListener('click', this.delete.bind(this)); this.form.addEventListener('submit', this.update.bind(this)); diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js index 9dc529963..1eaa7cfa4 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.js @@ -41,7 +41,7 @@ export class PageComments extends Component { }); this.elem.addEventListener('page-comment-reply', event => { - this.setReply(event.detail.id); + this.setReply(event.detail.id, event.detail.element); }); if (this.form) { @@ -66,15 +66,16 @@ export class PageComments extends Component { window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => { const newElem = htmlToDom(resp.data); - this.container.appendChild(newElem); + this.formContainer.after(newElem); window.$events.success(this.createdText); - this.resetForm(); + this.hideForm(); this.updateCount(); }).catch(err => { this.form.toggleAttribute('hidden', false); window.$events.showValidationErrors(err); }); + this.form.toggleAttribute('hidden', false); loading.remove(); } @@ -85,32 +86,38 @@ export class PageComments extends Component { resetForm() { this.formInput.value = ''; - this.hideForm(); this.removeReplyTo(); + this.container.append(this.formContainer); } showForm() { this.formContainer.toggleAttribute('hidden', false); this.addButtonContainer.toggleAttribute('hidden', true); - this.formInput.focus(); + setTimeout(() => { + this.formInput.focus(); + }, 100); } hideForm() { + this.resetForm(); this.formContainer.toggleAttribute('hidden', true); if (this.getCommentCount() > 0) { - this.elem.appendChild(this.addButtonContainer); + this.elem.append(this.addButtonContainer); } else { - this.commentCountBar.appendChild(this.addButtonContainer); + this.commentCountBar.append(this.addButtonContainer); } this.addButtonContainer.toggleAttribute('hidden', false); } getCommentCount() { - return this.container.querySelectorAll('[compontent="page-comment"]').length; + return this.container.querySelectorAll('[component="page-comment"]').length; } - setReply(commentLocalId) { + setReply(commentLocalId, commentElement) { + const targetFormLocation = commentElement.closest('.comment-branch').querySelector('.comment-branch-children'); this.showForm(); + targetFormLocation.append(this.formContainer); + this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'}); this.parentId = commentLocalId; this.replyToRow.toggleAttribute('hidden', false); const replyLink = this.replyToRow.querySelector('a'); diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index bd85bb99f..a3818168f 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -682,6 +682,9 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { &:hover .actions, &:focus-within .actions { opacity: 1; } + .actions button:focus { + outline: 1px dotted var(--color-primary); + } } .comment-box .header { @@ -709,6 +712,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { @include lightDark(border-color, #DDD, #444); margin-inline-start: $-xs; width: $-l; + height: calc(100% - $-m); } #tag-manager .drag-card { diff --git a/resources/views/comments/comment-branch.blade.php b/resources/views/comments/comment-branch.blade.php index 69c967cd2..78d19ac3e 100644 --- a/resources/views/comments/comment-branch.blade.php +++ b/resources/views/comments/comment-branch.blade.php @@ -2,16 +2,14 @@ <div class="mb-m"> @include('comments.comment', ['comment' => $branch['comment']]) </div> - @if(count($branch['children']) > 0) - <div class="flex-container-row"> - <div class="pb-m"> - <div class="comment-thread-indicator fill-height"></div> - </div> - <div class="flex"> - @foreach($branch['children'] as $childBranch) - @include('comments.comment-branch', ['branch' => $childBranch]) - @endforeach - </div> + <div class="flex-container-row"> + <div class="comment-thread-indicator-parent"> + <div class="comment-thread-indicator"></div> </div> - @endif + <div class="comment-branch-children flex"> + @foreach($branch['children'] as $childBranch) + @include('comments.comment-branch', ['branch' => $childBranch]) + @endforeach + </div> + </div> </div> \ No newline at end of file diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index ce0d59473..924b465fa 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -28,14 +28,14 @@ </div> <div class="actions text-right"> @if(userCan('comment-update', $comment)) - <button refs="page-comment@edit-button" type="button" class="text-button" aria-label="{{ trans('common.edit') }}" title="{{ trans('common.edit') }}">@icon('edit')</button> + <button refs="page-comment@edit-button" type="button" class="text-button icon p-xs" aria-label="{{ trans('common.edit') }}" title="{{ trans('common.edit') }}">@icon('edit')</button> @endif @if(userCan('comment-create-all')) - <button refs="page-comment@reply-button" type="button" class="text-button" aria-label="{{ trans('common.reply') }}" title="{{ trans('common.reply') }}">@icon('reply')</button> + <button refs="page-comment@reply-button" type="button" class="text-button icon p-xs" aria-label="{{ trans('common.reply') }}" title="{{ trans('common.reply') }}">@icon('reply')</button> @endif @if(userCan('comment-delete', $comment)) <div component="dropdown" class="dropdown-container"> - <button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" class="text-button" title="{{ trans('common.delete') }}">@icon('delete')</button> + <button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" class="text-button icon p-xs" title="{{ trans('common.delete') }}">@icon('delete')</button> <ul refs="dropdown@menu" class="dropdown-menu" role="menu"> <li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li> <li> diff --git a/resources/views/comments/create.blade.php b/resources/views/comments/create.blade.php index 5f9f6d449..88cec4876 100644 --- a/resources/views/comments/create.blade.php +++ b/resources/views/comments/create.blade.php @@ -1,30 +1,32 @@ -<div refs="page-comments@form-container" hidden class="comment-box"> +<div refs="page-comments@form-container" hidden class="comment-branch mb-m"> + <div class="comment-box"> - <div class="header p-s">{{ trans('entities.comment_new') }}</div> - <div refs="page-comments@reply-to-row" hidden class="primary-background-light text-muted px-s py-xs mb-s"> - <div class="grid left-focus v-center"> - <div> - {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href=""></a>']) !!} - </div> - <div class="text-right"> - <button refs="page-comments@remove-reply-to-button" class="text-button">{{ trans('common.remove') }}</button> + <div class="header p-s">{{ trans('entities.comment_new') }}</div> + <div refs="page-comments@reply-to-row" hidden class="primary-background-light text-muted px-s py-xs mb-s"> + <div class="grid left-focus v-center"> + <div> + {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href=""></a>']) !!} + </div> + <div class="text-right"> + <button refs="page-comments@remove-reply-to-button" class="text-button">{{ trans('common.remove') }}</button> + </div> </div> </div> - </div> - <div class="content px-s"> - <form refs="page-comments@form" novalidate> - <div class="form-group description-input"> + <div class="content px-s"> + <form refs="page-comments@form" novalidate> + <div class="form-group description-input"> <textarea refs="page-comments@form-input" name="markdown" rows="3" placeholder="{{ trans('entities.comment_placeholder') }}"></textarea> - </div> - <div class="form-group text-right"> - <button type="button" class="button outline" - refs="page-comments@hide-form-button">{{ trans('common.cancel') }}</button> - <button type="submit" class="button">{{ trans('entities.comment_save') }}</button> - </div> - </form> - </div> + </div> + <div class="form-group text-right"> + <button type="button" class="button outline" + refs="page-comments@hide-form-button">{{ trans('common.cancel') }}</button> + <button type="submit" class="button">{{ trans('entities.comment_save') }}</button> + </div> + </form> + </div> + </div> </div> \ No newline at end of file From 3bede42121d28d7bc37d4a4318de07aaa35597f1 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Fri, 9 Jun 2023 11:12:39 +0100 Subject: [PATCH 4/5] Comments: Added visual nesting limit, added nesting test --- resources/sass/_components.scss | 10 ++++++++++ resources/views/pages/show.blade.php | 2 +- tests/Entity/CommentTest.php | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index a3818168f..f75f8b8f8 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -663,6 +663,12 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } } +.comments-container { + padding-inline: $-xl; + @include smaller-than($m) { + padding-inline: $-xs; + } +} .comment-box { border-radius: 4px; border: 1px solid #DDD; @@ -715,6 +721,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { height: calc(100% - $-m); } +.comment-branch .comment-branch .comment-branch .comment-branch .comment-thread-indicator { + display: none; +} + #tag-manager .drag-card { max-width: 500px; } diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index fa6b1a2cd..ae6b273fe 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -34,7 +34,7 @@ </div> @endif - <div class="px-xl comments-container mb-l print-hidden"> + <div class="comments-container mb-l print-hidden"> @include('comments.comments', ['commentTree' => $commentTree, 'page' => $page]) <div class="clearfix"></div> </div> diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentTest.php index 1159df79d..a04933ada 100644 --- a/tests/Entity/CommentTest.php +++ b/tests/Entity/CommentTest.php @@ -114,4 +114,25 @@ class CommentTest extends TestCase $pageView->assertDontSee($script, false); $pageView->assertSee('sometextinthecommentupdated'); } + + public function test_reply_comments_are_nested() + { + $this->asAdmin(); + $page = $this->entities->page(); + + $this->postJson("/comment/$page->id", ['text' => 'My new comment']); + $this->postJson("/comment/$page->id", ['text' => 'My new comment']); + + $respHtml = $this->withHtml($this->get($page->getUrl())); + $respHtml->assertElementCount('.comment-branch', 3); + $respHtml->assertElementNotExists('.comment-branch .comment-branch'); + + $comment = $page->comments()->first(); + $resp = $this->postJson("/comment/$page->id", ['text' => 'My nested comment', 'parent_id' => $comment->local_id]); + $resp->assertStatus(200); + + $respHtml = $this->withHtml($this->get($page->getUrl())); + $respHtml->assertElementCount('.comment-branch', 4); + $respHtml->assertElementContains('.comment-branch .comment-branch', 'My nested comment'); + } } From 19e39ddd1f7f5fe3bef4ead24bc9090e8da58393 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Fri, 9 Jun 2023 17:36:30 +0100 Subject: [PATCH 5/5] Comments: Updated reply-to and general styling Updated reply inidicator to fit with new nesting system, only showing on view when nest within nesting structure. Updated the general design to be a bit cleaner and better adapt on mobile. Tested on FF+Chrome, inc. dark mode. --- lang/en/entities.php | 1 + resources/js/components/page-comment.js | 24 ++++-- resources/js/components/page-comments.js | 13 +++- resources/sass/_components.scss | 36 +++++---- resources/views/comments/comment.blade.php | 87 ++++++++++++---------- resources/views/comments/create.blade.php | 6 +- 6 files changed, 98 insertions(+), 69 deletions(-) diff --git a/lang/en/entities.php b/lang/en/entities.php index 8499bb30f..caf9e2361 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -365,6 +365,7 @@ return [ 'comment_new' => 'New Comment', 'comment_created' => 'commented :createDiff', 'comment_updated' => 'Updated :updateDiff by :username', + 'comment_updated_indicator' => 'Updated', 'comment_deleted_success' => 'Comment deleted', 'comment_created_success' => 'Comment added', 'comment_updated_success' => 'Comment updated', diff --git a/resources/js/components/page-comment.js b/resources/js/components/page-comment.js index 43159b42c..8284d7f20 100644 --- a/resources/js/components/page-comment.js +++ b/resources/js/components/page-comment.js @@ -25,14 +25,22 @@ export class PageComment extends Component { } setupListeners() { - this.replyButton.addEventListener('click', () => this.$emit('reply', { - id: this.commentLocalId, - element: this.container, - })); - this.editButton.addEventListener('click', this.startEdit.bind(this)); - this.deleteButton.addEventListener('click', this.delete.bind(this)); - this.form.addEventListener('submit', this.update.bind(this)); - this.formCancel.addEventListener('click', () => this.toggleEditMode(false)); + if (this.replyButton) { + this.replyButton.addEventListener('click', () => this.$emit('reply', { + id: this.commentLocalId, + element: this.container, + })); + } + + if (this.editButton) { + this.editButton.addEventListener('click', this.startEdit.bind(this)); + this.form.addEventListener('submit', this.update.bind(this)); + this.formCancel.addEventListener('click', () => this.toggleEditMode(false)); + } + + if (this.deleteButton) { + this.deleteButton.addEventListener('click', this.delete.bind(this)); + } } toggleEditMode(show) { diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js index 1eaa7cfa4..a46a5c3b3 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.js @@ -16,6 +16,7 @@ export class PageComments extends Component { this.formContainer = this.$refs.formContainer; this.form = this.$refs.form; this.formInput = this.$refs.formInput; + this.formReplyLink = this.$refs.formReplyLink; this.addCommentButton = this.$refs.addCommentButton; this.hideFormButton = this.$refs.hideFormButton; this.removeReplyToButton = this.$refs.removeReplyToButton; @@ -26,6 +27,7 @@ export class PageComments extends Component { // Internal State this.parentId = null; + this.formReplyText = this.formReplyLink.textContent; this.setupListeners(); } @@ -86,13 +88,15 @@ export class PageComments extends Component { resetForm() { this.formInput.value = ''; - this.removeReplyTo(); + this.parentId = null; + this.replyToRow.toggleAttribute('hidden', true); this.container.append(this.formContainer); } showForm() { this.formContainer.toggleAttribute('hidden', false); this.addButtonContainer.toggleAttribute('hidden', true); + this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'}); setTimeout(() => { this.formInput.focus(); }, 100); @@ -115,19 +119,20 @@ export class PageComments extends Component { setReply(commentLocalId, commentElement) { const targetFormLocation = commentElement.closest('.comment-branch').querySelector('.comment-branch-children'); - this.showForm(); targetFormLocation.append(this.formContainer); - this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'}); + this.showForm(); this.parentId = commentLocalId; this.replyToRow.toggleAttribute('hidden', false); const replyLink = this.replyToRow.querySelector('a'); - replyLink.textContent = `#${this.parentId}`; + replyLink.textContent = this.formReplyText.replace('1234', this.parentId); replyLink.href = `#comment${this.parentId}`; } removeReplyTo() { this.parentId = null; this.replyToRow.toggleAttribute('hidden', true); + this.container.append(this.formContainer); + this.showForm(); } } diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index f75f8b8f8..321c26e88 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -691,26 +691,28 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { .actions button:focus { outline: 1px dotted var(--color-primary); } + @include smaller-than($m) { + .actions { + opacity: 1; + } + } } .comment-box .header { - .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 { - padding-inline-start: $-xxs; - } + border-bottom: 1px solid #DDD; + @include lightDark(border-color, #DDD, #000); + button { + font-size: .8rem; + } + a { + color: inherit; } .text-muted { color: #999; } + .right-meta .text-muted { + opacity: .8; + } } .comment-thread-indicator { @@ -725,6 +727,14 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { display: none; } +.comment-reply { + display: none; +} + +.comment-branch .comment-branch .comment-branch .comment-branch .comment-reply { + display: block; +} + #tag-manager .drag-card { max-width: 500px; } diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 924b465fa..04468b83c 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -7,63 +7,68 @@ id="comment{{$comment->local_id}}" class="comment-box"> <div class="header p-s"> - <div class="grid half left-focus no-gap v-center"> - <div class="meta text-muted text-small"> - <a href="#comment{{$comment->local_id}}">#{{$comment->local_id}}</a> - + <div class="flex-container-row justify-space-between wrap"> + <div class="meta text-muted flex-container-row items-center"> @if ($comment->createdBy) - <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar" alt="{{ $comment->createdBy->name }}"> + <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar mx-xs" alt="{{ $comment->createdBy->name }}"> - <a href="{{ $comment->createdBy->getProfileUrl() }}">{{ $comment->createdBy->name }}</a> + <a href="{{ $comment->createdBy->getProfileUrl() }}">{{ $comment->createdBy->getShortName(16) }}</a> @else - <span>{{ trans('common.deleted_user') }}</span> + {{ trans('common.deleted_user') }} @endif - <span title="{{ $comment->created_at }}">{{ trans('entities.comment_created', ['createDiff' => $comment->created]) }}</span> + <span title="{{ $comment->created_at }}"> {{ trans('entities.comment_created', ['createDiff' => $comment->created]) }}</span> @if($comment->isUpdated()) - <span title="{{ $comment->updated_at }}"> - • - {{ trans('entities.comment_updated', ['updateDiff' => $comment->updated, 'username' => $comment->updatedBy? $comment->updatedBy->name : trans('common.deleted_user')]) }} - </span> + <span class="mx-xs">•</span> + <span title="{{ trans('entities.comment_updated', ['updateDiff' => $comment->updated_at, 'username' => $comment->updatedBy->name ?? trans('common.deleted_user')]) }}"> + {{ trans('entities.comment_updated_indicator') }} + </span> @endif </div> - <div class="actions text-right"> - @if(userCan('comment-update', $comment)) - <button refs="page-comment@edit-button" type="button" class="text-button icon p-xs" aria-label="{{ trans('common.edit') }}" title="{{ trans('common.edit') }}">@icon('edit')</button> - @endif - @if(userCan('comment-create-all')) - <button refs="page-comment@reply-button" type="button" class="text-button icon p-xs" aria-label="{{ trans('common.reply') }}" title="{{ trans('common.reply') }}">@icon('reply')</button> - @endif - @if(userCan('comment-delete', $comment)) - <div component="dropdown" class="dropdown-container"> - <button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" class="text-button icon p-xs" title="{{ trans('common.delete') }}">@icon('delete')</button> - <ul refs="dropdown@menu" class="dropdown-menu" role="menu"> - <li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li> - <li> - <button refs="page-comment@delete-button" type="button" class="text-button text-neg icon-item"> - @icon('delete') - <div>{{ trans('common.delete') }}</div> - </button> - </li> - </ul> - </div> - @endif + <div class="right-meta flex-container-row justify-flex-end items-center px-s"> + <div class="actions mr-s"> + @if(userCan('comment-create-all')) + <button refs="page-comment@reply-button" type="button" class="text-button text-muted hover-underline p-xs">@icon('reply') {{ trans('common.reply') }}</button> + @endif + @if(userCan('comment-update', $comment)) + <button refs="page-comment@edit-button" type="button" class="text-button text-muted hover-underline p-xs">@icon('edit') {{ trans('common.edit') }}</button> + @endif + @if(userCan('comment-delete', $comment)) + <div component="dropdown" class="dropdown-container"> + <button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" class="text-button text-muted hover-underline p-xs">@icon('delete') {{ trans('common.delete') }}</button> + <ul refs="dropdown@menu" class="dropdown-menu" role="menu"> + <li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li> + <li> + <button refs="page-comment@delete-button" type="button" class="text-button text-neg icon-item"> + @icon('delete') + <div>{{ trans('common.delete') }}</div> + </button> + </li> + </ul> + </div> + @endif + <span class="text-muted"> + • + </span> + </div> + <div> + <a class="bold text-muted" href="#comment{{$comment->local_id}}">#{{$comment->local_id}}</a> + </div> </div> </div> </div> - @if ($comment->parent_id) - <div class="reply-row primary-background-light text-muted px-s py-xs mb-s"> - {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href="#comment'.$comment->parent_id.'">#'.$comment->parent_id.'</a>']) !!} - </div> - @endif - - <div refs="page-comment@content-container" class="content px-s pb-s"> + <div refs="page-comment@content-container" class="content px-m py-s"> + @if ($comment->parent_id) + <p class="comment-reply mb-xxs"> + <a class="text-muted text-small" href="#comment{{ $comment->parent_id }}">@icon('reply'){{ trans('entities.comment_in_reply_to', ['commentId' => '#' . $comment->parent_id]) }}</a> + </p> + @endif {!! $comment->html !!} </div> @if(userCan('comment-update', $comment)) - <form novalidate refs="page-comment@form" hidden class="content px-s block"> + <form novalidate refs="page-comment@form" hidden class="content pt-s px-s block"> <div class="form-group description-input"> <textarea refs="page-comment@input" name="markdown" rows="3" placeholder="{{ trans('entities.comment_placeholder') }}">{{ $comment->text }}</textarea> </div> diff --git a/resources/views/comments/create.blade.php b/resources/views/comments/create.blade.php index 88cec4876..cb7905ddc 100644 --- a/resources/views/comments/create.blade.php +++ b/resources/views/comments/create.blade.php @@ -2,10 +2,10 @@ <div class="comment-box"> <div class="header p-s">{{ trans('entities.comment_new') }}</div> - <div refs="page-comments@reply-to-row" hidden class="primary-background-light text-muted px-s py-xs mb-s"> + <div refs="page-comments@reply-to-row" hidden class="primary-background-light text-muted px-s py-xs"> <div class="grid left-focus v-center"> <div> - {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href=""></a>']) !!} + <a refs="page-comments@form-reply-link" href="#">{{ trans('entities.comment_in_reply_to', ['commentId' => '1234']) }}</a> </div> <div class="text-right"> <button refs="page-comments@remove-reply-to-button" class="text-button">{{ trans('common.remove') }}</button> @@ -13,7 +13,7 @@ </div> </div> - <div class="content px-s"> + <div class="content px-s pt-s"> <form refs="page-comments@form" novalidate> <div class="form-group description-input"> <textarea refs="page-comments@form-input" name="markdown"