/* eslint-disable @typescript-eslint/no-explicit-any */ // Fake jQuery because jQuery is for faggots export class SelectorError extends Error { constructor(...args: any[]) { super(...args); this.name = 'SelectorError'; } } export default class Q { private document: Document; constructor(document: Document) { this.document = document; } public $(queryString: string): T { const element = this.document.querySelector(queryString); if (element === null) { throw new SelectorError(`unable to find [${queryString}] in document`); } return element as unknown as T; // dangerous but convenient } public $$(queryString: string): HTMLElement[] { return Array.from(this.document.querySelectorAll(queryString)); } public $$$(baseElement: Element, queryString: string): T { const element = baseElement.querySelector(queryString); if (element === null) { throw new SelectorError(`unable to find [${queryString}] in document`); } return element as unknown as T; // dangerous but convenient } public $$$$(baseElement: Element, queryString: string): HTMLElement[] { return Array.from(baseElement.querySelectorAll(queryString)); } public $_(queryString: string): HTMLElement | null { try { return this.$(queryString); } catch (e) { return null; } } public $$$_(baseElement: Element, queryString: string): HTMLElement | null { try { return this.$$$(baseElement, queryString); } catch (e) { return null; } } /** * @function $.zip * @description Zip arrays together * $.zip([2, 3, 4], [5, 6]) -> [ [2, 5], [3, 6], [4, undefined] ] * $.zip([3, 4], [5, 6, 7]) -> [ [3, 5], [4, 6], [undefined, 7] ] * @param {...any} arrays Lists to zip together * @return The arrays zipped together */ public static zip(...arrays: any[]): any[] { const n = 0; const result: any[] = []; for (const array of arrays) { if (!Array.isArray(array)) throw new Error('all parameters must be arrays'); for (let i = 0; i < array.length; ++i) { if (i == result.length) { result.push([]); for (let j = 0; j < n; ++j) { result[i].push(undefined); } } result[i].push(array[i]); } } return result; } /** * @description gets the previous Element in the DOM. Useful to skip any text nodes * @param element Element to get the previous element of * @returns the previous Element in the DOM or null if there are no Element before the specified element. */ public static previousElement(element: Element): Element | null { let current: ChildNode | Element = element; while (current.previousSibling) { if (current.previousSibling instanceof Element) { return current.previousSibling; } current = current.previousSibling; } return null; } /** * @description gets the next Element in the DOM. Useful to skip any text nodes * @param element Element to get the next element of * @returns the next Element in the DOM or null if there are no Elements after the specified element. */ public static nextElement(element: Element): Element | null { let current: ChildNode | Element = element; while (current.nextSibling) { if (current.nextSibling instanceof Element) { return current.nextSibling; } current = current.nextSibling; } return null; } /** * @function $.clearChildren * @description removes all children from an element * @param {Element} element The element to remove all children from */ public static clearChildren = function(element: Element): void { while (element.firstChild) { element.removeChild(element.firstChild); } } /** * @description Adds to the internal content of an Element * @param {Element} element The element to add content to * @param content The content to be added to the element */ public addContent(element: Element, content: any) { content = Array.isArray(content) ? content : [ content ]; for (const e of content) { element.appendChild(this.create(e)); } } /** * Recursively creates an Element from the specified content * @param obj An element initialization object { tag, id, class, content, ... } * @var tag The HTML tag of the element (default: 'div') * @var id The id of the elemnt * @var class The class of the element (string or list of strings) * @var content The internal content of the element. This is handled recursively. * Can be a string, Element, obj, list of objs, or list of Elements * @var ... All other attributes will be applied using element.setAttribute(key, value) */ public create(obj: Element | any | null): Node { if (obj instanceof Element) { return obj } else if (typeof obj !== 'object') { return this.document.createTextNode(obj.toString()); } else if (obj === null) { return this.document.createTextNode('[null]'); } else if (obj === undefined) { throw new Error('obj is undefined'); } let element: Element; if (obj.ns) { element = document.createElementNS(obj.ns, obj.tag ?? 'div'); } else { element = document.createElement(obj.tag ?? 'div'); } if (obj.id) { element.id = obj.id; } if (obj.class) { if (Array.isArray(obj.class)) { for (const c of obj.class) { if (typeof c !== 'string') throw new Error(`Invalid obj.class type: ${typeof c}`); element.classList.add(c); } } else if (typeof obj.class === 'string') { element.className = obj.class; } else { throw new Error(`Invalid obj.class type: ${obj.class}`); } } for (const key in obj) { if (key !== 'tag' && key !== 'id' && key != 'class' && key != 'content') { if (Object.prototype.hasOwnProperty.call(obj, key)) { element.setAttribute(key.toString(), obj[key].toString()); } } } if (obj.content !== undefined) { this.addContent(element, obj.content); } return element; } }