import { app, call, isElement, isFunction, isObject, isString, toCamel } from "./app"
import { emit } from "./events"
/**
* Returns a query parameter value from the current document location
* @param {string} name
* @param {string} [dflt]
* @return {string}
*/
export function $param(name, dflt)
{
return new URLSearchParams(location.search).get(name) || dflt || "";
}
const esc = (selector) => (selector.replace(/#([^\s"#']+)/g, (_, id) => `#${CSS.escape(id)}`))
/**
* An alias to **document.querySelector**, doc can be an Element, empty or non-string selectors will return null
* @param {string} selector
* @param {HTMLElement} [doc]
* @returns {null|HTMLElement}
* @example
* var el = app.$("#div")
*/
export function $(selector, doc)
{
return isString(selector) ? (isElement(doc) || document).querySelector(esc(selector)) : null
}
/**
* An alias for **document.querySelectorAll**
* @param {string} selector
* @param {HTMLElement} [doc]
* @returns {null|HTMLElement[]}
* @example
* Array.from(app.$all("input")).find((el) => !(el.readOnly || el.disabled || el.type == "hidden"));
*/
export function $all(selector, doc)
{
return isString(selector) ? (isElement(doc) || document).querySelectorAll(esc(selector)) : null
}
/**
* Send a CustomEvent using DispatchEvent to the given element, true is set to composed, cancelable and bubbles properties.
* @param {HTMLElement} element
* @param {string} name
* @param {object} [detail]
*/
export function $event(element, name, detail = {})
{
return element instanceof EventTarget && element.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true, cancelable: true }))
}
/**
* An alias for **element.addEventListener**
* @param {HTMLElement} element
* @param {string} event
* @param {function} callback
* @param {any} [...args] - additional params to addEventListener
* @example
* app.$on(window, "popstate", () => { ... })
*/
export function $on(element, event, callback, ...arg)
{
return isFunction(callback) && element.addEventListener(event, callback, ...arg)
}
/**
* An alias for **element.removeEventListener**
* @param {HTMLElement} element
* @param {string} event
* @param {function} callback
* @param {any} [...args] - additional params to removeEventListener
*/
export function $off(element, event, callback, ...arg)
{
return isFunction(callback) && element.removeEventListener(event, callback, ...arg)
}
/**
* Return or set attribute by name from the given element.
* @param {HTMLElement|string} element
* @param {string} attr
* @param {any} [value]
* - undefined - return the attribute value
* - null - remove attribute
* - any - assign new value
* @returns {undefined|any}
*/
export function $attr(element, attr, value)
{
if (isString(element)) element = $(element);
if (!isElement(element)) return;
return value === undefined ? element.getAttribute(attr) :
value === null ? element.removeAttribute(attr) :
element.setAttribute(attr, value);
}
/**
* Remove all nodes from the given element, call the cleanup callback for each node if given
* @param {HTMLElement|string} element
* @param {functions} [cleanup]
* @returns {HTMLElement}
*/
export function $empty(element, cleanup)
{
if (isString(element)) element = $(element);
if (!isElement(element)) return;
while (element.firstChild) {
const node = element.firstChild;
node.remove();
call(cleanup, node);
}
return element;
}
/**
* Create a DOM element with attributes, **-name** means **style.name**, **.name** means a property **name**,
* all other are attributes, functions are event listeners
* @param {string} name
* @param {any|object} [...args]
* @param {object} [options]
* @example
* $elem("div", "id", "123", "-display", "none", "._x-prop", "value", "click", () => {})
*
* @example <caption>Similar to above but all properties and attributes are taken from an object, in this form options can be passed, at the moment only
* options for addEventListener are supported.</caption>
*
* $elem("div", { id: "123", "-display": "none", "._x-prop": "value", click: () => {} }, { signal })
*/
export function $elem(name, ...args)
{
var element = document.createElement(name), key, val, opts;
if (isObject(args[0])) {
args = Object.entries(args[0]).flatMap(x => x);
opts = args[1];
}
for (let i = 0; i < args.length - 1; i += 2) {
key = args[i], val = args[i + 1];
if (!isString(key)) continue;
if (isFunction(val)) {
$on(element, key, val, { capture: opts?.capture, passive: opts?.passive, once: opts?.once, signal: opts?.signal });
} else
if (key.startsWith("-")) {
element.style[key.substr(1)] = val;
} else
if (key.startsWith(".")) {
element[key.substr(1)] = val;
} else
if (key.startsWith("data-")) {
element.dataset[toCamel(key.substr(5))] = val;
} else
if (key == "text") {
element.textContent = val || "";
} else
if (val !== null) {
element.setAttribute(key, val ?? "");
}
}
return element;
}
/**
* A shortcut to DOMParser, default is to return the .body.
* @param {string} html
* @param {string} [format] - defines the result format:
* - list - the result will be an array with all body child nodes, i.e. simpler to feed it to Element.append()
* - doc - return the whole parsed document
* @example
* document.append(...$parse("<div>...</div>"), 'list'))
*/
export function $parse(html, format)
{
html = new window.DOMParser().parseFromString(html || "", 'text/html');
return format === "doc" ? html : format === "list" ? Array.from(html.body.childNodes) : html.body;
}
/**
* Append nodes from the template to the given element, call optional setup callback for each node.
* @param {string|HTMLElement} element
* @param {string|HTMLElement} template can be a string with HTML or a template element.
* @param {function} [setup]
* @example
* app.$append(document, "<div>...</div>")
*/
export function $append(element, template, setup)
{
if (isString(element)) element = $(element);
if (!isElement(element)) return;
let doc;
if (isString(template)) {
doc = $parse(template, "doc");
} else
if (template?.content?.nodeType == 11) {
doc = { body: template.content.cloneNode(true) };
} else {
return element;
}
let node;
while (node = doc.head?.firstChild) {
element.appendChild(node);
}
while (node = doc.body.firstChild) {
element.appendChild(node);
if (setup && node.nodeType == 1) call(setup, node);
}
return element;
}
var _ready = []
/**
* Run callback once the document is loaded and ready, it uses setTimeout to schedule callbacks
* @param {function} callback
* @example
* app.$ready(() => {
*
* })
*/
export function $ready(callback)
{
_ready.push(callback);
if (document.readyState == "loading") return;
while (_ready.length) setTimeout(call, 0, _ready.shift());
}
$on(window, "DOMContentLoaded", () => {
while (_ready.length) setTimeout(call, 0, _ready.shift());
});
function domChanged()
{
var w = document.documentElement.clientWidth;
emit("dom:changed", {
breakPoint: w < 576 ? 'xs' : w < 768 ? 'sm' : w < 992 ? 'md' : w < 1200 ? 'lg' : w < 1400 ? 'xl' : 'xxl',
colorScheme: window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light",
});
}
$ready(() => {
domChanged();
$on(window.matchMedia('(prefers-color-scheme: dark)'), 'change', domChanged);
var _resize;
$on(window, "resize", () => {
clearTimeout(_resize);
_resize = setTimeout(domChanged, 250);
});
});