X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/ecda4e1d6f42108fef9c62ff4a9a73a056caa089..refs/pull/5681/head:/resources/js/components/page-comment.ts diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index 24964bf5c..a0bb7a55b 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -1,53 +1,47 @@ import {Component} from './component'; -import {findTargetNodeAndOffset, getLoading, hashElement, htmlToDom} from '../services/dom.ts'; +import {getLoading, htmlToDom} from '../services/dom'; import {buildForInput} from '../wysiwyg-tinymce/config'; -import {el} from "../wysiwyg/utils/dom"; +import {PageCommentReference} from "./page-comment-reference"; +import {HttpError} from "../services/http"; -import commentIcon from "@icons/comment.svg" -import closeIcon from "@icons/close.svg" -import {PageDisplay} from "./page-display"; +export interface PageCommentReplyEventData { + id: string; // ID of comment being replied to + element: HTMLElement; // Container for comment replied to +} -/** - * 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 interface PageCommentArchiveEventData { + new_thread_dom: HTMLElement; +} export class PageComment extends Component { - protected commentId: string; - protected commentLocalId: string; - protected commentContentRef: string; - protected deletedText: string; - protected updatedText: string; - protected viewCommentText: string; - protected jumpToThreadText: string; - protected closeText: string; + protected commentId!: string; + protected commentLocalId!: string; + protected deletedText!: string; + protected updatedText!: string; + protected archiveText!: string; protected wysiwygEditor: any = null; - protected wysiwygLanguage: string; - protected wysiwygTextDirection: string; - - protected container: HTMLElement; - protected contentContainer: HTMLElement; - protected form: HTMLFormElement; - protected formCancel: HTMLElement; - protected editButton: HTMLElement; - protected deleteButton: HTMLElement; - protected replyButton: HTMLElement; - protected input: HTMLInputElement; - protected contentRefLink: HTMLLinkElement|null; + protected wysiwygLanguage!: string; + protected wysiwygTextDirection!: string; + + protected container!: HTMLElement; + protected contentContainer!: HTMLElement; + protected form!: HTMLFormElement; + protected formCancel!: HTMLElement; + protected editButton!: HTMLElement; + protected deleteButton!: HTMLElement; + protected replyButton!: HTMLElement; + protected archiveButton!: HTMLElement; + protected input!: HTMLInputElement; setup() { // Options this.commentId = this.$opts.commentId; this.commentLocalId = this.$opts.commentLocalId; - this.commentContentRef = this.$opts.commentContentRef; 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; + this.archiveText = this.$opts.archiveText; // Editor reference and text options this.wysiwygLanguage = this.$opts.wysiwygLanguage; @@ -61,19 +55,19 @@ export class PageComment extends Component { this.editButton = this.$refs.editButton; this.deleteButton = this.$refs.deleteButton; this.replyButton = this.$refs.replyButton; + this.archiveButton = this.$refs.archiveButton; this.input = this.$refs.input as HTMLInputElement; - this.contentRefLink = (this.$refs.contentRef || null) as HTMLLinkElement|null; this.setupListeners(); - this.positionForReference(); } protected setupListeners(): void { if (this.replyButton) { - this.replyButton.addEventListener('click', () => this.$emit('reply', { + const data: PageCommentReplyEventData = { id: this.commentLocalId, element: this.container, - })); + }; + this.replyButton.addEventListener('click', () => this.$emit('reply', data)); } if (this.editButton) { @@ -85,6 +79,10 @@ export class PageComment extends Component { if (this.deleteButton) { this.deleteButton.addEventListener('click', this.delete.bind(this)); } + + if (this.archiveButton) { + this.archiveButton.addEventListener('click', this.archive.bind(this)); + } } protected toggleEditMode(show: boolean) : void { @@ -108,10 +106,10 @@ export class PageComment extends Component { drawioUrl: '', pageId: 0, translations: {}, - translationMap: (window as Record).editor_translations, + translationMap: (window as unknown as Record).editor_translations, }); - (window as {tinymce: {init: (Object) => Promise}}).tinymce.init(config).then(editors => { + (window as unknown as {tinymce: {init: (arg0: Object) => Promise}}).tinymce.init(config).then(editors => { this.wysiwygEditor = editors[0]; setTimeout(() => this.wysiwygEditor.focus(), 50); }); @@ -133,7 +131,9 @@ export class PageComment extends Component { window.$events.success(this.updatedText); } catch (err) { console.error(err); - window.$events.showValidationErrors(err); + if (err instanceof HttpError) { + window.$events.showValidationErrors(err); + } this.form.toggleAttribute('hidden', false); loading.remove(); } @@ -144,139 +144,41 @@ export class PageComment extends Component { await window.$http.delete(`/comment/${this.commentId}`); this.$emit('delete'); - this.container.closest('.comment-branch')?.remove(); - window.$events.success(this.deletedText); - } - - protected showLoading(): HTMLElement { - const loading = getLoading(); - loading.classList.add('px-l'); - this.container.append(loading); - return loading; - } - protected positionForReference() { - if (!this.commentContentRef || !this.contentRefLink) { - return; - } - - const [refId, refHash, refRange] = this.commentContentRef.split(':'); - const refEl = document.getElementById(refId); - if (!refEl) { - this.contentRefLink.classList.add('outdated', 'missing'); - return; - } - - const actualHash = hashElement(refEl); - if (actualHash !== refHash) { - this.contentRefLink.classList.add('outdated'); - } - - 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 branch = this.container.closest('.comment-branch'); + if (branch instanceof HTMLElement) { + const refs = window.$components.allWithinElement(branch, 'page-comment-reference'); + for (const ref of refs) { + ref.hideMarker(); } + branch.remove(); } - 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 = 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); - - this.contentRefLink.href = `#${refEl.id}`; - this.contentRefLink.addEventListener('click', (event: MouseEvent) => { - const pageDisplayComponent = window.$components.get('page-display')[0] as PageDisplay; - event.preventDefault(); - pageDisplayComponent.goToText(refId); - }); + window.$events.success(this.deletedText); } - protected showCommentAtMarker(marker: HTMLElement): void { - // Hide marker and close existing marker windows - if (openMarkerClose) { - openMarkerClose(); - } - marker.hidden = true; - - // 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(); + protected async archive(): Promise { + this.showLoading(); + const isArchived = this.archiveButton.dataset.isArchived === 'true'; + const action = isArchived ? 'unarchive' : 'archive'; + + const response = await window.$http.put(`/comment/${this.commentId}/${action}`); + window.$events.success(this.archiveText); + const eventData: PageCommentArchiveEventData = {new_thread_dom: htmlToDom(response.data as string)}; + this.$emit(action, eventData); + + const branch = this.container.closest('.comment-branch') as HTMLElement; + const references = window.$components.allWithinElement(branch, 'page-comment-reference'); + for (const reference of references) { + reference.hideMarker(); } + branch.remove(); + } - 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' - }, [ - el('div', { - class: 'content-comment-window-actions', - }, [jump, close]), - el('div', { - class: 'content-comment-window-content comment-container-compact comment-container-super-compact', - }, [readClone]), - ]); - - marker.parentElement?.append(commentWindow); - - // Handle interaction within window - const closeAction = () => { - commentWindow.remove(); - marker.hidden = false; - window.removeEventListener('click', windowCloseAction); - openMarkerClose = null; - }; - - 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'}); - const highlightTarget = this.container.querySelector('.header') as HTMLElement; - highlightTarget.classList.add('anim-highlight'); - highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight')) - }); - - // 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`; - } + protected showLoading(): HTMLElement { + const loading = getLoading(); + loading.classList.add('px-l'); + this.container.append(loading); + return loading; } }