From 2e8d6ce7d9aad7569cd8bfbdd1126c869a78d379 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 10 Oct 2024 12:03:24 +0100 Subject: [PATCH 1/3] TS: Coverted util service --- resources/js/components/add-remove-rows.js | 2 +- resources/js/components/auto-suggest.js | 2 +- resources/js/components/dropdown-search.js | 2 +- resources/js/components/global-search.js | 2 +- resources/js/components/page-display.js | 2 +- resources/js/components/page-editor.js | 2 +- resources/js/markdown/codemirror.js | 2 +- resources/js/services/{util.js => util.ts} | 54 +++++++++---------- resources/js/wysiwyg-tinymce/plugin-drawio.js | 2 +- 9 files changed, 32 insertions(+), 38 deletions(-) rename resources/js/services/{util.js => util.ts} (67%) diff --git a/resources/js/components/add-remove-rows.js b/resources/js/components/add-remove-rows.js index 3213c4835..488654279 100644 --- a/resources/js/components/add-remove-rows.js +++ b/resources/js/components/add-remove-rows.js @@ -1,5 +1,5 @@ import {onChildEvent} from '../services/dom'; -import {uniqueId} from '../services/util'; +import {uniqueId} from '../services/util.ts'; import {Component} from './component'; /** diff --git a/resources/js/components/auto-suggest.js b/resources/js/components/auto-suggest.js index 2eede241c..07711312f 100644 --- a/resources/js/components/auto-suggest.js +++ b/resources/js/components/auto-suggest.js @@ -1,4 +1,4 @@ -import {escapeHtml} from '../services/util'; +import {escapeHtml} from '../services/util.ts'; import {onChildEvent} from '../services/dom'; import {Component} from './component'; import {KeyboardNavigationHandler} from '../services/keyboard-navigation'; diff --git a/resources/js/components/dropdown-search.js b/resources/js/components/dropdown-search.js index 2344619f5..787e11c87 100644 --- a/resources/js/components/dropdown-search.js +++ b/resources/js/components/dropdown-search.js @@ -1,4 +1,4 @@ -import {debounce} from '../services/util'; +import {debounce} from '../services/util.ts'; import {transitionHeight} from '../services/animations'; import {Component} from './component'; diff --git a/resources/js/components/global-search.js b/resources/js/components/global-search.js index 798bd7aac..44c0d02f9 100644 --- a/resources/js/components/global-search.js +++ b/resources/js/components/global-search.js @@ -1,5 +1,5 @@ import {htmlToDom} from '../services/dom'; -import {debounce} from '../services/util'; +import {debounce} from '../services/util.ts'; import {KeyboardNavigationHandler} from '../services/keyboard-navigation'; import {Component} from './component'; diff --git a/resources/js/components/page-display.js b/resources/js/components/page-display.js index 1e13ae388..ff9d68c7a 100644 --- a/resources/js/components/page-display.js +++ b/resources/js/components/page-display.js @@ -1,5 +1,5 @@ import * as DOM from '../services/dom'; -import {scrollAndHighlightElement} from '../services/util'; +import {scrollAndHighlightElement} from '../services/util.ts'; import {Component} from './component'; function toggleAnchorHighlighting(elementId, shouldHighlight) { diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js index 216067552..9450444ca 100644 --- a/resources/js/components/page-editor.js +++ b/resources/js/components/page-editor.js @@ -1,5 +1,5 @@ import {onSelect} from '../services/dom'; -import {debounce} from '../services/util'; +import {debounce} from '../services/util.ts'; import {Component} from './component'; import {utcTimeStampToLocalTime} from '../services/dates.ts'; diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js index a6332cbb8..664767605 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.js @@ -1,5 +1,5 @@ import {provideKeyBindings} from './shortcuts'; -import {debounce} from '../services/util'; +import {debounce} from '../services/util.ts'; import {Clipboard} from '../services/clipboard.ts'; /** diff --git a/resources/js/services/util.js b/resources/js/services/util.ts similarity index 67% rename from resources/js/services/util.js rename to resources/js/services/util.ts index 1264d1058..7f684dd42 100644 --- a/resources/js/services/util.js +++ b/resources/js/services/util.ts @@ -4,37 +4,39 @@ * N milliseconds. If `immediate` is passed, trigger the function on the * leading edge, instead of the trailing. * @attribution https://davidwalsh.name/javascript-debounce-function - * @param {Function} func - * @param {Number} waitMs - * @param {Boolean} immediate - * @returns {Function} */ -export function debounce(func, waitMs, immediate) { - let timeout; - return function debouncedWrapper(...args) { - const context = this; +export function debounce(func: Function, waitMs: number, immediate: boolean): Function { + let timeout: number|null = null; + return function debouncedWrapper(this: any, ...args: any[]) { + const context: any = this; const later = function debouncedTimeout() { timeout = null; if (!immediate) func.apply(context, args); }; const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, waitMs); + if (timeout) { + clearTimeout(timeout); + } + timeout = window.setTimeout(later, waitMs); if (callNow) func.apply(context, args); }; } +function isDetailsElement(element: HTMLElement): element is HTMLDetailsElement { + return element.nodeName === 'DETAILS'; +} + /** - * Scroll and highlight an element. - * @param {HTMLElement} element + * Scroll-to and highlight an element. */ -export function scrollAndHighlightElement(element) { +export function scrollAndHighlightElement(element: HTMLElement): void { if (!element) return; + // Open up parent
elements if within let parent = element; while (parent.parentElement) { parent = parent.parentElement; - if (parent.nodeName === 'DETAILS' && !parent.open) { + if (isDetailsElement(parent) && !parent.open) { parent.open = true; } } @@ -44,15 +46,15 @@ export function scrollAndHighlightElement(element) { const highlight = getComputedStyle(document.body).getPropertyValue('--color-link'); element.style.outline = `2px dashed ${highlight}`; element.style.outlineOffset = '5px'; - element.style.transition = null; + element.style.removeProperty('transition'); setTimeout(() => { element.style.transition = 'outline linear 3s'; element.style.outline = '2px dashed rgba(0, 0, 0, 0)'; const listener = () => { element.removeEventListener('transitionend', listener); - element.style.transition = null; - element.style.outline = null; - element.style.outlineOffset = null; + element.style.removeProperty('transition'); + element.style.removeProperty('outline'); + element.style.removeProperty('outlineOffset'); }; element.addEventListener('transitionend', listener); }, 1000); @@ -61,10 +63,8 @@ export function scrollAndHighlightElement(element) { /** * Escape any HTML in the given 'unsafe' string. * Take from https://stackoverflow.com/a/6234804. - * @param {String} unsafe - * @returns {string} */ -export function escapeHtml(unsafe) { +export function escapeHtml(unsafe: string): string { return unsafe .replace(/&/g, '&') .replace(/ (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`); @@ -86,10 +84,8 @@ export function uniqueId() { /** * Generate a random smaller unique ID. - * - * @returns {string} */ -export function uniqueIdSmall() { +export function uniqueIdSmall(): string { // eslint-disable-next-line no-bitwise const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); return S4(); @@ -97,10 +93,8 @@ export function uniqueIdSmall() { /** * Create a promise that resolves after the given time. - * @param {int} timeMs - * @returns {Promise} */ -export function wait(timeMs) { +export function wait(timeMs: number): Promise { return new Promise(res => { setTimeout(res, timeMs); }); diff --git a/resources/js/wysiwyg-tinymce/plugin-drawio.js b/resources/js/wysiwyg-tinymce/plugin-drawio.js index 342cac0af..197c50b0e 100644 --- a/resources/js/wysiwyg-tinymce/plugin-drawio.js +++ b/resources/js/wysiwyg-tinymce/plugin-drawio.js @@ -1,5 +1,5 @@ import * as DrawIO from '../services/drawio.ts'; -import {wait} from '../services/util'; +import {wait} from '../services/util.ts'; let pageEditor = null; let currentNode = null; From f41c02cbd7e705155b5b4fdbf8dfb3dc09a445d1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 11 Oct 2024 15:19:19 +0100 Subject: [PATCH 2/3] TS: Converted app file and animations service Extracted functions out of app file during changes to clean up. Altered animation function to use normal css prop names instead of JS CSS prop names. --- resources/js/app.js | 33 -------- resources/js/app.ts | 23 ++++++ resources/js/components/chapter-contents.js | 2 +- resources/js/components/collapsible.js | 2 +- resources/js/components/dropdown-search.js | 2 +- resources/js/components/expand-toggle.js | 2 +- .../js/components/{index.js => index.ts} | 0 resources/js/components/popup.js | 2 +- resources/js/global.d.ts | 2 + .../services/{animations.js => animations.ts} | 78 ++++++++----------- resources/js/services/util.ts | 46 +++++++++++ 11 files changed, 108 insertions(+), 84 deletions(-) delete mode 100644 resources/js/app.js create mode 100644 resources/js/app.ts rename resources/js/components/{index.js => index.ts} (100%) rename resources/js/services/{animations.js => animations.ts} (63%) diff --git a/resources/js/app.js b/resources/js/app.js deleted file mode 100644 index 5f4902f86..000000000 --- a/resources/js/app.js +++ /dev/null @@ -1,33 +0,0 @@ -import {EventManager} from './services/events.ts'; -import {HttpManager} from './services/http.ts'; -import {Translator} from './services/translations.ts'; -import * as componentMap from './components'; -import {ComponentStore} from './services/components.ts'; - -// eslint-disable-next-line no-underscore-dangle -window.__DEV__ = false; - -// Url retrieval function -window.baseUrl = function baseUrl(path) { - let targetPath = path; - let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content'); - if (basePath[basePath.length - 1] === '/') basePath = basePath.slice(0, basePath.length - 1); - if (targetPath[0] === '/') targetPath = targetPath.slice(1); - return `${basePath}/${targetPath}`; -}; - -window.importVersioned = function importVersioned(moduleName) { - const version = document.querySelector('link[href*="/dist/styles.css?version="]').href.split('?version=').pop(); - const importPath = window.baseUrl(`dist/${moduleName}.js?version=${version}`); - return import(importPath); -}; - -// Set events, http & translation services on window -window.$http = new HttpManager(); -window.$events = new EventManager(); -window.$trans = new Translator(); - -// Load & initialise components -window.$components = new ComponentStore(); -window.$components.register(componentMap); -window.$components.init(); diff --git a/resources/js/app.ts b/resources/js/app.ts new file mode 100644 index 000000000..141a50ef5 --- /dev/null +++ b/resources/js/app.ts @@ -0,0 +1,23 @@ +import {EventManager} from './services/events'; +import {HttpManager} from './services/http'; +import {Translator} from './services/translations'; +import * as componentMap from './components/index'; +import {ComponentStore} from './services/components'; +import {baseUrl, importVersioned} from "./services/util"; + +// eslint-disable-next-line no-underscore-dangle +window.__DEV__ = false; + +// Make common important util functions global +window.baseUrl = baseUrl; +window.importVersioned = importVersioned; + +// Setup events, http & translation services +window.$http = new HttpManager(); +window.$events = new EventManager(); +window.$trans = new Translator(); + +// Load & initialise components +window.$components = new ComponentStore(); +window.$components.register(componentMap); +window.$components.init(); diff --git a/resources/js/components/chapter-contents.js b/resources/js/components/chapter-contents.js index 7c6480a1a..6b0707bdd 100644 --- a/resources/js/components/chapter-contents.js +++ b/resources/js/components/chapter-contents.js @@ -1,4 +1,4 @@ -import {slideUp, slideDown} from '../services/animations'; +import {slideUp, slideDown} from '../services/animations.ts'; import {Component} from './component'; export class ChapterContents extends Component { diff --git a/resources/js/components/collapsible.js b/resources/js/components/collapsible.js index 6f740ed71..7b6fa79fb 100644 --- a/resources/js/components/collapsible.js +++ b/resources/js/components/collapsible.js @@ -1,4 +1,4 @@ -import {slideDown, slideUp} from '../services/animations'; +import {slideDown, slideUp} from '../services/animations.ts'; import {Component} from './component'; /** diff --git a/resources/js/components/dropdown-search.js b/resources/js/components/dropdown-search.js index 787e11c87..fcbabc022 100644 --- a/resources/js/components/dropdown-search.js +++ b/resources/js/components/dropdown-search.js @@ -1,5 +1,5 @@ import {debounce} from '../services/util.ts'; -import {transitionHeight} from '../services/animations'; +import {transitionHeight} from '../services/animations.ts'; import {Component} from './component'; export class DropdownSearch extends Component { diff --git a/resources/js/components/expand-toggle.js b/resources/js/components/expand-toggle.js index 0d2018b9d..29173a058 100644 --- a/resources/js/components/expand-toggle.js +++ b/resources/js/components/expand-toggle.js @@ -1,4 +1,4 @@ -import {slideUp, slideDown} from '../services/animations'; +import {slideUp, slideDown} from '../services/animations.ts'; import {Component} from './component'; export class ExpandToggle extends Component { diff --git a/resources/js/components/index.js b/resources/js/components/index.ts similarity index 100% rename from resources/js/components/index.js rename to resources/js/components/index.ts diff --git a/resources/js/components/popup.js b/resources/js/components/popup.js index 662736548..edd428037 100644 --- a/resources/js/components/popup.js +++ b/resources/js/components/popup.js @@ -1,4 +1,4 @@ -import {fadeIn, fadeOut} from '../services/animations'; +import {fadeIn, fadeOut} from '../services/animations.ts'; import {onSelect} from '../services/dom'; import {Component} from './component'; diff --git a/resources/js/global.d.ts b/resources/js/global.d.ts index e505c96e0..b637c97c1 100644 --- a/resources/js/global.d.ts +++ b/resources/js/global.d.ts @@ -7,10 +7,12 @@ declare global { const __DEV__: boolean; interface Window { + __DEV__: boolean; $components: ComponentStore; $events: EventManager; $trans: Translator; $http: HttpManager; baseUrl: (path: string) => string; + importVersioned: (module: string) => Promise; } } \ No newline at end of file diff --git a/resources/js/services/animations.js b/resources/js/services/animations.ts similarity index 63% rename from resources/js/services/animations.js rename to resources/js/services/animations.ts index bc983c807..adf4cb3c9 100644 --- a/resources/js/services/animations.js +++ b/resources/js/services/animations.ts @@ -1,30 +1,30 @@ /** * Used in the function below to store references of clean-up functions. * Used to ensure only one transitionend function exists at any time. - * @type {WeakMap} */ -const animateStylesCleanupMap = new WeakMap(); +const animateStylesCleanupMap: WeakMap = new WeakMap(); /** * Animate the css styles of an element using FLIP animation techniques. - * Styles must be an object where the keys are style properties, camelcase, and the values + * Styles must be an object where the keys are style rule names and the values * are an array of two items in the format [initialValue, finalValue] - * @param {Element} element - * @param {Object} styles - * @param {Number} animTime - * @param {Function} onComplete */ -function animateStyles(element, styles, animTime = 400, onComplete = null) { +function animateStyles( + element: HTMLElement, + styles: Record, + animTime: number = 400, + onComplete: Function | null = null +): void { const styleNames = Object.keys(styles); for (const style of styleNames) { - element.style[style] = styles[style][0]; + element.style.setProperty(style, styles[style][0]); } const cleanup = () => { for (const style of styleNames) { - element.style[style] = null; + element.style.removeProperty(style); } - element.style.transition = null; + element.style.removeProperty('transition'); element.removeEventListener('transitionend', cleanup); animateStylesCleanupMap.delete(element); if (onComplete) onComplete(); @@ -33,7 +33,7 @@ function animateStyles(element, styles, animTime = 400, onComplete = null) { setTimeout(() => { element.style.transition = `all ease-in-out ${animTime}ms`; for (const style of styleNames) { - element.style[style] = styles[style][1]; + element.style.setProperty(style, styles[style][1]); } element.addEventListener('transitionend', cleanup); @@ -43,9 +43,8 @@ function animateStyles(element, styles, animTime = 400, onComplete = null) { /** * Run the active cleanup action for the given element. - * @param {Element} element */ -function cleanupExistingElementAnimation(element) { +function cleanupExistingElementAnimation(element: Element) { if (animateStylesCleanupMap.has(element)) { const oldCleanup = animateStylesCleanupMap.get(element); oldCleanup(); @@ -54,15 +53,12 @@ function cleanupExistingElementAnimation(element) { /** * Fade in the given element. - * @param {Element} element - * @param {Number} animTime - * @param {Function|null} onComplete */ -export function fadeIn(element, animTime = 400, onComplete = null) { +export function fadeIn(element: HTMLElement, animTime: number = 400, onComplete: Function | null = null): void { cleanupExistingElementAnimation(element); element.style.display = 'block'; animateStyles(element, { - opacity: ['0', '1'], + 'opacity': ['0', '1'], }, animTime, () => { if (onComplete) onComplete(); }); @@ -70,14 +66,11 @@ export function fadeIn(element, animTime = 400, onComplete = null) { /** * Fade out the given element. - * @param {Element} element - * @param {Number} animTime - * @param {Function|null} onComplete */ -export function fadeOut(element, animTime = 400, onComplete = null) { +export function fadeOut(element: HTMLElement, animTime: number = 400, onComplete: Function | null = null): void { cleanupExistingElementAnimation(element); animateStyles(element, { - opacity: ['1', '0'], + 'opacity': ['1', '0'], }, animTime, () => { element.style.display = 'none'; if (onComplete) onComplete(); @@ -86,20 +79,18 @@ export function fadeOut(element, animTime = 400, onComplete = null) { /** * Hide the element by sliding the contents upwards. - * @param {Element} element - * @param {Number} animTime */ -export function slideUp(element, animTime = 400) { +export function slideUp(element: HTMLElement, animTime: number = 400) { cleanupExistingElementAnimation(element); const currentHeight = element.getBoundingClientRect().height; const computedStyles = getComputedStyle(element); const currentPaddingTop = computedStyles.getPropertyValue('padding-top'); const currentPaddingBottom = computedStyles.getPropertyValue('padding-bottom'); const animStyles = { - maxHeight: [`${currentHeight}px`, '0px'], - overflow: ['hidden', 'hidden'], - paddingTop: [currentPaddingTop, '0px'], - paddingBottom: [currentPaddingBottom, '0px'], + 'max-height': [`${currentHeight}px`, '0px'], + 'overflow': ['hidden', 'hidden'], + 'padding-top': [currentPaddingTop, '0px'], + 'padding-bottom': [currentPaddingBottom, '0px'], }; animateStyles(element, animStyles, animTime, () => { @@ -109,10 +100,8 @@ export function slideUp(element, animTime = 400) { /** * Show the given element by expanding the contents. - * @param {Element} element - Element to animate - * @param {Number} animTime - Animation time in ms */ -export function slideDown(element, animTime = 400) { +export function slideDown(element: HTMLElement, animTime: number = 400) { cleanupExistingElementAnimation(element); element.style.display = 'block'; const targetHeight = element.getBoundingClientRect().height; @@ -120,10 +109,10 @@ export function slideDown(element, animTime = 400) { const targetPaddingTop = computedStyles.getPropertyValue('padding-top'); const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom'); const animStyles = { - maxHeight: ['0px', `${targetHeight}px`], - overflow: ['hidden', 'hidden'], - paddingTop: ['0px', targetPaddingTop], - paddingBottom: ['0px', targetPaddingBottom], + 'max-height': ['0px', `${targetHeight}px`], + 'overflow': ['hidden', 'hidden'], + 'padding-top': ['0px', targetPaddingTop], + 'padding-bottom': ['0px', targetPaddingBottom], }; animateStyles(element, animStyles, animTime); @@ -134,11 +123,8 @@ export function slideDown(element, animTime = 400) { * Call with first state, and you'll receive a function in return. * Call the returned function in the second state to animate between those two states. * If animating to/from 0-height use the slide-up/slide down as easier alternatives. - * @param {Element} element - Element to animate - * @param {Number} animTime - Animation time in ms - * @returns {function} - Function to run in second state to trigger animation. */ -export function transitionHeight(element, animTime = 400) { +export function transitionHeight(element: HTMLElement, animTime: number = 400): () => void { const startHeight = element.getBoundingClientRect().height; const initialComputedStyles = getComputedStyle(element); const startPaddingTop = initialComputedStyles.getPropertyValue('padding-top'); @@ -151,10 +137,10 @@ export function transitionHeight(element, animTime = 400) { const targetPaddingTop = computedStyles.getPropertyValue('padding-top'); const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom'); const animStyles = { - height: [`${startHeight}px`, `${targetHeight}px`], - overflow: ['hidden', 'hidden'], - paddingTop: [startPaddingTop, targetPaddingTop], - paddingBottom: [startPaddingBottom, targetPaddingBottom], + 'height': [`${startHeight}px`, `${targetHeight}px`], + 'overflow': ['hidden', 'hidden'], + 'padding-top': [startPaddingTop, targetPaddingTop], + 'padding-bottom': [startPaddingBottom, targetPaddingBottom], }; animateStyles(element, animStyles, animTime); diff --git a/resources/js/services/util.ts b/resources/js/services/util.ts index 7f684dd42..c5a5d2db8 100644 --- a/resources/js/services/util.ts +++ b/resources/js/services/util.ts @@ -99,3 +99,49 @@ export function wait(timeMs: number): Promise { setTimeout(res, timeMs); }); } + +/** + * Generate a full URL from the given relative URL, using a base + * URL defined in the head of the page. + */ +export function baseUrl(path: string): string { + let targetPath = path; + const baseUrlMeta = document.querySelector('meta[name="base-url"]'); + if (!baseUrlMeta) { + throw new Error('Could not find expected base-url meta tag in document'); + } + + let basePath = baseUrlMeta.getAttribute('content') || ''; + if (basePath[basePath.length - 1] === '/') { + basePath = basePath.slice(0, basePath.length - 1); + } + + if (targetPath[0] === '/') { + targetPath = targetPath.slice(1); + } + + return `${basePath}/${targetPath}`; +} + +/** + * Get the current version of BookStack in use. + * Grabs this from the version query used on app assets. + */ +function getVersion(): string { + const styleLink = document.querySelector('link[href*="/dist/styles.css?version="]'); + if (!styleLink) { + throw new Error('Could not find expected style link in document for version use'); + } + + const href = (styleLink.getAttribute('href') || ''); + return href.split('?version=').pop() || ''; +} + +/** + * Perform a module import, Ensuring the import is fetched with the current + * app version as a cache-breaker. + */ +export function importVersioned(moduleName: string): Promise { + const importPath = window.baseUrl(`dist/${moduleName}.js?version=${getVersion()}`); + return import(importPath); +} \ No newline at end of file From 209fa04752905644166e441eccff832a7a9fab52 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 11 Oct 2024 21:55:51 +0100 Subject: [PATCH 3/3] 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; }