@bquery/bquery 1.1.2 → 1.3.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 +501 -323
- package/dist/batch-4LAvfLE7.js +13 -0
- package/dist/batch-4LAvfLE7.js.map +1 -0
- package/dist/component/component.d.ts +69 -0
- package/dist/component/component.d.ts.map +1 -0
- package/dist/component/html.d.ts +35 -0
- package/dist/component/html.d.ts.map +1 -0
- package/dist/component/index.d.ts +3 -126
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/props.d.ts +18 -0
- package/dist/component/props.d.ts.map +1 -0
- package/dist/component/types.d.ts +77 -0
- package/dist/component/types.d.ts.map +1 -0
- package/dist/component.es.mjs +90 -59
- package/dist/component.es.mjs.map +1 -1
- package/dist/core/collection.d.ts +36 -0
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/dom.d.ts +6 -0
- package/dist/core/dom.d.ts.map +1 -0
- package/dist/core/element.d.ts +8 -0
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/utils/array.d.ts +74 -0
- package/dist/core/utils/array.d.ts.map +1 -0
- package/dist/core/utils/function.d.ts +70 -0
- package/dist/core/utils/function.d.ts.map +1 -0
- package/dist/core/utils/index.d.ts +70 -0
- package/dist/core/utils/index.d.ts.map +1 -0
- package/dist/core/utils/misc.d.ts +63 -0
- package/dist/core/utils/misc.d.ts.map +1 -0
- package/dist/core/utils/number.d.ts +65 -0
- package/dist/core/utils/number.d.ts.map +1 -0
- package/dist/core/utils/object.d.ts +133 -0
- package/dist/core/utils/object.d.ts.map +1 -0
- package/dist/core/utils/string.d.ts +80 -0
- package/dist/core/utils/string.d.ts.map +1 -0
- package/dist/core/utils/type-guards.d.ts +79 -0
- package/dist/core/utils/type-guards.d.ts.map +1 -0
- package/dist/core-COenAZjD.js +145 -0
- package/dist/core-COenAZjD.js.map +1 -0
- package/dist/core.es.mjs +411 -448
- package/dist/core.es.mjs.map +1 -1
- package/dist/full.d.ts +8 -2
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +86 -40
- package/dist/full.es.mjs.map +1 -1
- package/dist/full.iife.js +6 -1
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +6 -1
- package/dist/full.umd.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.mjs +137 -44
- package/dist/index.es.mjs.map +1 -1
- package/dist/motion/animate.d.ts +25 -0
- package/dist/motion/animate.d.ts.map +1 -0
- package/dist/motion/easing.d.ts +30 -0
- package/dist/motion/easing.d.ts.map +1 -0
- package/dist/motion/flip.d.ts +55 -0
- package/dist/motion/flip.d.ts.map +1 -0
- package/dist/motion/index.d.ts +11 -138
- package/dist/motion/index.d.ts.map +1 -1
- package/dist/motion/keyframes.d.ts +21 -0
- package/dist/motion/keyframes.d.ts.map +1 -0
- package/dist/motion/reduced-motion.d.ts +12 -0
- package/dist/motion/reduced-motion.d.ts.map +1 -0
- package/dist/motion/scroll.d.ts +15 -0
- package/dist/motion/scroll.d.ts.map +1 -0
- package/dist/motion/spring.d.ts +42 -0
- package/dist/motion/spring.d.ts.map +1 -0
- package/dist/motion/stagger.d.ts +22 -0
- package/dist/motion/stagger.d.ts.map +1 -0
- package/dist/motion/timeline.d.ts +21 -0
- package/dist/motion/timeline.d.ts.map +1 -0
- package/dist/motion/transition.d.ts +22 -0
- package/dist/motion/transition.d.ts.map +1 -0
- package/dist/motion/types.d.ts +182 -0
- package/dist/motion/types.d.ts.map +1 -0
- package/dist/motion.es.mjs +320 -61
- package/dist/motion.es.mjs.map +1 -1
- package/dist/persisted-Dz_ryNuC.js +278 -0
- package/dist/persisted-Dz_ryNuC.js.map +1 -0
- package/dist/reactive/batch.d.ts +13 -0
- package/dist/reactive/batch.d.ts.map +1 -0
- package/dist/reactive/computed.d.ts +50 -0
- package/dist/reactive/computed.d.ts.map +1 -0
- package/dist/reactive/core.d.ts +60 -0
- package/dist/reactive/core.d.ts.map +1 -0
- package/dist/reactive/effect.d.ts +15 -0
- package/dist/reactive/effect.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/internals.d.ts +36 -0
- package/dist/reactive/internals.d.ts.map +1 -0
- package/dist/reactive/linked.d.ts +36 -0
- package/dist/reactive/linked.d.ts.map +1 -0
- package/dist/reactive/persisted.d.ts +14 -0
- package/dist/reactive/persisted.d.ts.map +1 -0
- package/dist/reactive/readonly.d.ts +26 -0
- package/dist/reactive/readonly.d.ts.map +1 -0
- package/dist/reactive/signal.d.ts +13 -305
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive/type-guards.d.ts +20 -0
- package/dist/reactive/type-guards.d.ts.map +1 -0
- package/dist/reactive/untrack.d.ts +29 -0
- package/dist/reactive/untrack.d.ts.map +1 -0
- package/dist/reactive/watch.d.ts +42 -0
- package/dist/reactive/watch.d.ts.map +1 -0
- package/dist/reactive.es.mjs +30 -154
- package/dist/reactive.es.mjs.map +1 -1
- package/dist/router/index.d.ts +41 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/links.d.ts +44 -0
- package/dist/router/links.d.ts.map +1 -0
- package/dist/router/match.d.ts +20 -0
- package/dist/router/match.d.ts.map +1 -0
- package/dist/router/navigation.d.ts +45 -0
- package/dist/router/navigation.d.ts.map +1 -0
- package/dist/router/query.d.ts +16 -0
- package/dist/router/query.d.ts.map +1 -0
- package/dist/router/router.d.ts +34 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/state.d.ts +27 -0
- package/dist/router/state.d.ts.map +1 -0
- package/dist/router/types.d.ts +88 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/router/utils.d.ts +65 -0
- package/dist/router/utils.d.ts.map +1 -0
- package/dist/router.es.mjs +202 -0
- package/dist/router.es.mjs.map +1 -0
- package/dist/sanitize-1FBEPAFH.js +272 -0
- package/dist/sanitize-1FBEPAFH.js.map +1 -0
- package/dist/security/constants.d.ts +42 -0
- package/dist/security/constants.d.ts.map +1 -0
- package/dist/security/csp.d.ts +24 -0
- package/dist/security/csp.d.ts.map +1 -0
- package/dist/security/index.d.ts +4 -2
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/sanitize-core.d.ts +13 -0
- package/dist/security/sanitize-core.d.ts.map +1 -0
- package/dist/security/sanitize.d.ts +5 -57
- package/dist/security/sanitize.d.ts.map +1 -1
- package/dist/security/trusted-types.d.ts +25 -0
- package/dist/security/trusted-types.d.ts.map +1 -0
- package/dist/security/types.d.ts +36 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security.es.mjs +50 -277
- package/dist/security.es.mjs.map +1 -1
- package/dist/store/create-store.d.ts +15 -0
- package/dist/store/create-store.d.ts.map +1 -0
- package/dist/store/define-store.d.ts +28 -0
- package/dist/store/define-store.d.ts.map +1 -0
- package/dist/store/devtools.d.ts +22 -0
- package/dist/store/devtools.d.ts.map +1 -0
- package/dist/store/index.d.ts +12 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/mapping.d.ts +28 -0
- package/dist/store/mapping.d.ts.map +1 -0
- package/dist/store/persisted.d.ts +13 -0
- package/dist/store/persisted.d.ts.map +1 -0
- package/dist/store/plugins.d.ts +13 -0
- package/dist/store/plugins.d.ts.map +1 -0
- package/dist/store/registry.d.ts +28 -0
- package/dist/store/registry.d.ts.map +1 -0
- package/dist/store/types.d.ts +71 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/utils.d.ts +28 -0
- package/dist/store/utils.d.ts.map +1 -0
- package/dist/store/watch.d.ts +23 -0
- package/dist/store/watch.d.ts.map +1 -0
- package/dist/store.es.mjs +27 -0
- package/dist/store.es.mjs.map +1 -0
- package/dist/type-guards-DRma3-Kc.js +16 -0
- package/dist/type-guards-DRma3-Kc.js.map +1 -0
- package/dist/untrack-BuEQKH7_.js +6 -0
- package/dist/untrack-BuEQKH7_.js.map +1 -0
- package/dist/view/directives/bind.d.ts +7 -0
- package/dist/view/directives/bind.d.ts.map +1 -0
- package/dist/view/directives/class.d.ts +8 -0
- package/dist/view/directives/class.d.ts.map +1 -0
- package/dist/view/directives/for.d.ts +23 -0
- package/dist/view/directives/for.d.ts.map +1 -0
- package/dist/view/directives/html.d.ts +7 -0
- package/dist/view/directives/html.d.ts.map +1 -0
- package/dist/view/directives/if.d.ts +7 -0
- package/dist/view/directives/if.d.ts.map +1 -0
- package/dist/view/directives/index.d.ts +12 -0
- package/dist/view/directives/index.d.ts.map +1 -0
- package/dist/view/directives/model.d.ts +7 -0
- package/dist/view/directives/model.d.ts.map +1 -0
- package/dist/view/directives/on.d.ts +7 -0
- package/dist/view/directives/on.d.ts.map +1 -0
- package/dist/view/directives/ref.d.ts +7 -0
- package/dist/view/directives/ref.d.ts.map +1 -0
- package/dist/view/directives/show.d.ts +7 -0
- package/dist/view/directives/show.d.ts.map +1 -0
- package/dist/view/directives/style.d.ts +7 -0
- package/dist/view/directives/style.d.ts.map +1 -0
- package/dist/view/directives/text.d.ts +7 -0
- package/dist/view/directives/text.d.ts.map +1 -0
- package/dist/view/evaluate.d.ts +43 -0
- package/dist/view/evaluate.d.ts.map +1 -0
- package/dist/view/index.d.ts +111 -0
- package/dist/view/index.d.ts.map +1 -0
- package/dist/view/mount.d.ts +69 -0
- package/dist/view/mount.d.ts.map +1 -0
- package/dist/view/process.d.ts +26 -0
- package/dist/view/process.d.ts.map +1 -0
- package/dist/view/types.d.ts +36 -0
- package/dist/view/types.d.ts.map +1 -0
- package/dist/view.es.mjs +426 -0
- package/dist/view.es.mjs.map +1 -0
- package/dist/watch-CXyaBC_9.js +58 -0
- package/dist/watch-CXyaBC_9.js.map +1 -0
- package/package.json +26 -14
- package/src/component/component.ts +289 -0
- package/src/component/html.ts +53 -0
- package/src/component/index.ts +40 -414
- package/src/component/props.ts +116 -0
- package/src/component/types.ts +85 -0
- package/src/core/collection.ts +588 -454
- package/src/core/dom.ts +38 -0
- package/src/core/element.ts +746 -740
- package/src/core/index.ts +43 -0
- package/src/core/utils/array.ts +102 -0
- package/src/core/utils/function.ts +110 -0
- package/src/core/utils/index.ts +83 -0
- package/src/core/utils/misc.ts +82 -0
- package/src/core/utils/number.ts +78 -0
- package/src/core/utils/object.ts +206 -0
- package/src/core/utils/string.ts +112 -0
- package/src/core/utils/type-guards.ts +112 -0
- package/src/full.ts +187 -106
- package/src/index.ts +36 -27
- package/src/motion/animate.ts +113 -0
- package/src/motion/easing.ts +40 -0
- package/src/motion/flip.ts +176 -0
- package/src/motion/index.ts +41 -358
- package/src/motion/keyframes.ts +46 -0
- package/src/motion/reduced-motion.ts +17 -0
- package/src/motion/scroll.ts +57 -0
- package/src/motion/spring.ts +150 -0
- package/src/motion/stagger.ts +43 -0
- package/src/motion/timeline.ts +246 -0
- package/src/motion/transition.ts +51 -0
- package/src/motion/types.ts +198 -0
- package/src/reactive/batch.ts +22 -0
- package/src/reactive/computed.ts +92 -0
- package/src/reactive/core.ts +93 -0
- package/src/reactive/effect.ts +43 -0
- package/src/reactive/index.ts +23 -22
- package/src/reactive/internals.ts +105 -0
- package/src/reactive/linked.ts +56 -0
- package/src/reactive/persisted.ts +74 -0
- package/src/reactive/readonly.ts +35 -0
- package/src/reactive/signal.ts +20 -506
- package/src/reactive/type-guards.ts +22 -0
- package/src/reactive/untrack.ts +31 -0
- package/src/reactive/watch.ts +73 -0
- package/src/router/index.ts +41 -0
- package/src/router/links.ts +130 -0
- package/src/router/match.ts +106 -0
- package/src/router/navigation.ts +71 -0
- package/src/router/query.ts +35 -0
- package/src/router/router.ts +211 -0
- package/src/router/state.ts +46 -0
- package/src/router/types.ts +93 -0
- package/src/router/utils.ts +116 -0
- package/src/security/constants.ts +209 -0
- package/src/security/csp.ts +77 -0
- package/src/security/index.ts +4 -12
- package/src/security/sanitize-core.ts +343 -0
- package/src/security/sanitize.ts +66 -625
- package/src/security/trusted-types.ts +69 -0
- package/src/security/types.ts +40 -0
- package/src/store/create-store.ts +329 -0
- package/src/store/define-store.ts +48 -0
- package/src/store/devtools.ts +45 -0
- package/src/store/index.ts +22 -0
- package/src/store/mapping.ts +73 -0
- package/src/store/persisted.ts +61 -0
- package/src/store/plugins.ts +32 -0
- package/src/store/registry.ts +51 -0
- package/src/store/types.ts +94 -0
- package/src/store/utils.ts +141 -0
- package/src/store/watch.ts +52 -0
- package/src/view/directives/bind.ts +23 -0
- package/src/view/directives/class.ts +70 -0
- package/src/view/directives/for.ts +275 -0
- package/src/view/directives/html.ts +19 -0
- package/src/view/directives/if.ts +30 -0
- package/src/view/directives/index.ts +11 -0
- package/src/view/directives/model.ts +56 -0
- package/src/view/directives/on.ts +41 -0
- package/src/view/directives/ref.ts +41 -0
- package/src/view/directives/show.ts +26 -0
- package/src/view/directives/style.ts +47 -0
- package/src/view/directives/text.ts +15 -0
- package/src/view/evaluate.ts +274 -0
- package/src/view/index.ts +112 -0
- package/src/view/mount.ts +200 -0
- package/src/view/process.ts +92 -0
- package/src/view/types.ts +44 -0
- package/dist/core/utils.d.ts +0 -313
- package/dist/core/utils.d.ts.map +0 -1
- package/src/core/utils.ts +0 -444
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { isComputed, isSignal, type Signal } from '../reactive/index';
|
|
2
|
+
import type { BindingContext } from './types';
|
|
3
|
+
|
|
4
|
+
/** Maximum number of cached expression functions before LRU eviction */
|
|
5
|
+
const MAX_CACHE_SIZE = 500;
|
|
6
|
+
|
|
7
|
+
/** Compiled function type for expression evaluation */
|
|
8
|
+
type CompiledFn = (ctx: BindingContext) => unknown;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Simple LRU cache for compiled expression functions.
|
|
12
|
+
* Uses Map's insertion order to track recency - accessed items are re-inserted.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
class LRUCache {
|
|
16
|
+
private cache = new Map<string, CompiledFn>();
|
|
17
|
+
private maxSize: number;
|
|
18
|
+
|
|
19
|
+
constructor(maxSize: number) {
|
|
20
|
+
this.maxSize = maxSize;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get(key: string): CompiledFn | undefined {
|
|
24
|
+
const value = this.cache.get(key);
|
|
25
|
+
if (value !== undefined) {
|
|
26
|
+
// Move to end (most recently used) by re-inserting
|
|
27
|
+
this.cache.delete(key);
|
|
28
|
+
this.cache.set(key, value);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
set(key: string, value: CompiledFn): void {
|
|
34
|
+
// Delete first if exists to update insertion order
|
|
35
|
+
if (this.cache.has(key)) {
|
|
36
|
+
this.cache.delete(key);
|
|
37
|
+
} else if (this.cache.size >= this.maxSize) {
|
|
38
|
+
// Evict oldest (first) entry
|
|
39
|
+
const oldest = this.cache.keys().next().value;
|
|
40
|
+
if (oldest !== undefined) {
|
|
41
|
+
this.cache.delete(oldest);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
this.cache.set(key, value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clear(): void {
|
|
48
|
+
this.cache.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get size(): number {
|
|
52
|
+
return this.cache.size;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** LRU cache for compiled evaluate functions, keyed by expression string */
|
|
57
|
+
const evaluateCache = new LRUCache(MAX_CACHE_SIZE);
|
|
58
|
+
|
|
59
|
+
/** LRU cache for compiled evaluateRaw functions, keyed by expression string */
|
|
60
|
+
const evaluateRawCache = new LRUCache(MAX_CACHE_SIZE);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Clears all cached compiled expression functions.
|
|
64
|
+
* Call this when unmounting views or to free memory after heavy template usage.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* import { clearExpressionCache } from 'bquery/view';
|
|
69
|
+
*
|
|
70
|
+
* // After destroying a view or when cleaning up
|
|
71
|
+
* clearExpressionCache();
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export const clearExpressionCache = (): void => {
|
|
75
|
+
evaluateCache.clear();
|
|
76
|
+
evaluateRawCache.clear();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a proxy that lazily unwraps signals/computed only when accessed.
|
|
81
|
+
* This avoids subscribing to signals that aren't referenced in the expression.
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
const createLazyContext = (context: BindingContext): BindingContext =>
|
|
85
|
+
new Proxy(context, {
|
|
86
|
+
get(target, prop: string | symbol) {
|
|
87
|
+
// Only handle string keys for BindingContext indexing
|
|
88
|
+
if (typeof prop !== 'string') {
|
|
89
|
+
return Reflect.get(target, prop);
|
|
90
|
+
}
|
|
91
|
+
const value = target[prop];
|
|
92
|
+
// Auto-unwrap signals/computed only when actually accessed
|
|
93
|
+
if (isSignal(value) || isComputed(value)) {
|
|
94
|
+
return (value as Signal<unknown>).value;
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
},
|
|
98
|
+
has(target, prop: string | symbol) {
|
|
99
|
+
// Required for `with` statement to resolve identifiers correctly
|
|
100
|
+
if (typeof prop !== 'string') {
|
|
101
|
+
return Reflect.has(target, prop);
|
|
102
|
+
}
|
|
103
|
+
return prop in target;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Evaluates an expression in the given context using `new Function()`.
|
|
109
|
+
*
|
|
110
|
+
* Signals and computed values in the context are lazily unwrapped only when
|
|
111
|
+
* accessed by the expression, avoiding unnecessary subscriptions to unused values.
|
|
112
|
+
*
|
|
113
|
+
* @security **WARNING:** This function uses dynamic code execution via `new Function()`.
|
|
114
|
+
* - NEVER pass expressions derived from user input or untrusted sources
|
|
115
|
+
* - Expressions should only come from developer-controlled templates
|
|
116
|
+
* - Malicious expressions can access and exfiltrate context data
|
|
117
|
+
* - Consider this equivalent to `eval()` in terms of security implications
|
|
118
|
+
*
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
export const evaluate = <T = unknown>(expression: string, context: BindingContext): T => {
|
|
122
|
+
try {
|
|
123
|
+
// Create a proxy that lazily unwraps signals/computed on access
|
|
124
|
+
const lazyContext = createLazyContext(context);
|
|
125
|
+
|
|
126
|
+
// Use cached function or compile and cache a new one
|
|
127
|
+
let fn = evaluateCache.get(expression);
|
|
128
|
+
if (!fn) {
|
|
129
|
+
// Use `with` to enable direct property access from proxy scope.
|
|
130
|
+
// Note: `new Function()` runs in non-strict mode, so `with` is allowed.
|
|
131
|
+
fn = new Function('$ctx', `with($ctx) { return (${expression}); }`) as (
|
|
132
|
+
ctx: BindingContext
|
|
133
|
+
) => unknown;
|
|
134
|
+
evaluateCache.set(expression, fn);
|
|
135
|
+
}
|
|
136
|
+
return fn(lazyContext) as T;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(`bQuery view: Error evaluating "${expression}"`, error);
|
|
139
|
+
return undefined as T;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Evaluates an expression and returns the raw value (for signal access).
|
|
145
|
+
*
|
|
146
|
+
* @security **WARNING:** Uses dynamic code execution. See {@link evaluate} for security notes.
|
|
147
|
+
* @internal
|
|
148
|
+
*/
|
|
149
|
+
export const evaluateRaw = <T = unknown>(expression: string, context: BindingContext): T => {
|
|
150
|
+
try {
|
|
151
|
+
// Use cached function or compile and cache a new one
|
|
152
|
+
let fn = evaluateRawCache.get(expression);
|
|
153
|
+
if (!fn) {
|
|
154
|
+
// Use `with` to enable direct property access from context scope.
|
|
155
|
+
// Unlike `evaluate`, we don't use a lazy proxy - values are accessed directly.
|
|
156
|
+
fn = new Function('$ctx', `with($ctx) { return (${expression}); }`) as (
|
|
157
|
+
ctx: BindingContext
|
|
158
|
+
) => unknown;
|
|
159
|
+
evaluateRawCache.set(expression, fn);
|
|
160
|
+
}
|
|
161
|
+
return fn(context) as T;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(`bQuery view: Error evaluating "${expression}"`, error);
|
|
164
|
+
return undefined as T;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Parses object expression like "{ active: isActive, disabled: !enabled }".
|
|
170
|
+
* Handles nested structures like function calls, arrays, and template literals.
|
|
171
|
+
* @internal
|
|
172
|
+
*/
|
|
173
|
+
export const parseObjectExpression = (expression: string): Record<string, string> => {
|
|
174
|
+
const result: Record<string, string> = {};
|
|
175
|
+
|
|
176
|
+
// Remove outer braces and trim
|
|
177
|
+
const inner = expression
|
|
178
|
+
.trim()
|
|
179
|
+
.replace(/^\{|\}$/g, '')
|
|
180
|
+
.trim();
|
|
181
|
+
if (!inner) return result;
|
|
182
|
+
|
|
183
|
+
// Split by comma at depth 0, respecting strings and nesting
|
|
184
|
+
const parts: string[] = [];
|
|
185
|
+
let current = '';
|
|
186
|
+
let depth = 0;
|
|
187
|
+
let inString: string | null = null;
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < inner.length; i++) {
|
|
190
|
+
const char = inner[i];
|
|
191
|
+
const prevChar = i > 0 ? inner[i - 1] : '';
|
|
192
|
+
|
|
193
|
+
// Handle string literals (including escape sequences)
|
|
194
|
+
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
|
195
|
+
if (inString === null) {
|
|
196
|
+
inString = char;
|
|
197
|
+
} else if (inString === char) {
|
|
198
|
+
inString = null;
|
|
199
|
+
}
|
|
200
|
+
current += char;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Skip if inside string
|
|
205
|
+
if (inString !== null) {
|
|
206
|
+
current += char;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Track nesting depth for parentheses, brackets, and braces
|
|
211
|
+
if (char === '(' || char === '[' || char === '{') {
|
|
212
|
+
depth++;
|
|
213
|
+
current += char;
|
|
214
|
+
} else if (char === ')' || char === ']' || char === '}') {
|
|
215
|
+
depth--;
|
|
216
|
+
current += char;
|
|
217
|
+
} else if (char === ',' && depth === 0) {
|
|
218
|
+
// Top-level comma - split point
|
|
219
|
+
parts.push(current.trim());
|
|
220
|
+
current = '';
|
|
221
|
+
} else {
|
|
222
|
+
current += char;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Add the last part
|
|
227
|
+
if (current.trim()) {
|
|
228
|
+
parts.push(current.trim());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Parse each part to extract key and value
|
|
232
|
+
for (const part of parts) {
|
|
233
|
+
// Find the first colon at depth 0 (to handle ternary operators in values)
|
|
234
|
+
let colonIndex = -1;
|
|
235
|
+
let partDepth = 0;
|
|
236
|
+
let partInString: string | null = null;
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < part.length; i++) {
|
|
239
|
+
const char = part[i];
|
|
240
|
+
const prevChar = i > 0 ? part[i - 1] : '';
|
|
241
|
+
|
|
242
|
+
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
|
243
|
+
if (partInString === null) {
|
|
244
|
+
partInString = char;
|
|
245
|
+
} else if (partInString === char) {
|
|
246
|
+
partInString = null;
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (partInString !== null) continue;
|
|
252
|
+
|
|
253
|
+
if (char === '(' || char === '[' || char === '{') {
|
|
254
|
+
partDepth++;
|
|
255
|
+
} else if (char === ')' || char === ']' || char === '}') {
|
|
256
|
+
partDepth--;
|
|
257
|
+
} else if (char === ':' && partDepth === 0) {
|
|
258
|
+
colonIndex = i;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (colonIndex > -1) {
|
|
264
|
+
const key = part
|
|
265
|
+
.slice(0, colonIndex)
|
|
266
|
+
.trim()
|
|
267
|
+
.replace(/^['"]|['"]$/g, '');
|
|
268
|
+
const value = part.slice(colonIndex + 1).trim();
|
|
269
|
+
result[key] = value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return result;
|
|
274
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative DOM bindings via data attributes.
|
|
3
|
+
*
|
|
4
|
+
* This module provides Vue/Svelte-style template directives without
|
|
5
|
+
* requiring a compiler. Bindings are evaluated at runtime using
|
|
6
|
+
* bQuery's reactive system. Features include:
|
|
7
|
+
* - Conditional rendering (bq-if)
|
|
8
|
+
* - List rendering (bq-for)
|
|
9
|
+
* - Two-way binding (bq-model)
|
|
10
|
+
* - Class binding (bq-class)
|
|
11
|
+
* - Text/HTML binding (bq-text, bq-html)
|
|
12
|
+
* - Attribute binding (bq-bind)
|
|
13
|
+
* - Event binding (bq-on)
|
|
14
|
+
*
|
|
15
|
+
* ## Security Considerations
|
|
16
|
+
*
|
|
17
|
+
* **WARNING:** This module uses `new Function()` to evaluate expressions at runtime.
|
|
18
|
+
* This is similar to Vue/Alpine's approach but carries inherent security risks:
|
|
19
|
+
*
|
|
20
|
+
* - **NEVER** use expressions derived from user input or untrusted sources
|
|
21
|
+
* - Expressions should only come from developer-controlled templates
|
|
22
|
+
* - The context object should not contain sensitive data that could be exfiltrated
|
|
23
|
+
* - For user-generated content, use static bindings with sanitized values instead
|
|
24
|
+
*
|
|
25
|
+
* Since bQuery is runtime-only (no build-time compilation), expressions are evaluated
|
|
26
|
+
* dynamically. If your application loads templates from external sources (APIs, databases),
|
|
27
|
+
* ensure they are trusted and validated before mounting.
|
|
28
|
+
*
|
|
29
|
+
* ## Content Security Policy (CSP) Compatibility
|
|
30
|
+
*
|
|
31
|
+
* **IMPORTANT:** This module requires `'unsafe-eval'` in your CSP `script-src` directive.
|
|
32
|
+
* The `new Function()` constructor used for expression evaluation will be blocked by
|
|
33
|
+
* strict CSP policies that omit `'unsafe-eval'`.
|
|
34
|
+
*
|
|
35
|
+
* ### Required CSP Header
|
|
36
|
+
* ```
|
|
37
|
+
* Content-Security-Policy: script-src 'self' 'unsafe-eval';
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* ### CSP-Strict Alternatives
|
|
41
|
+
*
|
|
42
|
+
* If your application requires a strict CSP without `'unsafe-eval'`, consider these alternatives:
|
|
43
|
+
*
|
|
44
|
+
* 1. **Use bQuery's core reactive system directly** - Bind signals to DOM elements manually
|
|
45
|
+
* using `effect()` without the view module's template directives:
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { signal, effect } from 'bquery/reactive';
|
|
48
|
+
* import { $ } from 'bquery';
|
|
49
|
+
*
|
|
50
|
+
* const count = signal(0);
|
|
51
|
+
* effect(() => {
|
|
52
|
+
* $('#counter').text(String(count.value));
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* 2. **Use bQuery's component module** - Web Components with typed props don't require
|
|
57
|
+
* dynamic expression evaluation:
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { component } from 'bquery/component';
|
|
60
|
+
* component('my-counter', {
|
|
61
|
+
* props: { count: { type: Number } },
|
|
62
|
+
* render: ({ props }) => `<span>${props.count}</span>`,
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* 3. **Pre-compile templates at build time** - Use a build step to transform bq-* attributes
|
|
67
|
+
* into static JavaScript (similar to Svelte/Vue SFC compilation). This is outside bQuery's
|
|
68
|
+
* scope but can be achieved with custom Vite/Rollup plugins.
|
|
69
|
+
*
|
|
70
|
+
* The view module is designed for rapid prototyping and applications where CSP flexibility
|
|
71
|
+
* is acceptable. For security-critical applications requiring strict CSP, use the alternatives above.
|
|
72
|
+
*
|
|
73
|
+
* @module bquery/view
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```html
|
|
77
|
+
* <div id="app">
|
|
78
|
+
* <input bq-model="name" />
|
|
79
|
+
* <p bq-text="greeting"></p>
|
|
80
|
+
* <ul>
|
|
81
|
+
* <li bq-for="item in items" bq-text="item.name"></li>
|
|
82
|
+
* </ul>
|
|
83
|
+
* <button bq-on:click="handleClick">Click me</button>
|
|
84
|
+
* <div bq-if="showDetails" bq-class="{ active: isActive }">
|
|
85
|
+
* Details here
|
|
86
|
+
* </div>
|
|
87
|
+
* </div>
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { mount } from 'bquery/view';
|
|
92
|
+
* import { signal } from 'bquery/reactive';
|
|
93
|
+
*
|
|
94
|
+
* mount('#app', {
|
|
95
|
+
* name: signal('World'),
|
|
96
|
+
* greeting: computed(() => `Hello, ${name.value}!`),
|
|
97
|
+
* items: signal([{ name: 'Item 1' }, { name: 'Item 2' }]),
|
|
98
|
+
* showDetails: signal(true),
|
|
99
|
+
* isActive: signal(false),
|
|
100
|
+
* handleClick: () => console.log('Clicked!'),
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
export { clearExpressionCache } from './evaluate';
|
|
106
|
+
export { createTemplate, mount } from './mount';
|
|
107
|
+
export type { BindingContext, MountOptions, View } from './types';
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Re-export reactive primitives for convenience.
|
|
111
|
+
*/
|
|
112
|
+
export { batch, computed, effect, signal } from '../reactive/index';
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { CleanupFn } from '../reactive/index';
|
|
2
|
+
import {
|
|
3
|
+
createForHandler,
|
|
4
|
+
handleBind,
|
|
5
|
+
handleClass,
|
|
6
|
+
handleHtml,
|
|
7
|
+
handleIf,
|
|
8
|
+
handleModel,
|
|
9
|
+
handleOn,
|
|
10
|
+
handleRef,
|
|
11
|
+
handleShow,
|
|
12
|
+
handleStyle,
|
|
13
|
+
handleText,
|
|
14
|
+
} from './directives/index';
|
|
15
|
+
import { processChildren, processElement, type DirectiveHandlers } from './process';
|
|
16
|
+
import type { BindingContext, MountOptions, View } from './types';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Mounts a reactive view to an element.
|
|
20
|
+
*
|
|
21
|
+
* @param selector - CSS selector or Element
|
|
22
|
+
* @param context - Binding context with signals, computed, and functions
|
|
23
|
+
* @param options - Mount options
|
|
24
|
+
* @returns The mounted View instance
|
|
25
|
+
*
|
|
26
|
+
* @security **WARNING:** Directive expressions (bq-text, bq-if, bq-on, etc.) are evaluated
|
|
27
|
+
* using `new Function()` at runtime. This means:
|
|
28
|
+
* - Template attributes must come from trusted sources only
|
|
29
|
+
* - NEVER load templates containing bq-* attributes from user input or untrusted APIs
|
|
30
|
+
* - If you must use external templates, validate/sanitize attribute values first
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { mount } from 'bquery/view';
|
|
35
|
+
* import { signal, computed } from 'bquery/reactive';
|
|
36
|
+
*
|
|
37
|
+
* const name = signal('World');
|
|
38
|
+
* const greeting = computed(() => `Hello, ${name.value}!`);
|
|
39
|
+
* const items = signal([
|
|
40
|
+
* { id: 1, text: 'Item 1' },
|
|
41
|
+
* { id: 2, text: 'Item 2' },
|
|
42
|
+
* ]);
|
|
43
|
+
*
|
|
44
|
+
* const view = mount('#app', {
|
|
45
|
+
* name,
|
|
46
|
+
* greeting,
|
|
47
|
+
* items,
|
|
48
|
+
* addItem: () => {
|
|
49
|
+
* items.value = [...items.value, { id: Date.now(), text: 'New Item' }];
|
|
50
|
+
* },
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* // Later, cleanup
|
|
54
|
+
* view.destroy();
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export const mount = (
|
|
58
|
+
selector: string | Element,
|
|
59
|
+
context: BindingContext,
|
|
60
|
+
options: MountOptions = {}
|
|
61
|
+
): View => {
|
|
62
|
+
const { prefix = 'bq', sanitize = true } = options;
|
|
63
|
+
|
|
64
|
+
const el = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
65
|
+
|
|
66
|
+
if (!el) {
|
|
67
|
+
throw new Error(`bQuery view: Element "${selector}" not found.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Reject if root element has bq-for directive
|
|
71
|
+
// bq-for replaces the element with a placeholder comment, which would leave View.el detached
|
|
72
|
+
if (el.hasAttribute(`${prefix}-for`)) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`bQuery view: Cannot mount on element with ${prefix}-for directive. ` +
|
|
75
|
+
`Wrap the ${prefix}-for element in a container instead.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const cleanups: CleanupFn[] = [];
|
|
80
|
+
|
|
81
|
+
const handlers: DirectiveHandlers = {
|
|
82
|
+
text: handleText,
|
|
83
|
+
html: handleHtml(sanitize),
|
|
84
|
+
if: handleIf,
|
|
85
|
+
show: handleShow,
|
|
86
|
+
class: handleClass,
|
|
87
|
+
style: handleStyle,
|
|
88
|
+
model: handleModel,
|
|
89
|
+
ref: handleRef,
|
|
90
|
+
for: createForHandler({
|
|
91
|
+
prefix,
|
|
92
|
+
processElement: (node, nodeContext, nodePrefix, nodeCleanups) =>
|
|
93
|
+
processElement(node, nodeContext, nodePrefix, nodeCleanups, handlers),
|
|
94
|
+
processChildren: (node, nodeContext, nodePrefix, nodeCleanups) =>
|
|
95
|
+
processChildren(node, nodeContext, nodePrefix, nodeCleanups, handlers),
|
|
96
|
+
}),
|
|
97
|
+
bind: handleBind,
|
|
98
|
+
on: handleOn,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const processWithHandlers = (
|
|
102
|
+
node: Element,
|
|
103
|
+
nodeContext: BindingContext,
|
|
104
|
+
nodeCleanups: CleanupFn[]
|
|
105
|
+
) => {
|
|
106
|
+
// Check if element has bq-for before processing
|
|
107
|
+
// bq-for replaces the element and handles its children internally
|
|
108
|
+
const hasFor = node.hasAttribute(`${prefix}-for`);
|
|
109
|
+
|
|
110
|
+
processElement(node, nodeContext, prefix, nodeCleanups, handlers);
|
|
111
|
+
|
|
112
|
+
// Skip processChildren if bq-for was on this element - it handles children itself
|
|
113
|
+
if (!hasFor) {
|
|
114
|
+
processChildren(node, nodeContext, prefix, nodeCleanups, handlers);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Process the root element and its children
|
|
119
|
+
processWithHandlers(el, context, cleanups);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
el,
|
|
123
|
+
context,
|
|
124
|
+
|
|
125
|
+
update: (newContext: Partial<BindingContext>) => {
|
|
126
|
+
Object.assign(context, newContext);
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
destroy: () => {
|
|
130
|
+
for (const cleanup of cleanups) {
|
|
131
|
+
cleanup();
|
|
132
|
+
}
|
|
133
|
+
cleanups.length = 0;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Creates a reactive template function.
|
|
140
|
+
*
|
|
141
|
+
* @param template - HTML template string
|
|
142
|
+
* @returns A function that creates a mounted element with the given context
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* import { createTemplate } from 'bquery/view';
|
|
147
|
+
* import { signal } from 'bquery/reactive';
|
|
148
|
+
*
|
|
149
|
+
* const TodoItem = createTemplate(`
|
|
150
|
+
* <li bq-class="{ completed: done }">
|
|
151
|
+
* <input type="checkbox" bq-model="done" />
|
|
152
|
+
* <span bq-text="text"></span>
|
|
153
|
+
* </li>
|
|
154
|
+
* `);
|
|
155
|
+
*
|
|
156
|
+
* const item = TodoItem({
|
|
157
|
+
* done: signal(false),
|
|
158
|
+
* text: 'Buy groceries',
|
|
159
|
+
* });
|
|
160
|
+
*
|
|
161
|
+
* document.querySelector('#list').append(item.el);
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export const createTemplate = (
|
|
165
|
+
template: string,
|
|
166
|
+
options: MountOptions = {}
|
|
167
|
+
): ((context: BindingContext) => View) => {
|
|
168
|
+
return (context: BindingContext) => {
|
|
169
|
+
const container = document.createElement('div');
|
|
170
|
+
container.innerHTML = template.trim();
|
|
171
|
+
|
|
172
|
+
const el = container.firstElementChild;
|
|
173
|
+
if (!el) {
|
|
174
|
+
throw new Error('bQuery view: Template must contain a single root element.');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// We know at least one element exists (firstElementChild is not null above)
|
|
178
|
+
// Reject if there are multiple root elements
|
|
179
|
+
if (container.childElementCount > 1) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`bQuery view: Template must contain exactly one root element, found ${container.childElementCount}.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { prefix = 'bq' } = options;
|
|
186
|
+
// Reject templates with bq-for or bq-if on the root element
|
|
187
|
+
// These directives replace the element with a placeholder comment, which would leave View.el detached
|
|
188
|
+
// Since processing happens while el is still in the temporary container, the placeholder
|
|
189
|
+
// would remain there while view.el is inserted elsewhere, causing desync on future toggles
|
|
190
|
+
if (el.hasAttribute(`${prefix}-for`) || el.hasAttribute(`${prefix}-if`)) {
|
|
191
|
+
const directive = el.hasAttribute(`${prefix}-for`) ? 'for' : 'if';
|
|
192
|
+
throw new Error(
|
|
193
|
+
`bQuery view: Template root element cannot have ${prefix}-${directive} directive. ` +
|
|
194
|
+
`Wrap the ${prefix}-${directive} element in a container instead.`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return mount(el, context, options);
|
|
199
|
+
};
|
|
200
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { CleanupFn } from '../reactive/index';
|
|
2
|
+
import type { BindingContext, DirectiveHandler } from './types';
|
|
3
|
+
|
|
4
|
+
export type DirectiveHandlers = {
|
|
5
|
+
text: DirectiveHandler;
|
|
6
|
+
html: DirectiveHandler;
|
|
7
|
+
if: DirectiveHandler;
|
|
8
|
+
show: DirectiveHandler;
|
|
9
|
+
class: DirectiveHandler;
|
|
10
|
+
style: DirectiveHandler;
|
|
11
|
+
model: DirectiveHandler;
|
|
12
|
+
ref: DirectiveHandler;
|
|
13
|
+
for: DirectiveHandler;
|
|
14
|
+
bind: (attrName: string) => DirectiveHandler;
|
|
15
|
+
on: (eventName: string) => DirectiveHandler;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Processes a single element for directives.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export const processElement = (
|
|
23
|
+
el: Element,
|
|
24
|
+
context: BindingContext,
|
|
25
|
+
prefix: string,
|
|
26
|
+
cleanups: CleanupFn[],
|
|
27
|
+
handlers: DirectiveHandlers
|
|
28
|
+
): void => {
|
|
29
|
+
const attributes = Array.from(el.attributes);
|
|
30
|
+
|
|
31
|
+
for (const attr of attributes) {
|
|
32
|
+
const { name, value } = attr;
|
|
33
|
+
|
|
34
|
+
if (!name.startsWith(`${prefix}-`)) continue;
|
|
35
|
+
|
|
36
|
+
const directive = name.slice(prefix.length + 1); // Remove prefix and dash
|
|
37
|
+
|
|
38
|
+
// Handle bq-for specially (creates new scope)
|
|
39
|
+
if (directive === 'for') {
|
|
40
|
+
handlers.for(el, value, context, cleanups);
|
|
41
|
+
return; // Don't process children, bq-for handles it
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Handle other directives
|
|
45
|
+
if (directive === 'text') {
|
|
46
|
+
handlers.text(el, value, context, cleanups);
|
|
47
|
+
} else if (directive === 'html') {
|
|
48
|
+
handlers.html(el, value, context, cleanups);
|
|
49
|
+
} else if (directive === 'if') {
|
|
50
|
+
handlers.if(el, value, context, cleanups);
|
|
51
|
+
} else if (directive === 'show') {
|
|
52
|
+
handlers.show(el, value, context, cleanups);
|
|
53
|
+
} else if (directive === 'class') {
|
|
54
|
+
handlers.class(el, value, context, cleanups);
|
|
55
|
+
} else if (directive === 'style') {
|
|
56
|
+
handlers.style(el, value, context, cleanups);
|
|
57
|
+
} else if (directive === 'model') {
|
|
58
|
+
handlers.model(el, value, context, cleanups);
|
|
59
|
+
} else if (directive === 'ref') {
|
|
60
|
+
handlers.ref(el, value, context, cleanups);
|
|
61
|
+
} else if (directive.startsWith('bind:')) {
|
|
62
|
+
const attrName = directive.slice(5);
|
|
63
|
+
handlers.bind(attrName)(el, value, context, cleanups);
|
|
64
|
+
} else if (directive.startsWith('on:')) {
|
|
65
|
+
const eventName = directive.slice(3);
|
|
66
|
+
handlers.on(eventName)(el, value, context, cleanups);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Recursively processes children of an element.
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
export const processChildren = (
|
|
76
|
+
el: Element,
|
|
77
|
+
context: BindingContext,
|
|
78
|
+
prefix: string,
|
|
79
|
+
cleanups: CleanupFn[],
|
|
80
|
+
handlers: DirectiveHandlers
|
|
81
|
+
): void => {
|
|
82
|
+
const children = Array.from(el.children);
|
|
83
|
+
for (const child of children) {
|
|
84
|
+
// Skip if element has bq-for (handled separately)
|
|
85
|
+
if (!child.hasAttribute(`${prefix}-for`)) {
|
|
86
|
+
processElement(child, context, prefix, cleanups, handlers);
|
|
87
|
+
processChildren(child, context, prefix, cleanups, handlers);
|
|
88
|
+
} else {
|
|
89
|
+
processElement(child, context, prefix, cleanups, handlers);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|