@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.
Files changed (305) hide show
  1. package/README.md +501 -427
  2. package/dist/batch-4LAvfLE7.js +13 -0
  3. package/dist/batch-4LAvfLE7.js.map +1 -0
  4. package/dist/component/component.d.ts +69 -0
  5. package/dist/component/component.d.ts.map +1 -0
  6. package/dist/component/html.d.ts +35 -0
  7. package/dist/component/html.d.ts.map +1 -0
  8. package/dist/component/index.d.ts +3 -126
  9. package/dist/component/index.d.ts.map +1 -1
  10. package/dist/component/props.d.ts +18 -0
  11. package/dist/component/props.d.ts.map +1 -0
  12. package/dist/component/types.d.ts +77 -0
  13. package/dist/component/types.d.ts.map +1 -0
  14. package/dist/component.es.mjs +90 -59
  15. package/dist/component.es.mjs.map +1 -1
  16. package/dist/core/collection.d.ts +36 -0
  17. package/dist/core/collection.d.ts.map +1 -1
  18. package/dist/core/dom.d.ts +6 -0
  19. package/dist/core/dom.d.ts.map +1 -0
  20. package/dist/core/element.d.ts +8 -0
  21. package/dist/core/element.d.ts.map +1 -1
  22. package/dist/core/index.d.ts +1 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/utils/array.d.ts +74 -0
  25. package/dist/core/utils/array.d.ts.map +1 -0
  26. package/dist/core/utils/function.d.ts +70 -0
  27. package/dist/core/utils/function.d.ts.map +1 -0
  28. package/dist/core/utils/index.d.ts +70 -0
  29. package/dist/core/utils/index.d.ts.map +1 -0
  30. package/dist/core/utils/misc.d.ts +63 -0
  31. package/dist/core/utils/misc.d.ts.map +1 -0
  32. package/dist/core/utils/number.d.ts +65 -0
  33. package/dist/core/utils/number.d.ts.map +1 -0
  34. package/dist/core/utils/object.d.ts +133 -0
  35. package/dist/core/utils/object.d.ts.map +1 -0
  36. package/dist/core/utils/string.d.ts +80 -0
  37. package/dist/core/utils/string.d.ts.map +1 -0
  38. package/dist/core/utils/type-guards.d.ts +79 -0
  39. package/dist/core/utils/type-guards.d.ts.map +1 -0
  40. package/dist/core-COenAZjD.js +145 -0
  41. package/dist/core-COenAZjD.js.map +1 -0
  42. package/dist/core.es.mjs +411 -448
  43. package/dist/core.es.mjs.map +1 -1
  44. package/dist/full.d.ts +2 -2
  45. package/dist/full.d.ts.map +1 -1
  46. package/dist/full.es.mjs +87 -64
  47. package/dist/full.es.mjs.map +1 -1
  48. package/dist/full.iife.js +2 -2
  49. package/dist/full.iife.js.map +1 -1
  50. package/dist/full.umd.js +2 -2
  51. package/dist/full.umd.js.map +1 -1
  52. package/dist/index.es.mjs +138 -68
  53. package/dist/index.es.mjs.map +1 -1
  54. package/dist/motion/animate.d.ts +25 -0
  55. package/dist/motion/animate.d.ts.map +1 -0
  56. package/dist/motion/easing.d.ts +30 -0
  57. package/dist/motion/easing.d.ts.map +1 -0
  58. package/dist/motion/flip.d.ts +55 -0
  59. package/dist/motion/flip.d.ts.map +1 -0
  60. package/dist/motion/index.d.ts +11 -138
  61. package/dist/motion/index.d.ts.map +1 -1
  62. package/dist/motion/keyframes.d.ts +21 -0
  63. package/dist/motion/keyframes.d.ts.map +1 -0
  64. package/dist/motion/reduced-motion.d.ts +12 -0
  65. package/dist/motion/reduced-motion.d.ts.map +1 -0
  66. package/dist/motion/scroll.d.ts +15 -0
  67. package/dist/motion/scroll.d.ts.map +1 -0
  68. package/dist/motion/spring.d.ts +42 -0
  69. package/dist/motion/spring.d.ts.map +1 -0
  70. package/dist/motion/stagger.d.ts +22 -0
  71. package/dist/motion/stagger.d.ts.map +1 -0
  72. package/dist/motion/timeline.d.ts +21 -0
  73. package/dist/motion/timeline.d.ts.map +1 -0
  74. package/dist/motion/transition.d.ts +22 -0
  75. package/dist/motion/transition.d.ts.map +1 -0
  76. package/dist/motion/types.d.ts +182 -0
  77. package/dist/motion/types.d.ts.map +1 -0
  78. package/dist/motion.es.mjs +320 -61
  79. package/dist/motion.es.mjs.map +1 -1
  80. package/dist/persisted-Dz_ryNuC.js +278 -0
  81. package/dist/persisted-Dz_ryNuC.js.map +1 -0
  82. package/dist/reactive/batch.d.ts +13 -0
  83. package/dist/reactive/batch.d.ts.map +1 -0
  84. package/dist/reactive/computed.d.ts +50 -0
  85. package/dist/reactive/computed.d.ts.map +1 -0
  86. package/dist/reactive/core.d.ts +60 -0
  87. package/dist/reactive/core.d.ts.map +1 -0
  88. package/dist/reactive/effect.d.ts +15 -0
  89. package/dist/reactive/effect.d.ts.map +1 -0
  90. package/dist/reactive/index.d.ts +2 -2
  91. package/dist/reactive/index.d.ts.map +1 -1
  92. package/dist/reactive/internals.d.ts +36 -0
  93. package/dist/reactive/internals.d.ts.map +1 -0
  94. package/dist/reactive/linked.d.ts +36 -0
  95. package/dist/reactive/linked.d.ts.map +1 -0
  96. package/dist/reactive/persisted.d.ts +14 -0
  97. package/dist/reactive/persisted.d.ts.map +1 -0
  98. package/dist/reactive/readonly.d.ts +26 -0
  99. package/dist/reactive/readonly.d.ts.map +1 -0
  100. package/dist/reactive/signal.d.ts +13 -312
  101. package/dist/reactive/signal.d.ts.map +1 -1
  102. package/dist/reactive/type-guards.d.ts +20 -0
  103. package/dist/reactive/type-guards.d.ts.map +1 -0
  104. package/dist/reactive/untrack.d.ts +29 -0
  105. package/dist/reactive/untrack.d.ts.map +1 -0
  106. package/dist/reactive/watch.d.ts +42 -0
  107. package/dist/reactive/watch.d.ts.map +1 -0
  108. package/dist/reactive.es.mjs +30 -163
  109. package/dist/reactive.es.mjs.map +1 -1
  110. package/dist/router/index.d.ts +6 -252
  111. package/dist/router/index.d.ts.map +1 -1
  112. package/dist/router/links.d.ts +44 -0
  113. package/dist/router/links.d.ts.map +1 -0
  114. package/dist/router/match.d.ts +20 -0
  115. package/dist/router/match.d.ts.map +1 -0
  116. package/dist/router/navigation.d.ts +45 -0
  117. package/dist/router/navigation.d.ts.map +1 -0
  118. package/dist/router/query.d.ts +16 -0
  119. package/dist/router/query.d.ts.map +1 -0
  120. package/dist/router/router.d.ts +34 -0
  121. package/dist/router/router.d.ts.map +1 -0
  122. package/dist/router/state.d.ts +27 -0
  123. package/dist/router/state.d.ts.map +1 -0
  124. package/dist/router/types.d.ts +88 -0
  125. package/dist/router/types.d.ts.map +1 -0
  126. package/dist/router/utils.d.ts +65 -0
  127. package/dist/router/utils.d.ts.map +1 -0
  128. package/dist/router.es.mjs +168 -132
  129. package/dist/router.es.mjs.map +1 -1
  130. package/dist/sanitize-1FBEPAFH.js +272 -0
  131. package/dist/sanitize-1FBEPAFH.js.map +1 -0
  132. package/dist/security/constants.d.ts +42 -0
  133. package/dist/security/constants.d.ts.map +1 -0
  134. package/dist/security/csp.d.ts +24 -0
  135. package/dist/security/csp.d.ts.map +1 -0
  136. package/dist/security/index.d.ts +4 -2
  137. package/dist/security/index.d.ts.map +1 -1
  138. package/dist/security/sanitize-core.d.ts +13 -0
  139. package/dist/security/sanitize-core.d.ts.map +1 -0
  140. package/dist/security/sanitize.d.ts +5 -57
  141. package/dist/security/sanitize.d.ts.map +1 -1
  142. package/dist/security/trusted-types.d.ts +25 -0
  143. package/dist/security/trusted-types.d.ts.map +1 -0
  144. package/dist/security/types.d.ts +36 -0
  145. package/dist/security/types.d.ts.map +1 -0
  146. package/dist/security.es.mjs +50 -277
  147. package/dist/security.es.mjs.map +1 -1
  148. package/dist/store/create-store.d.ts +15 -0
  149. package/dist/store/create-store.d.ts.map +1 -0
  150. package/dist/store/define-store.d.ts +28 -0
  151. package/dist/store/define-store.d.ts.map +1 -0
  152. package/dist/store/devtools.d.ts +22 -0
  153. package/dist/store/devtools.d.ts.map +1 -0
  154. package/dist/store/index.d.ts +10 -286
  155. package/dist/store/index.d.ts.map +1 -1
  156. package/dist/store/mapping.d.ts +28 -0
  157. package/dist/store/mapping.d.ts.map +1 -0
  158. package/dist/store/persisted.d.ts +13 -0
  159. package/dist/store/persisted.d.ts.map +1 -0
  160. package/dist/store/plugins.d.ts +13 -0
  161. package/dist/store/plugins.d.ts.map +1 -0
  162. package/dist/store/registry.d.ts +28 -0
  163. package/dist/store/registry.d.ts.map +1 -0
  164. package/dist/store/types.d.ts +71 -0
  165. package/dist/store/types.d.ts.map +1 -0
  166. package/dist/store/utils.d.ts +28 -0
  167. package/dist/store/utils.d.ts.map +1 -0
  168. package/dist/store/watch.d.ts +23 -0
  169. package/dist/store/watch.d.ts.map +1 -0
  170. package/dist/store.es.mjs +22 -224
  171. package/dist/store.es.mjs.map +1 -1
  172. package/dist/type-guards-DRma3-Kc.js +16 -0
  173. package/dist/type-guards-DRma3-Kc.js.map +1 -0
  174. package/dist/untrack-BuEQKH7_.js +6 -0
  175. package/dist/untrack-BuEQKH7_.js.map +1 -0
  176. package/dist/view/directives/bind.d.ts +7 -0
  177. package/dist/view/directives/bind.d.ts.map +1 -0
  178. package/dist/view/directives/class.d.ts +8 -0
  179. package/dist/view/directives/class.d.ts.map +1 -0
  180. package/dist/view/directives/for.d.ts +23 -0
  181. package/dist/view/directives/for.d.ts.map +1 -0
  182. package/dist/view/directives/html.d.ts +7 -0
  183. package/dist/view/directives/html.d.ts.map +1 -0
  184. package/dist/view/directives/if.d.ts +7 -0
  185. package/dist/view/directives/if.d.ts.map +1 -0
  186. package/dist/view/directives/index.d.ts +12 -0
  187. package/dist/view/directives/index.d.ts.map +1 -0
  188. package/dist/view/directives/model.d.ts +7 -0
  189. package/dist/view/directives/model.d.ts.map +1 -0
  190. package/dist/view/directives/on.d.ts +7 -0
  191. package/dist/view/directives/on.d.ts.map +1 -0
  192. package/dist/view/directives/ref.d.ts +7 -0
  193. package/dist/view/directives/ref.d.ts.map +1 -0
  194. package/dist/view/directives/show.d.ts +7 -0
  195. package/dist/view/directives/show.d.ts.map +1 -0
  196. package/dist/view/directives/style.d.ts +7 -0
  197. package/dist/view/directives/style.d.ts.map +1 -0
  198. package/dist/view/directives/text.d.ts +7 -0
  199. package/dist/view/directives/text.d.ts.map +1 -0
  200. package/dist/view/evaluate.d.ts +43 -0
  201. package/dist/view/evaluate.d.ts.map +1 -0
  202. package/dist/view/index.d.ts +3 -93
  203. package/dist/view/index.d.ts.map +1 -1
  204. package/dist/view/mount.d.ts +69 -0
  205. package/dist/view/mount.d.ts.map +1 -0
  206. package/dist/view/process.d.ts +26 -0
  207. package/dist/view/process.d.ts.map +1 -0
  208. package/dist/view/types.d.ts +36 -0
  209. package/dist/view/types.d.ts.map +1 -0
  210. package/dist/view.es.mjs +368 -267
  211. package/dist/view.es.mjs.map +1 -1
  212. package/dist/watch-CXyaBC_9.js +58 -0
  213. package/dist/watch-CXyaBC_9.js.map +1 -0
  214. package/package.json +132 -132
  215. package/src/component/component.ts +289 -0
  216. package/src/component/html.ts +53 -0
  217. package/src/component/index.ts +40 -414
  218. package/src/component/props.ts +116 -0
  219. package/src/component/types.ts +85 -0
  220. package/src/core/collection.ts +588 -454
  221. package/src/core/dom.ts +38 -0
  222. package/src/core/element.ts +746 -740
  223. package/src/core/index.ts +43 -0
  224. package/src/core/utils/array.ts +102 -0
  225. package/src/core/utils/function.ts +110 -0
  226. package/src/core/utils/index.ts +83 -0
  227. package/src/core/utils/misc.ts +82 -0
  228. package/src/core/utils/number.ts +78 -0
  229. package/src/core/utils/object.ts +206 -0
  230. package/src/core/utils/string.ts +112 -0
  231. package/src/core/utils/type-guards.ts +112 -0
  232. package/src/full.ts +187 -150
  233. package/src/index.ts +36 -36
  234. package/src/motion/animate.ts +113 -0
  235. package/src/motion/easing.ts +40 -0
  236. package/src/motion/flip.ts +176 -0
  237. package/src/motion/index.ts +41 -358
  238. package/src/motion/keyframes.ts +46 -0
  239. package/src/motion/reduced-motion.ts +17 -0
  240. package/src/motion/scroll.ts +57 -0
  241. package/src/motion/spring.ts +150 -0
  242. package/src/motion/stagger.ts +43 -0
  243. package/src/motion/timeline.ts +246 -0
  244. package/src/motion/transition.ts +51 -0
  245. package/src/motion/types.ts +198 -0
  246. package/src/reactive/batch.ts +22 -0
  247. package/src/reactive/computed.ts +92 -0
  248. package/src/reactive/core.ts +93 -0
  249. package/src/reactive/effect.ts +43 -0
  250. package/src/reactive/index.ts +23 -22
  251. package/src/reactive/internals.ts +105 -0
  252. package/src/reactive/linked.ts +56 -0
  253. package/src/reactive/persisted.ts +74 -0
  254. package/src/reactive/readonly.ts +35 -0
  255. package/src/reactive/signal.ts +20 -520
  256. package/src/reactive/type-guards.ts +22 -0
  257. package/src/reactive/untrack.ts +31 -0
  258. package/src/reactive/watch.ts +73 -0
  259. package/src/router/index.ts +41 -718
  260. package/src/router/links.ts +130 -0
  261. package/src/router/match.ts +106 -0
  262. package/src/router/navigation.ts +71 -0
  263. package/src/router/query.ts +35 -0
  264. package/src/router/router.ts +211 -0
  265. package/src/router/state.ts +46 -0
  266. package/src/router/types.ts +93 -0
  267. package/src/router/utils.ts +116 -0
  268. package/src/security/constants.ts +209 -0
  269. package/src/security/csp.ts +77 -0
  270. package/src/security/index.ts +4 -12
  271. package/src/security/sanitize-core.ts +343 -0
  272. package/src/security/sanitize.ts +66 -625
  273. package/src/security/trusted-types.ts +69 -0
  274. package/src/security/types.ts +40 -0
  275. package/src/store/create-store.ts +329 -0
  276. package/src/store/define-store.ts +48 -0
  277. package/src/store/devtools.ts +45 -0
  278. package/src/store/index.ts +22 -848
  279. package/src/store/mapping.ts +73 -0
  280. package/src/store/persisted.ts +61 -0
  281. package/src/store/plugins.ts +32 -0
  282. package/src/store/registry.ts +51 -0
  283. package/src/store/types.ts +94 -0
  284. package/src/store/utils.ts +141 -0
  285. package/src/store/watch.ts +52 -0
  286. package/src/view/directives/bind.ts +23 -0
  287. package/src/view/directives/class.ts +70 -0
  288. package/src/view/directives/for.ts +275 -0
  289. package/src/view/directives/html.ts +19 -0
  290. package/src/view/directives/if.ts +30 -0
  291. package/src/view/directives/index.ts +11 -0
  292. package/src/view/directives/model.ts +56 -0
  293. package/src/view/directives/on.ts +41 -0
  294. package/src/view/directives/ref.ts +41 -0
  295. package/src/view/directives/show.ts +26 -0
  296. package/src/view/directives/style.ts +47 -0
  297. package/src/view/directives/text.ts +15 -0
  298. package/src/view/evaluate.ts +274 -0
  299. package/src/view/index.ts +112 -1041
  300. package/src/view/mount.ts +200 -0
  301. package/src/view/process.ts +92 -0
  302. package/src/view/types.ts +44 -0
  303. package/dist/core/utils.d.ts +0 -313
  304. package/dist/core/utils.d.ts.map +0 -1
  305. 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
+ };