/**
* 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();
$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);
// 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,
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');
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');
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);
}
}
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
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;
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<EditorToolboxChangeEventData>) => {
+ 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 => {
}, {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<TabsChangeEvent>) => {
+ const sectionId = event.detail.showing;
if (!sectionId.startsWith('comment-tab-panel')) {
return;
}
} else {
this.hideMarker();
}
- });
+ }) as EventListener);
}
public showForDisplay() {
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 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 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
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) {
drawioUrl: '',
pageId: 0,
translations: {},
- translationMap: (window as Record<string, Object>).editor_translations,
+ translationMap: (window as unknown as Record<string, Object>).editor_translations,
});
- (window as {tinymce: {init: (Object) => Promise<any>}}).tinymce.init(config).then(editors => {
+ (window as unknown as {tinymce: {init: (arg0: Object) => Promise<any>}}).tinymce.init(config).then(editors => {
this.wysiwygEditor = editors[0];
setTimeout(() => this.wysiwygEditor.focus(), 50);
});
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();
}
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<PageCommentReference>(branch, 'page-comment-reference');
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 = '';
this.hideForm();
});
- this.elem.addEventListener('page-comment-reply', (event: CommentReplyEvent) => {
+ this.elem.addEventListener('page-comment-reply', ((event: CustomEvent<PageCommentReplyEventData>) => {
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<PageCommentArchiveEventData>) => {
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<PageCommentArchiveEventData>) => {
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));
}
}
- protected saveComment(event): void {
+ protected saveComment(event: SubmitEvent): void {
event.preventDefault();
event.stopPropagation();
drawioUrl: '',
pageId: 0,
translations: {},
- translationMap: (window as Record<string, Object>).editor_translations,
+ translationMap: (window as unknown as Record<string, Object>).editor_translations,
});
- (window as {tinymce: {init: (Object) => Promise<any>}}).tinymce.init(config).then(editors => {
+ (window as unknown as {tinymce: {init: (arg0: Object) => Promise<any>}}).tinymce.init(config).then(editors => {
this.wysiwygEditor = editors[0];
setTimeout(() => this.wysiwygEditor.focus(), 50);
});
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}`;
-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 {
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;
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);
}
/**
* 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);
window.removeEventListener('scroll', scrollListener);
};
- element.parentElement.insertBefore(this.pointer, element);
+ element.parentElement?.insertBefore(this.pointer, element);
if (!keyboardMode) {
window.addEventListener('scroll', scrollListener, {passive: true});
}
/**
* 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}}}`;
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');
}
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;
}
import {Component} from './component';
+export interface TabsChangeEvent {
+ showing: string;
+}
+
/**
* Tabs
* Uses accessible attributes to drive its functionality.
*/
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() {
tab.setAttribute('aria-selected', selected ? 'true' : 'false');
}
- this.$emit('change', {showing: sectionId});
+ const data: TabsChangeEvent = {showing: sectionId};
+ this.$emit('change', data);
}
protected updateActiveState(): void {
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,
} 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;
import {HttpError} from "./http";
+type Listener = (data: any) => void;
+
export class EventManager {
- protected listeners: Record<string, ((data: any) => void)[]> = {};
+ protected listeners: Record<string, Listener[]> = {};
protected stack: {name: string, data: {}}[] = [];
/**
/**
* 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) {
/**
* 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);