]> BookStack Code Mirror - bookstack/blobdiff - resources/js/components/page-comment.ts
Comments: Started inline comment display windows
[bookstack] / resources / js / components / page-comment.ts
index b2e2bac2784d9e3306636d3f13fce430bf1c54cf..5a148c258856f59aa708f06daa79f883a6db3e7f 100644 (file)
@@ -1,6 +1,9 @@
 import {Component} from './component';
-import {getLoading, htmlToDom} from '../services/dom.ts';
+import {findTargetNodeAndOffset, getLoading, hashElement, htmlToDom} from '../services/dom.ts';
 import {buildForInput} from '../wysiwyg-tinymce/config';
+import {el} from "../wysiwyg/utils/dom";
+
+import commentIcon from "@icons/comment.svg"
 
 export class PageComment extends Component {
 
@@ -9,6 +12,7 @@ export class PageComment extends Component {
     protected commentContentRef: string;
     protected deletedText: string;
     protected updatedText: string;
+    protected viewCommentText: string;
 
     protected wysiwygEditor: any = null;
     protected wysiwygLanguage: string;
@@ -30,6 +34,7 @@ export class PageComment extends Component {
         this.commentContentRef = this.$opts.commentContentRef;
         this.deletedText = this.$opts.deletedText;
         this.updatedText = this.$opts.updatedText;
+        this.viewCommentText = this.$opts.viewCommentText;
 
         // Editor reference and text options
         this.wysiwygLanguage = this.$opts.wysiwygLanguage;
@@ -46,6 +51,7 @@ export class PageComment extends Component {
         this.input = this.$refs.input as HTMLInputElement;
 
         this.setupListeners();
+        this.positionForReference();
     }
 
     protected setupListeners(): void {
@@ -135,4 +141,101 @@ export class PageComment extends Component {
         return loading;
     }
 
+    protected positionForReference() {
+        if (!this.commentContentRef) {
+            return;
+        }
+
+        const [refId, refHash, refRange] = this.commentContentRef.split(':');
+        const refEl = document.getElementById(refId);
+        if (!refEl) {
+            // TODO - Show outdated marker for comment
+            return;
+        }
+
+        const actualHash = hashElement(refEl);
+        if (actualHash !== refHash) {
+            // TODO - Show outdated marker for comment
+            return;
+        }
+
+        const refElBounds = refEl.getBoundingClientRect();
+        let bounds = refElBounds;
+        const [rangeStart, rangeEnd] = refRange.split('-');
+        if (rangeStart && rangeEnd) {
+            const range = new Range();
+            const relStart = findTargetNodeAndOffset(refEl, Number(rangeStart));
+            const relEnd = findTargetNodeAndOffset(refEl, Number(rangeEnd));
+            if (relStart && relEnd) {
+                range.setStart(relStart.node, relStart.offset);
+                range.setEnd(relEnd.node, relEnd.offset);
+                bounds = range.getBoundingClientRect();
+            }
+        }
+
+        const relLeft = bounds.left - refElBounds.left;
+        const relTop = bounds.top - refElBounds.top;
+
+        const marker = el('button', {
+            type: 'button',
+            class: 'content-comment-marker',
+            title: this.viewCommentText,
+        });
+        marker.innerHTML = <string>commentIcon;
+        marker.addEventListener('click', event => {
+            this.showCommentAtMarker(marker);
+        });
+
+        const markerWrap = el('div', {
+            class: 'content-comment-highlight',
+            style: `left: ${relLeft}px; top: ${relTop}px; width: ${bounds.width}px; height: ${bounds.height}px;`
+        }, [marker]);
+
+        refEl.style.position = 'relative';
+        refEl.append(markerWrap);
+    }
+
+    protected showCommentAtMarker(marker: HTMLElement): void {
+
+        marker.hidden = true;
+        const readClone = this.container.closest('.comment-branch').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 commentWindow = el('div', {
+            class: 'content-comment-window'
+        }, [
+            el('div', {
+                class: 'content-comment-window-actions',
+            }, [jump, close]),
+            el('div', {
+                class: 'content-comment-window-content',
+            }, [readClone]),
+        ]);
+
+        marker.parentElement.append(commentWindow);
+
+        const closeAction = () => {
+            commentWindow.remove();
+            marker.hidden = false;
+        };
+
+        close.addEventListener('click', closeAction.bind(this));
+
+        jump.addEventListener('click', () => {
+            closeAction();
+            this.container.scrollIntoView({behavior: 'smooth'});
+            const highlightTarget = this.container.querySelector('.header') as HTMLElement;
+            highlightTarget.classList.add('anim-highlight');
+            highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight'))
+        });
+
+        // TODO - Position wrapper sensibly
+        // TODO - Movement control?
+    }
 }