lume-js 2.1.0 → 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 +11 -1
- 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 -6
- package/src/addons/index.d.ts +26 -7
- package/src/addons/repeat.js +2 -0
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared-Dcokqj5a.mjs","sources":["../src/utils/log.js","../src/core/state.js","../src/core/effect.js"],"sourcesContent":["/**\n * Environment-safe logging utilities for constrained runtimes\n * (e.g. service workers, embedded engines, SSR environments).\n *\n * All core and addon files should import these instead of\n * calling console.* directly to avoid ReferenceError when\n * console is not defined.\n */\n\nexport function logWarn(msg, ...rest) {\n if (typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn(msg, ...rest);\n }\n}\n\nexport function logError(msg, ...rest) {\n if (typeof console !== 'undefined' && typeof console.error === 'function') {\n console.error(msg, ...rest);\n }\n}\n","/**\n * Lume-JS Reactive State Core\n *\n * Provides minimal reactive state with standard JavaScript.\n * Features automatic microtask batching for performance.\n * Read tracking is opt-in via withReadObserver — state.js has zero permanent\n * dependency on effect.js or any other module.\n *\n * Features:\n * - Lightweight and Go-style\n * - Explicit nested states\n * - $subscribe for listening to key changes\n * - Cleanup with unsubscribe\n * - Per-state microtask batching for writes\n * - Scope-based read tracking via withReadObserver (multi-observer safe)\n *\n * Usage:\n * import { state } from \"lume-js\";\n *\n * const store = state({ count: 0 });\n * const unsub = store.$subscribe(\"count\", val => console.log(val));\n * unsub(); // cleanup\n */\n\nimport { logError } from '../utils/log.js';\n\n// Per-state batching – each state object maintains its own microtask flush.\n// This keeps effects simple and aligned with Lume's minimal philosophy.\n\n/**\n * Creates a reactive state object.\n *\n * @param {Object} obj - Initial state object (must be plain object)\n * @returns {Proxy} Reactive proxy with $subscribe method\n *\n * @example\n * const store = state({ count: 0 });\n */\n\n// Active read observers — only populated during withReadObserver scopes.\n// This keeps state.js pure: tracking only happens when someone explicitly\n// asks to observe reads within a synchronous function call.\n//\n// Note: This Set is module-level, so all reactive state instances and effects\n// within the SAME module instance share it. This is standard behavior for\n// auto-tracking reactive libraries (Vue, MobX, Solid, etc.). Multiple copies\n// of the lume-js module (e.g. from different bundled chunks) each get their\n// own independent Set via ES module / CommonJS isolation.\nconst readers = new Set();\n\n/**\n * Run a function with a read observer active.\n * The observer receives (proxy, key, registerEffect) for every property read.\n * Multiple observers can be active simultaneously (nested effects, devtools, etc.)\n *\n * Internal API — used by effect.js for auto-tracking. May be stabilized\n * for third-party addons in a future release.\n * @param {function} onRead - Called on each property access inside fn\n * @param {function} fn - The function to run under observation\n */\nexport function withReadObserver(onRead, fn) {\n readers.add(onRead);\n try {\n return fn();\n } finally {\n readers.delete(onRead);\n }\n}\n\nexport function state(obj) {\n // Validate input\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {\n throw new Error('state() requires a plain object');\n }\n if (Object.isFrozen(obj) || Object.isSealed(obj)) {\n throw new Error('state() requires a mutable plain object');\n }\n\n // Object.create(null) - no prototype chain lookups\n const listeners = Object.create(null);\n const pendingNotifications = new Map(); // Per-state pending changes\n const pendingEffects = new Set(); // Dedupe effects per state\n const beforeFlushHooks = [];\n let flushScheduled = false;\n\n /**\n * Schedule a single microtask flush for this state object.\n *\n * Flush order per state:\n * 1) Notify subscribers for changed keys (key → subscribers)\n * 2) Run each queued effect exactly once (Set-based dedupe)\n * 3) Repeat up to 100 iterations to handle cascading updates,\n * then log an error to prevent infinite loops.\n *\n * Notes:\n * - Batching is per state; effects that depend on multiple states\n * may run once per state that changed (by design).\n */\n function scheduleFlush() {\n if (flushScheduled) return;\n\n flushScheduled = true;\n queueMicrotask(() => {\n let iterations = 0;\n const MAX_ITERATIONS = 100;\n\n try {\n while ((pendingNotifications.size > 0 || pendingEffects.size > 0) && iterations < MAX_ITERATIONS) {\n iterations++;\n\n // Run registered before-flush hooks (e.g. plugin onNotify)\n for (let i = 0; i < beforeFlushHooks.length; i++) {\n try {\n beforeFlushHooks[i]();\n } catch (err) {\n logError('[Lume.js state] Error in beforeFlush hook:', err);\n }\n }\n\n // Notify all subscribers of changed keys\n for (const [key, value] of pendingNotifications) {\n if (listeners[key]) {\n const subs = listeners[key];\n let i = 0;\n while (i < subs.length) {\n const fn = subs[i];\n try {\n fn(value);\n } catch (err) {\n logError(`[Lume.js state] Error notifying subscriber for key \"${String(key)}\":`, err);\n }\n // Only advance if fn wasn't removed (something shifted into its place)\n if (subs[i] === fn) i++;\n }\n }\n }\n\n pendingNotifications.clear();\n\n // Run each effect exactly once (Set deduplicates)\n const effects = new Array(pendingEffects.size);\n let idx = 0;\n for (const effect of pendingEffects) {\n effects[idx++] = effect;\n }\n pendingEffects.clear();\n for (let i = 0; i < effects.length; i++) {\n try {\n effects[i]();\n } catch (err) {\n logError('[Lume.js state] Error in effect:', err);\n }\n }\n }\n } finally {\n flushScheduled = false;\n }\n\n if (iterations >= MAX_ITERATIONS) {\n logError(\n '[Lume.js state] Maximum flush iterations reached (100). ' +\n 'This usually indicates an infinite loop caused by an effect or computed mutating state it depends on.'\n );\n }\n });\n }\n\n // Brand symbol for type-level reactive identification\n const REACTIVE_BRAND = Symbol('lume.reactive');\n obj[REACTIVE_BRAND] = true;\n\n // Defined once per state instance — not per property read — to avoid per-read closure allocation.\n const registerEffect = (key, executeFn) => {\n if (!listeners[key]) listeners[key] = [];\n\n const callback = () => {\n pendingEffects.add(executeFn);\n };\n\n listeners[key].push(callback);\n\n return () => {\n if (listeners[key]) {\n const idx = listeners[key].indexOf(callback);\n if (idx !== -1) {\n listeners[key].splice(idx, 1);\n if (listeners[key].length === 0) delete listeners[key];\n }\n }\n };\n };\n\n const proxy = new Proxy(obj, {\n get(target, key) {\n // Skip effect tracking for internal meta methods (e.g. $subscribe)\n if (typeof key === 'string' && key.startsWith('$')) {\n return target[key];\n }\n\n const value = target[key];\n\n // Notify active read observers (effects, devtools, etc.)\n if (readers.size > 0) {\n for (const reader of readers) {\n reader(proxy, key, registerEffect);\n }\n }\n\n return value;\n },\n\n set(target, key, value) {\n const oldValue = target[key];\n\n // Skip update if value unchanged - Object.is() handles NaN and -0 correctly\n if (Object.is(oldValue, value)) return true;\n\n target[key] = value;\n\n // Batch notifications at the state level (per-state, not global)\n pendingNotifications.set(key, value);\n scheduleFlush();\n\n return true;\n }\n });\n\n /**\n * Subscribe to changes for a specific key.\n * Calls the callback immediately with the current value.\n * Returns an unsubscribe function for cleanup.\n *\n * @param {string} key - Property key to watch\n * @param {function} fn - Callback function\n * @returns {function} Unsubscribe function\n */\n // Set on obj (not proxy) to avoid triggering the set trap.\n // The get trap already returns target[key] directly for $-prefixed keys.\n /**\n * Register a callback to run before each flush.\n * Returns an unsubscribe function.\n */\n obj.$beforeFlush = (fn) => {\n if (typeof fn !== 'function') {\n throw new Error('$beforeFlush requires a function');\n }\n if (beforeFlushHooks.indexOf(fn) === -1) {\n beforeFlushHooks.push(fn);\n }\n return () => {\n const idx = beforeFlushHooks.indexOf(fn);\n if (idx !== -1) {\n beforeFlushHooks.splice(idx, 1);\n }\n };\n };\n\n obj.$subscribe = (key, fn) => {\n if (typeof fn !== 'function') {\n throw new Error('Subscriber must be a function');\n }\n\n if (!listeners[key]) listeners[key] = [];\n listeners[key].push(fn);\n\n // Call immediately with current value (NOT batched)\n fn(proxy[key]);\n\n // Return unsubscribe function\n return () => {\n if (listeners[key]) {\n const idx = listeners[key].indexOf(fn);\n if (idx !== -1) {\n listeners[key].splice(idx, 1);\n if (listeners[key].length === 0) delete listeners[key];\n }\n }\n };\n };\n\n return proxy;\n}\n","import { withReadObserver } from './state.js';\nimport { logError } from '../utils/log.js';\n\n/**\n * Lume-JS Effect\n *\n * Reactive effects with two modes:\n * 1. Auto-tracking (default): Tracks dependencies automatically via withReadObserver\n * 2. Explicit deps: You specify exactly what triggers re-runs\n *\n * Auto-tracking uses scope-based read observation — state.js has zero permanent\n * dependency on this module. Read tracking is only active during the synchronous\n * execution of an effect's body.\n *\n * Usage:\n * import { effect } from \"lume-js\";\n *\n * // Auto-tracking mode (existing behavior)\n * effect(() => {\n * console.log('Count is:', store.count);\n * // Automatically re-runs when store.count changes\n * });\n *\n * // Explicit deps mode (new - no magic)\n * effect(() => {\n * console.log('Count is:', store.count);\n * }, [[store, 'count']]); // Only re-runs when store.count changes\n *\n * Features:\n * - Automatic dependency collection via withReadObserver scope (default)\n * - Explicit dependencies for side-effects\n * - Returns cleanup function\n * - Compatible with per-state batching\n */\n\n// Module-scoped effect context (prevents third-party spoofing via globalThis)\nlet currentEffect = null;\n\n// withReadObserver is used below to scope read tracking to synchronous effect execution.\n\n/**\n * Creates an effect that runs reactively\n *\n * @param {function} fn - Function to run reactively\n * @param {Array<[object, string]>} [deps] - Optional explicit dependencies as [store, key] tuples\n * @returns {function} Cleanup function to stop the effect\n *\n * @example\n * // Auto-tracking (default)\n * const store = state({ count: 0 });\n * effect(() => {\n * document.title = `Count: ${store.count}`;\n * });\n * \n * @example\n * // Explicit deps (no magic)\n * effect(() => {\n * analytics.log(store.count); // Won't track store.count automatically\n * }, [[store, 'count']]); // Explicit: only re-run on store.count\n */\nexport function effect(fn, deps) {\n if (typeof fn !== 'function') {\n throw new Error('effect() requires a function');\n }\n\n const cleanups = [];\n let isRunning = false;\n\n /**\n * Execute the effect function\n */\n const execute = () => {\n /* v8 ignore next -- re-entry guard: unreachable because $subscribe fires via microtask after isRunning resets in finally */\n if (isRunning) return;\n isRunning = true;\n\n try {\n fn();\n } catch (error) {\n logError('[Lume.js effect] Error in effect:', error);\n throw error;\n } finally {\n isRunning = false;\n }\n };\n\n // EXPLICIT DEPS MODE: deps array provided\n if (Array.isArray(deps)) {\n // Subscribe to each [store, key1, key2, ...] tuple explicitly\n for (const dep of deps) {\n if (Array.isArray(dep) && dep.length >= 2) {\n const [store, ...keys] = dep;\n if (store && typeof store.$subscribe === 'function') {\n // Subscribe to each key in this tuple\n for (const key of keys) {\n // $subscribe calls immediately, then on changes\n // We want: call execute immediately once, then on changes\n let isFirst = true;\n const unsub = store.$subscribe(key, () => {\n if (isFirst) {\n isFirst = false;\n return; // Skip first call, we'll run execute() below\n }\n execute();\n });\n cleanups.push(unsub);\n }\n }\n }\n }\n // Run immediately\n execute();\n }\n // AUTO-TRACKING MODE: no deps (existing behavior)\n else {\n const executeWithTracking = () => {\n /* v8 ignore next -- defensive guard: synchronous re-entry is unreachable through the public API */\n if (isRunning) return;\n\n // Save previous subscriptions instead of cleaning immediately.\n // If fn() doesn't read any state (early return / error), we restore\n // them so the effect stays reactive.\n const oldCleanups = cleanups.splice(0);\n\n // Create effect context for tracking\n const myContext = {\n fn,\n cleanups,\n execute: executeWithTracking,\n tracking: {}\n };\n\n // Set as current effect (for state.js to detect)\n // Save previous context to support nested effects/computed\n const previousEffect = currentEffect;\n currentEffect = myContext;\n isRunning = true;\n\n try {\n const onRead = (proxy, key, registerEffect) => {\n // Only the currently active effect (not a nested one) creates subscriptions\n if (currentEffect !== myContext) return;\n if (myContext.tracking[key]) return;\n myContext.tracking[key] = true;\n myContext.cleanups.push(registerEffect(key, myContext.execute));\n };\n withReadObserver(onRead, fn);\n } catch (error) {\n // On error, restore old subscriptions so the effect stays reactive\n cleanups.length = 0;\n cleanups.push(...oldCleanups);\n logError('[Lume.js effect] Error in effect:', error);\n throw error;\n } finally {\n // Restore previous context (not undefined) to support nesting\n currentEffect = previousEffect;\n isRunning = false;\n }\n\n // If fn() created new subscriptions, clean old ones.\n // If it didn't (e.g., early return), keep old subscriptions intact.\n if (cleanups.length > 0) {\n for (const cleanup of oldCleanups) cleanup();\n } else {\n cleanups.push(...oldCleanups);\n }\n };\n\n // Run immediately to collect initial dependencies\n executeWithTracking();\n }\n\n // Return cleanup function\n return () => {\n // while/pop is faster than forEach\n while (cleanups.length) cleanups.pop()();\n };\n}"],"names":["effect"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"shared-Dcokqj5a.mjs","sources":["../src/utils/log.js","../src/core/state.js","../src/core/effect.js"],"sourcesContent":["/**\n * Environment-safe logging utilities for constrained runtimes\n * (e.g. service workers, embedded engines, SSR environments).\n *\n * All core and addon files should import these instead of\n * calling console.* directly to avoid ReferenceError when\n * console is not defined.\n */\n\nexport function logWarn(msg, ...rest) {\n if (typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn(msg, ...rest);\n }\n}\n\nexport function logError(msg, ...rest) {\n if (typeof console !== 'undefined' && typeof console.error === 'function') {\n console.error(msg, ...rest);\n }\n}\n","/**\n * Lume-JS Reactive State Core\n *\n * Provides minimal reactive state with standard JavaScript.\n * Features automatic microtask batching for performance.\n * Read tracking is opt-in via withReadObserver — state.js has zero permanent\n * dependency on effect.js or any other module.\n *\n * Features:\n * - Lightweight and Go-style\n * - Explicit nested states\n * - $subscribe for listening to key changes\n * - Cleanup with unsubscribe\n * - Per-state microtask batching for writes\n * - Scope-based read tracking via withReadObserver (multi-observer safe)\n *\n * Usage:\n * import { state } from \"lume-js\";\n *\n * const store = state({ count: 0 });\n * const unsub = store.$subscribe(\"count\", val => console.log(val));\n * unsub(); // cleanup\n */\n\nimport { logError } from '../utils/log.js';\n\n// Per-state batching – each state object maintains its own microtask flush.\n// This keeps effects simple and aligned with Lume's minimal philosophy.\n\n/**\n * Creates a reactive state object.\n *\n * @param {Object} obj - Initial state object (must be plain object)\n * @returns {Proxy} Reactive proxy with $subscribe method\n *\n * @example\n * const store = state({ count: 0 });\n */\n\n// Active read observers — only populated during withReadObserver scopes.\n// This keeps state.js pure: tracking only happens when someone explicitly\n// asks to observe reads within a synchronous function call.\n//\n// Note: This Set is module-level, so all reactive state instances and effects\n// within the SAME module instance share it. This is standard behavior for\n// auto-tracking reactive libraries (Vue, MobX, Solid, etc.). Multiple copies\n// of the lume-js module (e.g. from different bundled chunks) each get their\n// own independent Set via ES module / CommonJS isolation.\nconst readers = new Set();\n\n/**\n * Run a function with a read observer active.\n * The observer receives (proxy, key, registerEffect) for every property read.\n * Multiple observers can be active simultaneously (nested effects, devtools, etc.)\n *\n * Internal API — used by effect.js for auto-tracking. May be stabilized\n * for third-party addons in a future release.\n * @param {function} onRead - Called on each property access inside fn\n * @param {function} fn - The function to run under observation\n */\nexport function withReadObserver(onRead, fn) {\n readers.add(onRead);\n try {\n return fn();\n } finally {\n readers.delete(onRead);\n }\n}\n\nexport function state(obj) {\n // Validate input\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {\n throw new Error('state() requires a plain object');\n }\n if (Object.isFrozen(obj) || Object.isSealed(obj)) {\n throw new Error('state() requires a mutable plain object');\n }\n\n // Object.create(null) - no prototype chain lookups\n const listeners = Object.create(null);\n const pendingNotifications = new Map(); // Per-state pending changes\n const pendingEffects = new Set(); // Dedupe effects per state\n const beforeFlushHooks = [];\n let flushScheduled = false;\n\n /**\n * Schedule a single microtask flush for this state object.\n *\n * Flush order per state:\n * 1) Notify subscribers for changed keys (key → subscribers)\n * 2) Run each queued effect exactly once (Set-based dedupe)\n * 3) Repeat up to 100 iterations to handle cascading updates,\n * then log an error to prevent infinite loops.\n *\n * Notes:\n * - Batching is per state; effects that depend on multiple states\n * may run once per state that changed (by design).\n */\n function scheduleFlush() {\n if (flushScheduled) return;\n\n flushScheduled = true;\n // eslint-disable-next-line sonarjs/cognitive-complexity -- single-pass flush loop: hooks → subscribers → effects → cycle detection; must stay atomic\n queueMicrotask(() => {\n let iterations = 0;\n const MAX_ITERATIONS = 100;\n\n try {\n while ((pendingNotifications.size > 0 || pendingEffects.size > 0) && iterations < MAX_ITERATIONS) {\n iterations++;\n\n // Run registered before-flush hooks (e.g. plugin onNotify)\n for (let i = 0; i < beforeFlushHooks.length; i++) {\n try {\n beforeFlushHooks[i]();\n } catch (err) {\n logError('[Lume.js state] Error in beforeFlush hook:', err);\n }\n }\n\n // Notify all subscribers of changed keys\n for (const [key, value] of pendingNotifications) {\n if (listeners[key]) {\n const subs = listeners[key];\n let i = 0;\n while (i < subs.length) {\n const fn = subs[i];\n try {\n fn(value);\n } catch (err) {\n logError(`[Lume.js state] Error notifying subscriber for key \"${String(key)}\":`, err);\n }\n // Only advance if fn wasn't removed (something shifted into its place)\n if (subs[i] === fn) i++;\n }\n }\n }\n\n pendingNotifications.clear();\n\n // Run each effect exactly once (Set deduplicates)\n const effects = new Array(pendingEffects.size);\n let idx = 0;\n for (const effect of pendingEffects) {\n effects[idx++] = effect;\n }\n pendingEffects.clear();\n for (let i = 0; i < effects.length; i++) {\n try {\n effects[i]();\n } catch (err) {\n logError('[Lume.js state] Error in effect:', err);\n }\n }\n }\n } finally {\n flushScheduled = false;\n }\n\n if (iterations >= MAX_ITERATIONS) {\n logError(\n '[Lume.js state] Maximum flush iterations reached (100). ' +\n 'This usually indicates an infinite loop caused by an effect or computed mutating state it depends on.'\n );\n }\n });\n }\n\n // Brand symbol for type-level reactive identification\n const REACTIVE_BRAND = Symbol('lume.reactive');\n obj[REACTIVE_BRAND] = true;\n\n // Defined once per state instance — not per property read — to avoid per-read closure allocation.\n const registerEffect = (key, executeFn) => {\n if (!listeners[key]) listeners[key] = [];\n\n const callback = () => {\n pendingEffects.add(executeFn);\n };\n\n listeners[key].push(callback);\n\n return () => {\n if (listeners[key]) {\n const idx = listeners[key].indexOf(callback);\n if (idx !== -1) {\n listeners[key].splice(idx, 1);\n if (listeners[key].length === 0) delete listeners[key];\n }\n }\n };\n };\n\n const proxy = new Proxy(obj, {\n get(target, key) {\n // Skip effect tracking for internal meta methods (e.g. $subscribe)\n if (typeof key === 'string' && key.startsWith('$')) {\n return target[key];\n }\n\n const value = target[key];\n\n // Notify active read observers (effects, devtools, etc.)\n if (readers.size > 0) {\n for (const reader of readers) {\n reader(proxy, key, registerEffect);\n }\n }\n\n return value;\n },\n\n set(target, key, value) {\n const oldValue = target[key];\n\n // Skip update if value unchanged - Object.is() handles NaN and -0 correctly\n if (Object.is(oldValue, value)) return true;\n\n target[key] = value;\n\n // Batch notifications at the state level (per-state, not global)\n pendingNotifications.set(key, value);\n scheduleFlush();\n\n return true;\n }\n });\n\n /**\n * Subscribe to changes for a specific key.\n * Calls the callback immediately with the current value.\n * Returns an unsubscribe function for cleanup.\n *\n * @param {string} key - Property key to watch\n * @param {function} fn - Callback function\n * @returns {function} Unsubscribe function\n */\n // Set on obj (not proxy) to avoid triggering the set trap.\n // The get trap already returns target[key] directly for $-prefixed keys.\n /**\n * Register a callback to run before each flush.\n * Returns an unsubscribe function.\n */\n obj.$beforeFlush = (fn) => {\n if (typeof fn !== 'function') {\n throw new Error('$beforeFlush requires a function');\n }\n if (beforeFlushHooks.indexOf(fn) === -1) {\n beforeFlushHooks.push(fn);\n }\n return () => {\n const idx = beforeFlushHooks.indexOf(fn);\n if (idx !== -1) {\n beforeFlushHooks.splice(idx, 1);\n }\n };\n };\n\n obj.$subscribe = (key, fn) => {\n if (typeof fn !== 'function') {\n throw new Error('Subscriber must be a function');\n }\n\n if (!listeners[key]) listeners[key] = [];\n listeners[key].push(fn);\n\n // Call immediately with current value (NOT batched)\n fn(proxy[key]);\n\n // Return unsubscribe function\n return () => {\n if (listeners[key]) {\n const idx = listeners[key].indexOf(fn);\n if (idx !== -1) {\n listeners[key].splice(idx, 1);\n if (listeners[key].length === 0) delete listeners[key];\n }\n }\n };\n };\n\n return proxy;\n}\n","import { withReadObserver } from './state.js';\nimport { logError } from '../utils/log.js';\n\n/**\n * Lume-JS Effect\n *\n * Reactive effects with two modes:\n * 1. Auto-tracking (default): Tracks dependencies automatically via withReadObserver\n * 2. Explicit deps: You specify exactly what triggers re-runs\n *\n * Auto-tracking uses scope-based read observation — state.js has zero permanent\n * dependency on this module. Read tracking is only active during the synchronous\n * execution of an effect's body.\n *\n * Usage:\n * import { effect } from \"lume-js\";\n *\n * // Auto-tracking mode (existing behavior)\n * effect(() => {\n * console.log('Count is:', store.count);\n * // Automatically re-runs when store.count changes\n * });\n *\n * // Explicit deps mode (new - no magic)\n * effect(() => {\n * console.log('Count is:', store.count);\n * }, [[store, 'count']]); // Only re-runs when store.count changes\n *\n * Features:\n * - Automatic dependency collection via withReadObserver scope (default)\n * - Explicit dependencies for side-effects\n * - Returns cleanup function\n * - Compatible with per-state batching\n */\n\n// Module-scoped effect context (prevents third-party spoofing via globalThis)\nlet currentEffect = null;\n\n// withReadObserver is used below to scope read tracking to synchronous effect execution.\n\n/**\n * Creates an effect that runs reactively\n *\n * @param {function} fn - Function to run reactively\n * @param {Array<[object, string]>} [deps] - Optional explicit dependencies as [store, key] tuples\n * @returns {function} Cleanup function to stop the effect\n *\n * @example\n * // Auto-tracking (default)\n * const store = state({ count: 0 });\n * effect(() => {\n * document.title = `Count: ${store.count}`;\n * });\n * \n * @example\n * // Explicit deps (no magic)\n * effect(() => {\n * analytics.log(store.count); // Won't track store.count automatically\n * }, [[store, 'count']]); // Explicit: only re-run on store.count\n */\n// eslint-disable-next-line sonarjs/cognitive-complexity -- handles both auto-tracking and explicit-deps modes with cleanup; splitting would require exporting internal state\nexport function effect(fn, deps) {\n if (typeof fn !== 'function') {\n throw new Error('effect() requires a function');\n }\n\n const cleanups = [];\n let isRunning = false;\n\n /**\n * Execute the effect function\n */\n const execute = () => {\n /* v8 ignore next -- re-entry guard: unreachable because $subscribe fires via microtask after isRunning resets in finally */\n if (isRunning) return;\n isRunning = true;\n\n try {\n fn();\n } catch (error) {\n logError('[Lume.js effect] Error in effect:', error);\n throw error;\n } finally {\n isRunning = false;\n }\n };\n\n // EXPLICIT DEPS MODE: deps array provided\n if (Array.isArray(deps)) {\n // Subscribe to each [store, key1, key2, ...] tuple explicitly\n for (const dep of deps) {\n if (Array.isArray(dep) && dep.length >= 2) {\n const [store, ...keys] = dep;\n if (store && typeof store.$subscribe === 'function') {\n // Subscribe to each key in this tuple\n for (const key of keys) {\n // $subscribe calls immediately, then on changes\n // We want: call execute immediately once, then on changes\n let isFirst = true;\n const unsub = store.$subscribe(key, () => {\n if (isFirst) {\n isFirst = false;\n return; // Skip first call, we'll run execute() below\n }\n execute();\n });\n cleanups.push(unsub);\n }\n }\n }\n }\n // Run immediately\n execute();\n }\n // AUTO-TRACKING MODE: no deps (existing behavior)\n else {\n const executeWithTracking = () => {\n /* v8 ignore next -- defensive guard: synchronous re-entry is unreachable through the public API */\n if (isRunning) return;\n\n // Save previous subscriptions instead of cleaning immediately.\n // If fn() doesn't read any state (early return / error), we restore\n // them so the effect stays reactive.\n const oldCleanups = cleanups.splice(0);\n\n // Create effect context for tracking\n const myContext = {\n fn,\n cleanups,\n execute: executeWithTracking,\n tracking: {}\n };\n\n // Set as current effect (for state.js to detect)\n // Save previous context to support nested effects/computed\n const previousEffect = currentEffect;\n currentEffect = myContext;\n isRunning = true;\n\n try {\n const onRead = (proxy, key, registerEffect) => {\n // Only the currently active effect (not a nested one) creates subscriptions\n if (currentEffect !== myContext) return;\n if (myContext.tracking[key]) return;\n myContext.tracking[key] = true;\n myContext.cleanups.push(registerEffect(key, myContext.execute));\n };\n withReadObserver(onRead, fn);\n } catch (error) {\n // On error, restore old subscriptions so the effect stays reactive\n cleanups.length = 0;\n cleanups.push(...oldCleanups);\n logError('[Lume.js effect] Error in effect:', error);\n throw error;\n } finally {\n // Restore previous context (not undefined) to support nesting\n currentEffect = previousEffect;\n isRunning = false;\n }\n\n // If fn() created new subscriptions, clean old ones.\n // If it didn't (e.g., early return), keep old subscriptions intact.\n if (cleanups.length > 0) {\n for (const cleanup of oldCleanups) cleanup();\n } else {\n cleanups.push(...oldCleanups);\n }\n };\n\n // Run immediately to collect initial dependencies\n executeWithTracking();\n }\n\n // Return cleanup function\n return () => {\n // while/pop is faster than forEach\n while (cleanups.length) cleanups.pop()();\n };\n}"],"names":["effect"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lume-js",
|
|
3
|
-
"version": "2.
|
|
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,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/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
|
}
|
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
|
+
];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a handler for any string attribute (href, src, title, alt, action, etc.)
|
|
3
|
+
* Sets the attribute value as a string. Removes the attribute when value is null/undefined.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} name - HTML attribute name (e.g., 'href', 'src', 'title')
|
|
6
|
+
* @returns {{ attr: string, apply: function }}
|
|
7
|
+
*/
|
|
8
|
+
export function stringAttr(name) {
|
|
9
|
+
return {
|
|
10
|
+
attr: `data-${name}`,
|
|
11
|
+
apply(el, val) {
|
|
12
|
+
if (val == null) el.removeAttribute(name);
|
|
13
|
+
else el.setAttribute(name, String(val));
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|