Add types to various low-level functions (#31781)
Adds types to various low-level modules. All changes are type-only, no runtime changes. `tsc` now reports 38 less errors. One problem was that `@types/sortablejs` does not accept promise return in its functions which triggered the linter, so I disabled the rules on those line.
This commit is contained in:
parent
9633f336c8
commit
32075d2880
13 changed files with 123 additions and 77 deletions
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue