1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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
},
})
}
|