lume-js 2.0.1 → 2.2.0
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 +54 -11
- package/dist/addons.min.mjs +1 -1
- package/dist/addons.mjs +61 -2
- package/dist/addons.mjs.map +1 -1
- package/dist/handlers.min.mjs +1 -1
- package/dist/handlers.mjs +15 -8
- package/dist/handlers.mjs.map +1 -1
- package/dist/lume.global.js +1 -1
- package/dist/lume.global.js.map +1 -1
- package/dist/shared-Dcokqj5a.mjs.map +1 -1
- package/package.json +12 -5
- package/src/addons/index.d.ts +26 -7
- package/src/addons/repeat.js +63 -3
- package/src/addons/watch.js +10 -1
- package/src/core/effect.js +1 -0
- package/src/core/state.js +1 -0
- 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 +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lume-js",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
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,14 +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 --base /",
|
|
31
32
|
"build": "node scripts/build.js",
|
|
32
|
-
"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/",
|
|
33
38
|
"test": "vitest run",
|
|
34
39
|
"test:watch": "vitest",
|
|
35
40
|
"coverage": "vitest run --coverage",
|
|
36
41
|
"typecheck": "tsc --noEmit",
|
|
37
|
-
"validate": "npm run size && npm run typecheck && npm
|
|
38
|
-
"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"
|
|
39
44
|
},
|
|
40
45
|
"files": [
|
|
41
46
|
"dist",
|
|
@@ -77,6 +82,8 @@
|
|
|
77
82
|
"@tailwindcss/typography": "^0.5.19",
|
|
78
83
|
"@vitest/coverage-v8": "^2.1.4",
|
|
79
84
|
"autoprefixer": "^10.4.22",
|
|
85
|
+
"eslint": "^10.3.0",
|
|
86
|
+
"eslint-plugin-sonarjs": "^4.0.3",
|
|
80
87
|
"highlight.js": "^11.11.1",
|
|
81
88
|
"jsdom": "^25.0.1",
|
|
82
89
|
"marked": "^17.0.1",
|
|
@@ -90,4 +97,4 @@
|
|
|
90
97
|
"engines": {
|
|
91
98
|
"node": ">=20.19.0"
|
|
92
99
|
}
|
|
93
|
-
}
|
|
100
|
+
}
|
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
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* ═══════════════════════════════════════════════════════════════════════
|
|
36
36
|
* PATTERN 2: Clean separation (create + update) - recommended
|
|
37
37
|
* ═══════════════════════════════════════════════════════════════════════
|
|
38
|
-
*
|
|
38
|
+
*
|
|
39
39
|
* repeat('#list', store, 'todos', {
|
|
40
40
|
* key: todo => todo.id,
|
|
41
41
|
* create: (todo, el) => {
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
* btn.textContent = 'Delete';
|
|
48
48
|
* btn.onclick = () => deleteTodo(todo.id);
|
|
49
49
|
* el.appendChild(btn);
|
|
50
|
+
*
|
|
51
|
+
* // Return a cleanup function — called automatically when element is removed
|
|
52
|
+
* return () => {
|
|
53
|
+
* // Unsubscribe from external listeners, remove timers, etc.
|
|
54
|
+
* };
|
|
50
55
|
* },
|
|
51
56
|
* update: (todo, el, index, { isFirstRender }) => {
|
|
52
57
|
* // Called on every update - bind data
|
|
@@ -165,8 +170,9 @@ export function defaultScrollPreservation(container, context = {}) {
|
|
|
165
170
|
* @param {Object} options - Configuration
|
|
166
171
|
* @param {Function} options.key - Function to extract unique key: (item) => key
|
|
167
172
|
* @param {Function} [options.render] - Function to render item (called for all items): (item, element, index) => void
|
|
168
|
-
* @param {Function} [options.create] - Function for new elements only: (item, element, index) => void
|
|
173
|
+
* @param {Function} [options.create] - Function for new elements only: (item, element, index) => void | Function. If a function is returned, it is registered as the element's cleanup and called automatically when the element is removed (by list update or full cleanup).
|
|
169
174
|
* @param {Function} [options.update] - Function for data binding: (item, element, index, { isFirstRender }) => void. Skipped if same item reference AND same index.
|
|
175
|
+
* @param {Function} [options.remove] - Additional cleanup when element is removed: (item, element) => void. Called after any cleanup function returned by create(). Optional — prefer returning a cleanup from create() for automatic lifecycle management.
|
|
170
176
|
* @param {string|Function} [options.element='div'] - Element tag name or factory function
|
|
171
177
|
* @param {Function|null} [options.preserveFocus=defaultFocusPreservation] - Focus preservation strategy (null to disable)
|
|
172
178
|
* @param {Function|null} [options.preserveScroll=defaultScrollPreservation] - Scroll preservation strategy (null to disable)
|
|
@@ -179,6 +185,7 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
179
185
|
render,
|
|
180
186
|
create,
|
|
181
187
|
update,
|
|
188
|
+
remove,
|
|
182
189
|
element = 'div',
|
|
183
190
|
preserveFocus = defaultFocusPreservation,
|
|
184
191
|
preserveScroll = defaultScrollPreservation
|
|
@@ -209,6 +216,8 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
209
216
|
const prevItemsByKey = new Map();
|
|
210
217
|
// key -> previous index (for reorder detection)
|
|
211
218
|
const prevIndexByKey = new Map();
|
|
219
|
+
// key -> cleanup function returned by create()
|
|
220
|
+
const cleanupByKey = new Map();
|
|
212
221
|
const seenKeys = new Set();
|
|
213
222
|
|
|
214
223
|
function createElement() {
|
|
@@ -250,6 +259,7 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
250
259
|
if (restoreScroll) restoreScroll();
|
|
251
260
|
}
|
|
252
261
|
|
|
262
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- keyed DOM reconciliation: create/reuse/remove nodes, key dedup, scroll/focus preservation
|
|
253
263
|
function updateList() {
|
|
254
264
|
const items = store[arrayKey];
|
|
255
265
|
|
|
@@ -293,7 +303,10 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
293
303
|
try {
|
|
294
304
|
// Call create for new elements (DOM structure)
|
|
295
305
|
if (isFirstRender && create) {
|
|
296
|
-
create(item, el, i);
|
|
306
|
+
const cleanup = create(item, el, i);
|
|
307
|
+
if (typeof cleanup === 'function') {
|
|
308
|
+
cleanupByKey.set(k, cleanup);
|
|
309
|
+
}
|
|
297
310
|
}
|
|
298
311
|
|
|
299
312
|
// Call update for data binding (new and existing elements)
|
|
@@ -320,6 +333,7 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
320
333
|
nextEls.push(el);
|
|
321
334
|
}
|
|
322
335
|
|
|
336
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- DOM cleanup pass: remove stale nodes, call per-item cleanup callbacks, update maps
|
|
323
337
|
applyPreservation(containerEl, () => {
|
|
324
338
|
reconcileDOM(containerEl, nextEls);
|
|
325
339
|
|
|
@@ -327,9 +341,24 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
327
341
|
if (elementsByKey.size !== seenKeys.size) {
|
|
328
342
|
for (const k of elementsByKey.keys()) {
|
|
329
343
|
if (!seenKeys.has(k)) {
|
|
344
|
+
const el = elementsByKey.get(k);
|
|
345
|
+
const prevItem = prevItemsByKey.get(k);
|
|
346
|
+
// Call create-returned cleanup first, then remove callback
|
|
347
|
+
const cleanup = cleanupByKey.get(k);
|
|
348
|
+
if (typeof cleanup === 'function') {
|
|
349
|
+
try {
|
|
350
|
+
cleanup();
|
|
351
|
+
} catch (err) {
|
|
352
|
+
logError(`[Lume.js] repeat(): cleanup error for key "${k}":`, err);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (typeof remove === 'function' && el) {
|
|
356
|
+
remove(prevItem, el);
|
|
357
|
+
}
|
|
330
358
|
elementsByKey.delete(k);
|
|
331
359
|
prevItemsByKey.delete(k);
|
|
332
360
|
prevIndexByKey.delete(k);
|
|
361
|
+
cleanupByKey.delete(k);
|
|
333
362
|
}
|
|
334
363
|
}
|
|
335
364
|
}
|
|
@@ -354,10 +383,25 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
354
383
|
updateList();
|
|
355
384
|
logWarn('[Lume.js] repeat(): store is not reactive (no $subscribe or subscribe method)');
|
|
356
385
|
return () => {
|
|
386
|
+
for (const [k, el] of elementsByKey) {
|
|
387
|
+
const prevItem = prevItemsByKey.get(k);
|
|
388
|
+
const cleanup = cleanupByKey.get(k);
|
|
389
|
+
if (typeof cleanup === 'function') {
|
|
390
|
+
try {
|
|
391
|
+
cleanup();
|
|
392
|
+
} catch (err) {
|
|
393
|
+
logError(`[Lume.js] repeat(): cleanup error for key "${k}":`, err);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (typeof remove === 'function') {
|
|
397
|
+
remove(prevItem, el);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
357
400
|
containerEl.replaceChildren();
|
|
358
401
|
elementsByKey.clear();
|
|
359
402
|
prevItemsByKey.clear();
|
|
360
403
|
prevIndexByKey.clear();
|
|
404
|
+
cleanupByKey.clear();
|
|
361
405
|
seenKeys.clear();
|
|
362
406
|
};
|
|
363
407
|
}
|
|
@@ -366,11 +410,27 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
366
410
|
if (typeof unsubscribe === 'function') {
|
|
367
411
|
unsubscribe();
|
|
368
412
|
}
|
|
413
|
+
// Invoke cleanup and remove callback for all remaining elements before clearing
|
|
414
|
+
for (const [k, el] of elementsByKey) {
|
|
415
|
+
const prevItem = prevItemsByKey.get(k);
|
|
416
|
+
const cleanup = cleanupByKey.get(k);
|
|
417
|
+
if (typeof cleanup === 'function') {
|
|
418
|
+
try {
|
|
419
|
+
cleanup();
|
|
420
|
+
} catch (err) {
|
|
421
|
+
logError(`[Lume.js] repeat(): cleanup error for key "${k}":`, err);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (typeof remove === 'function') {
|
|
425
|
+
remove(prevItem, el);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
369
428
|
// Clear DOM elements (replaceChildren is faster than loop)
|
|
370
429
|
containerEl.replaceChildren();
|
|
371
430
|
elementsByKey.clear();
|
|
372
431
|
prevItemsByKey.clear();
|
|
373
432
|
prevIndexByKey.clear();
|
|
433
|
+
cleanupByKey.clear();
|
|
374
434
|
seenKeys.clear();
|
|
375
435
|
};
|
|
376
436
|
}
|
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
|
}
|
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
|
@@ -100,6 +100,7 @@ export function state(obj) {
|
|
|
100
100
|
if (flushScheduled) return;
|
|
101
101
|
|
|
102
102
|
flushScheduled = true;
|
|
103
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- single-pass flush loop: hooks → subscribers → effects → cycle detection; must stay atomic
|
|
103
104
|
queueMicrotask(() => {
|
|
104
105
|
let iterations = 0;
|
|
105
106
|
const MAX_ITERATIONS = 100;
|
|
@@ -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.
|
package/src/handlers/index.js
CHANGED
|
@@ -19,199 +19,11 @@
|
|
|
19
19
|
* { attr: string, apply(el: HTMLElement, val: any): void }
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
apply(el, val) { el.hidden = !Boolean(val); }
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// --- Factory Functions ---
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Create a handler for any HTML boolean attribute.
|
|
37
|
-
* Uses toggleAttribute() — works correctly with any attribute name
|
|
38
|
-
* (readonly, contenteditable, etc.) without worrying about camelCase property names.
|
|
39
|
-
*
|
|
40
|
-
* Note: built-in boolean handlers (hidden, disabled, checked, required) use
|
|
41
|
-
* property assignment directly. This factory uses toggleAttribute for broader
|
|
42
|
-
* attribute name compatibility.
|
|
43
|
-
*
|
|
44
|
-
* @param {string} name - Attribute name (e.g., 'readonly', 'open', 'contenteditable')
|
|
45
|
-
* @returns {{ attr: string, apply: function }}
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* bindDom(root, store, { handlers: [boolAttr('readonly')] });
|
|
49
|
-
* // <input data-readonly="isReadonly" />
|
|
50
|
-
*/
|
|
51
|
-
export function boolAttr(name) {
|
|
52
|
-
return {
|
|
53
|
-
attr: `data-${name}`,
|
|
54
|
-
apply(el, val) { el.toggleAttribute(name, Boolean(val)); }
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Create a handler for an ARIA attribute.
|
|
60
|
-
* Use for ARIA attrs beyond the built-in aria-expanded/aria-hidden.
|
|
61
|
-
*
|
|
62
|
-
* @param {string} name - ARIA name, with or without "aria-" prefix
|
|
63
|
-
* @returns {{ attr: string, apply: function }}
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* bindDom(root, store, { handlers: [ariaAttr('pressed'), ariaAttr('selected')] });
|
|
67
|
-
* // <button data-aria-pressed="isPressed">Toggle</button>
|
|
68
|
-
*/
|
|
69
|
-
export function ariaAttr(name) {
|
|
70
|
-
const fullName = name.startsWith('aria-') ? name : `aria-${name}`;
|
|
71
|
-
return {
|
|
72
|
-
attr: `data-${fullName}`,
|
|
73
|
-
apply(el, val) { el.setAttribute(fullName, val ? 'true' : 'false'); }
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Create handlers for CSS class toggling.
|
|
79
|
-
* Each name creates a handler: data-class-{name}="key" → el.classList.toggle(name, Boolean(val))
|
|
80
|
-
*
|
|
81
|
-
* Returns an array — pass directly to handlers (auto-flattened by bindDom).
|
|
82
|
-
*
|
|
83
|
-
* @param {...string} names - CSS class names to create handlers for
|
|
84
|
-
* @returns {Array<{ attr: string, apply: function }>}
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* bindDom(root, store, { handlers: [classToggle('active', 'loading', 'error')] });
|
|
88
|
-
* // <div data-class-active="isActive" data-class-loading="isLoading">
|
|
89
|
-
*/
|
|
90
|
-
export function classToggle(...names) {
|
|
91
|
-
return names.map(name => ({
|
|
92
|
-
attr: `data-class-${name}`,
|
|
93
|
-
apply(el, val) { el.classList.toggle(name, Boolean(val)); }
|
|
94
|
-
}));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Create a handler for any string attribute (href, src, title, alt, action, etc.)
|
|
99
|
-
* Sets the attribute value as a string. Removes attribute when value is null/undefined.
|
|
100
|
-
*
|
|
101
|
-
* @param {string} name - HTML attribute name (e.g., 'href', 'src', 'title')
|
|
102
|
-
* @returns {{ attr: string, apply: function }}
|
|
103
|
-
*
|
|
104
|
-
* @example
|
|
105
|
-
* bindDom(root, store, { handlers: [stringAttr('href'), stringAttr('src')] });
|
|
106
|
-
* // <a data-href="profileUrl">Profile</a>
|
|
107
|
-
* // <img data-src="imageUrl" />
|
|
108
|
-
*/
|
|
109
|
-
export function stringAttr(name) {
|
|
110
|
-
return {
|
|
111
|
-
attr: `data-${name}`,
|
|
112
|
-
apply(el, val) {
|
|
113
|
-
if (val == null) el.removeAttribute(name);
|
|
114
|
-
else el.setAttribute(name, String(val));
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// --- Presets ---
|
|
120
|
-
|
|
121
|
-
/** Form-related handlers (beyond built-in disabled/checked/required) */
|
|
122
|
-
export const formHandlers = [
|
|
123
|
-
boolAttr('readonly'),
|
|
124
|
-
];
|
|
125
|
-
|
|
126
|
-
/** Additional ARIA handlers (beyond built-in aria-expanded/aria-hidden) */
|
|
127
|
-
export const a11yHandlers = [
|
|
128
|
-
ariaAttr('pressed'),
|
|
129
|
-
ariaAttr('selected'),
|
|
130
|
-
ariaAttr('disabled'),
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
// --- htmlAttrs() — All Standard HTML Attributes ---
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Standard HTML boolean attributes (beyond built-in hidden/disabled/checked/required).
|
|
137
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#boolean_attributes
|
|
138
|
-
*/
|
|
139
|
-
const BOOL_ATTRS = [
|
|
140
|
-
'readonly', 'open', 'novalidate', 'formnovalidate', 'multiple',
|
|
141
|
-
'autofocus', 'autoplay', 'controls', 'loop', 'muted', 'defer',
|
|
142
|
-
'async', 'reversed', 'selected', 'inert', 'allowfullscreen',
|
|
143
|
-
];
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Standard HTML string attributes.
|
|
147
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
|
|
148
|
-
*/
|
|
149
|
-
const STRING_ATTRS = [
|
|
150
|
-
'href', 'src', 'alt', 'title', 'placeholder', 'action', 'method',
|
|
151
|
-
'target', 'rel', 'type', 'name', 'role', 'lang', 'tabindex',
|
|
152
|
-
'pattern', 'min', 'max', 'step', 'minlength', 'maxlength',
|
|
153
|
-
'width', 'height', 'for', 'form', 'accept', 'autocomplete',
|
|
154
|
-
'loading', 'decoding', 'inputmode', 'enterkeyhint', 'draggable',
|
|
155
|
-
'contenteditable', 'spellcheck', 'translate', 'dir', 'id',
|
|
156
|
-
'poster', 'preload', 'download', 'media', 'sizes', 'srcset',
|
|
157
|
-
'colspan', 'rowspan', 'scope', 'headers', 'wrap', 'sandbox',
|
|
158
|
-
];
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* ARIA boolean state attributes — toggled between "true" and "false".
|
|
162
|
-
* Use ariaAttr() for these (coerces to "true"/"false" string).
|
|
163
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
|
|
164
|
-
*/
|
|
165
|
-
const ARIA_BOOL_ATTRS = [
|
|
166
|
-
'pressed', 'selected', 'disabled', 'checked', 'invalid', 'required',
|
|
167
|
-
'busy', 'modal', 'multiselectable', 'multiline', 'readonly', 'atomic',
|
|
168
|
-
];
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* ARIA string/token/numeric attributes — value passed through as-is.
|
|
172
|
-
* Use stringAttr() with "aria-" prefix for these.
|
|
173
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
|
|
174
|
-
*/
|
|
175
|
-
const ARIA_STRING_ATTRS = [
|
|
176
|
-
'current', 'live', 'relevant', 'haspopup',
|
|
177
|
-
'sort', 'autocomplete', 'orientation',
|
|
178
|
-
'label', 'describedby', 'labelledby', 'controls', 'owns',
|
|
179
|
-
'activedescendant', 'errormessage', 'details', 'flowto',
|
|
180
|
-
'valuenow', 'valuemin', 'valuemax', 'valuetext',
|
|
181
|
-
'colcount', 'colindex', 'colspan', 'rowcount', 'rowindex', 'rowspan',
|
|
182
|
-
'level', 'setsize', 'posinset', 'placeholder', 'roledescription',
|
|
183
|
-
'keyshortcuts', 'braillelabel', 'brailleroledescription',
|
|
184
|
-
];
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* One-import preset that enables all standard HTML attributes as reactive handlers.
|
|
188
|
-
*
|
|
189
|
-
* Includes:
|
|
190
|
-
* - Boolean attributes: readonly, open, autofocus, controls, muted, inert, etc.
|
|
191
|
-
* - String attributes: href, src, alt, title, placeholder, role, tabindex, etc.
|
|
192
|
-
* - ARIA attributes: aria-pressed, aria-label, aria-describedby, aria-valuenow, etc.
|
|
193
|
-
* - Show handler: data-show (inverse of data-hidden)
|
|
194
|
-
*
|
|
195
|
-
* Returns a flat array — pass directly to handlers option.
|
|
196
|
-
*
|
|
197
|
-
* @returns {Array<{ attr: string, apply: function }>}
|
|
198
|
-
*
|
|
199
|
-
* @example
|
|
200
|
-
* import { htmlAttrs } from 'lume-js/handlers';
|
|
201
|
-
*
|
|
202
|
-
* bindDom(document.body, store, { handlers: [htmlAttrs()] });
|
|
203
|
-
* // Now use any data-* attribute:
|
|
204
|
-
* // <a data-href="url">Link</a>
|
|
205
|
-
* // <input data-readonly="isLocked" />
|
|
206
|
-
* // <div data-aria-label="labelText">...</div>
|
|
207
|
-
* // <div data-show="isVisible">...</div>
|
|
208
|
-
*/
|
|
209
|
-
export function htmlAttrs() {
|
|
210
|
-
return [
|
|
211
|
-
show,
|
|
212
|
-
...BOOL_ATTRS.map(name => boolAttr(name)),
|
|
213
|
-
...STRING_ATTRS.map(name => stringAttr(name)),
|
|
214
|
-
...ARIA_BOOL_ATTRS.map(name => ariaAttr(name)),
|
|
215
|
-
...ARIA_STRING_ATTRS.map(name => stringAttr(`aria-${name}`)),
|
|
216
|
-
];
|
|
217
|
-
}
|
|
22
|
+
export { show } from './show.js';
|
|
23
|
+
export { className } from './className.js';
|
|
24
|
+
export { boolAttr } from './boolAttr.js';
|
|
25
|
+
export { ariaAttr } from './ariaAttr.js';
|
|
26
|
+
export { classToggle } from './classToggle.js';
|
|
27
|
+
export { stringAttr } from './stringAttr.js';
|
|
28
|
+
export { htmlAttrs } from './htmlAttrs.js';
|
|
29
|
+
export { formHandlers, a11yHandlers } from './presets.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { boolAttr } from './boolAttr.js';
|
|
2
|
+
import { ariaAttr } from './ariaAttr.js';
|
|
3
|
+
|
|
4
|
+
/** Form-related handlers (beyond built-in disabled/checked/required) */
|
|
5
|
+
export const formHandlers = [
|
|
6
|
+
boolAttr('readonly'),
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
/** Additional ARIA handlers (beyond built-in aria-expanded/aria-hidden) */
|
|
10
|
+
export const a11yHandlers = [
|
|
11
|
+
ariaAttr('pressed'),
|
|
12
|
+
ariaAttr('selected'),
|
|
13
|
+
ariaAttr('disabled'),
|
|
14
|
+
];
|