summaryrefslogtreecommitdiffstats
path: root/assets/lib/shared.js
diff options
context:
space:
mode:
Diffstat (limited to 'assets/lib/shared.js')
-rw-r--r--assets/lib/shared.js130
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
+ },
+ })
+}