]> BookStack Code Mirror - bookstack/blobdiff - resources/js/components/page-comments.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / components / page-comments.ts
index a19d2c7d4b128d4deeae63cd36bffaa2e6c89c40..5c1cd014c54d8a67a7012e85a693ec0825f9c690 100644 (file)
@@ -1,35 +1,39 @@
 import {Component} from './component';
-import {getLoading, htmlToDom} from '../services/dom.ts';
+import {getLoading, htmlToDom} from '../services/dom';
 import {buildForInput} from '../wysiwyg-tinymce/config';
-
-export interface CommentReplyEvent extends Event {
-    detail: {
-        id: string; // ID of comment being replied to
-        element: HTMLElement; // Container for comment replied to
-    }
-}
+import {Tabs} from "./tabs";
+import {PageCommentReference} from "./page-comment-reference";
+import {scrollAndHighlightElement} from "../services/util";
+import {PageCommentArchiveEventData, PageCommentReplyEventData} from "./page-comment";
 
 export class PageComments extends Component {
 
-    private elem: HTMLElement;
-    private pageId: number;
-    private container: HTMLElement;
-    private commentCountBar: HTMLElement;
-    private commentsTitle: HTMLElement;
-    private addButtonContainer: HTMLElement;
-    private replyToRow: HTMLElement;
-    private formContainer: HTMLElement;
-    private form: HTMLFormElement;
-    private formInput: HTMLInputElement;
-    private formReplyLink: HTMLAnchorElement;
-    private addCommentButton: HTMLElement;
-    private hideFormButton: HTMLElement;
-    private removeReplyToButton: HTMLElement;
-    private wysiwygLanguage: string;
-    private wysiwygTextDirection: string;
+    private elem!: HTMLElement;
+    private pageId!: number;
+    private container!: HTMLElement;
+    private commentCountBar!: HTMLElement;
+    private activeTab!: HTMLElement;
+    private archivedTab!: HTMLElement;
+    private addButtonContainer!: HTMLElement;
+    private archiveContainer!: HTMLElement;
+    private activeContainer!: HTMLElement;
+    private replyToRow!: HTMLElement;
+    private referenceRow!: HTMLElement;
+    private formContainer!: HTMLElement;
+    private form!: HTMLFormElement;
+    private formInput!: HTMLInputElement;
+    private formReplyLink!: HTMLAnchorElement;
+    private formReferenceLink!: HTMLAnchorElement;
+    private addCommentButton!: HTMLElement;
+    private hideFormButton!: HTMLElement;
+    private removeReplyToButton!: HTMLElement;
+    private removeReferenceButton!: HTMLElement;
+    private wysiwygLanguage!: string;
+    private wysiwygTextDirection!: string;
     private wysiwygEditor: any = null;
-    private createdText: string;
-    private countText: string;
+    private createdText!: string;
+    private countText!: string;
+    private archivedCountText!: string;
     private parentId: number | null = null;
     private contentReference: string = '';
     private formReplyText: string = '';
@@ -41,16 +45,22 @@ 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.activeContainer = this.$refs.activeContainer;
         this.replyToRow = this.$refs.replyToRow;
+        this.referenceRow = this.$refs.referenceRow;
         this.formContainer = this.$refs.formContainer;
         this.form = this.$refs.form as HTMLFormElement;
         this.formInput = this.$refs.formInput as HTMLInputElement;
         this.formReplyLink = this.$refs.formReplyLink as HTMLAnchorElement;
+        this.formReferenceLink = this.$refs.formReferenceLink as HTMLAnchorElement;
         this.addCommentButton = this.$refs.addCommentButton;
         this.hideFormButton = this.$refs.hideFormButton;
         this.removeReplyToButton = this.$refs.removeReplyToButton;
+        this.removeReferenceButton = this.$refs.removeReferenceButton;
 
         // WYSIWYG options
         this.wysiwygLanguage = this.$opts.wysiwygLanguage;
@@ -59,6 +69,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 || '';
 
@@ -67,23 +78,36 @@ export class PageComments extends Component {
 
     protected setupListeners(): void {
         this.elem.addEventListener('page-comment-delete', () => {
-            setTimeout(() => this.updateCount(), 1);
-            this.hideForm();
+            setTimeout(() => {
+                this.updateCount();
+                this.hideForm();
+            }, 1);
         });
 
-        this.elem.addEventListener('page-comment-reply', (event: CommentReplyEvent) => {
+        this.elem.addEventListener('page-comment-reply', ((event: CustomEvent<PageCommentReplyEventData>) => {
             this.setReply(event.detail.id, event.detail.element);
-        });
+        }) as EventListener);
+
+        this.elem.addEventListener('page-comment-archive', ((event: CustomEvent<PageCommentArchiveEventData>) => {
+            this.archiveContainer.append(event.detail.new_thread_dom);
+            setTimeout(() => this.updateCount(), 1);
+        }) as EventListener);
+
+        this.elem.addEventListener('page-comment-unarchive', ((event: CustomEvent<PageCommentArchiveEventData>) => {
+            this.container.append(event.detail.new_thread_dom);
+            setTimeout(() => this.updateCount(), 1);
+        }) as EventListener);
 
         if (this.form) {
             this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this));
+            this.removeReferenceButton.addEventListener('click', () => this.setContentReference(''));
             this.hideFormButton.addEventListener('click', this.hideForm.bind(this));
             this.addCommentButton.addEventListener('click', this.showForm.bind(this));
             this.form.addEventListener('submit', this.saveComment.bind(this));
         }
     }
 
-    protected saveComment(event): void {
+    protected saveComment(event: SubmitEvent): void {
         event.preventDefault();
         event.stopPropagation();
 
@@ -95,7 +119,7 @@ export class PageComments extends Component {
         const reqData = {
             html: this.wysiwygEditor.getContent(),
             parent_id: this.parentId || null,
-            content_reference: this.contentReference || '',
+            content_ref: this.contentReference,
         };
 
         window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
@@ -107,6 +131,11 @@ export class PageComments extends Component {
                 this.container.append(newElem);
             }
 
+            const refs = window.$components.allWithinElement<PageCommentReference>(newElem, 'page-comment-reference');
+            for (const ref of refs) {
+                ref.showForDisplay();
+            }
+
             window.$events.success(this.createdText);
             this.hideForm();
             this.updateCount();
@@ -120,17 +149,19 @@ 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 {
         this.removeEditor();
         this.formInput.value = '';
         this.parentId = null;
-        this.contentReference = '';
         this.replyToRow.toggleAttribute('hidden', true);
         this.container.append(this.formContainer);
+        this.setContentReference('');
     }
 
     protected showForm(): void {
@@ -139,13 +170,19 @@ 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 if that's where we're showing the form
+        const tabs = window.$components.firstOnElement(this.elem, 'tabs');
+        if (tabs instanceof Tabs && this.formContainer.closest('#comment-tab-panel-active')) {
+            tabs.show('comment-tab-panel-active');
+        }
     }
 
     protected hideForm(): void {
         this.resetForm();
         this.formContainer.toggleAttribute('hidden', true);
-        if (this.getCommentCount() > 0) {
-            this.elem.append(this.addButtonContainer);
+        if (this.getActiveThreadCount() > 0) {
+            this.activeContainer.append(this.addButtonContainer);
         } else {
             this.commentCountBar.append(this.addButtonContainer);
         }
@@ -166,10 +203,10 @@ export class PageComments extends Component {
             drawioUrl: '',
             pageId: 0,
             translations: {},
-            translationMap: (window as Record<string, Object>).editor_translations,
+            translationMap: (window as unknown as Record<string, Object>).editor_translations,
         });
 
-        (window as {tinymce: {init: (Object) => Promise<any>}}).tinymce.init(config).then(editors => {
+        (window as unknown as {tinymce: {init: (arg0: Object) => Promise<any>}}).tinymce.init(config).then(editors => {
             this.wysiwygEditor = editors[0];
             setTimeout(() => this.wysiwygEditor.focus(), 50);
         });
@@ -182,15 +219,19 @@ 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 setReply(commentLocalId, commentElement): void {
-        const targetFormLocation = commentElement.closest('.comment-branch').querySelector('.comment-branch-children');
+    protected getArchivedThreadCount(): number {
+        return this.archiveContainer.querySelectorAll(':scope > .comment-branch').length;
+    }
+
+    protected setReply(commentLocalId: string, commentElement: HTMLElement): void {
+        const targetFormLocation = (commentElement.closest('.comment-branch') as HTMLElement).querySelector('.comment-branch-children') as HTMLElement;
         targetFormLocation.append(this.formContainer);
         this.showForm();
-        this.parentId = commentLocalId;
+        this.parentId = Number(commentLocalId);
         this.replyToRow.toggleAttribute('hidden', false);
         this.formReplyLink.textContent = this.formReplyText.replace('1234', String(this.parentId));
         this.formReplyLink.href = `#comment${this.parentId}`;
@@ -204,8 +245,23 @@ export class PageComments extends Component {
     }
 
     public startNewComment(contentReference: string): void {
-        this.removeReplyTo();
-        this.contentReference = contentReference;
+        this.resetForm();
+        this.showForm();
+        this.setContentReference(contentReference);
+    }
+
+    protected setContentReference(reference: string): void {
+        this.contentReference = reference;
+        this.referenceRow.toggleAttribute('hidden', !Boolean(reference));
+        const [id] = reference.split(':');
+        this.formReferenceLink.href = `#${id}`;
+        this.formReferenceLink.onclick = function(event) {
+            event.preventDefault();
+            const el = document.getElementById(id);
+            if (el) {
+                scrollAndHighlightElement(el);
+            }
+        };
     }
 
 }