Source: utils/shortcuts.js

/**
 * @typedef FlexibleHTMLProps
 * @property {boolean} [checked] (HTMLInputElement)
 * @property {string} [value] (HTMLInputElement)
 * @property {number} [rowHeight] (HTMLTableRowElement)
 *
 * A flexible type HTMLElement type that does not require using instanceof all over the place
 * Make sure that your selector is indeed valid
 * Only includes most commons props
 * @typedef {HTMLElement & FlexibleHTMLProps} FlexibleHTMLElement
 */

/**
 * Keep this as reference for easy documentation
 * @typedef {HTMLElement&HTMLInputElement&HTMLTableRowElement} MixedHTMLElement
 */

/**
 * @typedef FlexibleEventProps
 * @property {FlexibleHTMLElement} target
 * @property {FlexibleHTMLElement} currentTarget
 * @property {DataTransfer} [dataTransfer] (DragEvent)
 * @property {number} [clientX] (MouseEvent)
 * @property {number} [clientY] (MouseEvent)
 *
 * @typedef {Event & FlexibleEventProps} FlexibleEvent
 */

/**
 * Keep this as reference for easy documentation
 * @typedef {Event&MouseEvent&InputEvent&DragEvent&FocusEvent&KeyboardEvent&PointerEvent} MixedEvent
 */

/**
 * @callback FlexibleListener
 * @param {FlexibleEvent} event
 */

class FlexibleEventListenerObject {
    /**
     * @param {FlexibleEvent} e
     */
    handleEvent(e) {}
}

const supportedPassiveTypes = [
    "scroll",
    "wheel",
    "touchstart",
    "touchmove",
    "touchenter",
    "touchend",
    "touchleave",
    "mouseout",
    "mouseleave",
    "mouseup",
    "mousedown",
    "mousemove",
    "mouseenter",
    "mousewheel",
    "mouseover",
];

/**
 * Automatically set passive options based on type
 * @param {string} type
 * @returns {AddEventListenerOptions}
 */
function passiveOpts(type) {
    if (supportedPassiveTypes.includes(type)) {
        return { passive: true };
    }
    return {};
}

/**
 * @param {Element} el
 * @param {String} name
 * @returns {any}
 */
export function getAttribute(el, name) {
    return el.getAttribute(name);
}

/**
 * @param {Element} el
 * @param {String} name
 * @returns {Boolean}
 */
export function hasAttribute(el, name) {
    return el.hasAttribute(name);
}

/**
 * @param {Element} el
 * @param {String} name
 * @param {any} v
 * @param {Boolean} check Prevent setting if attribute is already there
 */
export function setAttribute(el, name, v = "", check = false) {
    if (check && hasAttribute(el, name)) return;
    el.setAttribute(name, `${v}`);
}

/**
 * @param {Element} el
 * @param {String} name
 */
export function removeAttribute(el, name) {
    if (hasAttribute(el, name)) {
        el.removeAttribute(name);
    }
}

/**
 * @param {EventTarget} el
 * @param {String} type
 * @param {EventListenerObject|FlexibleListener} listener
 */
export function on(el, type, listener) {
    el.addEventListener(type, listener, passiveOpts(type));
}

/**
 * @param {EventTarget} el
 * @param {String} type
 * @param {EventListenerObject|FlexibleListener} listener
 */
export function off(el, type, listener) {
    el.removeEventListener(type, listener, passiveOpts(type));
}

/**
 * @param {EventTarget} el
 * @param {String} type
 * @param {EventListenerObject|FlexibleListener} listener
 */
export function one(el, type, listener) {
    el.addEventListener(type, listener, {
        once: true,
    });
}

/**
 * @param {HTMLElement} el
 * @param {String} name
 * @param {any} data
 * @param {Boolean} bubbles
 */
export function dispatch(el, name, data = {}, bubbles = false) {
    const opts = {};
    if (bubbles) {
        opts.bubbles = true;
    }
    if (data) {
        opts.detail = data;
    }
    el.dispatchEvent(new CustomEvent(name, opts));
}

/**
 * @param {Element} el
 * @param {String} name
 * @returns {Boolean}
 */
export function hasClass(el, name) {
    return el.classList.contains(name);
}

/**
 * @param {Element} el
 * @param {String} name
 */
export function addClass(el, name) {
    el.classList.add(...name.split(" "));
}

/**
 * @param {Element} el
 * @param {String} name
 */
export function removeClass(el, name) {
    el.classList.remove(...name.split(" "));
}

/**
 * @param {Element} el
 * @param {String} name
 */
export function toggleClass(el, name) {
    el.classList.toggle(name);
}

/**
 * @param {String|HTMLElement} selector
 * @param {HTMLElement|Document} base
 * @returns {FlexibleHTMLElement|null}
 */
export function $(selector, base = document) {
    if (selector instanceof HTMLElement) {
        return selector;
    }
    return base.querySelector(selector);
}

/**
 * @param {String} selector
 * @param {Element|Document} base
 * @returns {Array<FlexibleHTMLElement>}
 */
export function $$(selector, base = document) {
    return Array.from(base.querySelectorAll(selector));
}

/**
 * Easily retrieve untyped element
 * For actual type, prefer use of el.querySelector
 * @param {HTMLElement} el
 * @param {String|HTMLElement} selector
 * @returns {FlexibleHTMLElement}
 */
export function find(el, selector) {
    return $(selector, el);
}

/**
 * Easily retrieve untyped elements
 * For actual type, prefer use of el.querySelectorAll
 * @param {Element} el
 * @param {String} selector
 * @returns {Array<FlexibleHTMLElement>}
 */
export function findAll(el, selector) {
    return $$(selector, el);
}

/**
 * @param {*} el
 * @returns {FlexibleHTMLElement}
 */
export function el(el) {
    return el;
}

/**
 * @template {keyof HTMLElementTagNameMap} K
 * @param {K} tagName
 * @param {HTMLElement} parent
 * @returns {HTMLElementTagNameMap[K]}
 */
export function ce(tagName, parent = null) {
    const el = document.createElement(tagName);
    if (parent) {
        parent.appendChild(el);
    }
    return el;
}

/**
 * @param {HTMLElement} newNode
 * @param {HTMLElement} existingNode
 */
export function insertAfter(newNode, existingNode) {
    existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling);
}