fusee-framework 1.0.1 → 1.1.1
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 +1 -1
- package/app/components/Counter.ts +0 -2
- package/app/components/CounterSection.ts +1 -2
- package/app/components/UserForm.js +0 -2
- package/app/main.js +1 -1
- package/app/routes/About.js +0 -2
- package/app/routes/Home.js +0 -1
- package/app/routes/Home.ts +3 -5
- package/app/routes/TestFor.js +0 -2
- package/app/routes/TestFor.ts +54 -0
- package/auto-imports.d.ts +19 -0
- package/framework/auto-import-preset.js +8 -0
- package/framework/bin/create-fusee.js +85 -33
- package/framework/core/compiler.js +45 -42
- package/framework/core/directives.js +292 -280
- package/framework/index.js +1 -0
- package/framework/router/router.js +5 -3
- package/framework/types/compiler.d.ts +0 -10
- package/framework/types/index.d.ts +7 -0
- package/index.html +12 -3
- package/package.json +2 -1
- package/tsconfig.json +7 -2
- package/vite.config.js +11 -3
|
@@ -1,369 +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
|
-
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
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
const expr = el.getAttribute('f-for')
|
|
69
|
+
el.removeAttribute('f-for')
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
const itemName = match[4] || match[1]
|
|
78
|
+
const keyName = match[2]
|
|
79
|
+
const indexName = match[3]
|
|
80
|
+
const arrayExpr = match[5]
|
|
84
81
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
const anchor = document.createComment('f-for')
|
|
89
|
+
const parent = el.parentNode
|
|
90
|
+
parent.insertBefore(anchor, el)
|
|
94
91
|
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
const templateNode = el.cloneNode(true)
|
|
93
|
+
parent.removeChild(el)
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
// Cache of currently rendered items: Map<key, { node, effects, context }>
|
|
96
|
+
let cache = new Map()
|
|
97
|
+
let previousOrder = []
|
|
101
98
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (Array.isArray(rawItems)) {
|
|
107
|
-
items = rawItems.map((item, index) => ({ value: item, rawIndex: index }))
|
|
108
|
-
} else if (typeof rawItems === 'number' && !isNaN(rawItems)) {
|
|
109
|
-
items = Array.from({ length: Math.max(0, Math.floor(rawItems)) }, (_, i) => ({ value: i + 1, rawIndex: i }))
|
|
110
|
-
} else if (rawItems && typeof rawItems === 'object') {
|
|
111
|
-
items = Object.entries(rawItems).map(([key, value], index) => ({ value, key, rawIndex: index }))
|
|
112
|
-
}
|
|
99
|
+
effects.push(effect(() => {
|
|
100
|
+
const rawItems = evaluateExpression(arrayExpr, context)
|
|
101
|
+
let items = []
|
|
113
102
|
|
|
114
|
-
|
|
115
|
-
|
|
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 }))
|
|
109
|
+
}
|
|
116
110
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
itemContext[itemName] = item.value
|
|
120
|
-
if (keyName) itemContext[keyName] = item.hasOwnProperty('key') ? item.key : item.rawIndex
|
|
121
|
-
if (indexName) itemContext[indexName] = index
|
|
111
|
+
const nextCache = new Map()
|
|
112
|
+
const nextOrder = []
|
|
122
113
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
126
119
|
|
|
127
|
-
const
|
|
128
|
-
|
|
120
|
+
const key = keyExpr ? evaluateExpression(keyExpr, itemContext) : item.rawIndex
|
|
121
|
+
return { item, key }
|
|
122
|
+
})
|
|
129
123
|
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
const parentNode = anchor.parentNode
|
|
125
|
+
let currentCursor = anchor.nextSibling
|
|
132
126
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (keyName) entry.context[keyName] = item.hasOwnProperty('key') ? item.key : item.rawIndex
|
|
136
|
-
if (indexName) entry.context[indexName] = index
|
|
127
|
+
newItemsWithKeys.forEach(({ item, key }, index) => {
|
|
128
|
+
let entry = cache.get(key)
|
|
137
129
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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)
|
|
142
135
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
})
|
|
149
143
|
|
|
150
|
-
|
|
151
|
-
entry
|
|
144
|
+
if (entry.node !== currentCursor) {
|
|
145
|
+
parentNode.insertBefore(entry.node, currentCursor)
|
|
146
|
+
} else {
|
|
147
|
+
currentCursor = currentCursor.nextSibling
|
|
152
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)
|
|
153
156
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
const clone = templateNode.cloneNode(true)
|
|
158
|
+
const childEffects = []
|
|
159
|
+
compileNode(clone, itemContext, components, childEffects)
|
|
157
160
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
entry.node.parentNode?.removeChild(entry.node)
|
|
161
|
+
parentNode.insertBefore(clone, currentCursor)
|
|
162
|
+
entry = { node: clone, effects: childEffects, context: itemContext }
|
|
161
163
|
}
|
|
162
164
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
}
|
|
165
|
+
nextCache.set(key, entry)
|
|
166
|
+
nextOrder.push(entry.node)
|
|
167
|
+
})
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
}
|
|
172
174
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
cache = nextCache
|
|
176
|
+
previousOrder = nextOrder
|
|
177
|
+
}))
|
|
175
178
|
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
})
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
181
188
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const type = curr.hasAttribute('f-if') ? 'if' :
|
|
185
|
-
curr.hasAttribute('f-elif') ? 'elif' :
|
|
186
|
-
curr.hasAttribute('f-else-if') ? 'elif' :
|
|
187
|
-
curr.hasAttribute('f-else') ? 'else' : null
|
|
189
|
+
export function processIf(el, context, components, effects) {
|
|
190
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-if')) return false
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
group.push({ node: curr, expr, type, isElement: true })
|
|
192
|
-
processedEls.add(curr)
|
|
192
|
+
const parent = el.parentNode
|
|
193
|
+
if (!parent) return false
|
|
193
194
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
curr.removeAttribute('f-else-if')
|
|
197
|
-
curr.removeAttribute('f-else')
|
|
195
|
+
const group = []
|
|
196
|
+
let curr = el
|
|
198
197
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
204
|
+
|
|
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 })
|
|
208
|
+
|
|
209
|
+
curr.removeAttribute('f-if')
|
|
210
|
+
curr.removeAttribute('f-elif')
|
|
211
|
+
curr.removeAttribute('f-else-if')
|
|
212
|
+
curr.removeAttribute('f-else')
|
|
213
|
+
|
|
214
|
+
if (type === 'else') break
|
|
205
215
|
} else {
|
|
206
216
|
break
|
|
207
217
|
}
|
|
208
|
-
|
|
218
|
+
} else if (curr.nodeType === 3 && curr.textContent.trim() === '' || curr.nodeType === 8) {
|
|
219
|
+
group.push({ node: curr, isElement: false })
|
|
220
|
+
} else {
|
|
221
|
+
break
|
|
209
222
|
}
|
|
223
|
+
curr = curr.nextSibling
|
|
224
|
+
}
|
|
210
225
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}))
|
|
218
233
|
|
|
219
|
-
|
|
220
|
-
|
|
234
|
+
const anchor = document.createComment('f-condition-anchor')
|
|
235
|
+
parent.insertBefore(anchor, el)
|
|
221
236
|
|
|
222
|
-
|
|
237
|
+
for (const item of group) item.node.remove()
|
|
223
238
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
239
|
+
let activeIndex = -1
|
|
240
|
+
let activeNode = null
|
|
241
|
+
let activeCleanup = []
|
|
227
242
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
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
|
|
236
250
|
}
|
|
251
|
+
}
|
|
237
252
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
}
|
|
245
260
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
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)
|
|
252
265
|
}
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
255
276
|
}
|
|
256
277
|
|
|
257
|
-
export function processShow(
|
|
258
|
-
|
|
259
|
-
for (const el of showEls) {
|
|
260
|
-
if (!el.parentNode) continue
|
|
278
|
+
export function processShow(el, context, effects) {
|
|
279
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-show')) return
|
|
261
280
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
281
|
+
const expr = el.getAttribute('f-show')
|
|
282
|
+
el.removeAttribute('f-show')
|
|
283
|
+
const originalDisplay = el.style.display === 'none' ? '' : el.style.display
|
|
265
284
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
285
|
+
effects.push(effect(() => {
|
|
286
|
+
const val = evaluateExpression(expr, context)
|
|
287
|
+
el.style.display = val ? originalDisplay : 'none'
|
|
288
|
+
}))
|
|
271
289
|
}
|
|
272
290
|
|
|
273
|
-
export function processHtml(
|
|
274
|
-
|
|
275
|
-
for (const el of els) {
|
|
276
|
-
if (!el.parentNode) continue
|
|
277
|
-
|
|
278
|
-
const expr = el.getAttribute('f-html')
|
|
279
|
-
el.removeAttribute('f-html')
|
|
291
|
+
export function processHtml(el, context, effects) {
|
|
292
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-html')) return
|
|
280
293
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
console.warn(`[framework] f-html can expose the site to XSS. Use only with trusted content.`)
|
|
284
|
-
}
|
|
294
|
+
const expr = el.getAttribute('f-html')
|
|
295
|
+
el.removeAttribute('f-html')
|
|
285
296
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}))
|
|
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.`)
|
|
290
300
|
}
|
|
301
|
+
|
|
302
|
+
effects.push(effect(() => {
|
|
303
|
+
const val = evaluateExpression(expr, context)
|
|
304
|
+
el.innerHTML = String(val ?? '')
|
|
305
|
+
}))
|
|
291
306
|
}
|
|
292
|
-
export function processEvents(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (m === 'space') return key === ' ' || key === 'spacebar'
|
|
322
|
-
return key === m
|
|
323
|
-
})
|
|
324
|
-
if (!match) return
|
|
325
|
-
}
|
|
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
|
|
326
336
|
}
|
|
327
|
-
|
|
328
|
-
batch(() => {
|
|
329
|
-
const potentialFn = context[expr]
|
|
330
|
-
if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
|
|
331
|
-
potentialFn(e)
|
|
332
|
-
} else {
|
|
333
|
-
evaluateExpression(expr, context, { $event: e }, false)
|
|
334
|
-
}
|
|
335
|
-
})
|
|
336
337
|
}
|
|
337
338
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
}
|
|
343
348
|
|
|
344
|
-
|
|
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
|
+
})
|
|
345
360
|
}
|
|
346
361
|
}
|
|
347
362
|
}
|
|
348
363
|
}
|
|
349
364
|
|
|
350
|
-
export function processText(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
el.textContent = String(val ?? '')
|
|
359
|
-
}))
|
|
360
|
-
}
|
|
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
|
+
}))
|
|
361
373
|
}
|
|
362
374
|
|
|
363
|
-
export function processCloak(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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')
|
|
369
381
|
}
|