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.
- package/README.md +6 -7
- package/app/components/Counter.ts +1 -2
- package/app/components/CounterSection.ts +2 -2
- package/app/components/UserForm.js +0 -2
- package/app/main.js +3 -1
- package/app/routes/About.js +0 -2
- package/app/routes/Home.js +0 -1
- package/app/routes/Home.ts +4 -5
- package/app/routes/TestFor.js +41 -0
- package/app/routes/TestFor.ts +56 -0
- package/auto-imports.d.ts +12 -0
- package/framework/auto-import-preset.js +8 -0
- package/framework/bin/create-fusee.js +87 -33
- package/framework/core/compiler.js +45 -42
- package/framework/core/directives.js +298 -252
- package/framework/core/evaluator.js +7 -4
- package/framework/router/router.js +5 -3
- package/framework/types/compiler.d.ts +0 -10
- package/index.html +13 -3
- package/package.json +2 -1
- package/tsconfig.json +7 -2
- package/vite.config.js +11 -8
|
@@ -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(
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
23
|
+
const refName = el.getAttribute('f-ref')
|
|
24
|
+
el.removeAttribute('f-ref')
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
38
|
+
const expr = el.getAttribute('f-model')
|
|
39
|
+
el.removeAttribute('f-model')
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
95
|
+
// Cache of currently rendered items: Map<key, { node, effects, context }>
|
|
96
|
+
let cache = new Map()
|
|
97
|
+
let previousOrder = []
|
|
73
98
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
81
|
-
const
|
|
82
|
-
const indexName = match[3]
|
|
83
|
-
const arrayExpr = match[5]
|
|
111
|
+
const nextCache = new Map()
|
|
112
|
+
const nextOrder = []
|
|
84
113
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
120
|
+
const key = keyExpr ? evaluateExpression(keyExpr, itemContext) : item.rawIndex
|
|
121
|
+
return { item, key }
|
|
122
|
+
})
|
|
91
123
|
|
|
92
|
-
|
|
93
|
-
let
|
|
124
|
+
const parentNode = anchor.parentNode
|
|
125
|
+
let currentCursor = anchor.nextSibling
|
|
94
126
|
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
158
|
+
const childEffects = []
|
|
159
|
+
compileNode(clone, itemContext, components, childEffects)
|
|
117
160
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
161
|
+
parentNode.insertBefore(clone, currentCursor)
|
|
162
|
+
entry = { node: clone, effects: childEffects, context: itemContext }
|
|
163
|
+
}
|
|
121
164
|
|
|
122
|
-
|
|
123
|
-
|
|
165
|
+
nextCache.set(key, entry)
|
|
166
|
+
nextOrder.push(entry.node)
|
|
167
|
+
})
|
|
124
168
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
const processedEls = new Set()
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
138
188
|
|
|
139
|
-
|
|
140
|
-
|
|
189
|
+
export function processIf(el, context, components, effects) {
|
|
190
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-if')) return false
|
|
141
191
|
|
|
142
|
-
|
|
143
|
-
|
|
192
|
+
const parent = el.parentNode
|
|
193
|
+
if (!parent) return false
|
|
144
194
|
|
|
145
|
-
|
|
146
|
-
|
|
195
|
+
const group = []
|
|
196
|
+
let curr = el
|
|
147
197
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
209
|
+
curr.removeAttribute('f-if')
|
|
210
|
+
curr.removeAttribute('f-elif')
|
|
211
|
+
curr.removeAttribute('f-else-if')
|
|
212
|
+
curr.removeAttribute('f-else')
|
|
164
213
|
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
234
|
+
const anchor = document.createComment('f-condition-anchor')
|
|
235
|
+
parent.insertBefore(anchor, el)
|
|
187
236
|
|
|
188
|
-
|
|
237
|
+
for (const item of group) item.node.remove()
|
|
189
238
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
239
|
+
let activeIndex = -1
|
|
240
|
+
let activeNode = null
|
|
241
|
+
let activeCleanup = []
|
|
193
242
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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(
|
|
224
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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(
|
|
240
|
-
|
|
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
|
-
|
|
245
|
-
|
|
294
|
+
const expr = el.getAttribute('f-html')
|
|
295
|
+
el.removeAttribute('f-html')
|
|
246
296
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
}
|