From 209fa04752905644166e441eccff832a7a9fab52 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 11 Oct 2024 21:55:51 +0100 Subject: [PATCH] TS: Converted dom and keyboard nav services --- dev/build/esbuild.js | 2 +- resources/js/components/add-remove-rows.js | 2 +- resources/js/components/ajax-delete-row.js | 2 +- resources/js/components/ajax-form.js | 2 +- resources/js/components/attachments.js | 2 +- resources/js/components/auto-suggest.js | 4 +- resources/js/components/book-sort.js | 2 +- resources/js/components/code-editor.js | 2 +- resources/js/components/confirm-dialog.js | 2 +- resources/js/components/dropdown.js | 4 +- resources/js/components/dropzone.js | 2 +- resources/js/components/entity-permissions.js | 2 +- resources/js/components/entity-search.js | 2 +- resources/js/components/entity-selector.js | 2 +- resources/js/components/event-emit-select.js | 2 +- resources/js/components/global-search.js | 4 +- resources/js/components/image-manager.js | 2 +- resources/js/components/optional-input.js | 2 +- resources/js/components/page-comment.js | 2 +- resources/js/components/page-comments.js | 2 +- resources/js/components/page-display.js | 2 +- resources/js/components/page-editor.js | 2 +- resources/js/components/pointer.js | 2 +- resources/js/components/popup.js | 2 +- resources/js/components/template-manager.js | 2 +- resources/js/components/user-select.js | 2 +- resources/js/services/{dom.js => dom.ts} | 85 ++++++++----------- ...d-navigation.js => keyboard-navigation.ts} | 42 ++++----- 28 files changed, 87 insertions(+), 98 deletions(-) rename resources/js/services/{dom.js => dom.ts} (63%) rename resources/js/services/{keyboard-navigation.js => keyboard-navigation.ts} (66%) diff --git a/dev/build/esbuild.js b/dev/build/esbuild.js index fea8c01e3..cd8bf279f 100644 --- a/dev/build/esbuild.js +++ b/dev/build/esbuild.js @@ -10,7 +10,7 @@ const isProd = process.argv[2] === 'production'; // Gather our input files const entryPoints = { - app: path.join(__dirname, '../../resources/js/app.js'), + app: path.join(__dirname, '../../resources/js/app.ts'), code: path.join(__dirname, '../../resources/js/code/index.mjs'), 'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.mjs'), markdown: path.join(__dirname, '../../resources/js/markdown/index.mjs'), diff --git a/resources/js/components/add-remove-rows.js b/resources/js/components/add-remove-rows.js index 488654279..e7de15ae5 100644 --- a/resources/js/components/add-remove-rows.js +++ b/resources/js/components/add-remove-rows.js @@ -1,4 +1,4 @@ -import {onChildEvent} from '../services/dom'; +import {onChildEvent} from '../services/dom.ts'; import {uniqueId} from '../services/util.ts'; import {Component} from './component'; diff --git a/resources/js/components/ajax-delete-row.js b/resources/js/components/ajax-delete-row.js index aa2801f19..6ed3deedf 100644 --- a/resources/js/components/ajax-delete-row.js +++ b/resources/js/components/ajax-delete-row.js @@ -1,4 +1,4 @@ -import {onSelect} from '../services/dom'; +import {onSelect} from '../services/dom.ts'; import {Component} from './component'; export class AjaxDeleteRow extends Component { diff --git a/resources/js/components/ajax-form.js b/resources/js/components/ajax-form.js index 583dde572..de1a6db43 100644 --- a/resources/js/components/ajax-form.js +++ b/resources/js/components/ajax-form.js @@ -1,4 +1,4 @@ -import {onEnterPress, onSelect} from '../services/dom'; +import {onEnterPress, onSelect} from '../services/dom.ts'; import {Component} from './component'; /** diff --git a/resources/js/components/attachments.js b/resources/js/components/attachments.js index f45b25e36..2dc7313a8 100644 --- a/resources/js/components/attachments.js +++ b/resources/js/components/attachments.js @@ -1,4 +1,4 @@ -import {showLoading} from '../services/dom'; +import {showLoading} from '../services/dom.ts'; import {Component} from './component'; export class Attachments extends Component { diff --git a/resources/js/components/auto-suggest.js b/resources/js/components/auto-suggest.js index 07711312f..0b828e71b 100644 --- a/resources/js/components/auto-suggest.js +++ b/resources/js/components/auto-suggest.js @@ -1,7 +1,7 @@ import {escapeHtml} from '../services/util.ts'; -import {onChildEvent} from '../services/dom'; +import {onChildEvent} from '../services/dom.ts'; import {Component} from './component'; -import {KeyboardNavigationHandler} from '../services/keyboard-navigation'; +import {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts'; const ajaxCache = {}; diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js index 2ba7d5d36..48557141f 100644 --- a/resources/js/components/book-sort.js +++ b/resources/js/components/book-sort.js @@ -1,6 +1,6 @@ import Sortable, {MultiDrag} from 'sortablejs'; import {Component} from './component'; -import {htmlToDom} from '../services/dom'; +import {htmlToDom} from '../services/dom.ts'; // Auto sort control const sortOperations = { diff --git a/resources/js/components/code-editor.js b/resources/js/components/code-editor.js index 091c3483f..12937d472 100644 --- a/resources/js/components/code-editor.js +++ b/resources/js/components/code-editor.js @@ -1,4 +1,4 @@ -import {onChildEvent, onEnterPress, onSelect} from '../services/dom'; +import {onChildEvent, onEnterPress, onSelect} from '../services/dom.ts'; import {Component} from './component'; export class CodeEditor extends Component { diff --git a/resources/js/components/confirm-dialog.js b/resources/js/components/confirm-dialog.js index 184618fcc..00f3cfed2 100644 --- a/resources/js/components/confirm-dialog.js +++ b/resources/js/components/confirm-dialog.js @@ -1,4 +1,4 @@ -import {onSelect} from '../services/dom'; +import {onSelect} from '../services/dom.ts'; import {Component} from './component'; /** diff --git a/resources/js/components/dropdown.js b/resources/js/components/dropdown.js index 4efd428ac..5dd5dd93b 100644 --- a/resources/js/components/dropdown.js +++ b/resources/js/components/dropdown.js @@ -1,5 +1,5 @@ -import {onSelect} from '../services/dom'; -import {KeyboardNavigationHandler} from '../services/keyboard-navigation'; +import {onSelect} from '../services/dom.ts'; +import {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts'; import {Component} from './component'; /** diff --git a/resources/js/components/dropzone.js b/resources/js/components/dropzone.js index 920fe875f..598e0d8d4 100644 --- a/resources/js/components/dropzone.js +++ b/resources/js/components/dropzone.js @@ -2,7 +2,7 @@ import {Component} from './component'; import {Clipboard} from '../services/clipboard.ts'; import { elem, getLoading, onSelect, removeLoading, -} from '../services/dom'; +} from '../services/dom.ts'; export class Dropzone extends Component { diff --git a/resources/js/components/entity-permissions.js b/resources/js/components/entity-permissions.js index 7ab99a2a7..b020c5d85 100644 --- a/resources/js/components/entity-permissions.js +++ b/resources/js/components/entity-permissions.js @@ -1,4 +1,4 @@ -import {htmlToDom} from '../services/dom'; +import {htmlToDom} from '../services/dom.ts'; import {Component} from './component'; export class EntityPermissions extends Component { diff --git a/resources/js/components/entity-search.js b/resources/js/components/entity-search.js index 7a5044470..9d4513326 100644 --- a/resources/js/components/entity-search.js +++ b/resources/js/components/entity-search.js @@ -1,4 +1,4 @@ -import {onSelect} from '../services/dom'; +import {onSelect} from '../services/dom.ts'; import {Component} from './component'; export class EntitySearch extends Component { diff --git a/resources/js/components/entity-selector.js b/resources/js/components/entity-selector.js index 561370d7a..7491119a1 100644 --- a/resources/js/components/entity-selector.js +++ b/resources/js/components/entity-selector.js @@ -1,4 +1,4 @@ -import {onChildEvent} from '../services/dom'; +import {onChildEvent} from '../services/dom.ts'; import {Component} from './component'; /** diff --git a/resources/js/components/event-emit-select.js b/resources/js/components/event-emit-select.js index 2097c0528..f722a25e7 100644 --- a/resources/js/components/event-emit-select.js +++ b/resources/js/components/event-emit-select.js @@ -1,4 +1,4 @@ -import {onSelect} from '../services/dom'; +import {onSelect} from '../services/dom.ts'; import {Component} from './component'; /** diff --git a/resources/js/components/global-search.js b/resources/js/components/global-search.js index 44c0d02f9..2cdaf591a 100644 --- a/resources/js/components/global-search.js +++ b/resources/js/components/global-search.js @@ -1,6 +1,6 @@ -import {htmlToDom} from '../services/dom'; +import {htmlToDom} from '../services/dom.ts'; import {debounce} from '../services/util.ts'; -import {KeyboardNavigationHandler} from '../services/keyboard-navigation'; +import {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts'; import {Component} from './component'; /** diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js index 47231477b..c8108ab28 100644 --- a/resources/js/components/image-manager.js +++ b/resources/js/components/image-manager.js @@ -1,6 +1,6 @@ import { onChildEvent, onSelect, removeLoading, showLoading, -} from '../services/dom'; +} from '../services/dom.ts'; import {Component} from './component'; export class ImageManager extends Component { diff --git a/resources/js/components/optional-input.js b/resources/js/components/optional-input.js index 64cee12cd..1b133047d 100644 --- a/resources/js/components/optional-input.js +++ b/resources/js/components/optional-input.js @@ -1,4 +1,4 @@ -import {onSelect} from '../services/dom'; +import {onSelect} from '../services/dom.ts'; import {Component} from './component'; export class OptionalInput extends Component { diff --git a/resources/js/components/page-comment.js b/resources/js/components/page-comment.js index fd8ad1f2e..8c0a8b33e 100644 --- a/resources/js/components/page-comment.js +++ b/resources/js/components/page-comment.js @@ -1,5 +1,5 @@ import {Component} from './component'; -import {getLoading, htmlToDom} from '../services/dom'; +import {getLoading, htmlToDom} from '../services/dom.ts'; import {buildForInput} from '../wysiwyg-tinymce/config'; export class PageComment extends Component { diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js index 1d6abfe20..3d7e1365f 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.js @@ -1,5 +1,5 @@ import {Component} from './component'; -import {getLoading, htmlToDom} from '../services/dom'; +import {getLoading, htmlToDom} from '../services/dom.ts'; import {buildForInput} from '../wysiwyg-tinymce/config'; export class PageComments extends Component { diff --git a/resources/js/components/page-display.js b/resources/js/components/page-display.js index ff9d68c7a..d3ac78a4a 100644 --- a/resources/js/components/page-display.js +++ b/resources/js/components/page-display.js @@ -1,4 +1,4 @@ -import * as DOM from '../services/dom'; +import * as DOM from '../services/dom.ts'; import {scrollAndHighlightElement} from '../services/util.ts'; import {Component} from './component'; diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js index 9450444ca..7ffceb0f9 100644 --- a/resources/js/components/page-editor.js +++ b/resources/js/components/page-editor.js @@ -1,4 +1,4 @@ -import {onSelect} from '../services/dom'; +import {onSelect} from '../services/dom.ts'; import {debounce} from '../services/util.ts'; import {Component} from './component'; import {utcTimeStampToLocalTime} from '../services/dates.ts'; diff --git a/resources/js/components/pointer.js b/resources/js/components/pointer.js index 607576cb9..292b923e5 100644 --- a/resources/js/components/pointer.js +++ b/resources/js/components/pointer.js @@ -1,4 +1,4 @@ -import * as DOM from '../services/dom'; +import * as DOM from '../services/dom.ts'; import {Component} from './component'; import {copyTextToClipboard} from '../services/clipboard.ts'; diff --git a/resources/js/components/popup.js b/resources/js/components/popup.js index edd428037..6bd8f9c72 100644 --- a/resources/js/components/popup.js +++ b/resources/js/components/popup.js @@ -1,5 +1,5 @@ import {fadeIn, fadeOut} from '../services/animations.ts'; -import {onSelect} from '../services/dom'; +import {onSelect} from '../services/dom.ts'; import {Component} from './component'; /** diff --git a/resources/js/components/template-manager.js b/resources/js/components/template-manager.js index 56ec876d4..cf81990ab 100644 --- a/resources/js/components/template-manager.js +++ b/resources/js/components/template-manager.js @@ -1,4 +1,4 @@ -import * as DOM from '../services/dom'; +import * as DOM from '../services/dom.ts'; import {Component} from './component'; export class TemplateManager extends Component { diff --git a/resources/js/components/user-select.js b/resources/js/components/user-select.js index e6adc3c23..f9ec03ed3 100644 --- a/resources/js/components/user-select.js +++ b/resources/js/components/user-select.js @@ -1,4 +1,4 @@ -import {onChildEvent} from '../services/dom'; +import {onChildEvent} from '../services/dom.ts'; import {Component} from './component'; export class UserSelect extends Component { diff --git a/resources/js/services/dom.js b/resources/js/services/dom.ts similarity index 63% rename from resources/js/services/dom.js rename to resources/js/services/dom.ts index bcfd0b565..c88827bac 100644 --- a/resources/js/services/dom.js +++ b/resources/js/services/dom.ts @@ -1,12 +1,15 @@ +/** + * Check if the given param is a HTMLElement + */ +export function isHTMLElement(el: any): el is HTMLElement { + return el instanceof HTMLElement; +} + /** * Create a new element with the given attrs and children. * Children can be a string for text nodes or other elements. - * @param {String} tagName - * @param {Object} attrs - * @param {Element[]|String[]}children - * @return {*} */ -export function elem(tagName, attrs = {}, children = []) { +export function elem(tagName: string, attrs: Record = {}, children: Element[]|string[] = []): HTMLElement { const el = document.createElement(tagName); for (const [key, val] of Object.entries(attrs)) { @@ -30,10 +33,8 @@ export function elem(tagName, attrs = {}, children = []) { /** * Run the given callback against each element that matches the given selector. - * @param {String} selector - * @param {Function} callback */ -export function forEach(selector, callback) { +export function forEach(selector: string, callback: (el: Element) => any) { const elements = document.querySelectorAll(selector); for (const element of elements) { callback(element); @@ -42,11 +43,8 @@ export function forEach(selector, callback) { /** * Helper to listen to multiple DOM events - * @param {Element} listenerElement - * @param {Array} events - * @param {Function} callback */ -export function onEvents(listenerElement, events, callback) { +export function onEvents(listenerElement: Element, events: string[], callback: (e: Event) => any): void { for (const eventName of events) { listenerElement.addEventListener(eventName, callback); } @@ -55,10 +53,8 @@ export function onEvents(listenerElement, events, callback) { /** * Helper to run an action when an element is selected. * A "select" is made to be accessible, So can be a click, space-press or enter-press. - * @param {HTMLElement|Array} elements - * @param {function} callback */ -export function onSelect(elements, callback) { +export function onSelect(elements: HTMLElement|HTMLElement[], callback: (e: Event) => any): void { if (!Array.isArray(elements)) { elements = [elements]; } @@ -76,16 +72,13 @@ export function onSelect(elements, callback) { /** * Listen to key press on the given element(s). - * @param {String} key - * @param {HTMLElement|Array} elements - * @param {function} callback */ -function onKeyPress(key, elements, callback) { +function onKeyPress(key: string, elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void { if (!Array.isArray(elements)) { elements = [elements]; } - const listener = event => { + const listener = (event: KeyboardEvent) => { if (event.key === key) { callback(event); } @@ -96,19 +89,15 @@ function onKeyPress(key, elements, callback) { /** * Listen to enter press on the given element(s). - * @param {HTMLElement|Array} elements - * @param {function} callback */ -export function onEnterPress(elements, callback) { +export function onEnterPress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void { onKeyPress('Enter', elements, callback); } /** * Listen to escape press on the given element(s). - * @param {HTMLElement|Array} elements - * @param {function} callback */ -export function onEscapePress(elements, callback) { +export function onEscapePress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void { onKeyPress('Escape', elements, callback); } @@ -116,14 +105,15 @@ export function onEscapePress(elements, callback) { * Set a listener on an element for an event emitted by a child * matching the given childSelector param. * Used in a similar fashion to jQuery's $('listener').on('eventName', 'childSelector', callback) - * @param {Element} listenerElement - * @param {String} childSelector - * @param {String} eventName - * @param {Function} callback */ -export function onChildEvent(listenerElement, childSelector, eventName, callback) { - listenerElement.addEventListener(eventName, event => { - const matchingChild = event.target.closest(childSelector); +export function onChildEvent( + listenerElement: HTMLElement, + childSelector: string, + eventName: string, + callback: (this: HTMLElement, e: Event, child: HTMLElement) => any +): void { + listenerElement.addEventListener(eventName, (event: Event) => { + const matchingChild = (event.target as HTMLElement|null)?.closest(childSelector) as HTMLElement; if (matchingChild) { callback.call(matchingChild, event, matchingChild); } @@ -132,16 +122,13 @@ export function onChildEvent(listenerElement, childSelector, eventName, callback /** * Look for elements that match the given selector and contain the given text. - * Is case insensitive and returns the first result or null if nothing is found. - * @param {String} selector - * @param {String} text - * @returns {Element} + * Is case-insensitive and returns the first result or null if nothing is found. */ -export function findText(selector, text) { +export function findText(selector: string, text: string): Element|null { const elements = document.querySelectorAll(selector); text = text.toLowerCase(); for (const element of elements) { - if (element.textContent.toLowerCase().includes(text)) { + if ((element.textContent || '').toLowerCase().includes(text) && isHTMLElement(element)) { return element; } } @@ -151,17 +138,15 @@ export function findText(selector, text) { /** * Show a loading indicator in the given element. * This will effectively clear the element. - * @param {Element} element */ -export function showLoading(element) { +export function showLoading(element: HTMLElement): void { element.innerHTML = '
'; } /** * Get a loading element indicator element. - * @returns {Element} */ -export function getLoading() { +export function getLoading(): HTMLElement { const wrap = document.createElement('div'); wrap.classList.add('loading-container'); wrap.innerHTML = '
'; @@ -170,9 +155,8 @@ export function getLoading() { /** * Remove any loading indicators within the given element. - * @param {Element} element */ -export function removeLoading(element) { +export function removeLoading(element: HTMLElement): void { const loadingEls = element.querySelectorAll('.loading-container'); for (const el of loadingEls) { el.remove(); @@ -182,12 +166,15 @@ export function removeLoading(element) { /** * Convert the given html data into a live DOM element. * Initiates any components defined in the data. - * @param {String} html - * @returns {Element} */ -export function htmlToDom(html) { +export function htmlToDom(html: string): HTMLElement { const wrap = document.createElement('div'); wrap.innerHTML = html; window.$components.init(wrap); - return wrap.children[0]; + const firstChild = wrap.children[0]; + if (!isHTMLElement(firstChild)) { + throw new Error('Could not find child HTMLElement when creating DOM element from HTML'); + } + + return firstChild; } diff --git a/resources/js/services/keyboard-navigation.js b/resources/js/services/keyboard-navigation.ts similarity index 66% rename from resources/js/services/keyboard-navigation.js rename to resources/js/services/keyboard-navigation.ts index 34111bb2d..13fbdfecc 100644 --- a/resources/js/services/keyboard-navigation.js +++ b/resources/js/services/keyboard-navigation.ts @@ -1,14 +1,17 @@ +import {isHTMLElement} from "./dom"; + +type OptionalKeyEventHandler = ((e: KeyboardEvent) => any)|null; + /** * Handle common keyboard navigation events within a given container. */ export class KeyboardNavigationHandler { - /** - * @param {Element} container - * @param {Function|null} onEscape - * @param {Function|null} onEnter - */ - constructor(container, onEscape = null, onEnter = null) { + protected containers: HTMLElement[]; + protected onEscape: OptionalKeyEventHandler; + protected onEnter: OptionalKeyEventHandler; + + constructor(container: HTMLElement, onEscape: OptionalKeyEventHandler = null, onEnter: OptionalKeyEventHandler = null) { this.containers = [container]; this.onEscape = onEscape; this.onEnter = onEnter; @@ -18,9 +21,8 @@ export class KeyboardNavigationHandler { /** * Also share the keyboard event handling to the given element. * Only elements within the original container are considered focusable though. - * @param {Element} element */ - shareHandlingToEl(element) { + shareHandlingToEl(element: HTMLElement) { this.containers.push(element); element.addEventListener('keydown', this.#keydownHandler.bind(this)); } @@ -30,7 +32,8 @@ export class KeyboardNavigationHandler { */ focusNext() { const focusable = this.#getFocusable(); - const currentIndex = focusable.indexOf(document.activeElement); + const activeEl = document.activeElement; + const currentIndex = isHTMLElement(activeEl) ? focusable.indexOf(activeEl) : -1; let newIndex = currentIndex + 1; if (newIndex >= focusable.length) { newIndex = 0; @@ -44,7 +47,8 @@ export class KeyboardNavigationHandler { */ focusPrevious() { const focusable = this.#getFocusable(); - const currentIndex = focusable.indexOf(document.activeElement); + const activeEl = document.activeElement; + const currentIndex = isHTMLElement(activeEl) ? focusable.indexOf(activeEl) : -1; let newIndex = currentIndex - 1; if (newIndex < 0) { newIndex = focusable.length - 1; @@ -53,12 +57,9 @@ export class KeyboardNavigationHandler { focusable[newIndex].focus(); } - /** - * @param {KeyboardEvent} event - */ - #keydownHandler(event) { + #keydownHandler(event: KeyboardEvent) { // Ignore certain key events in inputs to allow text editing. - if (event.target.matches('input') && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) { + if (isHTMLElement(event.target) && event.target.matches('input') && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) { return; } @@ -71,7 +72,7 @@ export class KeyboardNavigationHandler { } else if (event.key === 'Escape') { if (this.onEscape) { this.onEscape(event); - } else if (document.activeElement) { + } else if (isHTMLElement(document.activeElement)) { document.activeElement.blur(); } } else if (event.key === 'Enter' && this.onEnter) { @@ -81,14 +82,15 @@ export class KeyboardNavigationHandler { /** * Get an array of focusable elements within the current containers. - * @returns {Element[]} */ - #getFocusable() { - const focusable = []; + #getFocusable(): HTMLElement[] { + const focusable: HTMLElement[] = []; const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])'; for (const container of this.containers) { - focusable.push(...container.querySelectorAll(selector)); + const toAdd = [...container.querySelectorAll(selector)].filter(e => isHTMLElement(e)); + focusable.push(...toAdd); } + return focusable; }