@bquery/bquery 1.2.0 → 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 -427
- 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 +2 -2
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +87 -64
- package/dist/full.es.mjs.map +1 -1
- package/dist/full.iife.js +2 -2
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +2 -2
- package/dist/full.umd.js.map +1 -1
- package/dist/index.es.mjs +138 -68
- 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 -312
- 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 -163
- package/dist/reactive.es.mjs.map +1 -1
- package/dist/router/index.d.ts +6 -252
- package/dist/router/index.d.ts.map +1 -1
- 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 +168 -132
- package/dist/router.es.mjs.map +1 -1
- 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 +10 -286
- package/dist/store/index.d.ts.map +1 -1
- 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 +22 -224
- package/dist/store.es.mjs.map +1 -1
- 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 +3 -93
- package/dist/view/index.d.ts.map +1 -1
- 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 +368 -267
- package/dist/view.es.mjs.map +1 -1
- package/dist/watch-CXyaBC_9.js +58 -0
- package/dist/watch-CXyaBC_9.js.map +1 -0
- package/package.json +132 -132
- 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 -150
- package/src/index.ts +36 -36
- 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 -520
- 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 -718
- 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 -848
- 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 -1041
- 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,275 @@
|
|
|
1
|
+
import { effect, signal, type CleanupFn, type Signal } from '../../reactive/index';
|
|
2
|
+
import { evaluate } from '../evaluate';
|
|
3
|
+
import type { BindingContext, DirectiveHandler } from '../types';
|
|
4
|
+
|
|
5
|
+
type ProcessElementFn = (
|
|
6
|
+
el: Element,
|
|
7
|
+
context: BindingContext,
|
|
8
|
+
prefix: string,
|
|
9
|
+
cleanups: CleanupFn[]
|
|
10
|
+
) => void;
|
|
11
|
+
|
|
12
|
+
type ProcessChildrenFn = (
|
|
13
|
+
el: Element,
|
|
14
|
+
context: BindingContext,
|
|
15
|
+
prefix: string,
|
|
16
|
+
cleanups: CleanupFn[]
|
|
17
|
+
) => void;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Represents a rendered item in bq-for with its DOM element and associated cleanup functions.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
type RenderedItem = {
|
|
24
|
+
key: unknown;
|
|
25
|
+
element: Element;
|
|
26
|
+
cleanups: CleanupFn[];
|
|
27
|
+
item: unknown;
|
|
28
|
+
index: number;
|
|
29
|
+
itemSignal: Signal<unknown>; // Reactive item value for item-dependent bindings
|
|
30
|
+
indexSignal: Signal<number> | null; // Reactive index for index-dependent bindings
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extracts a key from an item using the key expression or falls back to index.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
const getItemKey = (
|
|
38
|
+
item: unknown,
|
|
39
|
+
index: number,
|
|
40
|
+
keyExpression: string | null,
|
|
41
|
+
itemName: string,
|
|
42
|
+
indexName: string | undefined,
|
|
43
|
+
context: BindingContext
|
|
44
|
+
): unknown => {
|
|
45
|
+
if (!keyExpression) {
|
|
46
|
+
return index; // Fallback to index-based keying
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const keyContext: BindingContext = {
|
|
50
|
+
...context,
|
|
51
|
+
[itemName]: item,
|
|
52
|
+
};
|
|
53
|
+
if (indexName) {
|
|
54
|
+
keyContext[indexName] = index;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return evaluate(keyExpression, keyContext);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Handles bq-for directive - list rendering with keyed reconciliation.
|
|
62
|
+
*
|
|
63
|
+
* Supports optional `:key` attribute for efficient DOM reuse:
|
|
64
|
+
* ```html
|
|
65
|
+
* <li bq-for="item in items" :key="item.id">...</li>
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* Without a key, falls back to index-based tracking (less efficient for reordering).
|
|
69
|
+
*
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
export const createForHandler = (options: {
|
|
73
|
+
prefix: string;
|
|
74
|
+
processElement: ProcessElementFn;
|
|
75
|
+
processChildren: ProcessChildrenFn;
|
|
76
|
+
}): DirectiveHandler => {
|
|
77
|
+
const { prefix, processElement, processChildren } = options;
|
|
78
|
+
|
|
79
|
+
return (el, expression, context, cleanups) => {
|
|
80
|
+
const parent = el.parentNode;
|
|
81
|
+
if (!parent) return;
|
|
82
|
+
|
|
83
|
+
// Parse expression: "item in items" or "(item, index) in items"
|
|
84
|
+
// Use \S.* instead of .+ to prevent ReDoS by requiring non-whitespace start
|
|
85
|
+
const match = expression.match(/^\(?(\w+)(?:\s*,\s*(\w+))?\)?\s+in\s+(\S.*)$/);
|
|
86
|
+
if (!match) {
|
|
87
|
+
console.error(`bQuery view: Invalid bq-for expression "${expression}"`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const [, itemName, indexName, listExpression] = match;
|
|
92
|
+
|
|
93
|
+
// Extract :key attribute if present
|
|
94
|
+
const keyExpression = el.getAttribute(':key') || el.getAttribute(`${prefix}-key`);
|
|
95
|
+
|
|
96
|
+
const template = el.cloneNode(true) as Element;
|
|
97
|
+
template.removeAttribute(`${prefix}-for`);
|
|
98
|
+
template.removeAttribute(':key');
|
|
99
|
+
template.removeAttribute(`${prefix}-key`);
|
|
100
|
+
|
|
101
|
+
// Create placeholder comment
|
|
102
|
+
const placeholder = document.createComment(`bq-for: ${expression}`);
|
|
103
|
+
parent.replaceChild(placeholder, el);
|
|
104
|
+
|
|
105
|
+
// Track rendered items by key for reconciliation
|
|
106
|
+
let renderedItemsMap = new Map<unknown, RenderedItem>();
|
|
107
|
+
let renderedOrder: unknown[] = [];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Creates a new DOM element for an item.
|
|
111
|
+
*/
|
|
112
|
+
const createItemElement = (item: unknown, index: number, key: unknown): RenderedItem => {
|
|
113
|
+
const clone = template.cloneNode(true) as Element;
|
|
114
|
+
const itemCleanups: CleanupFn[] = [];
|
|
115
|
+
|
|
116
|
+
// Create reactive signals for item and index
|
|
117
|
+
const itemSig = signal(item);
|
|
118
|
+
const indexSig = indexName ? signal(index) : null;
|
|
119
|
+
|
|
120
|
+
const childContext: BindingContext = {
|
|
121
|
+
...context,
|
|
122
|
+
[itemName]: itemSig,
|
|
123
|
+
};
|
|
124
|
+
if (indexName && indexSig) {
|
|
125
|
+
childContext[indexName] = indexSig;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Process bindings on the clone
|
|
129
|
+
processElement(clone, childContext, prefix, itemCleanups);
|
|
130
|
+
processChildren(clone, childContext, prefix, itemCleanups);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
key,
|
|
134
|
+
element: clone,
|
|
135
|
+
cleanups: itemCleanups,
|
|
136
|
+
item,
|
|
137
|
+
index,
|
|
138
|
+
itemSignal: itemSig,
|
|
139
|
+
indexSignal: indexSig,
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Removes a rendered item and cleans up its effects.
|
|
145
|
+
*/
|
|
146
|
+
const removeItem = (rendered: RenderedItem): void => {
|
|
147
|
+
for (const cleanup of rendered.cleanups) {
|
|
148
|
+
cleanup();
|
|
149
|
+
}
|
|
150
|
+
rendered.element.remove();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Updates an existing item's data and index when reused.
|
|
155
|
+
* Updates the reactive signals so bindings re-render.
|
|
156
|
+
*/
|
|
157
|
+
const updateItem = (rendered: RenderedItem, newItem: unknown, newIndex: number): void => {
|
|
158
|
+
// Update item if it changed
|
|
159
|
+
if (!Object.is(rendered.item, newItem)) {
|
|
160
|
+
rendered.item = newItem;
|
|
161
|
+
rendered.itemSignal.value = newItem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Update index if it changed
|
|
165
|
+
if (rendered.index !== newIndex) {
|
|
166
|
+
rendered.index = newIndex;
|
|
167
|
+
if (rendered.indexSignal) {
|
|
168
|
+
rendered.indexSignal.value = newIndex;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const cleanup = effect(() => {
|
|
174
|
+
const list = evaluate<unknown[]>(listExpression, context);
|
|
175
|
+
|
|
176
|
+
if (!Array.isArray(list)) {
|
|
177
|
+
// Clear all if list is invalid
|
|
178
|
+
for (const rendered of renderedItemsMap.values()) {
|
|
179
|
+
removeItem(rendered);
|
|
180
|
+
}
|
|
181
|
+
renderedItemsMap.clear();
|
|
182
|
+
renderedOrder = [];
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Build new key order and detect changes
|
|
187
|
+
const newKeys: unknown[] = [];
|
|
188
|
+
const newItemsByKey = new Map<unknown, { item: unknown; index: number }>();
|
|
189
|
+
const seenKeys = new Set<unknown>();
|
|
190
|
+
|
|
191
|
+
list.forEach((item, index) => {
|
|
192
|
+
let key = getItemKey(item, index, keyExpression, itemName, indexName, context);
|
|
193
|
+
|
|
194
|
+
// Detect duplicate keys - warn developer and fall back to unique composite key
|
|
195
|
+
if (seenKeys.has(key)) {
|
|
196
|
+
console.warn(
|
|
197
|
+
`bq-for: Duplicate key "${String(key)}" detected at index ${index}. ` +
|
|
198
|
+
`Falling back to index-based key for this item. ` +
|
|
199
|
+
`Ensure :key expressions produce unique values for each item.`
|
|
200
|
+
);
|
|
201
|
+
// Create a unique composite key to avoid corrupting rendered output
|
|
202
|
+
key = { __bqDuplicateKey: key, __bqIndex: index };
|
|
203
|
+
}
|
|
204
|
+
seenKeys.add(key);
|
|
205
|
+
|
|
206
|
+
newKeys.push(key);
|
|
207
|
+
newItemsByKey.set(key, { item, index });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Identify items to remove (in old but not in new)
|
|
211
|
+
const keysToRemove: unknown[] = [];
|
|
212
|
+
for (const key of renderedOrder) {
|
|
213
|
+
if (!newItemsByKey.has(key)) {
|
|
214
|
+
keysToRemove.push(key);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Remove deleted items
|
|
219
|
+
for (const key of keysToRemove) {
|
|
220
|
+
const rendered = renderedItemsMap.get(key);
|
|
221
|
+
if (rendered) {
|
|
222
|
+
removeItem(rendered);
|
|
223
|
+
renderedItemsMap.delete(key);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Process new list: create new items, update indices, reorder
|
|
228
|
+
const newRenderedMap = new Map<unknown, RenderedItem>();
|
|
229
|
+
let lastInsertedElement: Element | Comment = placeholder;
|
|
230
|
+
|
|
231
|
+
for (let i = 0; i < newKeys.length; i++) {
|
|
232
|
+
const key = newKeys[i];
|
|
233
|
+
const { item, index } = newItemsByKey.get(key)!;
|
|
234
|
+
let rendered = renderedItemsMap.get(key);
|
|
235
|
+
|
|
236
|
+
if (rendered) {
|
|
237
|
+
// Reuse existing element
|
|
238
|
+
updateItem(rendered, item, index);
|
|
239
|
+
newRenderedMap.set(key, rendered);
|
|
240
|
+
|
|
241
|
+
// Check if element needs to be moved
|
|
242
|
+
const currentNext: ChildNode | null = lastInsertedElement.nextSibling;
|
|
243
|
+
if (currentNext !== rendered.element) {
|
|
244
|
+
// Move element to correct position
|
|
245
|
+
lastInsertedElement.after(rendered.element);
|
|
246
|
+
}
|
|
247
|
+
lastInsertedElement = rendered.element;
|
|
248
|
+
} else {
|
|
249
|
+
// Create new element
|
|
250
|
+
rendered = createItemElement(item, index, key);
|
|
251
|
+
newRenderedMap.set(key, rendered);
|
|
252
|
+
|
|
253
|
+
// Insert at correct position
|
|
254
|
+
lastInsertedElement.after(rendered.element);
|
|
255
|
+
lastInsertedElement = rendered.element;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Update tracking state
|
|
260
|
+
renderedItemsMap = newRenderedMap;
|
|
261
|
+
renderedOrder = newKeys;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// When the bq-for itself is cleaned up, also cleanup all rendered items
|
|
265
|
+
cleanups.push(() => {
|
|
266
|
+
cleanup();
|
|
267
|
+
for (const rendered of renderedItemsMap.values()) {
|
|
268
|
+
for (const itemCleanup of rendered.cleanups) {
|
|
269
|
+
itemCleanup();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
renderedItemsMap.clear();
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { effect } from '../../reactive/index';
|
|
2
|
+
import { sanitizeHtml } from '../../security/index';
|
|
3
|
+
import { evaluate } from '../evaluate';
|
|
4
|
+
import type { DirectiveHandler } from '../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles bq-html directive - sets innerHTML (sanitized by default).
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export const handleHtml = (sanitize: boolean): DirectiveHandler => {
|
|
11
|
+
return (el, expression, context, cleanups) => {
|
|
12
|
+
const cleanup = effect(() => {
|
|
13
|
+
const value = evaluate<string>(expression, context);
|
|
14
|
+
const html = String(value ?? '');
|
|
15
|
+
el.innerHTML = sanitize ? sanitizeHtml(html) : html;
|
|
16
|
+
});
|
|
17
|
+
cleanups.push(cleanup);
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { effect } from '../../reactive/index';
|
|
2
|
+
import { evaluate } from '../evaluate';
|
|
3
|
+
import type { DirectiveHandler } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handles bq-if directive - conditional rendering.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export const handleIf: DirectiveHandler = (el, expression, context, cleanups) => {
|
|
10
|
+
const placeholder = document.createComment(`bq-if: ${expression}`);
|
|
11
|
+
|
|
12
|
+
// Store original element state
|
|
13
|
+
let isInserted = true;
|
|
14
|
+
|
|
15
|
+
const cleanup = effect(() => {
|
|
16
|
+
const condition = evaluate<boolean>(expression, context);
|
|
17
|
+
|
|
18
|
+
if (condition && !isInserted) {
|
|
19
|
+
// Insert element using replaceWith to handle moved elements
|
|
20
|
+
placeholder.replaceWith(el);
|
|
21
|
+
isInserted = true;
|
|
22
|
+
} else if (!condition && isInserted) {
|
|
23
|
+
// Remove element using replaceWith to handle moved elements
|
|
24
|
+
el.replaceWith(placeholder);
|
|
25
|
+
isInserted = false;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
cleanups.push(cleanup);
|
|
30
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { handleBind } from './bind';
|
|
2
|
+
export { handleClass } from './class';
|
|
3
|
+
export { createForHandler } from './for';
|
|
4
|
+
export { handleHtml } from './html';
|
|
5
|
+
export { handleIf } from './if';
|
|
6
|
+
export { handleModel } from './model';
|
|
7
|
+
export { handleOn } from './on';
|
|
8
|
+
export { handleRef } from './ref';
|
|
9
|
+
export { handleShow } from './show';
|
|
10
|
+
export { handleStyle } from './style';
|
|
11
|
+
export { handleText } from './text';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { effect, isSignal, type Signal } from '../../reactive/index';
|
|
2
|
+
import { evaluateRaw } from '../evaluate';
|
|
3
|
+
import type { DirectiveHandler } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handles bq-model directive - two-way binding.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export const handleModel: DirectiveHandler = (el, expression, context, cleanups) => {
|
|
10
|
+
const input = el as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
11
|
+
const rawValue = evaluateRaw<Signal<unknown>>(expression, context);
|
|
12
|
+
|
|
13
|
+
if (!isSignal(rawValue)) {
|
|
14
|
+
console.warn(`bQuery view: bq-model requires a signal, got "${expression}"`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sig = rawValue as Signal<unknown>;
|
|
19
|
+
|
|
20
|
+
// Initial value sync
|
|
21
|
+
const isCheckbox = input.type === 'checkbox';
|
|
22
|
+
const isRadio = input.type === 'radio';
|
|
23
|
+
|
|
24
|
+
const updateInput = () => {
|
|
25
|
+
if (isCheckbox) {
|
|
26
|
+
(input as HTMLInputElement).checked = Boolean(sig.value);
|
|
27
|
+
} else if (isRadio) {
|
|
28
|
+
(input as HTMLInputElement).checked = sig.value === input.value;
|
|
29
|
+
} else {
|
|
30
|
+
input.value = String(sig.value ?? '');
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Effect to sync signal -> input
|
|
35
|
+
const cleanup = effect(() => {
|
|
36
|
+
updateInput();
|
|
37
|
+
});
|
|
38
|
+
cleanups.push(cleanup);
|
|
39
|
+
|
|
40
|
+
// Event listener to sync input -> signal
|
|
41
|
+
const eventType = input.tagName === 'SELECT' ? 'change' : 'input';
|
|
42
|
+
const handler = () => {
|
|
43
|
+
if (isCheckbox) {
|
|
44
|
+
sig.value = (input as HTMLInputElement).checked;
|
|
45
|
+
} else if (isRadio) {
|
|
46
|
+
if ((input as HTMLInputElement).checked) {
|
|
47
|
+
sig.value = input.value;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
sig.value = input.value;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
input.addEventListener(eventType, handler);
|
|
55
|
+
cleanups.push(() => input.removeEventListener(eventType, handler));
|
|
56
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { evaluateRaw } from '../evaluate';
|
|
2
|
+
import type { DirectiveHandler } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles bq-on:event directive - event binding.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export const handleOn = (eventName: string): DirectiveHandler => {
|
|
9
|
+
return (el, expression, context, cleanups) => {
|
|
10
|
+
const handler = (event: Event) => {
|
|
11
|
+
// Add $event to context for expression evaluation
|
|
12
|
+
const eventContext = { ...context, $event: event, $el: el };
|
|
13
|
+
|
|
14
|
+
// Check if expression contains a function call (has parentheses)
|
|
15
|
+
// If not, it might be a plain function reference like "handleClick"
|
|
16
|
+
// Note: Method references like "handlers.onClick" will lose their receiver
|
|
17
|
+
// when auto-invoked. For methods, use explicit calls: "handlers.onClick($event)"
|
|
18
|
+
const containsCall = expression.includes('(');
|
|
19
|
+
|
|
20
|
+
if (!containsCall) {
|
|
21
|
+
// Evaluate the expression - if it returns a function, invoke it with $event
|
|
22
|
+
const result = evaluateRaw<unknown>(expression, eventContext);
|
|
23
|
+
if (typeof result === 'function') {
|
|
24
|
+
// Auto-invoke with event. Note: `this` will be undefined for method references.
|
|
25
|
+
// For proper method binding, use explicit syntax: "obj.method($event)"
|
|
26
|
+
result(event);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// If not a function, the expression was already evaluated (e.g., "count.value++")
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Otherwise evaluate as expression using evaluateRaw to allow signal mutations
|
|
34
|
+
// (e.g., "count.value++" or "handleClick($event)")
|
|
35
|
+
evaluateRaw(expression, eventContext);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
el.addEventListener(eventName, handler);
|
|
39
|
+
cleanups.push(() => el.removeEventListener(eventName, handler));
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isSignal, type Signal } from '../../reactive/index';
|
|
2
|
+
import { evaluateRaw } from '../evaluate';
|
|
3
|
+
import type { DirectiveHandler } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if an object has a writable `value` property.
|
|
7
|
+
* Returns true if `value` is an own data property or an accessor with a setter.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
function hasWritableValue(obj: object): obj is { value: Element | null } {
|
|
11
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, 'value');
|
|
12
|
+
if (!descriptor) return false;
|
|
13
|
+
// Data property: check writable flag
|
|
14
|
+
if ('value' in descriptor) return descriptor.writable === true;
|
|
15
|
+
// Accessor property: check for setter
|
|
16
|
+
return typeof descriptor.set === 'function';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handles bq-ref directive - element reference.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export const handleRef: DirectiveHandler = (el, expression, context, cleanups) => {
|
|
24
|
+
const rawValue = evaluateRaw<Signal<Element | null> | { value: Element | null }>(
|
|
25
|
+
expression,
|
|
26
|
+
context
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (isSignal(rawValue)) {
|
|
30
|
+
rawValue.value = el;
|
|
31
|
+
cleanups.push(() => {
|
|
32
|
+
rawValue.value = null;
|
|
33
|
+
});
|
|
34
|
+
} else if (typeof rawValue === 'object' && rawValue !== null && hasWritableValue(rawValue)) {
|
|
35
|
+
// Object with writable .value property (e.g., { value: null })
|
|
36
|
+
rawValue.value = el;
|
|
37
|
+
cleanups.push(() => {
|
|
38
|
+
rawValue.value = null;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { effect } from '../../reactive/index';
|
|
2
|
+
import { evaluate } from '../evaluate';
|
|
3
|
+
import type { DirectiveHandler } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handles bq-show directive - toggle visibility.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export const handleShow: DirectiveHandler = (el, expression, context, cleanups) => {
|
|
10
|
+
const htmlEl = el as HTMLElement;
|
|
11
|
+
// Capture the computed display value to properly restore visibility.
|
|
12
|
+
// If inline display is 'none' or empty, we need to use the computed value.
|
|
13
|
+
// Use ownerDocument.defaultView for cross-document/iframe compatibility.
|
|
14
|
+
let originalDisplay = htmlEl.style.display;
|
|
15
|
+
if (!originalDisplay || originalDisplay === 'none') {
|
|
16
|
+
const computed = htmlEl.ownerDocument.defaultView?.getComputedStyle(htmlEl).display ?? '';
|
|
17
|
+
originalDisplay = computed !== 'none' ? computed : '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const cleanup = effect(() => {
|
|
21
|
+
const condition = evaluate<boolean>(expression, context);
|
|
22
|
+
htmlEl.style.display = condition ? originalDisplay : 'none';
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
cleanups.push(cleanup);
|
|
26
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { effect } from '../../reactive/index';
|
|
2
|
+
import { evaluate, parseObjectExpression } from '../evaluate';
|
|
3
|
+
import type { DirectiveHandler } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handles bq-style directive - dynamic style binding.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export const handleStyle: DirectiveHandler = (el, expression, context, cleanups) => {
|
|
10
|
+
const htmlEl = el as HTMLElement;
|
|
11
|
+
let appliedStyles: Set<string> = new Set();
|
|
12
|
+
|
|
13
|
+
const cleanup = effect(() => {
|
|
14
|
+
const newStyles = new Set<string>();
|
|
15
|
+
|
|
16
|
+
if (expression.trimStart().startsWith('{')) {
|
|
17
|
+
const styleMap = parseObjectExpression(expression);
|
|
18
|
+
for (const [prop, valueExpr] of Object.entries(styleMap)) {
|
|
19
|
+
const value = evaluate<string>(valueExpr, context);
|
|
20
|
+
const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
21
|
+
htmlEl.style.setProperty(cssProp, String(value ?? ''));
|
|
22
|
+
newStyles.add(cssProp);
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
const result = evaluate<Record<string, string>>(expression, context);
|
|
26
|
+
if (result && typeof result === 'object') {
|
|
27
|
+
for (const [prop, value] of Object.entries(result)) {
|
|
28
|
+
const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
29
|
+
htmlEl.style.setProperty(cssProp, String(value ?? ''));
|
|
30
|
+
newStyles.add(cssProp);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Remove styles that were previously applied but are no longer present
|
|
36
|
+
for (const cssProp of appliedStyles) {
|
|
37
|
+
if (!newStyles.has(cssProp)) {
|
|
38
|
+
htmlEl.style.removeProperty(cssProp);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Update the set of applied styles
|
|
43
|
+
appliedStyles = newStyles;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
cleanups.push(cleanup);
|
|
47
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { effect } from '../../reactive/index';
|
|
2
|
+
import { evaluate } from '../evaluate';
|
|
3
|
+
import type { DirectiveHandler } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handles bq-text directive - sets text content.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export const handleText: DirectiveHandler = (el, expression, context, cleanups) => {
|
|
10
|
+
const cleanup = effect(() => {
|
|
11
|
+
const value = evaluate(expression, context);
|
|
12
|
+
el.textContent = String(value ?? '');
|
|
13
|
+
});
|
|
14
|
+
cleanups.push(cleanup);
|
|
15
|
+
};
|