]> BookStack Code Mirror - bookstack/commitdiff
Comments: Further range of content reference ux improvements
authorDan Brown <redacted>
Thu, 1 May 2025 16:22:12 +0000 (17:22 +0100)
committerDan Brown <redacted>
Thu, 1 May 2025 16:22:12 +0000 (17:22 +0100)
- Added reference indicator to comment create form.
  - Added remove action.
- Extracted reference text to translations.
- Changed reference hash to be text-based instead of HTML based.
- Added reference display for newly added comments.
- Handled reference marker delete on comment delete.

lang/en/entities.php
resources/js/components/page-comment-reference.ts
resources/js/components/page-comment.ts
resources/js/components/page-comments.ts
resources/js/services/dom.ts
resources/sass/_components.scss
resources/views/comments/comment.blade.php
resources/views/comments/comments.blade.php
resources/views/comments/create.blade.php

index c70658c01764942ecfd32a785ef83e449ea3e65b..e25a83299f3365aa561b80f0de69ff529fa830c9 100644 (file)
@@ -410,6 +410,8 @@ return [
     'comment_jump_to_thread' => 'Jump to thread',
     'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
     'comment_in_reply_to' => 'In reply to :commentId',
+    'comment_reference' => 'Reference',
+    'comment_reference_outdated' => '(Outdated)',
     'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',
 
     // Revision
index edf24354b3ba663f3a73f3c1557bd01beb9c0893..5a71f0c513b8c176182aa64e30b16c278acaa62b 100644 (file)
@@ -28,10 +28,7 @@ export class PageCommentReference extends Component {
         this.closeText = this.$opts.closeText;
 
         // Show within page display area if seen
-        const pageContentArea = document.querySelector('.page-content');
-        if (pageContentArea instanceof HTMLElement && this.link.checkVisibility()) {
-            this.updateMarker(pageContentArea);
-        }
+        this.showForDisplay();
 
         // Handle editor view to show on comments toolbox view
         window.addEventListener('editor-toolbox-change', (event) => {
@@ -47,19 +44,26 @@ export class PageCommentReference extends Component {
         // Handle comments tab changes to hide/show markers & indicators
         window.addEventListener('tabs-change', event => {
             const sectionId = (event as {detail: {showing: string}}).detail.showing;
-            if (!sectionId.startsWith('comment-tab-panel') || !(pageContentArea instanceof HTMLElement)) {
+            if (!sectionId.startsWith('comment-tab-panel')) {
                 return;
             }
 
             const panel = document.getElementById(sectionId);
             if (panel?.contains(this.link)) {
-                this.updateMarker(pageContentArea);
+                this.showForDisplay();
             } else {
                 this.hideMarker();
             }
         });
     }
 
+    public showForDisplay() {
+        const pageContentArea = document.querySelector('.page-content');
+        if (pageContentArea instanceof HTMLElement && this.link.checkVisibility()) {
+            this.updateMarker(pageContentArea);
+        }
+    }
+
     protected showForEditor() {
         const contentWrap = document.querySelector('.editor-content-wrap');
         if (contentWrap instanceof HTMLElement) {
@@ -90,15 +94,7 @@ export class PageCommentReference extends Component {
             return;
         }
 
-        const refCloneToAssess = refEl.cloneNode(true) as HTMLElement;
-        const toRemove = refCloneToAssess.querySelectorAll('[data-lexical-text]');
-        refCloneToAssess.removeAttribute('style');
-        for (const el of toRemove) {
-            el.after(...el.childNodes);
-            el.remove();
-        }
-
-        const actualHash = hashElement(refCloneToAssess);
+        const actualHash = hashElement(refEl);
         if (actualHash !== refHash) {
             this.link.classList.add('outdated');
         }
index 18b9ab73bf0c82c48cb8380eb42f02e38327a892..ce35cdc4a243e90e058217f5c45178b037dd3e1e 100644 (file)
@@ -131,7 +131,16 @@ export class PageComment extends Component {
 
         await window.$http.delete(`/comment/${this.commentId}`);
         this.$emit('delete');
-        this.container.closest('.comment-branch')?.remove();
+
+        const branch = this.container.closest('.comment-branch');
+        if (branch instanceof HTMLElement) {
+            const refs = window.$components.allWithinElement<PageCommentReference>(branch, 'page-comment-reference');
+            for (const ref of refs) {
+                ref.hideMarker();
+            }
+            branch.remove();
+        }
+
         window.$events.success(this.deletedText);
     }
 
index 2482c9dcb7dd30667821830c03e5eeccb1fa56aa..04c8125808cb8ffe00186073eb8176e0beea86d5 100644 (file)
@@ -2,6 +2,8 @@ import {Component} from './component';
 import {getLoading, htmlToDom} from '../services/dom.ts';
 import {buildForInput} from '../wysiwyg-tinymce/config';
 import {Tabs} from "./tabs";
+import {PageCommentReference} from "./page-comment-reference";
+import {scrollAndHighlightElement} from "../services/util";
 
 export interface CommentReplyEvent extends Event {
     detail: {
@@ -27,13 +29,16 @@ export class PageComments extends Component {
     private addButtonContainer: HTMLElement;
     private archiveContainer: 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;
@@ -56,13 +61,16 @@ export class PageComments extends Component {
         this.addButtonContainer = this.$refs.addButtonContainer;
         this.archiveContainer = this.$refs.archiveContainer;
         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;
@@ -100,6 +108,7 @@ export class PageComments extends Component {
 
         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));
@@ -118,7 +127,7 @@ export class PageComments extends Component {
         const reqData = {
             html: this.wysiwygEditor.getContent(),
             parent_id: this.parentId || null,
-            content_ref: this.contentReference || '',
+            content_ref: this.contentReference,
         };
 
         window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
@@ -130,6 +139,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();
@@ -152,10 +166,8 @@ export class PageComments extends Component {
     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('');
+        this.removeReplyTo();
     }
 
     protected showForm(): void {
@@ -240,7 +252,21 @@ export class PageComments extends Component {
 
     public startNewComment(contentReference: string): void {
         this.removeReplyTo();
-        this.contentReference = contentReference;
+        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);
+            }
+        };
     }
 
 }
index 661ed7ca3e5068a2907cae785299c592a4b86ba6..77c19a761058aa2aa9d2f08f8ba4f55e996a5298 100644 (file)
@@ -251,9 +251,9 @@ export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number)
 }
 
 /**
- * Create a hash for the given HTML element.
+ * Create a hash for the given HTML element content.
  */
 export function hashElement(element: HTMLElement): string {
-    const normalisedElemHtml = element.outerHTML.replace(/\s{2,}/g, '');
-    return cyrb53(normalisedElemHtml);
+    const normalisedElemText = (element.textContent || '').replace(/\s{2,}/g, '');
+    return cyrb53(normalisedElemText);
 }
\ No newline at end of file
index d25fab299d91bee3d213910752728bc7df111be8..a86d31ce3730002ddea09b45cfa69a8bd866c62f 100644 (file)
@@ -569,6 +569,9 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   border-bottom: 0;
   padding: 0 vars.$xs;
 }
+.tab-container [role="tabpanel"].no-outline:focus {
+  outline: none;
+}
 
 .image-picker .none {
   display: none;
index fe61bf1a4f6b0e6e4d771962d3175cb5d436abd1..eadf3518722c0a6ff29ba90f8cb711cd1a6f903b 100644 (file)
@@ -87,7 +87,7 @@
                    option:page-comment-reference:view-comment-text="{{ trans('entities.comment_view') }}"
                    option:page-comment-reference:jump-to-thread-text="{{ trans('entities.comment_jump_to_thread') }}"
                    option:page-comment-reference:close-text="{{ trans('common.close') }}"
-                   href="#">@icon('bookmark')Reference <span>- Outdated</span></a>
+                   href="#">@icon('bookmark'){{ trans('entities.comment_reference') }} <span>{{ trans('entities.comment_reference_outdated') }}</span></a>
             </div>
         @endif
         {!! $commentHtml  !!}
index 882cfdf45ee08d099c92b0f7bda5c2df718e1618..51c08d69a851f8c80238eeb324fc9ebfacfc72ae 100644 (file)
@@ -36,7 +36,7 @@
          tabindex="0"
          role="tabpanel"
          aria-labelledby="comment-tab-active"
-         class="comment-container">
+         class="comment-container no-outline">
         <div refs="page-comments@comment-container">
             @foreach($commentTree->getActive() as $branch)
                 @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
@@ -63,7 +63,7 @@
          role="tabpanel"
          aria-labelledby="comment-tab-archived"
          hidden="hidden"
-         class="comment-container">
+         class="comment-container no-outline">
         @foreach($commentTree->getArchived() as $branch)
             @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
         @endforeach
index 417f0c6060244f5c187162caab7b8e42187655ea..134ed51642547dfcd3af1ee8e610159dae4a53a4 100644 (file)
                 </div>
             </div>
         </div>
+        <div refs="page-comments@reference-row" hidden class="primary-background-light text-muted px-s py-xs">
+            <div class="grid left-focus v-center">
+                <div>
+                    <a refs="page-comments@formReferenceLink" href="#">{{ trans('entities.comment_reference') }}</a>
+                </div>
+                <div class="text-right">
+                    <button refs="page-comments@remove-reference-button" class="text-button">{{ trans('common.remove') }}</button>
+                </div>
+            </div>
+        </div>
 
         <div class="content px-s pt-s">
             <form refs="page-comments@form" novalidate>