]> BookStack Code Mirror - bookstack/commitdiff
Comments: Styled content comments & improved interaction
authorDan Brown <redacted>
Thu, 24 Apr 2025 12:21:23 +0000 (13:21 +0100)
committerDan Brown <redacted>
Thu, 24 Apr 2025 12:21:23 +0000 (13:21 +0100)
lang/en/entities.php
resources/js/components/page-comment.ts
resources/sass/_components.scss
resources/sass/_pages.scss
resources/views/comments/comment-branch.blade.php
resources/views/comments/comment.blade.php

index 9ce684ac71b2ac229f88c59b855ab06ecdb77eb1..f9fab8ebfd3286edd5d6d71a910e28742f378265 100644 (file)
@@ -403,6 +403,7 @@ return [
     'comment_created_success' => 'Comment added',
     'comment_updated_success' => 'Comment updated',
     'comment_view' => 'View comment',
+    '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_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',
index 5a148c258856f59aa708f06daa79f883a6db3e7f..9192c7c56afc846605f5a4adbeb3e3f59ade4ce3 100644 (file)
@@ -4,6 +4,13 @@ import {buildForInput} from '../wysiwyg-tinymce/config';
 import {el} from "../wysiwyg/utils/dom";
 
 import commentIcon from "@icons/comment.svg"
+import closeIcon from "@icons/close.svg"
+
+/**
+ * Track the close function for the current open marker so it can be closed
+ * when another is opened so we only show one marker comment thread at one time.
+ */
+let openMarkerClose: Function|null = null;
 
 export class PageComment extends Component {
 
@@ -13,6 +20,8 @@ export class PageComment extends Component {
     protected deletedText: string;
     protected updatedText: string;
     protected viewCommentText: string;
+    protected jumpToThreadText: string;
+    protected closeText: string;
 
     protected wysiwygEditor: any = null;
     protected wysiwygLanguage: string;
@@ -35,6 +44,8 @@ export class PageComment extends Component {
         this.deletedText = this.$opts.deletedText;
         this.updatedText = this.$opts.updatedText;
         this.viewCommentText = this.$opts.viewCommentText;
+        this.jumpToThreadText = this.$opts.jumpToThreadText;
+        this.closeText = this.$opts.closeText;
 
         // Editor reference and text options
         this.wysiwygLanguage = this.$opts.wysiwygLanguage;
@@ -130,7 +141,7 @@ export class PageComment extends Component {
 
         await window.$http.delete(`/comment/${this.commentId}`);
         this.$emit('delete');
-        this.container.closest('.comment-branch').remove();
+        this.container.closest('.comment-branch')?.remove();
         window.$events.success(this.deletedText);
     }
 
@@ -196,16 +207,22 @@ export class PageComment extends Component {
     }
 
     protected showCommentAtMarker(marker: HTMLElement): void {
-
+        // Hide marker and close existing marker windows
+        if (openMarkerClose) {
+            openMarkerClose();
+        }
         marker.hidden = true;
-        const readClone = this.container.closest('.comment-branch').cloneNode(true) as HTMLElement;
+
+        // Build comment window
+        const readClone = (this.container.closest('.comment-branch') as HTMLElement).cloneNode(true) as HTMLElement;
         const toRemove = readClone.querySelectorAll('.actions, form');
         for (const el of toRemove) {
             el.remove();
         }
 
-        const close = el('button', {type: 'button'}, ['x']);
-        const jump = el('button', {type: 'button'}, ['Jump to thread']);
+        const close = el('button', {type: 'button', title: this.closeText});
+        close.innerHTML = (closeIcon as string);
+        const jump = el('button', {type: 'button', 'data-action': 'jump'}, [this.jumpToThreadText]);
 
         const commentWindow = el('div', {
             class: 'content-comment-window'
@@ -214,19 +231,29 @@ export class PageComment extends Component {
                 class: 'content-comment-window-actions',
             }, [jump, close]),
             el('div', {
-                class: 'content-comment-window-content',
+                class: 'content-comment-window-content comment-container-compact comment-container-super-compact',
             }, [readClone]),
         ]);
 
-        marker.parentElement.append(commentWindow);
+        marker.parentElement?.append(commentWindow);
 
+        // Handle interaction within window
         const closeAction = () => {
             commentWindow.remove();
             marker.hidden = false;
+            window.removeEventListener('click', windowCloseAction);
+            openMarkerClose = null;
         };
 
-        close.addEventListener('click', closeAction.bind(this));
+        const windowCloseAction = (event: MouseEvent) => {
+            if (!(marker.parentElement as HTMLElement).contains(event.target as HTMLElement)) {
+                closeAction();
+            }
+        };
+        window.addEventListener('click', windowCloseAction);
 
+        openMarkerClose = closeAction;
+        close.addEventListener('click', closeAction.bind(this));
         jump.addEventListener('click', () => {
             closeAction();
             this.container.scrollIntoView({behavior: 'smooth'});
@@ -235,7 +262,12 @@ export class PageComment extends Component {
             highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight'))
         });
 
-        // TODO - Position wrapper sensibly
-        // TODO - Movement control?
+        // Position window within bounds
+        const commentWindowBounds = commentWindow.getBoundingClientRect();
+        const contentBounds = document.querySelector('.page-content')?.getBoundingClientRect();
+        if (contentBounds && commentWindowBounds.right > contentBounds.right) {
+            const diff = commentWindowBounds.right - contentBounds.right;
+            commentWindow.style.left = `-${diff}px`;
+        }
     }
 }
index 58d39d3ee6e0e9c3f7e29d98bc17269fd6816ab7..26b0518275b17d3ca8db4ea9a10624bcc239affb 100644 (file)
@@ -746,6 +746,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   height: calc(100% - vars.$m);
 }
 
+.comment-branch .comment-box {
+  margin-bottom: vars.$m;
+}
+
 .comment-branch .comment-branch .comment-branch .comment-branch .comment-thread-indicator {
   display: none;
 }
@@ -761,6 +765,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
 }
 
 .comment-container-compact .comment-box {
+  margin-bottom: vars.$xs;
   .meta {
     font-size: 0.8rem;
   }
@@ -778,6 +783,28 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   width: vars.$m;
 }
 
+.comment-container-super-compact .comment-box {
+  .meta {
+    font-size: 12px;
+  }
+  .avatar {
+    width: 18px;
+    margin-inline-end: 2px !important;
+  }
+  .content {
+    padding: vars.$xxs vars.$s;
+    line-height: 1.2;
+  }
+  .content p {
+    font-size: 12px;
+  }
+}
+
+.comment-container-super-compact .comment-thread-indicator {
+  width: (vars.$xs + 3px);
+  margin-inline-start: 3px;
+}
+
 #tag-manager .drag-card {
   max-width: 500px;
 }
index ac2d195b4e8b2953a3d6d5d9cbe4ec671e67e17d..be5a0f7c36073760ac103b575881e6cb35ec1745 100755 (executable)
@@ -242,12 +242,13 @@ body.tox-fullscreen, body.markdown-fullscreen {
 .content-comment-window {
   font-size: vars.$fs-m;
   line-height: 1.4;
-  position: relative;
-  z-index: 90;
+  position: absolute;
+  top: calc(100% + 3px);
+  left: 0;
+  z-index: 92;
   pointer-events: all;
   min-width: min(340px, 80vw);
   background-color: #FFF;
-  //border: 1px solid var(--color-primary);
   box-shadow: vars.$bs-hover;
   border-radius: 4px;
   overflow: hidden;
@@ -258,9 +259,24 @@ body.tox-fullscreen, body.markdown-fullscreen {
   display: flex;
   align-items: center;
   justify-content: end;
+  gap: vars.$xs;
+  button {
+    color: #FFF;
+    font-size: 12px;
+    padding: vars.$xs;
+    line-height: 1;
+    cursor: pointer;
+  }
+  button[data-action="jump"] {
+    text-decoration: underline;
+  }
+  svg {
+    fill: currentColor;
+    width: 12px;
+  }
 }
 .content-comment-window-content {
-  padding: vars.$xs;
+  padding: vars.$xs vars.$s vars.$xs vars.$xs;
   max-height: 200px;
   overflow-y: scroll;
 }
@@ -280,11 +296,16 @@ body.tox-fullscreen, body.markdown-fullscreen {
   color: #FFF;
   cursor: pointer;
   z-index: 90;
+  transform: scale(1);
+  transition: transform ease-in-out 120ms;
   svg {
     fill: #FFF;
     width: 80%;
   }
 }
+.page-content [id^="bkmrk-"]:hover .content-comment-marker {
+  transform: scale(1.15);
+}
 
 // Page editor sidebar toolbox
 .floating-toolbox {
index 78d19ac3ea41393953a415d02d18eb94dcc21774..83fa4b5c595274efcc2a43c99af758d0d21e794e 100644 (file)
@@ -1,5 +1,5 @@
 <div class="comment-branch">
-    <div class="mb-m">
+    <div>
         @include('comments.comment', ['comment' => $branch['comment']])
     </div>
     <div class="flex-container-row">
index 1886dad51e4df4bd7b635f7bc97412b4d659d383..5b79da4ac685ec059453cfedce5d7bc64135b153 100644 (file)
@@ -8,6 +8,8 @@
      option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}"
      option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}"
      option:page-comment:view-comment-text="{{ trans('entities.comment_view') }}"
+     option:page-comment:jump-to-thread-text="{{ trans('entities.comment_jump_to_thread') }}"
+     option:page-comment:close-text="{{ trans('common.close') }}"
      option:page-comment:wysiwyg-language="{{ $locale->htmlLang() }}"
      option:page-comment:wysiwyg-text-direction="{{ $locale->htmlDirection() }}"
      id="comment{{$comment->local_id}}"
Morty Proxy This is a proxified and sanitized view of the page, visit original site.