diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index c194e7216..7005f8fcf 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -22,7 +22,7 @@ class CommentRepo /** * Create a new comment on an entity. */ - public function create(Entity $entity, string $html, ?int $parent_id, string $content_ref): Comment + public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment { $userId = user()->id; $comment = new Comment(); @@ -31,8 +31,8 @@ class CommentRepo $comment->created_by = $userId; $comment->updated_by = $userId; $comment->local_id = $this->getNextLocalId($entity); - $comment->parent_id = $parent_id; - $comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $content_ref) === 1 ? $content_ref : ''; + $comment->parent_id = $parentId; + $comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $contentRef) === 1 ? $contentRef : ''; $entity->comments()->save($comment); ActivityService::add(ActivityType::COMMENT_CREATE, $comment); diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index 7f16c17ff..479d57c4d 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -43,7 +43,8 @@ class CommentController extends Controller // Create a new comment. $this->checkPermission('comment-create-all'); - $comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $input['content_ref']); + $contentRef = $input['content_ref'] ?? ''; + $comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $contentRef); return view('comments.comment-branch', [ 'readOnly' => false, diff --git a/resources/js/components/editor-toolbox.js b/resources/js/components/editor-toolbox.ts similarity index 61% rename from resources/js/components/editor-toolbox.js rename to resources/js/components/editor-toolbox.ts index 953393285..60bdde05e 100644 --- a/resources/js/components/editor-toolbox.js +++ b/resources/js/components/editor-toolbox.ts @@ -1,39 +1,49 @@ import {Component} from './component'; +export interface EditorToolboxChangeEventData { + tab: string; + open: boolean; +} + export class EditorToolbox extends Component { + protected container!: HTMLElement; + protected buttons!: HTMLButtonElement[]; + protected contentElements!: HTMLElement[]; + protected toggleButton!: HTMLElement; + protected editorWrapEl!: HTMLElement; + + protected open: boolean = false; + protected tab: string = ''; + setup() { // Elements this.container = this.$el; - this.buttons = this.$manyRefs.tabButton; + this.buttons = this.$manyRefs.tabButton as HTMLButtonElement[]; this.contentElements = this.$manyRefs.tabContent; this.toggleButton = this.$refs.toggle; - this.editorWrapEl = this.container.closest('.page-editor'); - - // State - this.open = false; - this.tab = ''; + this.editorWrapEl = this.container.closest('.page-editor') as HTMLElement; this.setupListeners(); // Set the first tab as active on load - this.setActiveTab(this.contentElements[0].dataset.tabContent); + this.setActiveTab(this.contentElements[0].dataset.tabContent || ''); } - setupListeners() { + protected setupListeners(): void { // Toolbox toggle button click this.toggleButton.addEventListener('click', () => this.toggle()); // Tab button click - this.container.addEventListener('click', event => { - const button = event.target.closest('button'); - if (this.buttons.includes(button)) { - const name = button.dataset.tab; + this.container.addEventListener('click', (event: MouseEvent) => { + const button = (event.target as HTMLElement).closest('button'); + if (button instanceof HTMLButtonElement && this.buttons.includes(button)) { + const name = button.dataset.tab || ''; this.setActiveTab(name, true); } }); } - toggle() { + protected toggle(): void { this.container.classList.toggle('open'); const isOpen = this.container.classList.contains('open'); this.toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); @@ -42,7 +52,7 @@ export class EditorToolbox extends Component { this.emitState(); } - setActiveTab(tabName, openToolbox = false) { + protected setActiveTab(tabName: string, openToolbox: boolean = false): void { // Set button visibility for (const button of this.buttons) { button.classList.remove('active'); @@ -65,8 +75,9 @@ export class EditorToolbox extends Component { this.emitState(); } - emitState() { - this.$emit('change', {tab: this.tab, open: this.open}); + protected emitState(): void { + const data: EditorToolboxChangeEventData = {tab: this.tab, open: this.open}; + this.$emit('change', data); } } diff --git a/resources/js/components/page-comment-reference.ts b/resources/js/components/page-comment-reference.ts index 48fb8ee0a..009e806c1 100644 --- a/resources/js/components/page-comment-reference.ts +++ b/resources/js/components/page-comment-reference.ts @@ -4,6 +4,8 @@ import {el} from "../wysiwyg/utils/dom"; import commentIcon from "@icons/comment.svg"; import closeIcon from "@icons/close.svg"; import {debounce, scrollAndHighlightElement} from "../services/util"; +import {EditorToolboxChangeEventData} from "./editor-toolbox"; +import {TabsChangeEvent} from "./tabs"; /** * Track the close function for the current open marker so it can be closed @@ -12,13 +14,13 @@ import {debounce, scrollAndHighlightElement} from "../services/util"; let openMarkerClose: Function|null = null; export class PageCommentReference extends Component { - protected link: HTMLLinkElement; - protected reference: string; + protected link!: HTMLLinkElement; + protected reference!: string; protected markerWrap: HTMLElement|null = null; - protected viewCommentText: string; - protected jumpToThreadText: string; - protected closeText: string; + protected viewCommentText!: string; + protected jumpToThreadText!: string; + protected closeText!: string; setup() { this.link = this.$el as HTMLLinkElement; @@ -31,15 +33,15 @@ export class PageCommentReference extends Component { this.showForDisplay(); // Handle editor view to show on comments toolbox view - window.addEventListener('editor-toolbox-change', (event) => { - const tabName: string = (event as {detail: {tab: string, open: boolean}}).detail.tab; - const isOpen = (event as {detail: {tab: string, open: boolean}}).detail.open; - if (tabName === 'comments' && isOpen && this.link.checkVisibility()) { - this.showForEditor(); - } else { - this.hideMarker(); - } - }); + window.addEventListener('editor-toolbox-change', ((event: CustomEvent) => { + const tabName: string = event.detail.tab; + const isOpen = event.detail.open; + if (tabName === 'comments' && isOpen && this.link.checkVisibility()) { + this.showForEditor(); + } else { + this.hideMarker(); + } + }) as EventListener); // Handle visibility changes within editor toolbox archived details dropdown window.addEventListener('toggle', event => { @@ -55,8 +57,8 @@ export class PageCommentReference extends Component { }, {capture: true}); // Handle comments tab changes to hide/show markers & indicators - window.addEventListener('tabs-change', event => { - const sectionId = (event as {detail: {showing: string}}).detail.showing; + window.addEventListener('tabs-change', ((event: CustomEvent) => { + const sectionId = event.detail.showing; if (!sectionId.startsWith('comment-tab-panel')) { return; } @@ -67,7 +69,7 @@ export class PageCommentReference extends Component { } else { this.hideMarker(); } - }); + }) as EventListener); } public showForDisplay() { diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index ce35cdc4a..0c3e19f4b 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -1,29 +1,39 @@ import {Component} from './component'; -import {getLoading, htmlToDom} from '../services/dom.ts'; +import {getLoading, htmlToDom} from '../services/dom'; import {buildForInput} from '../wysiwyg-tinymce/config'; import {PageCommentReference} from "./page-comment-reference"; +import {HttpError} from "../services/http"; + +export interface PageCommentReplyEventData { + id: string; // ID of comment being replied to + element: HTMLElement; // Container for comment replied to +} + +export interface PageCommentArchiveEventData { + new_thread_dom: HTMLElement; +} export class PageComment extends Component { - protected commentId: string; - protected commentLocalId: string; - protected deletedText: string; - protected updatedText: string; - protected archiveText: 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 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; + 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 @@ -53,10 +63,11 @@ export class PageComment extends Component { 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) { @@ -95,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); }); @@ -120,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(); } @@ -151,7 +164,8 @@ export class PageComment extends Component { const response = await window.$http.put(`/comment/${this.commentId}/${action}`); window.$events.success(this.archiveText); - this.$emit(action, {new_thread_dom: htmlToDom(response.data as string)}); + 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'); diff --git a/resources/js/components/page-comments.ts b/resources/js/components/page-comments.ts index 04c812580..94f5ab3bb 100644 --- a/resources/js/components/page-comments.ts +++ b/resources/js/components/page-comments.ts @@ -1,50 +1,38 @@ import {Component} from './component'; -import {getLoading, htmlToDom} from '../services/dom.ts'; +import {getLoading, htmlToDom} from '../services/dom'; 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: { - id: string; // ID of comment being replied to - element: HTMLElement; // Container for comment replied to - } -} - -export interface ArchiveEvent extends Event { - detail: { - new_thread_dom: HTMLElement; - } -} +import {PageCommentArchiveEventData, PageCommentReplyEventData} from "./page-comment"; export class PageComments extends Component { - private elem: HTMLElement; - private pageId: number; - private container: HTMLElement; - private commentCountBar: HTMLElement; - private activeTab: HTMLElement; - private archivedTab: HTMLElement; - 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 elem!: HTMLElement; + private pageId!: number; + private container!: HTMLElement; + private commentCountBar!: HTMLElement; + private activeTab!: HTMLElement; + private archivedTab!: HTMLElement; + 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; - private createdText: string; - private countText: string; - private archivedCountText: string; + private createdText!: string; + private countText!: string; + private archivedCountText!: string; private parentId: number | null = null; private contentReference: string = ''; private formReplyText: string = ''; @@ -92,19 +80,19 @@ export class PageComments extends Component { this.hideForm(); }); - this.elem.addEventListener('page-comment-reply', (event: CommentReplyEvent) => { + this.elem.addEventListener('page-comment-reply', ((event: CustomEvent) => { this.setReply(event.detail.id, event.detail.element); - }); + }) as EventListener); - this.elem.addEventListener('page-comment-archive', (event: ArchiveEvent) => { + this.elem.addEventListener('page-comment-archive', ((event: CustomEvent) => { this.archiveContainer.append(event.detail.new_thread_dom); setTimeout(() => this.updateCount(), 1); - }); + }) as EventListener); - this.elem.addEventListener('page-comment-unarchive', (event: ArchiveEvent) => { + this.elem.addEventListener('page-comment-unarchive', ((event: CustomEvent) => { this.container.append(event.detail.new_thread_dom); setTimeout(() => this.updateCount(), 1); - }); + }) as EventListener); if (this.form) { this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this)); @@ -115,7 +103,7 @@ export class PageComments extends Component { } } - protected saveComment(event): void { + protected saveComment(event: SubmitEvent): void { event.preventDefault(); event.stopPropagation(); @@ -209,10 +197,10 @@ export class PageComments 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); }); @@ -233,11 +221,11 @@ export class PageComments extends Component { return this.archiveContainer.querySelectorAll(':scope > .comment-branch').length; } - protected setReply(commentLocalId, commentElement): void { - const targetFormLocation = commentElement.closest('.comment-branch').querySelector('.comment-branch-children'); + protected setReply(commentLocalId: string, commentElement: HTMLElement): void { + const targetFormLocation = (commentElement.closest('.comment-branch') as HTMLElement).querySelector('.comment-branch-children') as HTMLElement; targetFormLocation.append(this.formContainer); this.showForm(); - this.parentId = commentLocalId; + this.parentId = Number(commentLocalId); this.replyToRow.toggleAttribute('hidden', false); this.formReplyLink.textContent = this.formReplyText.replace('1234', String(this.parentId)); this.formReplyLink.href = `#comment${this.parentId}`; diff --git a/resources/js/components/pointer.ts b/resources/js/components/pointer.ts index d84186d87..4b927045a 100644 --- a/resources/js/components/pointer.ts +++ b/resources/js/components/pointer.ts @@ -1,7 +1,7 @@ -import * as DOM from '../services/dom.ts'; +import * as DOM from '../services/dom'; import {Component} from './component'; -import {copyTextToClipboard} from '../services/clipboard.ts'; -import {hashElement, normalizeNodeTextOffsetToParent} from "../services/dom.ts"; +import {copyTextToClipboard} from '../services/clipboard'; +import {hashElement, normalizeNodeTextOffsetToParent} from "../services/dom"; import {PageComments} from "./page-comments"; export class Pointer extends Component { @@ -11,16 +11,16 @@ export class Pointer extends Component { 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; + 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.pointer = this.$refs.pointer; @@ -67,7 +67,7 @@ export class Pointer extends Component { DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => { event.stopPropagation(); const targetEl = (event.target as HTMLElement).closest('[id^="bkmrk"]'); - if (targetEl && window.getSelection().toString().length > 0) { + if (targetEl instanceof HTMLElement && (window.getSelection() || '').toString().length > 0) { const xPos = (event instanceof MouseEvent) ? event.pageX : 0; this.showPointerAtTarget(targetEl, xPos, false); } @@ -102,11 +102,8 @@ export class Pointer extends Component { /** * Move and display the pointer at the given element, targeting the given screen x-position if possible. - * @param {Element} element - * @param {Number} xPosition - * @param {Boolean} keyboardMode */ - showPointerAtTarget(element, xPosition, keyboardMode) { + showPointerAtTarget(element: HTMLElement, xPosition: number, keyboardMode: boolean) { this.targetElement = element; this.targetSelectionRange = window.getSelection()?.getRangeAt(0) || null; this.updateDomForTarget(element); @@ -134,7 +131,7 @@ export class Pointer extends Component { window.removeEventListener('scroll', scrollListener); }; - element.parentElement.insertBefore(this.pointer, element); + element.parentElement?.insertBefore(this.pointer, element); if (!keyboardMode) { window.addEventListener('scroll', scrollListener, {passive: true}); } @@ -142,9 +139,8 @@ export class Pointer extends Component { /** * Update the pointer inputs/content for the given target element. - * @param {?Element} element */ - updateDomForTarget(element) { + updateDomForTarget(element: HTMLElement) { const permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`); const includeTag = `{{@${this.pageId}#${element.id}}}`; @@ -158,13 +154,13 @@ export class Pointer extends Component { const elementId = element.id; // Get the first 50 characters. - const queryContent = element.textContent && element.textContent.substring(0, 50); + const queryContent = (element.textContent || '').substring(0, 50); editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`; } } enterSectionSelectMode() { - const sections = Array.from(document.querySelectorAll('.page-content [id^="bkmrk"]')); + const sections = Array.from(document.querySelectorAll('.page-content [id^="bkmrk"]')) as HTMLElement[]; for (const section of sections) { section.setAttribute('tabindex', '0'); } @@ -172,12 +168,12 @@ export class Pointer extends Component { sections[0].focus(); DOM.onEnterPress(sections, event => { - this.showPointerAtTarget(event.target, 0, true); + this.showPointerAtTarget(event.target as HTMLElement, 0, true); this.pointer.focus(); }); } - createCommentAtPointer(event) { + createCommentAtPointer() { if (!this.targetElement) { return; } diff --git a/resources/js/components/tabs.ts b/resources/js/components/tabs.ts index 56405b8c7..a03d37cd4 100644 --- a/resources/js/components/tabs.ts +++ b/resources/js/components/tabs.ts @@ -1,5 +1,9 @@ import {Component} from './component'; +export interface TabsChangeEvent { + showing: string; +} + /** * Tabs * Uses accessible attributes to drive its functionality. @@ -19,12 +23,12 @@ import {Component} from './component'; */ export class Tabs extends Component { - protected container: HTMLElement; - protected tabList: HTMLElement; - protected tabs: HTMLElement[]; - protected panels: HTMLElement[]; + protected container!: HTMLElement; + protected tabList!: HTMLElement; + protected tabs!: HTMLElement[]; + protected panels!: HTMLElement[]; - protected activeUnder: number; + protected activeUnder!: number; protected active: null|boolean = null; setup() { @@ -58,7 +62,8 @@ export class Tabs extends Component { tab.setAttribute('aria-selected', selected ? 'true' : 'false'); } - this.$emit('change', {showing: sectionId}); + const data: TabsChangeEvent = {showing: sectionId}; + this.$emit('change', data); } protected updateActiveState(): void { diff --git a/resources/js/services/dom.ts b/resources/js/services/dom.ts index 77c19a761..c3817536c 100644 --- a/resources/js/services/dom.ts +++ b/resources/js/services/dom.ts @@ -225,7 +225,7 @@ export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number) if (currentNode.nodeType === Node.TEXT_NODE) { // For text nodes, count the length of their content // Returns if within range - const textLength = currentNode.textContent.length; + const textLength = (currentNode.textContent || '').length; if (currentOffset + textLength >= offset) { return { node: currentNode, @@ -237,9 +237,9 @@ export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number) } else if (currentNode.nodeType === Node.ELEMENT_NODE) { // Otherwise, if an element, track the text length and search within // if in range for the target offset - const elementTextLength = currentNode.textContent.length; + const elementTextLength = (currentNode.textContent || '').length; if (currentOffset + elementTextLength >= offset) { - return findTargetNodeAndOffset(currentNode, offset - currentOffset); + return findTargetNodeAndOffset(currentNode as HTMLElement, offset - currentOffset); } currentOffset += elementTextLength; diff --git a/resources/js/services/events.ts b/resources/js/services/events.ts index 7dae6dc29..6045d51f8 100644 --- a/resources/js/services/events.ts +++ b/resources/js/services/events.ts @@ -1,7 +1,9 @@ import {HttpError} from "./http"; +type Listener = (data: any) => void; + export class EventManager { - protected listeners: Record void)[]> = {}; + protected listeners: Record = {}; protected stack: {name: string, data: {}}[] = []; /** @@ -27,7 +29,7 @@ export class EventManager { /** * Remove an event listener which is using the given callback for the given event name. */ - remove(eventName: string, callback: Function): void { + remove(eventName: string, callback: Listener): void { const listeners = this.listeners[eventName] || []; const index = listeners.indexOf(callback); if (index !== -1) { @@ -64,8 +66,7 @@ export class EventManager { /** * Notify of standard server-provided validation errors. */ - showValidationErrors(responseErr: {status?: number, data?: object}): void { - if (!responseErr.status) return; + showValidationErrors(responseErr: HttpError): void { if (responseErr.status === 422 && responseErr.data) { const message = Object.values(responseErr.data).flat().join('\n'); this.error(message);