diff options
Diffstat (limited to 'web_src/js/utils/dom.ts')
-rw-r--r-- | web_src/js/utils/dom.ts | 75 |
1 files changed, 44 insertions, 31 deletions
diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 82e7b755a5..5fc2183194 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -1,14 +1,21 @@ import {debounce} from 'throttle-debounce'; +import type {Promisable} from 'type-fest'; +import type $ from 'jquery'; -function elementsCall(el, func, ...args) { +type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>; +type ElementsCallback = (el: Element) => Promisable<any>; +type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>; +type IterableElements = NodeListOf<Element> | Array<Element>; + +function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) { if (typeof el === 'string' || el instanceof String) { - el = document.querySelectorAll(el); + el = document.querySelectorAll(el as string); } if (el instanceof Node) { func(el, ...args); } else if (el.length !== undefined) { // this works for: NodeList, HTMLCollection, Array, jQuery - for (const e of el) { + for (const e of (el as IterableElements)) { func(e, ...args); } } else { @@ -17,10 +24,10 @@ function elementsCall(el, func, ...args) { } /** - * @param el string (selector), Node, NodeList, HTMLCollection, Array or jQuery + * @param el Element * @param force force=true to show or force=false to hide, undefined to toggle */ -function toggleShown(el, force) { +function toggleShown(el: Element, force: boolean) { if (force === true) { el.classList.remove('tw-hidden'); } else if (force === false) { @@ -32,26 +39,26 @@ function toggleShown(el, force) { } } -export function showElem(el) { +export function showElem(el: ElementArg) { elementsCall(el, toggleShown, true); } -export function hideElem(el) { +export function hideElem(el: ElementArg) { elementsCall(el, toggleShown, false); } -export function toggleElem(el, force) { +export function toggleElem(el: ElementArg, force?: boolean) { elementsCall(el, toggleShown, force); } -export function isElemHidden(el) { - const res = []; +export function isElemHidden(el: ElementArg) { + const res: boolean[] = []; elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden'))); if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`); return res[0]; } -function applyElemsCallback(elems, fn) { +function applyElemsCallback(elems: IterableElements, fn?: ElementsCallback) { if (fn) { for (const el of elems) { fn(el); @@ -60,20 +67,22 @@ function applyElemsCallback(elems, fn) { return elems; } -export function queryElemSiblings(el, selector = '*', fn) { - return applyElemsCallback(Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)), fn); +export function queryElemSiblings(el: Element, selector = '*', fn?: ElementsCallback) { + return applyElemsCallback(Array.from(el.parentNode.children).filter((child: Element) => { + return child !== el && child.matches(selector); + }), fn); } // it works like jQuery.children: only the direct children are selected -export function queryElemChildren(parent, selector = '*', fn) { +export function queryElemChildren(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback) { return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn); } -export function queryElems(selector, fn) { +export function queryElems(selector: string, fn?: ElementsCallback) { return applyElemsCallback(document.querySelectorAll(selector), fn); } -export function onDomReady(cb) { +export function onDomReady(cb: () => Promisable<void>) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', cb); } else { @@ -83,7 +92,7 @@ export function onDomReady(cb) { // checks whether an element is owned by the current document, and whether it is a document fragment or element node // if it is, it means it is a "normal" element managed by us, which can be modified safely. -export function isDocumentFragmentOrElementNode(el) { +export function isDocumentFragmentOrElementNode(el: Element) { try { return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE; } catch { @@ -108,12 +117,15 @@ export function isDocumentFragmentOrElementNode(el) { // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // --------------------------------------------------------------------- -export function autosize(textarea, {viewportMarginBottom = 0} = {}) { +export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom = 0}: {viewportMarginBottom?: number} = {}) { let isUserResized = false; // lastStyleHeight and initialStyleHeight are CSS values like '100px' - let lastMouseX, lastMouseY, lastStyleHeight, initialStyleHeight; + let lastMouseX: number; + let lastMouseY: number; + let lastStyleHeight: string; + let initialStyleHeight: string; - function onUserResize(event) { + function onUserResize(event: MouseEvent) { if (isUserResized) return; if (lastMouseX !== event.clientX || lastMouseY !== event.clientY) { const newStyleHeight = textarea.style.height; @@ -133,7 +145,7 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) { while (el !== document.body && el !== null) { offsetTop += el.offsetTop || 0; - el = el.offsetParent; + el = el.offsetParent as HTMLTextAreaElement; } const top = offsetTop - document.defaultView.scrollY; @@ -213,14 +225,15 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) { }; } -export function onInputDebounce(fn) { +export function onInputDebounce(fn: () => Promisable<any>) { return debounce(300, fn); } +type LoadableElement = HTMLEmbedElement | HTMLIFrameElement | HTMLImageElement | HTMLScriptElement | HTMLTrackElement; + // Set the `src` attribute on an element and returns a promise that resolves once the element -// has loaded or errored. Suitable for all elements mention in: -// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/load_event -export function loadElem(el, src) { +// has loaded or errored. +export function loadElem(el: LoadableElement, src: string) { return new Promise((resolve) => { el.addEventListener('load', () => resolve(true), {once: true}); el.addEventListener('error', () => resolve(false), {once: true}); @@ -256,14 +269,14 @@ export function initSubmitEventPolyfill() { * @param {HTMLElement} element The element to check. * @returns {boolean} True if the element is visible. */ -export function isElemVisible(element) { +export function isElemVisible(element: HTMLElement) { if (!element) return false; return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length); } // replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this -export function replaceTextareaSelection(textarea, text) { +export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: string) { const before = textarea.value.slice(0, textarea.selectionStart ?? undefined); const after = textarea.value.slice(textarea.selectionEnd ?? undefined); let success = true; @@ -287,13 +300,13 @@ export function replaceTextareaSelection(textarea, text) { } // Warning: Do not enter any unsanitized variables here -export function createElementFromHTML(htmlString) { +export function createElementFromHTML(htmlString: string) { const div = document.createElement('div'); div.innerHTML = htmlString.trim(); - return div.firstChild; + return div.firstChild as Element; } -export function createElementFromAttrs(tagName, attrs) { +export function createElementFromAttrs(tagName: string, attrs: Record<string, any>) { const el = document.createElement(tagName); for (const [key, value] of Object.entries(attrs)) { if (value === undefined || value === null) continue; @@ -307,7 +320,7 @@ export function createElementFromAttrs(tagName, attrs) { return el; } -export function animateOnce(el, animationClassName) { +export function animateOnce(el: Element, animationClassName: string): Promise<void> { return new Promise((resolve) => { el.addEventListener('animationend', function onAnimationEnd() { el.classList.remove(animationClassName); |