diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.ts similarity index 68% rename from resources/js/components/page-comments.js rename to resources/js/components/page-comments.ts index 8f023836b..a19d2c7d4 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.ts @@ -2,8 +2,38 @@ import {Component} from './component'; import {getLoading, htmlToDom} from '../services/dom.ts'; 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 + } +} + 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 wysiwygEditor: any = null; + private createdText: string; + private countText: string; + private parentId: number | null = null; + private contentReference: string = ''; + private formReplyText: string = ''; + setup() { this.elem = this.$el; this.pageId = Number(this.$opts.pageId); @@ -15,9 +45,9 @@ export class PageComments extends Component { this.addButtonContainer = this.$refs.addButtonContainer; this.replyToRow = this.$refs.replyToRow; this.formContainer = this.$refs.formContainer; - this.form = this.$refs.form; - this.formInput = this.$refs.formInput; - this.formReplyLink = this.$refs.formReplyLink; + this.form = this.$refs.form as HTMLFormElement; + this.formInput = this.$refs.formInput as HTMLInputElement; + this.formReplyLink = this.$refs.formReplyLink as HTMLAnchorElement; this.addCommentButton = this.$refs.addCommentButton; this.hideFormButton = this.$refs.hideFormButton; this.removeReplyToButton = this.$refs.removeReplyToButton; @@ -25,26 +55,23 @@ export class PageComments extends Component { // WYSIWYG options this.wysiwygLanguage = this.$opts.wysiwygLanguage; this.wysiwygTextDirection = this.$opts.wysiwygTextDirection; - this.wysiwygEditor = null; // Translations this.createdText = this.$opts.createdText; this.countText = this.$opts.countText; - // Internal State - this.parentId = null; this.formReplyText = this.formReplyLink?.textContent || ''; this.setupListeners(); } - setupListeners() { + protected setupListeners(): void { this.elem.addEventListener('page-comment-delete', () => { setTimeout(() => this.updateCount(), 1); this.hideForm(); }); - this.elem.addEventListener('page-comment-reply', event => { + this.elem.addEventListener('page-comment-reply', (event: CommentReplyEvent) => { this.setReply(event.detail.id, event.detail.element); }); @@ -56,7 +83,7 @@ export class PageComments extends Component { } } - saveComment(event) { + protected saveComment(event): void { event.preventDefault(); event.stopPropagation(); @@ -68,10 +95,11 @@ export class PageComments extends Component { const reqData = { html: this.wysiwygEditor.getContent(), parent_id: this.parentId || null, + content_reference: this.contentReference || '', }; window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => { - const newElem = htmlToDom(resp.data); + const newElem = htmlToDom(resp.data as string); if (reqData.parent_id) { this.formContainer.after(newElem); @@ -91,20 +119,21 @@ export class PageComments extends Component { loading.remove(); } - updateCount() { + protected updateCount(): void { const count = this.getCommentCount(); - this.commentsTitle.textContent = window.$trans.choice(this.countText, count, {count}); + this.commentsTitle.textContent = window.$trans.choice(this.countText, count); } - resetForm() { + protected resetForm(): void { this.removeEditor(); this.formInput.value = ''; this.parentId = null; + this.contentReference = ''; this.replyToRow.toggleAttribute('hidden', true); this.container.append(this.formContainer); } - showForm() { + protected showForm(): void { this.removeEditor(); this.formContainer.toggleAttribute('hidden', false); this.addButtonContainer.toggleAttribute('hidden', true); @@ -112,7 +141,7 @@ export class PageComments extends Component { this.loadEditor(); } - hideForm() { + protected hideForm(): void { this.resetForm(); this.formContainer.toggleAttribute('hidden', true); if (this.getCommentCount() > 0) { @@ -123,7 +152,7 @@ export class PageComments extends Component { this.addButtonContainer.toggleAttribute('hidden', false); } - loadEditor() { + protected loadEditor(): void { if (this.wysiwygEditor) { this.wysiwygEditor.focus(); return; @@ -134,42 +163,49 @@ export class PageComments extends Component { containerElement: this.formInput, darkMode: document.documentElement.classList.contains('dark-mode'), textDirection: this.wysiwygTextDirection, + drawioUrl: '', + pageId: 0, translations: {}, - translationMap: window.editor_translations, + translationMap: (window as Record).editor_translations, }); - window.tinymce.init(config).then(editors => { + (window as {tinymce: {init: (Object) => Promise}}).tinymce.init(config).then(editors => { this.wysiwygEditor = editors[0]; setTimeout(() => this.wysiwygEditor.focus(), 50); }); } - removeEditor() { + protected removeEditor(): void { if (this.wysiwygEditor) { this.wysiwygEditor.remove(); this.wysiwygEditor = null; } } - getCommentCount() { + protected getCommentCount(): number { return this.container.querySelectorAll('[component="page-comment"]').length; } - setReply(commentLocalId, commentElement) { + protected setReply(commentLocalId, commentElement): void { const targetFormLocation = commentElement.closest('.comment-branch').querySelector('.comment-branch-children'); targetFormLocation.append(this.formContainer); this.showForm(); this.parentId = commentLocalId; this.replyToRow.toggleAttribute('hidden', false); - this.formReplyLink.textContent = this.formReplyText.replace('1234', this.parentId); + this.formReplyLink.textContent = this.formReplyText.replace('1234', String(this.parentId)); this.formReplyLink.href = `#comment${this.parentId}`; } - removeReplyTo() { + protected removeReplyTo(): void { this.parentId = null; this.replyToRow.toggleAttribute('hidden', true); this.container.append(this.formContainer); this.showForm(); } + public startNewComment(contentReference: string): void { + this.removeReplyTo(); + this.contentReference = contentReference; + } + } diff --git a/resources/js/components/pointer.js b/resources/js/components/pointer.ts similarity index 79% rename from resources/js/components/pointer.js rename to resources/js/components/pointer.ts index 997df329a..c3883b7b5 100644 --- a/resources/js/components/pointer.js +++ b/resources/js/components/pointer.ts @@ -1,18 +1,33 @@ import * as DOM from '../services/dom.ts'; import {Component} from './component'; import {copyTextToClipboard} from '../services/clipboard.ts'; -import {el} from "../wysiwyg/utils/dom"; import {cyrb53} from "../services/util"; import {normalizeNodeTextOffsetToParent} from "../services/dom.ts"; +import {PageComments} from "./page-comments"; export class Pointer extends Component { + protected showing: boolean = false; + protected isMakingSelection: boolean = false; + protected targetElement: HTMLElement|null = null; + protected targetSelectionRange: Range|null = null; + + protected pointer: HTMLElement; + protected linkInput: HTMLInputElement; + protected linkButton: HTMLElement; + protected includeInput: HTMLInputElement; + protected includeButton: HTMLElement; + protected sectionModeButton: HTMLElement; + protected commentButton: HTMLElement; + protected modeToggles: HTMLElement[]; + protected modeSections: HTMLElement[]; + protected pageId: string; + setup() { - this.container = this.$el; this.pointer = this.$refs.pointer; - this.linkInput = this.$refs.linkInput; + this.linkInput = this.$refs.linkInput as HTMLInputElement; this.linkButton = this.$refs.linkButton; - this.includeInput = this.$refs.includeInput; + this.includeInput = this.$refs.includeInput as HTMLInputElement; this.includeButton = this.$refs.includeButton; this.sectionModeButton = this.$refs.sectionModeButton; this.commentButton = this.$refs.commentButton; @@ -20,12 +35,6 @@ export class Pointer extends Component { this.modeSections = this.$manyRefs.modeSection; this.pageId = this.$opts.pageId; - // Instance variables - this.showing = false; - this.isMakingSelection = false; - this.targetElement = null; - this.targetSelectionRange = null; - this.setupListeners(); } @@ -36,7 +45,7 @@ export class Pointer extends Component { // Select all contents on input click DOM.onSelect([this.includeInput, this.linkInput], event => { - event.target.select(); + (event.target as HTMLInputElement).select(); event.stopPropagation(); }); @@ -58,9 +67,10 @@ export class Pointer extends Component { const pageContent = document.querySelector('.page-content'); DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => { event.stopPropagation(); - const targetEl = event.target.closest('[id^="bkmrk"]'); + const targetEl = (event.target as HTMLElement).closest('[id^="bkmrk"]'); if (targetEl && window.getSelection().toString().length > 0) { - this.showPointerAtTarget(targetEl, event.pageX, false); + const xPos = (event instanceof MouseEvent) ? event.pageX : 0; + this.showPointerAtTarget(targetEl, xPos, false); } }); @@ -69,12 +79,14 @@ export class Pointer extends Component { // Toggle between pointer modes DOM.onSelect(this.modeToggles, event => { + const targetToggle = (event.target as HTMLElement); for (const section of this.modeSections) { - const show = !section.contains(event.target); + const show = !section.contains(targetToggle); section.toggleAttribute('hidden', !show); } - this.modeToggles.find(b => b !== event.target).focus(); + const otherToggle = this.modeToggles.find(b => b !== targetToggle); + otherToggle && otherToggle.focus(); }); if (this.commentButton) { @@ -83,7 +95,7 @@ export class Pointer extends Component { } hidePointer() { - this.pointer.style.display = null; + this.pointer.style.removeProperty('display'); this.showing = false; this.targetElement = null; this.targetSelectionRange = null; @@ -97,7 +109,7 @@ export class Pointer extends Component { */ showPointerAtTarget(element, xPosition, keyboardMode) { this.targetElement = element; - this.targetSelectionRange = window.getSelection()?.getRangeAt(0); + this.targetSelectionRange = window.getSelection()?.getRangeAt(0) || null; this.updateDomForTarget(element); this.pointer.style.display = 'block'; @@ -120,7 +132,7 @@ export class Pointer extends Component { const scrollListener = () => { this.hidePointer(); - window.removeEventListener('scroll', scrollListener, {passive: true}); + window.removeEventListener('scroll', scrollListener); }; element.parentElement.insertBefore(this.pointer, element); @@ -142,7 +154,7 @@ export class Pointer extends Component { // Update anchor if present const editAnchor = this.pointer.querySelector('#pointer-edit'); - if (editAnchor && element) { + if (editAnchor instanceof HTMLAnchorElement && element) { const {editHref} = editAnchor.dataset; const elementId = element.id; @@ -193,7 +205,8 @@ export class Pointer extends Component { } const reference = `${refId}:${hash}:${range}`; - console.log(reference); + const pageComments = window.$components.first('page-comments') as PageComments; + pageComments.startNewComment(reference); } } diff --git a/resources/js/services/dom.ts b/resources/js/services/dom.ts index 779b48547..537af816a 100644 --- a/resources/js/services/dom.ts +++ b/resources/js/services/dom.ts @@ -44,9 +44,11 @@ export function forEach(selector: string, callback: (el: Element) => any) { /** * Helper to listen to multiple DOM events */ -export function onEvents(listenerElement: Element, events: string[], callback: (e: Event) => any): void { - for (const eventName of events) { - listenerElement.addEventListener(eventName, callback); +export function onEvents(listenerElement: Element|null, events: string[], callback: (e: Event) => any): void { + if (listenerElement) { + for (const eventName of events) { + listenerElement.addEventListener(eventName, callback); + } } } diff --git a/resources/js/services/translations.ts b/resources/js/services/translations.ts index b37dbdfb0..821c34f18 100644 --- a/resources/js/services/translations.ts +++ b/resources/js/services/translations.ts @@ -10,6 +10,7 @@ export class Translator { * to use. Similar format at Laravel's 'trans_choice' helper. */ choice(translation: string, count: number, replacements: Record = {}): string { + replacements = Object.assign({}, replacements, {count: String(count)}); const splitText = translation.split('|'); const exactCountRegex = /^{([0-9]+)}/; const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;