fusee-framework 1.0.0 → 1.1.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.
@@ -1,335 +1,381 @@
1
- import { effect, batch } from './signal.js'
1
+ import { signal, effect, batch } from './signal.js'
2
2
  import { evaluateExpression } from './evaluator.js'
3
3
  import { compileNode } from './compiler.js'
4
4
 
5
5
  export function processDirectives(root, context, components, effects) {
6
- processFor(root, context, components, effects)
7
- processIf(root, context, components, effects)
6
+ if (processFor(root, context, components, effects)) return true
7
+ if (processIf(root, context, components, effects)) return true
8
+
8
9
  processShow(root, context, effects)
9
10
  processModel(root, context, effects)
10
11
  processRefs(root, context)
11
12
  processHtml(root, context, effects)
12
13
  processText(root, context, effects)
13
- processEvents(root, context)
14
+ processEvents(root, context, effects)
14
15
  processCloak(root)
16
+
17
+ return false
15
18
  }
16
19
 
17
- export function processRefs(root, context) {
18
- const refEls = root.querySelectorAll('[f-ref]')
19
- for (const el of refEls) {
20
- if (!el.parentNode) continue
20
+ export function processRefs(el, context) {
21
+ if (el.nodeType !== 1 || !el.hasAttribute('f-ref')) return
21
22
 
22
- const refName = el.getAttribute('f-ref')
23
- el.removeAttribute('f-ref')
23
+ const refName = el.getAttribute('f-ref')
24
+ el.removeAttribute('f-ref')
24
25
 
25
- const refVar = context[refName]
26
- if (typeof refVar === 'function' && refVar.isSignal && !refVar.readonly) {
27
- refVar(el)
28
- } else {
29
- const reason = refVar?.readonly ? 'it is read-only (computed)' : 'it is not a signal'
30
- console.warn(`[framework] f-ref requires a writable signal. Target "${refName}" is invalid: ${reason}`)
31
- }
26
+ const refVar = context[refName]
27
+ if (typeof refVar === 'function' && refVar.isSignal && !refVar.readonly) {
28
+ refVar(el)
29
+ } else {
30
+ const reason = refVar?.readonly ? 'it is read-only (computed)' : 'it is not a signal'
31
+ console.warn(`[framework] f-ref requires a writable signal. Target "${refName}" is invalid: ${reason}`)
32
32
  }
33
33
  }
34
34
 
35
- export function processModel(root, context, effects) {
36
- const modelEls = root.querySelectorAll('[f-model]')
37
- for (const el of modelEls) {
38
- if (!el.parentNode) continue
35
+ export function processModel(el, context, effects) {
36
+ if (el.nodeType !== 1 || !el.hasAttribute('f-model')) return
39
37
 
40
- const expr = el.getAttribute('f-model')
41
- el.removeAttribute('f-model')
38
+ const expr = el.getAttribute('f-model')
39
+ el.removeAttribute('f-model')
42
40
 
43
- const signalMethod = context[expr]
44
- if (typeof signalMethod === 'function' && signalMethod.isSignal && !signalMethod.readonly) {
45
- effects.push(effect(() => {
46
- const val = signalMethod()
47
- if (el.type === 'checkbox') {
48
- el.checked = !!val
49
- } else {
50
- el.value = val ?? ''
51
- }
52
- }))
41
+ const signalMethod = context[expr]
42
+ if (typeof signalMethod === 'function' && signalMethod.isSignal && !signalMethod.readonly) {
43
+ effects.push(effect(() => {
44
+ const val = signalMethod()
45
+ if (el.type === 'checkbox') {
46
+ el.checked = !!val
47
+ } else {
48
+ el.value = val ?? ''
49
+ }
50
+ }))
53
51
 
54
- const eventName = (el.tagName === 'SELECT' || el.type === 'checkbox' || el.type === 'radio') ? 'change' : 'input'
55
- el.addEventListener(eventName, () => {
56
- const newVal = el.type === 'checkbox' ? el.checked : el.value
57
- batch(() => signalMethod(newVal))
58
- })
59
- } else {
60
- const reason = signalMethod?.readonly ? 'it is read-only (computed)' : 'it is not a signal'
61
- console.warn(`[framework] f-model requires a writable signal. Expression "${expr}" is invalid: ${reason}`)
52
+ const eventName = (el.tagName === 'SELECT' || el.type === 'checkbox' || el.type === 'radio') ? 'change' : 'input'
53
+ const handler = () => {
54
+ const newVal = el.type === 'checkbox' ? el.checked : el.value
55
+ batch(() => signalMethod(newVal))
62
56
  }
57
+ el.addEventListener(eventName, handler)
58
+ effects.push(() => el.removeEventListener(eventName, handler))
59
+ } else {
60
+ const reason = signalMethod?.readonly ? 'it is read-only (computed)' : 'it is not a signal'
61
+ console.warn(`[framework] f-model requires a writable signal. Expression "${expr}" is invalid: ${reason}`)
63
62
  }
64
63
  }
65
64
 
66
- export function processFor(root, context, components, effects) {
67
- const forEls = root.querySelectorAll('[f-for]')
68
- for (const el of forEls) {
69
- if (!el.parentNode) continue
65
+ export function processFor(el, context, components, effects) {
66
+ if (el.nodeType !== 1 || !el.hasAttribute('f-for')) return false
67
+
68
+ const expr = el.getAttribute('f-for')
69
+ el.removeAttribute('f-for')
70
+
71
+ const match = expr.match(/^\s*(?:(?:\(\s*([a-zA-Z_$][0-9a-zA-Z_$]*)(?:\s*,\s*([a-zA-Z_$][0-9a-zA-Z_$]*))?(?:\s*,\s*([a-zA-Z_$][0-9a-zA-Z_$]*))?\s*\))|([a-zA-Z_$][0-9a-zA-Z_$]*))\s+in\s+(.+)\s*$/)
72
+ if (!match) {
73
+ console.warn(`[framework] Invalid f-for expression: "${expr}"`)
74
+ return false
75
+ }
76
+
77
+ const itemName = match[4] || match[1]
78
+ const keyName = match[2]
79
+ const indexName = match[3]
80
+ const arrayExpr = match[5]
81
+
82
+ const keyExpr = el.getAttribute('data-bind-key') || el.getAttribute(':key')
83
+ if (keyExpr) {
84
+ el.removeAttribute('data-bind-key')
85
+ el.removeAttribute(':key')
86
+ }
87
+
88
+ const anchor = document.createComment('f-for')
89
+ const parent = el.parentNode
90
+ parent.insertBefore(anchor, el)
91
+
92
+ const templateNode = el.cloneNode(true)
93
+ parent.removeChild(el)
70
94
 
71
- const expr = el.getAttribute('f-for')
72
- el.removeAttribute('f-for')
95
+ // Cache of currently rendered items: Map<key, { node, effects, context }>
96
+ let cache = new Map()
97
+ let previousOrder = []
73
98
 
74
- const match = expr.match(/^\s*(?:(?:\(\s*([a-zA-Z_$][0-9a-zA-Z_$]*)(?:\s*,\s*([a-zA-Z_$][0-9a-zA-Z_$]*))?(?:\s*,\s*([a-zA-Z_$][0-9a-zA-Z_$]*))?\s*\))|([a-zA-Z_$][0-9a-zA-Z_$]*))\s+in\s+(.+)\s*$/)
75
- if (!match) {
76
- console.warn(`[framework] Invalid f-for expression: "${expr}"`)
77
- continue
99
+ effects.push(effect(() => {
100
+ const rawItems = evaluateExpression(arrayExpr, context)
101
+ let items = []
102
+
103
+ if (Array.isArray(rawItems)) {
104
+ items = rawItems.map((item, index) => ({ value: item, rawIndex: index }))
105
+ } else if (typeof rawItems === 'number' && !isNaN(rawItems)) {
106
+ items = Array.from({ length: Math.max(0, Math.floor(rawItems)) }, (_, i) => ({ value: i + 1, rawIndex: i }))
107
+ } else if (rawItems && typeof rawItems === 'object') {
108
+ items = Object.entries(rawItems).map(([key, value], index) => ({ value, key, rawIndex: index }))
78
109
  }
79
110
 
80
- const itemName = match[4] || match[1]
81
- const keyName = match[2]
82
- const indexName = match[3]
83
- const arrayExpr = match[5]
111
+ const nextCache = new Map()
112
+ const nextOrder = []
84
113
 
85
- const anchor = document.createComment('f-for')
86
- const parent = el.parentNode
87
- parent.insertBefore(anchor, el)
114
+ const newItemsWithKeys = items.map((item, index) => {
115
+ const itemContext = Object.create(context)
116
+ itemContext[itemName] = item.value
117
+ if (keyName) itemContext[keyName] = item.hasOwnProperty('key') ? item.key : item.rawIndex
118
+ if (indexName) itemContext[indexName] = index
88
119
 
89
- const templateNode = el.cloneNode(true)
90
- parent.removeChild(el)
120
+ const key = keyExpr ? evaluateExpression(keyExpr, itemContext) : item.rawIndex
121
+ return { item, key }
122
+ })
91
123
 
92
- let previousNodes = []
93
- let previousEffects = []
124
+ const parentNode = anchor.parentNode
125
+ let currentCursor = anchor.nextSibling
94
126
 
95
- effects.push(effect(() => {
96
- const rawItems = evaluateExpression(arrayExpr, context)
97
- let items = []
98
-
99
- if (Array.isArray(rawItems)) {
100
- items = rawItems.map((item, index) => ({ value: item, key: index, index }))
101
- } else if (typeof rawItems === 'number' && !isNaN(rawItems)) {
102
- items = Array.from({ length: Math.max(0, Math.floor(rawItems)) }, (_, i) => ({ value: i + 1, key: i, index: i }))
103
- } else if (rawItems && typeof rawItems === 'object') {
104
- items = Object.entries(rawItems).map(([key, value], index) => ({ value, key, index }))
105
- }
127
+ newItemsWithKeys.forEach(({ item, key }, index) => {
128
+ let entry = cache.get(key)
106
129
 
107
- for (const e of previousEffects) if (typeof e === 'function') e()
108
- for (const node of previousNodes) node.parentNode?.removeChild(node)
130
+ if (entry) {
131
+ // Update existing reactive signals for reused items
132
+ batch(() => {
133
+ if (typeof entry.context[itemName] === 'function' && entry.context[itemName].isSignal) {
134
+ entry.context[itemName](item.value)
135
+ }
136
+ if (keyName && typeof entry.context[keyName] === 'function' && entry.context[keyName].isSignal) {
137
+ entry.context[keyName](item.hasOwnProperty('key') ? item.key : item.rawIndex)
138
+ }
139
+ if (indexName && typeof entry.context[indexName] === 'function' && entry.context[indexName].isSignal) {
140
+ entry.context[indexName](index)
141
+ }
142
+ })
109
143
 
110
- previousNodes = []
111
- previousEffects = []
144
+ if (entry.node !== currentCursor) {
145
+ parentNode.insertBefore(entry.node, currentCursor)
146
+ } else {
147
+ currentCursor = currentCursor.nextSibling
148
+ }
149
+ cache.delete(key)
150
+ } else {
151
+ // Create new item context with signals for local state
152
+ const itemContext = Object.create(context)
153
+ itemContext[itemName] = signal(item.value)
154
+ if (keyName) itemContext[keyName] = signal(item.hasOwnProperty('key') ? item.key : item.rawIndex)
155
+ if (indexName) itemContext[indexName] = signal(index)
112
156
 
113
- const fragment = document.createDocumentFragment()
114
- items.forEach((item) => {
115
157
  const clone = templateNode.cloneNode(true)
116
- const childContext = Object.create(context)
158
+ const childEffects = []
159
+ compileNode(clone, itemContext, components, childEffects)
117
160
 
118
- childContext[itemName] = item.value
119
- if (keyName) childContext[keyName] = item.key
120
- if (indexName) childContext[indexName] = item.index
161
+ parentNode.insertBefore(clone, currentCursor)
162
+ entry = { node: clone, effects: childEffects, context: itemContext }
163
+ }
121
164
 
122
- const childEffects = []
123
- compileNode(clone, childContext, components, childEffects)
165
+ nextCache.set(key, entry)
166
+ nextOrder.push(entry.node)
167
+ })
124
168
 
125
- fragment.appendChild(clone)
126
- previousNodes.push(clone)
127
- previousEffects.push(...childEffects)
128
- })
169
+ // Cleanup removed nodes
170
+ for (const [_, entry] of cache.entries()) {
171
+ for (const e of entry.effects) if (typeof e === 'function') e()
172
+ entry.node.parentNode?.removeChild(entry.node)
173
+ }
129
174
 
130
- anchor.parentNode.insertBefore(fragment, anchor)
131
- }))
132
- }
133
- }
175
+ cache = nextCache
176
+ previousOrder = nextOrder
177
+ }))
178
+
179
+ effects.push(() => {
180
+ for (const entry of cache.values()) {
181
+ for (const cleanup of entry.effects) if (typeof cleanup === 'function') cleanup()
182
+ }
183
+ cache.clear()
184
+ })
134
185
 
135
- export function processIf(root, context, components, effects) {
136
- const ifEls = Array.from(root.querySelectorAll('[f-if]'))
137
- const processedEls = new Set()
186
+ return true
187
+ }
138
188
 
139
- for (const ifEl of ifEls) {
140
- if (processedEls.has(ifEl)) continue
189
+ export function processIf(el, context, components, effects) {
190
+ if (el.nodeType !== 1 || !el.hasAttribute('f-if')) return false
141
191
 
142
- const parent = ifEl.parentNode
143
- if (!parent) continue
192
+ const parent = el.parentNode
193
+ if (!parent) return false
144
194
 
145
- const group = []
146
- let curr = ifEl
195
+ const group = []
196
+ let curr = el
147
197
 
148
- while (curr) {
149
- if (curr.nodeType === 1) {
150
- const type = curr.hasAttribute('f-if') ? 'if' :
151
- curr.hasAttribute('f-elif') ? 'elif' :
152
- curr.hasAttribute('f-else-if') ? 'elif' :
153
- curr.hasAttribute('f-else') ? 'else' : null
198
+ while (curr) {
199
+ if (curr.nodeType === 1) {
200
+ const type = curr.hasAttribute('f-if') ? 'if' :
201
+ curr.hasAttribute('f-elif') ? 'elif' :
202
+ curr.hasAttribute('f-else-if') ? 'elif' :
203
+ curr.hasAttribute('f-else') ? 'else' : null
154
204
 
155
- if (type) {
156
- const expr = type === 'else' ? 'true' : curr.getAttribute(`f-${type === 'elif' && curr.hasAttribute('f-else-if') ? 'else-if' : type}`)
157
- group.push({ node: curr, expr, type, isElement: true })
158
- processedEls.add(curr)
205
+ if (type) {
206
+ const expr = type === 'else' ? 'true' : curr.getAttribute(`f-${type === 'elif' && curr.hasAttribute('f-else-if') ? 'else-if' : type}`)
207
+ group.push({ node: curr, expr, type, isElement: true })
159
208
 
160
- curr.removeAttribute('f-if')
161
- curr.removeAttribute('f-elif')
162
- curr.removeAttribute('f-else-if')
163
- curr.removeAttribute('f-else')
209
+ curr.removeAttribute('f-if')
210
+ curr.removeAttribute('f-elif')
211
+ curr.removeAttribute('f-else-if')
212
+ curr.removeAttribute('f-else')
164
213
 
165
- if (type === 'else') break
166
- } else {
167
- break
168
- }
169
- } else if (curr.nodeType === 3 && curr.textContent.trim() === '' || curr.nodeType === 8) {
170
- group.push({ node: curr, isElement: false })
214
+ if (type === 'else') break
171
215
  } else {
172
216
  break
173
217
  }
174
- curr = curr.nextSibling
218
+ } else if (curr.nodeType === 3 && curr.textContent.trim() === '' || curr.nodeType === 8) {
219
+ group.push({ node: curr, isElement: false })
220
+ } else {
221
+ break
175
222
  }
223
+ curr = curr.nextSibling
224
+ }
176
225
 
177
- const templates = group
178
- .filter(item => item.isElement)
179
- .map(item => ({
180
- template: item.node.cloneNode(true),
181
- expr: item.expr,
182
- type: item.type
183
- }))
226
+ const templates = group
227
+ .filter(item => item.isElement)
228
+ .map(item => ({
229
+ template: item.node.cloneNode(true),
230
+ expr: item.expr,
231
+ type: item.type
232
+ }))
184
233
 
185
- const anchor = document.createComment('f-condition-anchor')
186
- parent.insertBefore(anchor, ifEl)
234
+ const anchor = document.createComment('f-condition-anchor')
235
+ parent.insertBefore(anchor, el)
187
236
 
188
- for (const item of group) item.node.remove()
237
+ for (const item of group) item.node.remove()
189
238
 
190
- let activeIndex = -1
191
- let activeNode = null
192
- let activeCleanup = []
239
+ let activeIndex = -1
240
+ let activeNode = null
241
+ let activeCleanup = []
193
242
 
194
- effects.push(effect(() => {
195
- let nextIndex = -1
196
- for (let i = 0; i < templates.length; i++) {
197
- const isMatch = templates[i].type === 'else' ? true : evaluateExpression(templates[i].expr, context)
198
- if (isMatch) {
199
- nextIndex = i
200
- break
201
- }
243
+ effects.push(effect(() => {
244
+ let nextIndex = -1
245
+ for (let i = 0; i < templates.length; i++) {
246
+ const isMatch = templates[i].type === 'else' ? true : evaluateExpression(templates[i].expr, context)
247
+ if (isMatch) {
248
+ nextIndex = i
249
+ break
202
250
  }
251
+ }
203
252
 
204
- if (nextIndex !== activeIndex) {
205
- for (const cleanup of activeCleanup) if (typeof cleanup === 'function') cleanup()
206
- activeCleanup = []
207
- if (activeNode) {
208
- activeNode.remove()
209
- activeNode = null
210
- }
253
+ if (nextIndex !== activeIndex) {
254
+ for (const cleanup of activeCleanup) if (typeof cleanup === 'function') cleanup()
255
+ activeCleanup = []
256
+ if (activeNode) {
257
+ activeNode.remove()
258
+ activeNode = null
259
+ }
211
260
 
212
- if (nextIndex !== -1) {
213
- activeNode = templates[nextIndex].template.cloneNode(true)
214
- compileNode(activeNode, context, components, activeCleanup)
215
- anchor.parentNode.insertBefore(activeNode, anchor.nextSibling)
216
- }
217
- activeIndex = nextIndex
261
+ if (nextIndex !== -1) {
262
+ activeNode = templates[nextIndex].template.cloneNode(true)
263
+ compileNode(activeNode, context, components, activeCleanup)
264
+ anchor.parentNode.insertBefore(activeNode, anchor.nextSibling)
218
265
  }
219
- }))
220
- }
266
+ activeIndex = nextIndex
267
+ }
268
+ }))
269
+
270
+ effects.push(() => {
271
+ for (const cleanup of activeCleanup) if (typeof cleanup === 'function') cleanup()
272
+ activeCleanup = []
273
+ })
274
+
275
+ return true
221
276
  }
222
277
 
223
- export function processShow(root, context, effects) {
224
- const showEls = root.querySelectorAll('[f-show]')
225
- for (const el of showEls) {
226
- if (!el.parentNode) continue
278
+ export function processShow(el, context, effects) {
279
+ if (el.nodeType !== 1 || !el.hasAttribute('f-show')) return
227
280
 
228
- const expr = el.getAttribute('f-show')
229
- el.removeAttribute('f-show')
230
- const originalDisplay = el.style.display === 'none' ? '' : el.style.display
281
+ const expr = el.getAttribute('f-show')
282
+ el.removeAttribute('f-show')
283
+ const originalDisplay = el.style.display === 'none' ? '' : el.style.display
231
284
 
232
- effects.push(effect(() => {
233
- const val = evaluateExpression(expr, context)
234
- el.style.display = val ? originalDisplay : 'none'
235
- }))
236
- }
285
+ effects.push(effect(() => {
286
+ const val = evaluateExpression(expr, context)
287
+ el.style.display = val ? originalDisplay : 'none'
288
+ }))
237
289
  }
238
290
 
239
- export function processHtml(root, context, effects) {
240
- const els = root.querySelectorAll('[f-html]')
241
- for (const el of els) {
242
- if (!el.parentNode) continue
291
+ export function processHtml(el, context, effects) {
292
+ if (el.nodeType !== 1 || !el.hasAttribute('f-html')) return
243
293
 
244
- const expr = el.getAttribute('f-html')
245
- el.removeAttribute('f-html')
294
+ const expr = el.getAttribute('f-html')
295
+ el.removeAttribute('f-html')
246
296
 
247
- const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
248
- if (isDev) {
249
- console.warn(`[framework] f-html can expose the site to XSS. Use only with trusted content.`)
250
- }
251
-
252
- effects.push(effect(() => {
253
- const val = evaluateExpression(expr, context)
254
- el.innerHTML = String(val ?? '')
255
- }))
297
+ const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
298
+ if (isDev) {
299
+ console.warn(`[framework] f-html can expose the site to XSS. Use only with trusted content.`)
256
300
  }
301
+
302
+ effects.push(effect(() => {
303
+ const val = evaluateExpression(expr, context)
304
+ el.innerHTML = String(val ?? '')
305
+ }))
257
306
  }
258
- export function processEvents(root, context) {
259
- const els = root.querySelectorAll('*')
260
- const targets = root.attributes ? [root, ...els] : els
261
-
262
- for (const el of targets) {
263
- if (!el.attributes) continue
264
-
265
- for (const attr of [...el.attributes]) {
266
- if (attr.name.startsWith('@')) {
267
- const fullEventName = attr.name.slice(1)
268
- const parts = fullEventName.split('.')
269
- const eventName = parts[0]
270
- const modifiers = parts.slice(1)
271
- const expr = attr.value
272
-
273
- el.removeAttribute(attr.name)
274
-
275
- const handler = (e) => {
276
- if (modifiers.includes('prevent')) e.preventDefault()
277
- if (modifiers.includes('stop')) e.stopPropagation()
278
- if (modifiers.includes('self') && e.target !== el) return
279
-
280
- // Specific handling for common modifiers like .enter, .escape etc on keyboard events
281
- if (e instanceof KeyboardEvent) {
282
- const key = e.key.toLowerCase()
283
- const keyModifiers = modifiers.filter(m => ['enter', 'escape', 'tab', 'space', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(m))
284
- if (keyModifiers.length > 0) {
285
- const match = keyModifiers.some(m => {
286
- if (m === 'escape') return key === 'escape' || key === 'esc'
287
- if (m === 'space') return key === ' ' || key === 'spacebar'
288
- return key === m
289
- })
290
- if (!match) return
291
- }
307
+ export function processEvents(el, context, effects) {
308
+ if (el.nodeType !== 1 || !el.attributes) return
309
+
310
+ for (const attr of [...el.attributes]) {
311
+ if (attr.name.startsWith('@')) {
312
+ const fullEventName = attr.name.slice(1)
313
+ const parts = fullEventName.split('.')
314
+ const eventName = parts[0]
315
+ const modifiers = parts.slice(1)
316
+ const expr = attr.value
317
+
318
+ el.removeAttribute(attr.name)
319
+
320
+ const handler = (e) => {
321
+ if (modifiers.includes('prevent')) e.preventDefault()
322
+ if (modifiers.includes('stop')) e.stopPropagation()
323
+ if (modifiers.includes('self') && e.target !== el) return
324
+
325
+ // Specific handling for common modifiers like .enter, .escape etc on keyboard events
326
+ if (e instanceof KeyboardEvent) {
327
+ const key = e.key.toLowerCase()
328
+ const keyModifiers = modifiers.filter(m => ['enter', 'escape', 'tab', 'space', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(m))
329
+ if (keyModifiers.length > 0) {
330
+ const match = keyModifiers.some(m => {
331
+ if (m === 'escape') return key === 'escape' || key === 'esc'
332
+ if (m === 'space') return key === ' ' || key === 'spacebar'
333
+ return key === m
334
+ })
335
+ if (!match) return
292
336
  }
293
-
294
- batch(() => {
295
- const potentialFn = context[expr]
296
- if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
297
- potentialFn(e)
298
- } else {
299
- evaluateExpression(expr, context, { $event: e }, false)
300
- }
301
- })
302
337
  }
303
338
 
304
- const targetNode = modifiers.includes('window') ? window : (modifiers.includes('document') ? document : el)
305
- const options = {
306
- once: modifiers.includes('once'),
307
- capture: modifiers.includes('capture') || modifiers.includes('away')
308
- }
339
+ batch(() => {
340
+ const potentialFn = context[expr]
341
+ if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
342
+ potentialFn(e)
343
+ } else {
344
+ evaluateExpression(expr, context, { $event: e }, false)
345
+ }
346
+ })
347
+ }
309
348
 
310
- targetNode.addEventListener(eventName, handler, options)
349
+ const targetNode = modifiers.includes('window') ? window : (modifiers.includes('document') ? document : el)
350
+ const options = {
351
+ once: modifiers.includes('once'),
352
+ capture: modifiers.includes('capture') || modifiers.includes('away')
353
+ }
354
+
355
+ targetNode.addEventListener(eventName, handler, options)
356
+ if (effects) {
357
+ effects.push(() => {
358
+ targetNode.removeEventListener(eventName, handler, options)
359
+ })
311
360
  }
312
361
  }
313
362
  }
314
363
  }
315
364
 
316
- export function processText(root, context, effects) {
317
- const els = root.querySelectorAll('[f-text]')
318
- for (const el of els) {
319
- if (!el.parentNode) continue
320
- const expr = el.getAttribute('f-text')
321
- el.removeAttribute('f-text')
322
- effects.push(effect(() => {
323
- const val = evaluateExpression(expr, context)
324
- el.textContent = String(val ?? '')
325
- }))
326
- }
365
+ export function processText(el, context, effects) {
366
+ if (el.nodeType !== 1 || !el.hasAttribute('f-text')) return
367
+ const expr = el.getAttribute('f-text')
368
+ el.removeAttribute('f-text')
369
+ effects.push(effect(() => {
370
+ const val = evaluateExpression(expr, context)
371
+ el.textContent = String(val ?? '')
372
+ }))
327
373
  }
328
374
 
329
- export function processCloak(root) {
330
- const els = root.querySelectorAll('[f-cloak]')
331
- const targets = root.hasAttribute && root.hasAttribute('f-cloak') ? [root, ...els] : els
332
- for (const el of targets) {
333
- el.removeAttribute('f-cloak')
334
- }
375
+ export function processCloak(el) {
376
+ if (el.nodeType !== 1) return
377
+ if (el.hasAttribute('f-cloak')) el.removeAttribute('f-cloak')
378
+
379
+ const els = el.querySelectorAll('[f-cloak]')
380
+ for (const e of els) e.removeAttribute('f-cloak')
335
381
  }