Comments: Fixed a range of TS errors + other

- Migrated toolbox component to TS
- Aligned how custom event types are managed
- Fixed PHP use of content_ref where not provided
This commit is contained in:
Dan Brown 2025-05-12 15:31:55 +01:00
parent 62f78f1c6d
commit 8f92b6f21b
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
10 changed files with 166 additions and 148 deletions

View File

@ -22,7 +22,7 @@ class CommentRepo
/** /**
* Create a new comment on an entity. * 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; $userId = user()->id;
$comment = new Comment(); $comment = new Comment();
@ -31,8 +31,8 @@ class CommentRepo
$comment->created_by = $userId; $comment->created_by = $userId;
$comment->updated_by = $userId; $comment->updated_by = $userId;
$comment->local_id = $this->getNextLocalId($entity); $comment->local_id = $this->getNextLocalId($entity);
$comment->parent_id = $parent_id; $comment->parent_id = $parentId;
$comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $content_ref) === 1 ? $content_ref : ''; $comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $contentRef) === 1 ? $contentRef : '';
$entity->comments()->save($comment); $entity->comments()->save($comment);
ActivityService::add(ActivityType::COMMENT_CREATE, $comment); ActivityService::add(ActivityType::COMMENT_CREATE, $comment);

View File

@ -43,7 +43,8 @@ class CommentController extends Controller
// Create a new comment. // Create a new comment.
$this->checkPermission('comment-create-all'); $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', [ return view('comments.comment-branch', [
'readOnly' => false, 'readOnly' => false,

View File

@ -1,39 +1,49 @@
import {Component} from './component'; import {Component} from './component';
export interface EditorToolboxChangeEventData {
tab: string;
open: boolean;
}
export class EditorToolbox extends Component { 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() { setup() {
// Elements // Elements
this.container = this.$el; this.container = this.$el;
this.buttons = this.$manyRefs.tabButton; this.buttons = this.$manyRefs.tabButton as HTMLButtonElement[];
this.contentElements = this.$manyRefs.tabContent; this.contentElements = this.$manyRefs.tabContent;
this.toggleButton = this.$refs.toggle; this.toggleButton = this.$refs.toggle;
this.editorWrapEl = this.container.closest('.page-editor'); this.editorWrapEl = this.container.closest('.page-editor') as HTMLElement;
// State
this.open = false;
this.tab = '';
this.setupListeners(); this.setupListeners();
// Set the first tab as active on load // 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 // Toolbox toggle button click
this.toggleButton.addEventListener('click', () => this.toggle()); this.toggleButton.addEventListener('click', () => this.toggle());
// Tab button click // Tab button click
this.container.addEventListener('click', event => { this.container.addEventListener('click', (event: MouseEvent) => {
const button = event.target.closest('button'); const button = (event.target as HTMLElement).closest('button');
if (this.buttons.includes(button)) { if (button instanceof HTMLButtonElement && this.buttons.includes(button)) {
const name = button.dataset.tab; const name = button.dataset.tab || '';
this.setActiveTab(name, true); this.setActiveTab(name, true);
} }
}); });
} }
toggle() { protected toggle(): void {
this.container.classList.toggle('open'); this.container.classList.toggle('open');
const isOpen = this.container.classList.contains('open'); const isOpen = this.container.classList.contains('open');
this.toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); this.toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
@ -42,7 +52,7 @@ export class EditorToolbox extends Component {
this.emitState(); this.emitState();
} }
setActiveTab(tabName, openToolbox = false) { protected setActiveTab(tabName: string, openToolbox: boolean = false): void {
// Set button visibility // Set button visibility
for (const button of this.buttons) { for (const button of this.buttons) {
button.classList.remove('active'); button.classList.remove('active');
@ -65,8 +75,9 @@ export class EditorToolbox extends Component {
this.emitState(); this.emitState();
} }
emitState() { protected emitState(): void {
this.$emit('change', {tab: this.tab, open: this.open}); const data: EditorToolboxChangeEventData = {tab: this.tab, open: this.open};
this.$emit('change', data);
} }
} }

View File

@ -4,6 +4,8 @@ import {el} from "../wysiwyg/utils/dom";
import commentIcon from "@icons/comment.svg"; import commentIcon from "@icons/comment.svg";
import closeIcon from "@icons/close.svg"; import closeIcon from "@icons/close.svg";
import {debounce, scrollAndHighlightElement} from "../services/util"; 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 * 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; let openMarkerClose: Function|null = null;
export class PageCommentReference extends Component { export class PageCommentReference extends Component {
protected link: HTMLLinkElement; protected link!: HTMLLinkElement;
protected reference: string; protected reference!: string;
protected markerWrap: HTMLElement|null = null; protected markerWrap: HTMLElement|null = null;
protected viewCommentText: string; protected viewCommentText!: string;
protected jumpToThreadText: string; protected jumpToThreadText!: string;
protected closeText: string; protected closeText!: string;
setup() { setup() {
this.link = this.$el as HTMLLinkElement; this.link = this.$el as HTMLLinkElement;
@ -31,15 +33,15 @@ export class PageCommentReference extends Component {
this.showForDisplay(); this.showForDisplay();
// Handle editor view to show on comments toolbox view // Handle editor view to show on comments toolbox view
window.addEventListener('editor-toolbox-change', (event) => { window.addEventListener('editor-toolbox-change', ((event: CustomEvent<EditorToolboxChangeEventData>) => {
const tabName: string = (event as {detail: {tab: string, open: boolean}}).detail.tab; const tabName: string = event.detail.tab;
const isOpen = (event as {detail: {tab: string, open: boolean}}).detail.open; const isOpen = event.detail.open;
if (tabName === 'comments' && isOpen && this.link.checkVisibility()) { if (tabName === 'comments' && isOpen && this.link.checkVisibility()) {
this.showForEditor(); this.showForEditor();
} else { } else {
this.hideMarker(); this.hideMarker();
} }
}); }) as EventListener);
// Handle visibility changes within editor toolbox archived details dropdown // Handle visibility changes within editor toolbox archived details dropdown
window.addEventListener('toggle', event => { window.addEventListener('toggle', event => {
@ -55,8 +57,8 @@ export class PageCommentReference extends Component {
}, {capture: true}); }, {capture: true});
// Handle comments tab changes to hide/show markers & indicators // Handle comments tab changes to hide/show markers & indicators
window.addEventListener('tabs-change', event => { window.addEventListener('tabs-change', ((event: CustomEvent<TabsChangeEvent>) => {
const sectionId = (event as {detail: {showing: string}}).detail.showing; const sectionId = event.detail.showing;
if (!sectionId.startsWith('comment-tab-panel')) { if (!sectionId.startsWith('comment-tab-panel')) {
return; return;
} }
@ -67,7 +69,7 @@ export class PageCommentReference extends Component {
} else { } else {
this.hideMarker(); this.hideMarker();
} }
}); }) as EventListener);
} }
public showForDisplay() { public showForDisplay() {

View File

@ -1,29 +1,39 @@
import {Component} from './component'; import {Component} from './component';
import {getLoading, htmlToDom} from '../services/dom.ts'; import {getLoading, htmlToDom} from '../services/dom';
import {buildForInput} from '../wysiwyg-tinymce/config'; import {buildForInput} from '../wysiwyg-tinymce/config';
import {PageCommentReference} from "./page-comment-reference"; 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 { export class PageComment extends Component {
protected commentId: string; protected commentId!: string;
protected commentLocalId: string; protected commentLocalId!: string;
protected deletedText: string; protected deletedText!: string;
protected updatedText: string; protected updatedText!: string;
protected archiveText: string; protected archiveText!: string;
protected wysiwygEditor: any = null; protected wysiwygEditor: any = null;
protected wysiwygLanguage: string; protected wysiwygLanguage!: string;
protected wysiwygTextDirection: string; protected wysiwygTextDirection!: string;
protected container: HTMLElement; protected container!: HTMLElement;
protected contentContainer: HTMLElement; protected contentContainer!: HTMLElement;
protected form: HTMLFormElement; protected form!: HTMLFormElement;
protected formCancel: HTMLElement; protected formCancel!: HTMLElement;
protected editButton: HTMLElement; protected editButton!: HTMLElement;
protected deleteButton: HTMLElement; protected deleteButton!: HTMLElement;
protected replyButton: HTMLElement; protected replyButton!: HTMLElement;
protected archiveButton: HTMLElement; protected archiveButton!: HTMLElement;
protected input: HTMLInputElement; protected input!: HTMLInputElement;
setup() { setup() {
// Options // Options
@ -53,10 +63,11 @@ export class PageComment extends Component {
protected setupListeners(): void { protected setupListeners(): void {
if (this.replyButton) { if (this.replyButton) {
this.replyButton.addEventListener('click', () => this.$emit('reply', { const data: PageCommentReplyEventData = {
id: this.commentLocalId, id: this.commentLocalId,
element: this.container, element: this.container,
})); };
this.replyButton.addEventListener('click', () => this.$emit('reply', data));
} }
if (this.editButton) { if (this.editButton) {
@ -95,10 +106,10 @@ export class PageComment extends Component {
drawioUrl: '', drawioUrl: '',
pageId: 0, pageId: 0,
translations: {}, 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]; this.wysiwygEditor = editors[0];
setTimeout(() => this.wysiwygEditor.focus(), 50); setTimeout(() => this.wysiwygEditor.focus(), 50);
}); });
@ -120,7 +131,9 @@ export class PageComment extends Component {
window.$events.success(this.updatedText); window.$events.success(this.updatedText);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
window.$events.showValidationErrors(err); if (err instanceof HttpError) {
window.$events.showValidationErrors(err);
}
this.form.toggleAttribute('hidden', false); this.form.toggleAttribute('hidden', false);
loading.remove(); loading.remove();
} }
@ -151,7 +164,8 @@ export class PageComment extends Component {
const response = await window.$http.put(`/comment/${this.commentId}/${action}`); const response = await window.$http.put(`/comment/${this.commentId}/${action}`);
window.$events.success(this.archiveText); 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 branch = this.container.closest('.comment-branch') as HTMLElement;
const references = window.$components.allWithinElement<PageCommentReference>(branch, 'page-comment-reference'); const references = window.$components.allWithinElement<PageCommentReference>(branch, 'page-comment-reference');

View File

@ -1,50 +1,38 @@
import {Component} from './component'; import {Component} from './component';
import {getLoading, htmlToDom} from '../services/dom.ts'; import {getLoading, htmlToDom} from '../services/dom';
import {buildForInput} from '../wysiwyg-tinymce/config'; import {buildForInput} from '../wysiwyg-tinymce/config';
import {Tabs} from "./tabs"; import {Tabs} from "./tabs";
import {PageCommentReference} from "./page-comment-reference"; import {PageCommentReference} from "./page-comment-reference";
import {scrollAndHighlightElement} from "../services/util"; import {scrollAndHighlightElement} from "../services/util";
import {PageCommentArchiveEventData, PageCommentReplyEventData} from "./page-comment";
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;
}
}
export class PageComments extends Component { export class PageComments extends Component {
private elem: HTMLElement; private elem!: HTMLElement;
private pageId: number; private pageId!: number;
private container: HTMLElement; private container!: HTMLElement;
private commentCountBar: HTMLElement; private commentCountBar!: HTMLElement;
private activeTab: HTMLElement; private activeTab!: HTMLElement;
private archivedTab: HTMLElement; private archivedTab!: HTMLElement;
private addButtonContainer: HTMLElement; private addButtonContainer!: HTMLElement;
private archiveContainer: HTMLElement; private archiveContainer!: HTMLElement;
private replyToRow: HTMLElement; private replyToRow!: HTMLElement;
private referenceRow: HTMLElement; private referenceRow!: HTMLElement;
private formContainer: HTMLElement; private formContainer!: HTMLElement;
private form: HTMLFormElement; private form!: HTMLFormElement;
private formInput: HTMLInputElement; private formInput!: HTMLInputElement;
private formReplyLink: HTMLAnchorElement; private formReplyLink!: HTMLAnchorElement;
private formReferenceLink: HTMLAnchorElement; private formReferenceLink!: HTMLAnchorElement;
private addCommentButton: HTMLElement; private addCommentButton!: HTMLElement;
private hideFormButton: HTMLElement; private hideFormButton!: HTMLElement;
private removeReplyToButton: HTMLElement; private removeReplyToButton!: HTMLElement;
private removeReferenceButton: HTMLElement; private removeReferenceButton!: HTMLElement;
private wysiwygLanguage: string; private wysiwygLanguage!: string;
private wysiwygTextDirection: string; private wysiwygTextDirection!: string;
private wysiwygEditor: any = null; private wysiwygEditor: any = null;
private createdText: string; private createdText!: string;
private countText: string; private countText!: string;
private archivedCountText: string; private archivedCountText!: string;
private parentId: number | null = null; private parentId: number | null = null;
private contentReference: string = ''; private contentReference: string = '';
private formReplyText: string = ''; private formReplyText: string = '';
@ -92,19 +80,19 @@ export class PageComments extends Component {
this.hideForm(); 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); 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); this.archiveContainer.append(event.detail.new_thread_dom);
setTimeout(() => this.updateCount(), 1); 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); this.container.append(event.detail.new_thread_dom);
setTimeout(() => this.updateCount(), 1); setTimeout(() => this.updateCount(), 1);
}); }) as EventListener);
if (this.form) { if (this.form) {
this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this)); 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.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -209,10 +197,10 @@ export class PageComments extends Component {
drawioUrl: '', drawioUrl: '',
pageId: 0, pageId: 0,
translations: {}, 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]; this.wysiwygEditor = editors[0];
setTimeout(() => this.wysiwygEditor.focus(), 50); setTimeout(() => this.wysiwygEditor.focus(), 50);
}); });
@ -233,11 +221,11 @@ export class PageComments extends Component {
return this.archiveContainer.querySelectorAll(':scope > .comment-branch').length; return this.archiveContainer.querySelectorAll(':scope > .comment-branch').length;
} }
protected setReply(commentLocalId, commentElement): void { protected setReply(commentLocalId: string, commentElement: HTMLElement): void {
const targetFormLocation = commentElement.closest('.comment-branch').querySelector('.comment-branch-children'); const targetFormLocation = (commentElement.closest('.comment-branch') as HTMLElement).querySelector('.comment-branch-children') as HTMLElement;
targetFormLocation.append(this.formContainer); targetFormLocation.append(this.formContainer);
this.showForm(); this.showForm();
this.parentId = commentLocalId; this.parentId = Number(commentLocalId);
this.replyToRow.toggleAttribute('hidden', false); this.replyToRow.toggleAttribute('hidden', false);
this.formReplyLink.textContent = this.formReplyText.replace('1234', String(this.parentId)); this.formReplyLink.textContent = this.formReplyText.replace('1234', String(this.parentId));
this.formReplyLink.href = `#comment${this.parentId}`; this.formReplyLink.href = `#comment${this.parentId}`;

View File

@ -1,7 +1,7 @@
import * as DOM from '../services/dom.ts'; import * as DOM from '../services/dom';
import {Component} from './component'; import {Component} from './component';
import {copyTextToClipboard} from '../services/clipboard.ts'; import {copyTextToClipboard} from '../services/clipboard';
import {hashElement, normalizeNodeTextOffsetToParent} from "../services/dom.ts"; import {hashElement, normalizeNodeTextOffsetToParent} from "../services/dom";
import {PageComments} from "./page-comments"; import {PageComments} from "./page-comments";
export class Pointer extends Component { export class Pointer extends Component {
@ -11,16 +11,16 @@ export class Pointer extends Component {
protected targetElement: HTMLElement|null = null; protected targetElement: HTMLElement|null = null;
protected targetSelectionRange: Range|null = null; protected targetSelectionRange: Range|null = null;
protected pointer: HTMLElement; protected pointer!: HTMLElement;
protected linkInput: HTMLInputElement; protected linkInput!: HTMLInputElement;
protected linkButton: HTMLElement; protected linkButton!: HTMLElement;
protected includeInput: HTMLInputElement; protected includeInput!: HTMLInputElement;
protected includeButton: HTMLElement; protected includeButton!: HTMLElement;
protected sectionModeButton: HTMLElement; protected sectionModeButton!: HTMLElement;
protected commentButton: HTMLElement; protected commentButton!: HTMLElement;
protected modeToggles: HTMLElement[]; protected modeToggles!: HTMLElement[];
protected modeSections: HTMLElement[]; protected modeSections!: HTMLElement[];
protected pageId: string; protected pageId!: string;
setup() { setup() {
this.pointer = this.$refs.pointer; this.pointer = this.$refs.pointer;
@ -67,7 +67,7 @@ export class Pointer extends Component {
DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => { DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => {
event.stopPropagation(); event.stopPropagation();
const targetEl = (event.target as HTMLElement).closest('[id^="bkmrk"]'); 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; const xPos = (event instanceof MouseEvent) ? event.pageX : 0;
this.showPointerAtTarget(targetEl, xPos, false); 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. * 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.targetElement = element;
this.targetSelectionRange = window.getSelection()?.getRangeAt(0) || null; this.targetSelectionRange = window.getSelection()?.getRangeAt(0) || null;
this.updateDomForTarget(element); this.updateDomForTarget(element);
@ -134,7 +131,7 @@ export class Pointer extends Component {
window.removeEventListener('scroll', scrollListener); window.removeEventListener('scroll', scrollListener);
}; };
element.parentElement.insertBefore(this.pointer, element); element.parentElement?.insertBefore(this.pointer, element);
if (!keyboardMode) { if (!keyboardMode) {
window.addEventListener('scroll', scrollListener, {passive: true}); 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. * 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 permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`);
const includeTag = `{{@${this.pageId}#${element.id}}}`; const includeTag = `{{@${this.pageId}#${element.id}}}`;
@ -158,13 +154,13 @@ export class Pointer extends Component {
const elementId = element.id; const elementId = element.id;
// Get the first 50 characters. // 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)}`; editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
} }
} }
enterSectionSelectMode() { 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) { for (const section of sections) {
section.setAttribute('tabindex', '0'); section.setAttribute('tabindex', '0');
} }
@ -172,12 +168,12 @@ export class Pointer extends Component {
sections[0].focus(); sections[0].focus();
DOM.onEnterPress(sections, event => { DOM.onEnterPress(sections, event => {
this.showPointerAtTarget(event.target, 0, true); this.showPointerAtTarget(event.target as HTMLElement, 0, true);
this.pointer.focus(); this.pointer.focus();
}); });
} }
createCommentAtPointer(event) { createCommentAtPointer() {
if (!this.targetElement) { if (!this.targetElement) {
return; return;
} }

View File

@ -1,5 +1,9 @@
import {Component} from './component'; import {Component} from './component';
export interface TabsChangeEvent {
showing: string;
}
/** /**
* Tabs * Tabs
* Uses accessible attributes to drive its functionality. * Uses accessible attributes to drive its functionality.
@ -19,12 +23,12 @@ import {Component} from './component';
*/ */
export class Tabs extends Component { export class Tabs extends Component {
protected container: HTMLElement; protected container!: HTMLElement;
protected tabList: HTMLElement; protected tabList!: HTMLElement;
protected tabs: HTMLElement[]; protected tabs!: HTMLElement[];
protected panels: HTMLElement[]; protected panels!: HTMLElement[];
protected activeUnder: number; protected activeUnder!: number;
protected active: null|boolean = null; protected active: null|boolean = null;
setup() { setup() {
@ -58,7 +62,8 @@ export class Tabs extends Component {
tab.setAttribute('aria-selected', selected ? 'true' : 'false'); tab.setAttribute('aria-selected', selected ? 'true' : 'false');
} }
this.$emit('change', {showing: sectionId}); const data: TabsChangeEvent = {showing: sectionId};
this.$emit('change', data);
} }
protected updateActiveState(): void { protected updateActiveState(): void {

View File

@ -225,7 +225,7 @@ export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number)
if (currentNode.nodeType === Node.TEXT_NODE) { if (currentNode.nodeType === Node.TEXT_NODE) {
// For text nodes, count the length of their content // For text nodes, count the length of their content
// Returns if within range // Returns if within range
const textLength = currentNode.textContent.length; const textLength = (currentNode.textContent || '').length;
if (currentOffset + textLength >= offset) { if (currentOffset + textLength >= offset) {
return { return {
node: currentNode, node: currentNode,
@ -237,9 +237,9 @@ export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number)
} else if (currentNode.nodeType === Node.ELEMENT_NODE) { } else if (currentNode.nodeType === Node.ELEMENT_NODE) {
// Otherwise, if an element, track the text length and search within // Otherwise, if an element, track the text length and search within
// if in range for the target offset // if in range for the target offset
const elementTextLength = currentNode.textContent.length; const elementTextLength = (currentNode.textContent || '').length;
if (currentOffset + elementTextLength >= offset) { if (currentOffset + elementTextLength >= offset) {
return findTargetNodeAndOffset(currentNode, offset - currentOffset); return findTargetNodeAndOffset(currentNode as HTMLElement, offset - currentOffset);
} }
currentOffset += elementTextLength; currentOffset += elementTextLength;

View File

@ -1,7 +1,9 @@
import {HttpError} from "./http"; import {HttpError} from "./http";
type Listener = (data: any) => void;
export class EventManager { export class EventManager {
protected listeners: Record<string, ((data: any) => void)[]> = {}; protected listeners: Record<string, Listener[]> = {};
protected stack: {name: string, data: {}}[] = []; 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 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 listeners = this.listeners[eventName] || [];
const index = listeners.indexOf(callback); const index = listeners.indexOf(callback);
if (index !== -1) { if (index !== -1) {
@ -64,8 +66,7 @@ export class EventManager {
/** /**
* Notify of standard server-provided validation errors. * Notify of standard server-provided validation errors.
*/ */
showValidationErrors(responseErr: {status?: number, data?: object}): void { showValidationErrors(responseErr: HttpError): void {
if (!responseErr.status) return;
if (responseErr.status === 422 && responseErr.data) { if (responseErr.status === 422 && responseErr.data) {
const message = Object.values(responseErr.data).flat().join('\n'); const message = Object.values(responseErr.data).flat().join('\n');
this.error(message); this.error(message);