diff options
Diffstat (limited to 'assets/lib/shared.js')
| -rw-r--r-- | assets/lib/shared.js | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/assets/lib/shared.js b/assets/lib/shared.js new file mode 100644 index 0000000..940b608 --- /dev/null +++ b/assets/lib/shared.js @@ -0,0 +1,130 @@ +/** + * Base class for web components. + * Calls render() on connect and on any observed attribute change. + */ +export class BaseElement extends HTMLElement { + connectedCallback() { + this.render() + } + + attributeChangedCallback() { + this.render() + } + + render() { } + + /** + * Read an attribute with type coercion based on the fallback type. + * - number fallback → parseFloat + * - boolean fallback → true if attribute present and not "false" + * - otherwise → string or fallback + */ + attr(name, fallback = null) { + const val = this.getAttribute(name) + if (val === null) return fallback + if (typeof fallback === 'number') return parseFloat(val) + if (typeof fallback === 'boolean') return val !== 'false' + return val + } + + /** + * Dispatch a CustomEvent from this element. + */ + dispatch(type, detail = {}, options = {}) { + this.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail, ...options })) + } +} + +/** + * Register a custom element and return the class. + * Lets you export default define('my-el', class extends BaseElement { ... }) + */ +export function define(tag, Cls) { + customElements.define(tag, Cls) + return Cls +} + +/** + * Tagged template for inline HTML strings. + */ +export function html(strings, ...values) { + return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '').trim() +} + +/** + * Tagged template for inline CSS strings. + */ +export function css(strings, ...values) { + return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '').trim() +} + +/** + * Create a DOM element with optional attributes and children. + * + * @param {string} tag + * @param {Record<string, string|boolean>} [attrs] + * @param {...(Node|string)} children + * @returns {HTMLElement} + * + * @example + * el('button', { type: 'button', class: 'btn' }, 'Click me') + */ +export function el(tag, attrs = {}, ...children) { + const node = document.createElement(tag) + for (const [k, v] of Object.entries(attrs)) { + if (v === false || v == null) continue + if (v === true) node.setAttribute(k, '') + else node.setAttribute(k, v) + } + node.append(...children) + return node +} + +/** + * Shorthand for querySelector. Scoped to `root` (default: document). + * @template {Element} T + * @param {string} selector + * @param {ParentNode} [root] + * @returns {T|null} + */ +export function qs(selector, root = document) { + return root.querySelector(selector) +} + +/** + * Shorthand for querySelectorAll. Returns a plain Array. + * @template {Element} T + * @param {string} selector + * @param {ParentNode} [root] + * @returns {T[]} + */ +export function qsa(selector, root = document) { + return Array.from(root.querySelectorAll(selector)) +} + +/** + * Add an event listener and return a cleanup function. + * @param {EventTarget} target + * @param {string} event + * @param {EventListener} handler + * @param {AddEventListenerOptions} [options] + * @returns {() => void} unlisten + */ +export function on(target, event, handler, options) { + target.addEventListener(event, handler, options) + return () => target.removeEventListener(event, handler, options) +} + +/** + * Wrap a plain object in a Proxy that calls onChange on any top-level property set. + * Shallow only — nested mutations do not trigger onChange. + */ +export function reactive(init, onChange) { + return new Proxy(init, { + set(target, key, value) { + target[key] = value + onChange() + return true + }, + }) +} |
