]> BookStack Code Mirror - bookstack/commitdiff
Comments: Moved to tab UI, Converted tabs component to ts
authorDan Brown <redacted>
Wed, 30 Apr 2025 16:42:09 +0000 (17:42 +0100)
committerDan Brown <redacted>
Wed, 30 Apr 2025 16:42:09 +0000 (17:42 +0100)
app/Activity/Tools/CommentTree.php
lang/en/entities.php
resources/js/components/page-comment.ts
resources/js/components/page-comments.ts
resources/js/components/tabs.ts [moved from resources/js/components/tabs.js with 78% similarity]
resources/sass/_components.scss
resources/views/comments/comments.blade.php
resources/views/pages/show.blade.php

index 13afc92521d452e659c51ae1fb55a1c13ef75d53..a05a9d24726f84ead259de6eb98670bd5b3a9881 100644 (file)
@@ -28,7 +28,7 @@ class CommentTree
 
     public function empty(): bool
     {
-        return count($this->tree) === 0;
+        return count($this->getActive()) === 0;
     }
 
     public function count(): int
@@ -41,11 +41,21 @@ class CommentTree
         return array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived);
     }
 
+    public function activeThreadCount(): int
+    {
+        return count($this->getActive());
+    }
+
     public function getArchived(): array
     {
         return array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived);
     }
 
+    public function archivedThreadCount(): int
+    {
+        return count($this->getArchived());
+    }
+
     public function getCommentNodeForId(int $commentId): ?CommentTreeNode
     {
         foreach ($this->tree as $node) {
index cda58e65bdf25846665ed4b32ec45567182b84bf..c70658c01764942ecfd32a785ef83e449ea3e65b 100644 (file)
@@ -392,9 +392,10 @@ return [
     'comment' => 'Comment',
     'comments' => 'Comments',
     'comment_add' => 'Add Comment',
-    'comment_archived' => ':count Archived Comment|:count Archived Comments',
+    'comment_none' => 'No comments to display',
     'comment_placeholder' => 'Leave a comment here',
-    'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
+    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
+    'comment_archived_count' => ':count Archived',
     'comment_save' => 'Save Comment',
     'comment_new' => 'New Comment',
     'comment_created' => 'commented :createDiff',
index 82cb95f13de027f6b869f50581e2bd7e7a7a4707..12485b807287d20c5a38b6cd0625f893749b9107 100644 (file)
@@ -140,8 +140,8 @@ export class PageComment extends Component {
         const action = isArchived ? 'unarchive' : 'archive';
 
         const response = await window.$http.put(`/comment/${this.commentId}/${action}`);
-        this.$emit(action, {new_thread_dom: htmlToDom(response.data as string)});
         window.$events.success(this.archiveText);
+        this.$emit(action, {new_thread_dom: htmlToDom(response.data as string)});
         this.container.closest('.comment-branch')?.remove();
     }
 
index 083919b826f34210dabce556bacd4d7bb8e9049d..2482c9dcb7dd30667821830c03e5eeccb1fa56aa 100644 (file)
@@ -1,6 +1,7 @@
 import {Component} from './component';
 import {getLoading, htmlToDom} from '../services/dom.ts';
 import {buildForInput} from '../wysiwyg-tinymce/config';
+import {Tabs} from "./tabs";
 
 export interface CommentReplyEvent extends Event {
     detail: {
@@ -21,7 +22,8 @@ export class PageComments extends Component {
     private pageId: number;
     private container: HTMLElement;
     private commentCountBar: HTMLElement;
-    private commentsTitle: HTMLElement;
+    private activeTab: HTMLElement;
+    private archivedTab: HTMLElement;
     private addButtonContainer: HTMLElement;
     private archiveContainer: HTMLElement;
     private replyToRow: HTMLElement;
@@ -37,6 +39,7 @@ export class PageComments extends Component {
     private wysiwygEditor: any = null;
     private createdText: string;
     private countText: string;
+    private archivedCountText: string;
     private parentId: number | null = null;
     private contentReference: string = '';
     private formReplyText: string = '';
@@ -48,7 +51,8 @@ export class PageComments extends Component {
         // Element references
         this.container = this.$refs.commentContainer;
         this.commentCountBar = this.$refs.commentCountBar;
-        this.commentsTitle = this.$refs.commentsTitle;
+        this.activeTab = this.$refs.activeTab;
+        this.archivedTab = this.$refs.archivedTab;
         this.addButtonContainer = this.$refs.addButtonContainer;
         this.archiveContainer = this.$refs.archiveContainer;
         this.replyToRow = this.$refs.replyToRow;
@@ -67,6 +71,7 @@ export class PageComments extends Component {
         // Translations
         this.createdText = this.$opts.createdText;
         this.countText = this.$opts.countText;
+        this.archivedCountText = this.$opts.archivedCountText;
 
         this.formReplyText = this.formReplyLink?.textContent || '';
 
@@ -85,10 +90,12 @@ export class PageComments extends Component {
 
         this.elem.addEventListener('page-comment-archive', (event: ArchiveEvent) => {
             this.archiveContainer.append(event.detail.new_thread_dom);
+            setTimeout(() => this.updateCount(), 1);
         });
 
         this.elem.addEventListener('page-comment-unarchive', (event: ArchiveEvent) => {
-            this.container.append(event.detail.new_thread_dom)
+            this.container.append(event.detail.new_thread_dom);
+            setTimeout(() => this.updateCount(), 1);
         });
 
         if (this.form) {
@@ -136,8 +143,10 @@ export class PageComments extends Component {
     }
 
     protected updateCount(): void {
-        const count = this.getCommentCount();
-        this.commentsTitle.textContent = window.$trans.choice(this.countText, count);
+        const activeCount = this.getActiveThreadCount();
+        this.activeTab.textContent = window.$trans.choice(this.countText, activeCount);
+        const archivedCount = this.getArchivedThreadCount();
+        this.archivedTab.textContent = window.$trans.choice(this.archivedCountText, archivedCount);
     }
 
     protected resetForm(): void {
@@ -155,12 +164,18 @@ export class PageComments extends Component {
         this.addButtonContainer.toggleAttribute('hidden', true);
         this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'});
         this.loadEditor();
+
+        // Ensure the active comments tab is displaying
+        const tabs = window.$components.firstOnElement(this.elem, 'tabs');
+        if (tabs instanceof Tabs) {
+            tabs.show('comment-tab-panel-active');
+        }
     }
 
     protected hideForm(): void {
         this.resetForm();
         this.formContainer.toggleAttribute('hidden', true);
-        if (this.getCommentCount() > 0) {
+        if (this.getActiveThreadCount() > 0) {
             this.elem.append(this.addButtonContainer);
         } else {
             this.commentCountBar.append(this.addButtonContainer);
@@ -198,8 +213,12 @@ export class PageComments extends Component {
         }
     }
 
-    protected getCommentCount(): number {
-        return this.container.querySelectorAll('[component="page-comment"]').length;
+    protected getActiveThreadCount(): number {
+        return this.container.querySelectorAll(':scope > .comment-branch:not([hidden])').length;
+    }
+
+    protected getArchivedThreadCount(): number {
+        return this.archiveContainer.querySelectorAll(':scope > .comment-branch').length;
     }
 
     protected setReply(commentLocalId, commentElement): void {
similarity index 78%
rename from resources/js/components/tabs.js
rename to resources/js/components/tabs.ts
index f0fc058ced7fd8377123f831ac93c631d27d4952..56405b8c78e2b409c509990d55aeac5362a033cb 100644 (file)
@@ -19,18 +19,25 @@ import {Component} from './component';
  */
 export class Tabs extends Component {
 
+    protected container: HTMLElement;
+    protected tabList: HTMLElement;
+    protected tabs: HTMLElement[];
+    protected panels: HTMLElement[];
+
+    protected activeUnder: number;
+    protected active: null|boolean = null;
+
     setup() {
         this.container = this.$el;
-        this.tabList = this.container.querySelector('[role="tablist"]');
+        this.tabList = this.container.querySelector('[role="tablist"]') as HTMLElement;
         this.tabs = Array.from(this.tabList.querySelectorAll('[role="tab"]'));
         this.panels = Array.from(this.container.querySelectorAll(':scope > [role="tabpanel"], :scope > * > [role="tabpanel"]'));
         this.activeUnder = this.$opts.activeUnder ? Number(this.$opts.activeUnder) : 10000;
-        this.active = null;
 
         this.container.addEventListener('click', event => {
-            const tab = event.target.closest('[role="tab"]');
-            if (tab && this.tabs.includes(tab)) {
-                this.show(tab.getAttribute('aria-controls'));
+            const tab = (event.target as HTMLElement).closest('[role="tab"]');
+            if (tab instanceof HTMLElement && this.tabs.includes(tab)) {
+                this.show(tab.getAttribute('aria-controls') || '');
             }
         });
 
@@ -40,7 +47,7 @@ export class Tabs extends Component {
         this.updateActiveState();
     }
 
-    show(sectionId) {
+    public show(sectionId: string): void {
         for (const panel of this.panels) {
             panel.toggleAttribute('hidden', panel.id !== sectionId);
         }
@@ -54,7 +61,7 @@ export class Tabs extends Component {
         this.$emit('change', {showing: sectionId});
     }
 
-    updateActiveState() {
+    protected updateActiveState(): void {
         const active = window.innerWidth < this.activeUnder;
         if (active === this.active) {
             return;
@@ -69,13 +76,13 @@ export class Tabs extends Component {
         this.active = active;
     }
 
-    activate() {
+    protected activate(): void {
         const panelToShow = this.panels.find(p => !p.hasAttribute('hidden')) || this.panels[0];
         this.show(panelToShow.id);
         this.tabList.toggleAttribute('hidden', false);
     }
 
-    deactivate() {
+    protected deactivate(): void {
         for (const panel of this.panels) {
             panel.removeAttribute('hidden');
         }
index 5486d61128827c87a96e7a5b46ca8e6be05a56b3..d25fab299d91bee3d213910752728bc7df111be8 100644 (file)
@@ -802,6 +802,13 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   display: block;
 }
 
+.comment-container .empty-state {
+  display: none;
+}
+.comment-container:not(:has([component="page-comment"])) .empty-state {
+  display: block;
+}
+
 .comment-container-compact .comment-box {
   margin-bottom: vars.$xs;
   .meta {
index 06e96cad689f2c547fc9007e3a2a63437ce11ff9..882cfdf45ee08d099c92b0f7bda5c2df718e1618 100644 (file)
@@ -1,49 +1,73 @@
-<section component="page-comments"
+<section components="page-comments tabs"
          option:page-comments:page-id="{{ $page->id }}"
          option:page-comments:created-text="{{ trans('entities.comment_created_success') }}"
-         option:page-comments:count-text="{{ trans('entities.comment_count') }}"
+         option:page-comments:count-text="{{ trans('entities.comment_thread_count') }}"
+         option:page-comments:archived-count-text="{{ trans('entities.comment_archived_count') }}"
          option:page-comments:wysiwyg-language="{{ $locale->htmlLang() }}"
          option:page-comments:wysiwyg-text-direction="{{ $locale->htmlDirection() }}"
-         class="comments-list"
+         class="comments-list tab-container"
          aria-label="{{ trans('entities.comments') }}">
 
-    <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>
+    <div refs="page-comments@comment-count-bar" class="flex-container-row items-center">
+        <div role="tablist" class="flex">
+            <button type="button"
+                    role="tab"
+                    id="comment-tab-active"
+                    aria-controls="comment-tab-panel-active"
+                    refs="page-comments@active-tab"
+                    aria-selected="true">{{ trans_choice('entities.comment_thread_count', $commentTree->activeThreadCount()) }}</button>
+            <button type="button"
+                    role="tab"
+                    id="comment-tab-archived"
+                    aria-controls="comment-tab-panel-archived"
+                    refs="page-comments@archived-tab"
+                    aria-selected="false">{{ trans_choice('entities.comment_archived_count', count($commentTree->getArchived())) }}</button>
+        </div>
         @if ($commentTree->empty() && userCan('comment-create-all'))
-            <div class="text-m-right" refs="page-comments@add-button-container">
+            <div class="ml-m" refs="page-comments@add-button-container">
                 <button type="button"
                         refs="page-comments@add-comment-button"
-                        class="button outline">{{ trans('entities.comment_add') }}</button>
+                        class="button outline mb-m">{{ trans('entities.comment_add') }}</button>
             </div>
         @endif
     </div>
 
-    <div refs="page-comments@comment-container" class="comment-container">
-        @foreach($commentTree->getActive() as $branch)
-            @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
-        @endforeach
-    </div>
+    <div id="comment-tab-panel-active"
+         tabindex="0"
+         role="tabpanel"
+         aria-labelledby="comment-tab-active"
+         class="comment-container">
+        <div refs="page-comments@comment-container">
+            @foreach($commentTree->getActive() as $branch)
+                @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
+            @endforeach
+        </div>
 
-    @if(userCan('comment-create-all'))
-        @include('comments.create')
-        @if (!$commentTree->empty())
-            <div refs="page-comments@addButtonContainer" class="flex-container-row">
-
-                <button type="button"
-                        refs="page-comments@show-archived-button"
-                        class="text-button hover-underline">{{ trans_choice('entities.comment_archived', count($commentTree->getArchived())) }}</button>
+        <p class="text-center text-muted italic empty-state">{{ trans('entities.comment_none') }}</p>
 
-                <button type="button"
-                        refs="page-comments@add-comment-button"
-                        class="button outline ml-auto">{{ trans('entities.comment_add') }}</button>
-            </div>
+        @if(userCan('comment-create-all'))
+            @include('comments.create')
+            @if (!$commentTree->empty())
+                <div refs="page-comments@addButtonContainer" class="flex-container-row">
+                    <button type="button"
+                            refs="page-comments@add-comment-button"
+                            class="button outline ml-auto">{{ trans('entities.comment_add') }}</button>
+                </div>
+            @endif
         @endif
-    @endif
+    </div>
 
-    <div refs="page-comments@archive-container" class="comment-container">
+    <div refs="page-comments@archive-container"
+         id="comment-tab-panel-archived"
+         tabindex="0"
+         role="tabpanel"
+         aria-labelledby="comment-tab-archived"
+         hidden="hidden"
+         class="comment-container">
         @foreach($commentTree->getArchived() as $branch)
             @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
         @endforeach
+            <p class="text-center text-muted italic empty-state">{{ trans('entities.comment_none') }}</p>
     </div>
 
     @if(userCan('comment-create-all') || $commentTree->canUpdateAny())
index e3a31dd5ebf1bcd18b6ce0977565ed6ef7db5d11..137d43bdb1af376794963266d216864e8e92633b 100644 (file)
     @include('entities.sibling-navigation', ['next' => $next, 'previous' => $previous])
 
     @if ($commentTree->enabled())
-        @if(($previous || $next))
-            <div class="px-xl print-hidden">
-                <hr class="darker">
-            </div>
-        @endif
-
         <div class="comments-container mb-l print-hidden">
             @include('comments.comments', ['commentTree' => $commentTree, 'page' => $page])
             <div class="clearfix"></div>
Morty Proxy This is a proxified and sanitized view of the page, visit original site.