lume-js 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -20
- package/dist/addons.min.mjs +1 -1
- package/dist/addons.mjs +23 -4
- package/dist/addons.mjs.map +1 -1
- package/dist/handlers.min.mjs +1 -1
- package/dist/handlers.mjs +27 -10
- package/dist/handlers.mjs.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/lume.global.js +1 -1
- package/dist/lume.global.js.map +1 -1
- package/dist/{shared-Dcokqj5a.mjs → shared-x2HJmEyO.mjs} +12 -1
- package/dist/shared-x2HJmEyO.mjs.map +1 -0
- package/package.json +12 -6
- package/src/addons/computed.js +5 -0
- package/src/addons/hydrateState.js +32 -4
- package/src/addons/index.d.ts +26 -7
- package/src/addons/repeat.js +2 -0
- package/src/addons/watch.js +10 -1
- package/src/addons/withPlugins.js +7 -1
- package/src/core/bindDom.js +5 -0
- package/src/core/effect.js +1 -0
- package/src/core/state.js +21 -1
- package/src/handlers/ariaAttr.js +14 -0
- package/src/handlers/boolAttr.js +14 -0
- package/src/handlers/className.js +5 -0
- package/src/handlers/classToggle.js +14 -0
- package/src/handlers/htmlAttrs.js +57 -0
- package/src/handlers/index.d.ts +13 -0
- package/src/handlers/index.js +8 -196
- package/src/handlers/presets.js +14 -0
- package/src/handlers/show.js +5 -0
- package/src/handlers/stringAttr.js +28 -0
- package/dist/shared-Dcokqj5a.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lume-js",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -28,15 +28,19 @@
|
|
|
28
28
|
"dev": "vite",
|
|
29
29
|
"dev:site": "node scripts/build-site-assets.js && vite -c vite.site.config.js",
|
|
30
30
|
"build:site": "node scripts/build-site-assets.js && vite build -c vite.site.config.js",
|
|
31
|
-
"preview:site": "node scripts/build-site-assets.js && vite build -c vite.site.config.js --base / && vite preview -c vite.site.config.js",
|
|
31
|
+
"preview:site": "node scripts/build-site-assets.js && vite build -c vite.site.config.js --base / && vite preview -c vite.site.config.js --base /",
|
|
32
32
|
"build": "node scripts/build.js",
|
|
33
|
-
"size": "node scripts/check-size.js",
|
|
33
|
+
"size": "npm run build && node scripts/check-size.js",
|
|
34
|
+
"size:ci": "node scripts/check-size.js",
|
|
35
|
+
"bench": "npm run build && node scripts/bench-core.js",
|
|
36
|
+
"complexity": "node scripts/complexity.js",
|
|
37
|
+
"lint": "eslint src/",
|
|
34
38
|
"test": "vitest run",
|
|
35
39
|
"test:watch": "vitest",
|
|
36
40
|
"coverage": "vitest run --coverage",
|
|
37
41
|
"typecheck": "tsc --noEmit",
|
|
38
|
-
"validate": "npm run size && npm run typecheck && npm
|
|
39
|
-
"prepublishOnly": "npm run
|
|
42
|
+
"validate": "npm run build && node scripts/check-size.js && node scripts/complexity.js && npm run lint && npm run typecheck && npm run coverage",
|
|
43
|
+
"prepublishOnly": "npm run validate"
|
|
40
44
|
},
|
|
41
45
|
"files": [
|
|
42
46
|
"dist",
|
|
@@ -78,6 +82,8 @@
|
|
|
78
82
|
"@tailwindcss/typography": "^0.5.19",
|
|
79
83
|
"@vitest/coverage-v8": "^2.1.4",
|
|
80
84
|
"autoprefixer": "^10.4.22",
|
|
85
|
+
"eslint": "^10.3.0",
|
|
86
|
+
"eslint-plugin-sonarjs": "^4.0.3",
|
|
81
87
|
"highlight.js": "^11.11.1",
|
|
82
88
|
"jsdom": "^25.0.1",
|
|
83
89
|
"marked": "^17.0.1",
|
|
@@ -91,4 +97,4 @@
|
|
|
91
97
|
"engines": {
|
|
92
98
|
"node": ">=20.19.0"
|
|
93
99
|
}
|
|
94
|
-
}
|
|
100
|
+
}
|
package/src/addons/computed.js
CHANGED
|
@@ -33,6 +33,11 @@ import { logError } from '../utils/log.js';
|
|
|
33
33
|
* mutates a state property it depends on, the flush triggered by that
|
|
34
34
|
* mutation is skipped to prevent an infinite microtask loop.
|
|
35
35
|
*
|
|
36
|
+
* @security After each computation, a re-entry guard stays active until the
|
|
37
|
+
* next microtask. If a dependency changes synchronously during this window,
|
|
38
|
+
* the computed will not recompute until a subsequent microtask. This is
|
|
39
|
+
* intentional — it prevents infinite loops from self-mutating computeds.
|
|
40
|
+
*
|
|
36
41
|
* @param {function} fn - Function that computes the value
|
|
37
42
|
* @returns {object} Object with .value property and methods
|
|
38
43
|
*
|
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
* Reads initial state from a `<script type="application/json">` element
|
|
3
3
|
* embedded in the server-rendered HTML. Useful for SSR / hydration patterns.
|
|
4
4
|
*
|
|
5
|
+
* @security Hydration trusts the DOM. An attacker who can inject HTML before
|
|
6
|
+
* the legitimate script (DOM clobbering) can control the parsed data. The
|
|
7
|
+
* element must be a real `<script type="application/json">` tag; non-script
|
|
8
|
+
* elements are rejected. Use the optional `validate` parameter to enforce a
|
|
9
|
+
* schema (e.g., whitelist allowed keys) before passing to `state()`.
|
|
10
|
+
*
|
|
5
11
|
* @param {string} [selector='#__LUME_DATA__'] - CSS selector for the script element
|
|
6
|
-
* @
|
|
12
|
+
* @param {function} [validate] - Optional validator: (data) => boolean. If it
|
|
13
|
+
* returns false, hydrateState returns {} instead of the parsed data.
|
|
14
|
+
* @returns {object} Parsed JSON object, or empty object if not found / invalid / rejected
|
|
7
15
|
*
|
|
8
16
|
* @example
|
|
9
17
|
* ```html
|
|
@@ -16,15 +24,35 @@
|
|
|
16
24
|
* import { state } from 'lume-js';
|
|
17
25
|
* import { hydrateState } from 'lume-js/addons';
|
|
18
26
|
*
|
|
19
|
-
*
|
|
27
|
+
* // With optional schema validation
|
|
28
|
+
* const data = hydrateState('#__LUME_DATA__', d =>
|
|
29
|
+
* typeof d.title === 'string' && typeof d.count === 'number'
|
|
30
|
+
* );
|
|
31
|
+
* const store = state(data);
|
|
20
32
|
* ```
|
|
21
33
|
*/
|
|
22
|
-
export function hydrateState(selector = '#__LUME_DATA__') {
|
|
34
|
+
export function hydrateState(selector = '#__LUME_DATA__', validate) {
|
|
23
35
|
const el = typeof document !== 'undefined' ? document.querySelector(selector) : null;
|
|
24
36
|
if (!el) return {};
|
|
37
|
+
|
|
38
|
+
// Reject non-script elements or scripts without the correct type.
|
|
39
|
+
// This mitigates DOM clobbering where an attacker injects a matching
|
|
40
|
+
// element with a different tag name (e.g., a div or a script with
|
|
41
|
+
// a different type that would still match querySelector by id).
|
|
42
|
+
if (el.tagName !== 'SCRIPT' || el.type !== 'application/json') {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let data;
|
|
25
47
|
try {
|
|
26
|
-
|
|
48
|
+
data = JSON.parse(el.textContent);
|
|
27
49
|
} catch {
|
|
28
50
|
return {};
|
|
29
51
|
}
|
|
52
|
+
|
|
53
|
+
if (typeof validate === 'function' && !validate(data)) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return data;
|
|
30
58
|
}
|
package/src/addons/index.d.ts
CHANGED
|
@@ -56,26 +56,44 @@ export interface Computed<T> {
|
|
|
56
56
|
*/
|
|
57
57
|
export function computed<T>(fn: () => T): Computed<T>;
|
|
58
58
|
|
|
59
|
+
export interface WatchOptions {
|
|
60
|
+
/**
|
|
61
|
+
* Whether to call the callback immediately with the current value (default: true).
|
|
62
|
+
* Set to false to skip the initial call and only react to future changes.
|
|
63
|
+
*/
|
|
64
|
+
immediate?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
/**
|
|
60
68
|
* Watch a single key on a reactive state object; convenience wrapper around $subscribe.
|
|
61
|
-
*
|
|
69
|
+
*
|
|
70
|
+
* By default the callback fires immediately with the current value, then on every change.
|
|
71
|
+
* Pass `{ immediate: false }` to skip the initial call and only react to future changes.
|
|
72
|
+
*
|
|
62
73
|
* @param store - Reactive state object created with state().
|
|
63
74
|
* @param key - Property key to observe.
|
|
64
|
-
* @param callback -
|
|
75
|
+
* @param callback - Called with new value on change (and immediately unless immediate=false).
|
|
76
|
+
* @param options - Watch options
|
|
65
77
|
* @returns Unsubscribe function for cleanup
|
|
66
78
|
* @throws {Error} If store is not a reactive state object
|
|
67
|
-
*
|
|
79
|
+
*
|
|
68
80
|
* @example
|
|
69
81
|
* ```typescript
|
|
70
82
|
* import { state } from 'lume-js';
|
|
71
83
|
* import { watch } from 'lume-js/addons';
|
|
72
|
-
*
|
|
84
|
+
*
|
|
73
85
|
* const store = state({ count: 0 });
|
|
74
|
-
*
|
|
86
|
+
*
|
|
87
|
+
* // Fires immediately with 0, then on every change
|
|
75
88
|
* const unwatch = watch(store, 'count', (value) => {
|
|
76
89
|
* console.log('Count is now:', value);
|
|
77
90
|
* });
|
|
78
|
-
*
|
|
91
|
+
*
|
|
92
|
+
* // Only fires on future changes (not the initial value)
|
|
93
|
+
* watch(store, 'count', (value) => {
|
|
94
|
+
* console.log('Count changed to:', value);
|
|
95
|
+
* }, { immediate: false });
|
|
96
|
+
*
|
|
79
97
|
* // Cleanup
|
|
80
98
|
* unwatch();
|
|
81
99
|
* ```
|
|
@@ -83,7 +101,8 @@ export function computed<T>(fn: () => T): Computed<T>;
|
|
|
83
101
|
export function watch<T extends object, K extends keyof T>(
|
|
84
102
|
store: ReactiveState<T>,
|
|
85
103
|
key: K,
|
|
86
|
-
callback: Subscriber<T[K]
|
|
104
|
+
callback: Subscriber<T[K]>,
|
|
105
|
+
options?: WatchOptions
|
|
87
106
|
): Unsubscribe;
|
|
88
107
|
|
|
89
108
|
/**
|
package/src/addons/repeat.js
CHANGED
|
@@ -259,6 +259,7 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
259
259
|
if (restoreScroll) restoreScroll();
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- keyed DOM reconciliation: create/reuse/remove nodes, key dedup, scroll/focus preservation
|
|
262
263
|
function updateList() {
|
|
263
264
|
const items = store[arrayKey];
|
|
264
265
|
|
|
@@ -332,6 +333,7 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
332
333
|
nextEls.push(el);
|
|
333
334
|
}
|
|
334
335
|
|
|
336
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- DOM cleanup pass: remove stale nodes, call per-item cleanup callbacks, update maps
|
|
335
337
|
applyPreservation(containerEl, () => {
|
|
336
338
|
reconcileDOM(containerEl, nextEls);
|
|
337
339
|
|
package/src/addons/watch.js
CHANGED
|
@@ -3,11 +3,20 @@
|
|
|
3
3
|
* @param {Object} store - reactive store created with state()
|
|
4
4
|
* @param {string} key - key in store to watch
|
|
5
5
|
* @param {Function} callback - called with new value
|
|
6
|
+
* @param {Object} [options]
|
|
7
|
+
* @param {boolean} [options.immediate=true] - call callback immediately with current value
|
|
6
8
|
* @returns {Function} unsubscribe function
|
|
7
9
|
*/
|
|
8
|
-
export function watch(store, key, callback) {
|
|
10
|
+
export function watch(store, key, callback, { immediate = true } = {}) {
|
|
9
11
|
if (!store.$subscribe) {
|
|
10
12
|
throw new Error("store must be created with state()");
|
|
11
13
|
}
|
|
14
|
+
if (!immediate) {
|
|
15
|
+
let skipped = false;
|
|
16
|
+
return store.$subscribe(key, (val) => {
|
|
17
|
+
if (!skipped) { skipped = true; return; }
|
|
18
|
+
callback(val);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
12
21
|
return store.$subscribe(key, callback);
|
|
13
22
|
}
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
* onNotify(key, value) — called before subscribers are notified
|
|
25
25
|
* onSubscribe(key) — called when $subscribe is invoked
|
|
26
26
|
*
|
|
27
|
+
* @security Plugins run with full application privilege. A plugin can read
|
|
28
|
+
* all state, alter any write, or suppress mutations. Only pass trusted objects.
|
|
29
|
+
* Plugin objects are frozen after registration to prevent post-init mutation.
|
|
30
|
+
*
|
|
27
31
|
* @param {object} store - A reactive proxy from state()
|
|
28
32
|
* @param {Array<object>} plugins - Array of plugin objects
|
|
29
33
|
* @returns {Proxy} A new proxy wrapping the store with plugin behavior
|
|
@@ -33,13 +37,15 @@ import { logError } from '../utils/log.js';
|
|
|
33
37
|
export function withPlugins(store, plugins = []) {
|
|
34
38
|
if (!plugins.length) return store;
|
|
35
39
|
|
|
36
|
-
// Call onInit hooks once at wrap time
|
|
40
|
+
// Call onInit hooks once at wrap time, then freeze each plugin to prevent
|
|
41
|
+
// post-registration mutation of its hooks (defense-in-depth).
|
|
37
42
|
for (const p of plugins) {
|
|
38
43
|
try {
|
|
39
44
|
p.onInit?.();
|
|
40
45
|
} catch (e) {
|
|
41
46
|
logError(`[Lume.js] Plugin "${p.name}" error in onInit:`, e);
|
|
42
47
|
}
|
|
48
|
+
Object.freeze(p);
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
// Track pending notifications for onNotify hooks.
|
package/src/core/bindDom.js
CHANGED
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
* Usage:
|
|
25
25
|
* import { bindDom } from "lume-js";
|
|
26
26
|
* const cleanup = bindDom(document.body, store);
|
|
27
|
+
*
|
|
28
|
+
* @security `data-bind` attribute values are resolved once at bind time and
|
|
29
|
+
* trusted as state path expressions. If an attacker can inject `data-bind`
|
|
30
|
+
* attributes into the DOM, they can subscribe to any reachable reactive state.
|
|
31
|
+
* Ensure your HTML is trusted or sanitize it before calling bindDom().
|
|
27
32
|
*/
|
|
28
33
|
|
|
29
34
|
import { logWarn } from '../utils/log.js';
|
package/src/core/effect.js
CHANGED
|
@@ -58,6 +58,7 @@ let currentEffect = null;
|
|
|
58
58
|
* analytics.log(store.count); // Won't track store.count automatically
|
|
59
59
|
* }, [[store, 'count']]); // Explicit: only re-run on store.count
|
|
60
60
|
*/
|
|
61
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- handles both auto-tracking and explicit-deps modes with cleanup; splitting would require exporting internal state
|
|
61
62
|
export function effect(fn, deps) {
|
|
62
63
|
if (typeof fn !== 'function') {
|
|
63
64
|
throw new Error('effect() requires a function');
|
package/src/core/state.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* unsub(); // cleanup
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { logError } from '../utils/log.js';
|
|
25
|
+
import { logError, logWarn } from '../utils/log.js';
|
|
26
26
|
|
|
27
27
|
// Per-state batching – each state object maintains its own microtask flush.
|
|
28
28
|
// This keeps effects simple and aligned with Lume's minimal philosophy.
|
|
@@ -55,6 +55,12 @@ const readers = new Set();
|
|
|
55
55
|
*
|
|
56
56
|
* Internal API — used by effect.js for auto-tracking. May be stabilized
|
|
57
57
|
* for third-party addons in a future release.
|
|
58
|
+
*
|
|
59
|
+
* @security The observer sees reads from ALL state instances within the same
|
|
60
|
+
* module instance, including nested scopes. Only pass trusted observer functions.
|
|
61
|
+
* A future scoped variant (e.g., scopedReadObserver(store, fn)) may limit
|
|
62
|
+
* observation to a single state instance.
|
|
63
|
+
*
|
|
58
64
|
* @param {function} onRead - Called on each property access inside fn
|
|
59
65
|
* @param {function} fn - The function to run under observation
|
|
60
66
|
*/
|
|
@@ -100,6 +106,7 @@ export function state(obj) {
|
|
|
100
106
|
if (flushScheduled) return;
|
|
101
107
|
|
|
102
108
|
flushScheduled = true;
|
|
109
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- single-pass flush loop: hooks → subscribers → effects → cycle detection; must stay atomic
|
|
103
110
|
queueMicrotask(() => {
|
|
104
111
|
let iterations = 0;
|
|
105
112
|
const MAX_ITERATIONS = 100;
|
|
@@ -190,6 +197,8 @@ export function state(obj) {
|
|
|
190
197
|
};
|
|
191
198
|
};
|
|
192
199
|
|
|
200
|
+
const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
201
|
+
|
|
193
202
|
const proxy = new Proxy(obj, {
|
|
194
203
|
get(target, key) {
|
|
195
204
|
// Skip effect tracking for internal meta methods (e.g. $subscribe)
|
|
@@ -210,6 +219,11 @@ export function state(obj) {
|
|
|
210
219
|
},
|
|
211
220
|
|
|
212
221
|
set(target, key, value) {
|
|
222
|
+
if (typeof key === 'string' && BLOCKED_KEYS.has(key)) {
|
|
223
|
+
logWarn(`[Lume.js state] Blocked write to reserved key "${key}"`);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
213
227
|
const oldValue = target[key];
|
|
214
228
|
|
|
215
229
|
// Skip update if value unchanged - Object.is() handles NaN and -0 correctly
|
|
@@ -255,12 +269,18 @@ export function state(obj) {
|
|
|
255
269
|
};
|
|
256
270
|
};
|
|
257
271
|
|
|
272
|
+
const MAX_SUBSCRIBERS = 1000;
|
|
273
|
+
|
|
258
274
|
obj.$subscribe = (key, fn) => {
|
|
259
275
|
if (typeof fn !== 'function') {
|
|
260
276
|
throw new Error('Subscriber must be a function');
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
if (!listeners[key]) listeners[key] = [];
|
|
280
|
+
if (listeners[key].length >= MAX_SUBSCRIBERS) {
|
|
281
|
+
logWarn(`[Lume.js state] Subscriber limit (${MAX_SUBSCRIBERS}) reached for key "${key}". New subscriber ignored.`);
|
|
282
|
+
return () => {};
|
|
283
|
+
}
|
|
264
284
|
listeners[key].push(fn);
|
|
265
285
|
|
|
266
286
|
// Call immediately with current value (NOT batched)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a handler for an ARIA attribute.
|
|
3
|
+
* Coerces value to "true"/"false" string — use stringAttr("aria-X") for token/string ARIA attrs.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} name - ARIA name, with or without "aria-" prefix
|
|
6
|
+
* @returns {{ attr: string, apply: function }}
|
|
7
|
+
*/
|
|
8
|
+
export function ariaAttr(name) {
|
|
9
|
+
const fullName = name.startsWith('aria-') ? name : `aria-${name}`;
|
|
10
|
+
return {
|
|
11
|
+
attr: `data-${fullName}`,
|
|
12
|
+
apply(el, val) { el.setAttribute(fullName, val ? 'true' : 'false'); }
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a handler for any HTML boolean attribute.
|
|
3
|
+
* Uses toggleAttribute() — works correctly with any attribute name
|
|
4
|
+
* (readonly, contenteditable, etc.) without worrying about camelCase property names.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} name - Attribute name (e.g., 'readonly', 'open', 'contenteditable')
|
|
7
|
+
* @returns {{ attr: string, apply: function }}
|
|
8
|
+
*/
|
|
9
|
+
export function boolAttr(name) {
|
|
10
|
+
return {
|
|
11
|
+
attr: `data-${name}`,
|
|
12
|
+
apply(el, val) { el.toggleAttribute(name, Boolean(val)); }
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create handlers for CSS class toggling.
|
|
3
|
+
* Each name creates a handler: data-class-{name}="key" → el.classList.toggle(name, Boolean(val))
|
|
4
|
+
* Returns an array — pass directly to handlers (auto-flattened by bindDom).
|
|
5
|
+
*
|
|
6
|
+
* @param {...string} names - CSS class names to create handlers for
|
|
7
|
+
* @returns {Array<{ attr: string, apply: function }>}
|
|
8
|
+
*/
|
|
9
|
+
export function classToggle(...names) {
|
|
10
|
+
return names.map(name => ({
|
|
11
|
+
attr: `data-class-${name}`,
|
|
12
|
+
apply(el, val) { el.classList.toggle(name, Boolean(val)); }
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { show } from './show.js';
|
|
2
|
+
import { boolAttr } from './boolAttr.js';
|
|
3
|
+
import { ariaAttr } from './ariaAttr.js';
|
|
4
|
+
import { stringAttr } from './stringAttr.js';
|
|
5
|
+
|
|
6
|
+
/** @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#boolean_attributes */
|
|
7
|
+
const BOOL_ATTRS = [
|
|
8
|
+
'readonly', 'open', 'novalidate', 'formnovalidate', 'multiple',
|
|
9
|
+
'autofocus', 'autoplay', 'controls', 'loop', 'muted', 'defer',
|
|
10
|
+
'async', 'reversed', 'selected', 'inert', 'allowfullscreen',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/** @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes */
|
|
14
|
+
const STRING_ATTRS = [
|
|
15
|
+
'href', 'src', 'alt', 'title', 'placeholder', 'action', 'method',
|
|
16
|
+
'target', 'rel', 'type', 'name', 'role', 'lang', 'tabindex',
|
|
17
|
+
'pattern', 'min', 'max', 'step', 'minlength', 'maxlength',
|
|
18
|
+
'width', 'height', 'for', 'form', 'accept', 'autocomplete',
|
|
19
|
+
'loading', 'decoding', 'inputmode', 'enterkeyhint', 'draggable',
|
|
20
|
+
'contenteditable', 'spellcheck', 'translate', 'dir', 'id',
|
|
21
|
+
'poster', 'preload', 'download', 'media', 'sizes', 'srcset',
|
|
22
|
+
'colspan', 'rowspan', 'scope', 'headers', 'wrap', 'sandbox',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** ARIA boolean state attributes — coerced to "true"/"false" string. */
|
|
26
|
+
const ARIA_BOOL_ATTRS = [
|
|
27
|
+
'pressed', 'selected', 'disabled', 'checked', 'invalid', 'required',
|
|
28
|
+
'busy', 'modal', 'multiselectable', 'multiline', 'readonly', 'atomic',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** ARIA string/token/numeric attributes — value passed through as-is. */
|
|
32
|
+
const ARIA_STRING_ATTRS = [
|
|
33
|
+
'current', 'live', 'relevant', 'haspopup',
|
|
34
|
+
'sort', 'autocomplete', 'orientation',
|
|
35
|
+
'label', 'describedby', 'labelledby', 'controls', 'owns',
|
|
36
|
+
'activedescendant', 'errormessage', 'details', 'flowto',
|
|
37
|
+
'valuenow', 'valuemin', 'valuemax', 'valuetext',
|
|
38
|
+
'colcount', 'colindex', 'colspan', 'rowcount', 'rowindex', 'rowspan',
|
|
39
|
+
'level', 'setsize', 'posinset', 'placeholder', 'roledescription',
|
|
40
|
+
'keyshortcuts', 'braillelabel', 'brailleroledescription',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* One-import preset that enables all standard HTML attributes as reactive handlers.
|
|
45
|
+
* Returns a flat array — pass directly to handlers option.
|
|
46
|
+
*
|
|
47
|
+
* @returns {Array<{ attr: string, apply: function }>}
|
|
48
|
+
*/
|
|
49
|
+
export function htmlAttrs() {
|
|
50
|
+
return [
|
|
51
|
+
show,
|
|
52
|
+
...BOOL_ATTRS.map(name => boolAttr(name)),
|
|
53
|
+
...STRING_ATTRS.map(name => stringAttr(name)),
|
|
54
|
+
...ARIA_BOOL_ATTRS.map(name => ariaAttr(name)),
|
|
55
|
+
...ARIA_STRING_ATTRS.map(name => stringAttr(`aria-${name}`)),
|
|
56
|
+
];
|
|
57
|
+
}
|
package/src/handlers/index.d.ts
CHANGED
|
@@ -14,6 +14,19 @@ import type { Handler } from '../index.js';
|
|
|
14
14
|
*/
|
|
15
15
|
export const show: Handler;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* data-classname="key" → el.className = val (replaces full class string)
|
|
19
|
+
* Use classToggle() for toggling individual classes.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* bindDom(root, store, { handlers: [className] });
|
|
24
|
+
* // <div data-classname="cardClass">...</div>
|
|
25
|
+
* // store.cardClass = 'card card--error card--large';
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const className: Handler;
|
|
29
|
+
|
|
17
30
|
/**
|
|
18
31
|
* Create a handler for any HTML boolean property.
|
|
19
32
|
* Use for properties beyond the built-in hidden/disabled/checked/required.
|