fusee-framework 1.5.3 → 1.7.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 +30 -13
- package/app/benchmark/benchmark.js +633 -0
- package/app/benchmark/index.html +102 -0
- package/app/benchmark/styles.css +249 -0
- package/app/components/Counter.js +17 -7
- package/app/components/Counter.ts +1 -1
- package/app/components/CounterSection.js +3 -4
- package/app/main.js +29 -62
- package/app/main.ts +72 -63
- package/app/pages/404.js +16 -0
- package/app/pages/[...all].js +35 -0
- package/app/pages/[...all].ts +35 -0
- package/app/pages/_layout.js +26 -0
- package/app/pages/_layout.ts +26 -0
- package/app/pages/about.js +23 -0
- package/app/pages/about.ts +29 -0
- package/app/pages/composables.js +158 -0
- package/app/pages/composables.ts +158 -0
- package/app/pages/dynamic.js +40 -0
- package/app/pages/dynamic.ts +40 -0
- package/app/pages/index.js +117 -0
- package/app/pages/index.ts +78 -0
- package/app/pages/login.ts +21 -0
- package/app/pages/objects/[id].js +56 -0
- package/app/pages/objects/[id].ts +56 -0
- package/app/pages/posts/[category]/[slug].js +56 -0
- package/app/pages/posts/[category]/[slug].ts +56 -0
- package/app/pages/router-example.js +56 -0
- package/app/pages/router-example.ts +56 -0
- package/app/pages/test-for.js +65 -0
- package/app/pages/test-for.ts +80 -0
- package/app/pages/users/[id].js +24 -0
- package/app/pages/users/[id].ts +46 -0
- package/app/pages/users/index.js +22 -0
- package/app/pages/users/index.ts +29 -0
- package/app/pages/users.js +43 -0
- package/app/pages/users.ts +78 -0
- package/app/routes/About.js +1 -1
- package/app/routes/About.ts +1 -1
- package/app/routes/AuthRoutes.ts +41 -0
- package/app/routes/Home.js +9 -0
- package/app/routes/NotFound.js +16 -0
- package/app/routes/NotFound.ts +16 -0
- package/app/routes/RouterExample.js +56 -0
- package/app/routes/RouterExample.ts +56 -0
- package/app/routes/WildcardDemo.js +35 -0
- package/app/routes/WildcardDemo.ts +35 -0
- package/auto-imports.d.ts +1 -0
- package/bin/create-fusee-linux +0 -0
- package/bin/create-fusee-mac-arm64 +0 -0
- package/bin/create-fusee-mac-intel +0 -0
- package/bin/create-fusee-win.exe +0 -0
- package/bin/create-fusee.exe +0 -0
- package/bin/index.js +38 -0
- package/create-fusee-go/cmd/generate.go +72 -0
- package/create-fusee-go/cmd/init.go +38 -0
- package/create-fusee-go/cmd/root.go +121 -0
- package/create-fusee-go/go.mod +10 -0
- package/create-fusee-go/go.sum +10 -0
- package/create-fusee-go/internal/assets/assets.go +78 -0
- package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +9 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler.js +253 -0
- package/create-fusee-go/internal/assets/embed/framework/core/component.js +266 -0
- package/create-fusee-go/internal/assets/embed/framework/core/composable.js +27 -0
- package/create-fusee-go/internal/assets/embed/framework/core/directives.js +875 -0
- package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +153 -0
- package/create-fusee-go/internal/assets/embed/framework/core/event-delegation.js +191 -0
- package/create-fusee-go/internal/assets/embed/framework/core/signal.js +412 -0
- package/create-fusee-go/internal/assets/embed/framework/core/store.js +81 -0
- package/create-fusee-go/internal/assets/embed/framework/index.d.ts +3 -0
- package/create-fusee-go/internal/assets/embed/framework/index.js +10 -0
- package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +177 -0
- package/create-fusee-go/internal/assets/embed/framework/router/router.js +462 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler.d.ts +15 -0
- package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +88 -0
- package/create-fusee-go/internal/assets/embed/framework/types/composable.d.ts +14 -0
- package/create-fusee-go/internal/assets/embed/framework/types/directives.d.ts +55 -0
- package/create-fusee-go/internal/assets/embed/framework/types/evaluator.d.ts +13 -0
- package/create-fusee-go/internal/assets/embed/framework/types/event-delegation.d.ts +32 -0
- package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +22 -0
- package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +32 -0
- package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +79 -0
- package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +8 -0
- package/create-fusee-go/internal/assets/templates/Counter.tmpl +26 -0
- package/create-fusee-go/internal/assets/templates/component.tmpl +11 -0
- package/create-fusee-go/internal/assets/templates/components/Loading.tmpl +24 -0
- package/create-fusee-go/internal/assets/templates/index.html.tmpl +15 -0
- package/create-fusee-go/internal/assets/templates/main.tmpl +14 -0
- package/create-fusee-go/internal/assets/templates/package.json.tmpl +14 -0
- package/create-fusee-go/internal/assets/templates/page.tmpl +12 -0
- package/create-fusee-go/internal/assets/templates/pages/about.tmpl +20 -0
- package/create-fusee-go/internal/assets/templates/pages/index.tmpl +22 -0
- package/create-fusee-go/internal/assets/templates/pages/layout.tmpl +18 -0
- package/create-fusee-go/internal/assets/templates/tsconfig.json.tmpl +14 -0
- package/create-fusee-go/internal/assets/templates/vite.config.js.tmpl +18 -0
- package/create-fusee-go/main.go +7 -0
- package/framework/auto-import-preset.js +1 -1
- package/framework/core/compiler.js +252 -249
- package/framework/core/component.js +265 -260
- package/framework/core/directives.js +875 -632
- package/framework/core/evaluator.js +153 -70
- package/framework/core/event-delegation.js +191 -0
- package/framework/core/signal.js +412 -404
- package/framework/index.js +10 -11
- package/framework/router/file-router.js +177 -0
- package/framework/router/router.js +462 -80
- package/framework/types/component.d.ts +0 -4
- package/framework/types/event-delegation.d.ts +32 -0
- package/framework/types/index.d.ts +3 -0
- package/framework/types/router.d.ts +32 -15
- package/framework/types/signal.d.ts +1 -0
- package/index.html +638 -563
- package/package.json +5 -3
- package/tsconfig.json +1 -0
- /package/{framework → create-fusee-go/internal/assets/embed/framework}/bin/create-fusee.js +0 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
import { signal, effect, batch } from './signal.js'
|
|
2
|
+
import { evaluateExpression } from './evaluator.js'
|
|
3
|
+
import { compileNode } from './compiler.js'
|
|
4
|
+
import { registerDelegatedEvent, createEventHandler, isDelegatedEvent } from './event-delegation.js'
|
|
5
|
+
import { currentRoute } from '../router/router.js'
|
|
6
|
+
|
|
7
|
+
const customDirectives = new Map()
|
|
8
|
+
|
|
9
|
+
export function directive(name, definition) {
|
|
10
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
11
|
+
console.error('[framework] directive() requires a non-empty name')
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
if (!definition || typeof definition !== 'object') {
|
|
15
|
+
console.error(`[framework] directive("${name}") requires a definition object`)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
customDirectives.set(name, definition)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function processCustomDirectives(el, context, effects) {
|
|
22
|
+
if (el.nodeType !== 1 || !el.hasAttribute || !el.attributes) return
|
|
23
|
+
|
|
24
|
+
for (const attr of [...el.attributes]) {
|
|
25
|
+
const match = attr.name.match(/^f-([a-zA-Z0-9-]+)(?::([a-zA-Z0-9-]+))?(?:\.(.+))?$/)
|
|
26
|
+
|
|
27
|
+
if (!match) continue
|
|
28
|
+
|
|
29
|
+
const directiveName = match[1]
|
|
30
|
+
const arg = match[2] || null
|
|
31
|
+
const modifiersStr = match[3]
|
|
32
|
+
const modifiers = modifiersStr ? modifiersStr.split('.') : []
|
|
33
|
+
|
|
34
|
+
const definition = customDirectives.get(directiveName)
|
|
35
|
+
if (!definition) {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
el.removeAttribute(attr.name)
|
|
40
|
+
|
|
41
|
+
const value = evaluateExpression(attr.value, context)
|
|
42
|
+
const expression = attr.value
|
|
43
|
+
|
|
44
|
+
const binding = {
|
|
45
|
+
value,
|
|
46
|
+
expression,
|
|
47
|
+
arg,
|
|
48
|
+
modifiers
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof definition.mounted === 'function') {
|
|
52
|
+
definition.mounted(el, binding)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof definition.updated === 'function') {
|
|
56
|
+
effects.push(effect(() => {
|
|
57
|
+
const newValue = evaluateExpression(expression, context)
|
|
58
|
+
|
|
59
|
+
if (!Object.is(binding.value, newValue)) {
|
|
60
|
+
binding.value = newValue
|
|
61
|
+
definition.updated(el, binding)
|
|
62
|
+
}
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof definition.unmounted === 'function') {
|
|
67
|
+
effects.push(() => {
|
|
68
|
+
definition.unmounted(el, binding)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function processDirectives(root, context, components, effects) {
|
|
75
|
+
if (processFor(root, context, components, effects)) return true
|
|
76
|
+
if (processIf(root, context, components, effects)) return true
|
|
77
|
+
if (processIs(root, context, components, effects)) return true
|
|
78
|
+
|
|
79
|
+
processShow(root, context, effects)
|
|
80
|
+
processModel(root, context, effects)
|
|
81
|
+
processRefs(root, context)
|
|
82
|
+
processHtml(root, context, effects)
|
|
83
|
+
processText(root, context, effects)
|
|
84
|
+
processEvents(root, context, effects)
|
|
85
|
+
processCustomDirectives(root, context, effects)
|
|
86
|
+
processClassList(root, context, effects)
|
|
87
|
+
processLinks(root, effects)
|
|
88
|
+
processCloak(root)
|
|
89
|
+
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function processRefs(el, context) {
|
|
94
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-ref')) return
|
|
95
|
+
|
|
96
|
+
const refName = el.getAttribute('f-ref')
|
|
97
|
+
el.removeAttribute('f-ref')
|
|
98
|
+
|
|
99
|
+
const refVar = context[refName]
|
|
100
|
+
if (typeof refVar === 'function' && refVar.isSignal && !refVar.readonly) {
|
|
101
|
+
refVar(el)
|
|
102
|
+
} else {
|
|
103
|
+
const reason = refVar?.readonly ? 'it is read-only (computed)' : 'it is not a signal'
|
|
104
|
+
console.warn(`[framework] f-ref requires a writable signal. Target "${refName}" is invalid: ${reason}`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function processModel(el, context, effects) {
|
|
109
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-model')) return
|
|
110
|
+
|
|
111
|
+
const expr = el.getAttribute('f-model')
|
|
112
|
+
el.removeAttribute('f-model')
|
|
113
|
+
|
|
114
|
+
const signalMethod = context[expr]
|
|
115
|
+
if (typeof signalMethod === 'function' && signalMethod.isSignal && !signalMethod.readonly) {
|
|
116
|
+
effects.push(effect(() => {
|
|
117
|
+
const val = signalMethod()
|
|
118
|
+
if (el.type === 'checkbox') {
|
|
119
|
+
el.checked = !!val
|
|
120
|
+
} else {
|
|
121
|
+
el.value = val ?? ''
|
|
122
|
+
}
|
|
123
|
+
}))
|
|
124
|
+
|
|
125
|
+
const eventName = (el.tagName === 'SELECT' || el.type === 'checkbox' || el.type === 'radio') ? 'change' : 'input'
|
|
126
|
+
const handler = () => {
|
|
127
|
+
const newVal = el.type === 'checkbox' ? el.checked : el.value
|
|
128
|
+
batch(() => signalMethod(newVal))
|
|
129
|
+
}
|
|
130
|
+
el.addEventListener(eventName, handler)
|
|
131
|
+
effects.push(() => el.removeEventListener(eventName, handler))
|
|
132
|
+
} else {
|
|
133
|
+
const reason = signalMethod?.readonly ? 'it is read-only (computed)' : 'it is not a signal'
|
|
134
|
+
console.warn(`[framework] f-model requires a writable signal. Expression "${expr}" is invalid: ${reason}`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function processFor(el, context, components, effects) {
|
|
139
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-for')) return false
|
|
140
|
+
|
|
141
|
+
const expr = el.getAttribute('f-for')
|
|
142
|
+
el.removeAttribute('f-for')
|
|
143
|
+
|
|
144
|
+
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*$/)
|
|
145
|
+
if (!match) {
|
|
146
|
+
console.warn(`[framework] Invalid f-for expression: "${expr}"`)
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const itemName = match[4] || match[1]
|
|
151
|
+
const keyName = match[2]
|
|
152
|
+
const indexName = match[3]
|
|
153
|
+
const arrayExpr = match[5]
|
|
154
|
+
|
|
155
|
+
const keyExpr = el.getAttribute('data-bind-key') || el.getAttribute(':key')
|
|
156
|
+
if (keyExpr) {
|
|
157
|
+
el.removeAttribute('data-bind-key')
|
|
158
|
+
el.removeAttribute(':key')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const anchor = document.createComment('f-for')
|
|
162
|
+
const parent = el.parentNode
|
|
163
|
+
parent.insertBefore(anchor, el)
|
|
164
|
+
|
|
165
|
+
const templateNode = el.cloneNode(true)
|
|
166
|
+
parent.removeChild(el)
|
|
167
|
+
|
|
168
|
+
let cache = new Map()
|
|
169
|
+
let previousOrder = []
|
|
170
|
+
|
|
171
|
+
effects.push(effect(() => {
|
|
172
|
+
const rawItems = evaluateExpression(arrayExpr, context)
|
|
173
|
+
let items = []
|
|
174
|
+
|
|
175
|
+
if (Array.isArray(rawItems)) {
|
|
176
|
+
items = rawItems.map((item, index) => ({ value: item, rawIndex: index }))
|
|
177
|
+
} else if (typeof rawItems === 'number' && !isNaN(rawItems)) {
|
|
178
|
+
items = Array.from({ length: Math.max(0, Math.floor(rawItems)) }, (_, i) => ({ value: i + 1, rawIndex: i }))
|
|
179
|
+
} else if (rawItems && typeof rawItems === 'object') {
|
|
180
|
+
items = Object.entries(rawItems).map(([key, value], index) => ({ value, key, rawIndex: index }))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const nextCache = new Map()
|
|
184
|
+
const nextOrder = []
|
|
185
|
+
|
|
186
|
+
const newItemsWithKeys = items.map((item, index) => {
|
|
187
|
+
const itemContext = Object.create(context)
|
|
188
|
+
itemContext[itemName] = item.value
|
|
189
|
+
if (keyName) itemContext[keyName] = item.hasOwnProperty('key') ? item.key : item.rawIndex
|
|
190
|
+
if (indexName) itemContext[indexName] = index
|
|
191
|
+
|
|
192
|
+
const key = keyExpr ? evaluateExpression(keyExpr, itemContext) : item.rawIndex
|
|
193
|
+
return { item, key }
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const parentNode = anchor.parentNode
|
|
197
|
+
let currentCursor = anchor.nextSibling
|
|
198
|
+
|
|
199
|
+
const isClear = items.length === 0 && previousOrder.length > 0
|
|
200
|
+
if (isClear) {
|
|
201
|
+
const nodesToRemove = []
|
|
202
|
+
let node = anchor.nextSibling
|
|
203
|
+
while (node) {
|
|
204
|
+
const next = node.nextSibling
|
|
205
|
+
nodesToRemove.push(node)
|
|
206
|
+
node = next
|
|
207
|
+
}
|
|
208
|
+
if (nodesToRemove.length > 0) {
|
|
209
|
+
const fragment = document.createDocumentFragment()
|
|
210
|
+
nodesToRemove.forEach(n => fragment.appendChild(n))
|
|
211
|
+
}
|
|
212
|
+
for (const entry of cache.values()) {
|
|
213
|
+
for (let i = entry.effects.length - 1; i >= 0; i--) {
|
|
214
|
+
const cleanup = entry.effects[i]
|
|
215
|
+
if (typeof cleanup === 'function') cleanup()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
cache.clear()
|
|
219
|
+
previousOrder = []
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const fragment = document.createDocumentFragment()
|
|
224
|
+
const nodesToInsert = []
|
|
225
|
+
|
|
226
|
+
newItemsWithKeys.forEach(({ item, key }, index) => {
|
|
227
|
+
let entry = cache.get(key)
|
|
228
|
+
|
|
229
|
+
if (entry) {
|
|
230
|
+
batch(() => {
|
|
231
|
+
if (typeof entry.context[itemName] === 'function' && entry.context[itemName].isSignal) {
|
|
232
|
+
entry.context[itemName](item.value)
|
|
233
|
+
}
|
|
234
|
+
if (keyName && typeof entry.context[keyName] === 'function' && entry.context[keyName].isSignal) {
|
|
235
|
+
entry.context[keyName](item.hasOwnProperty('key') ? item.key : item.rawIndex)
|
|
236
|
+
}
|
|
237
|
+
if (indexName && typeof entry.context[indexName] === 'function' && entry.context[indexName].isSignal) {
|
|
238
|
+
entry.context[indexName](index)
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
if (entry.node !== currentCursor) {
|
|
243
|
+
parentNode.insertBefore(entry.node, currentCursor)
|
|
244
|
+
} else {
|
|
245
|
+
currentCursor = currentCursor.nextSibling
|
|
246
|
+
}
|
|
247
|
+
cache.delete(key)
|
|
248
|
+
} else {
|
|
249
|
+
const itemContext = Object.create(context)
|
|
250
|
+
itemContext[itemName] = signal(item.value)
|
|
251
|
+
if (keyName) itemContext[keyName] = signal(item.hasOwnProperty('key') ? item.key : item.rawIndex)
|
|
252
|
+
if (indexName) itemContext[indexName] = signal(index)
|
|
253
|
+
|
|
254
|
+
const clone = templateNode.cloneNode(true)
|
|
255
|
+
const childEffects = []
|
|
256
|
+
compileNode(clone, itemContext, components, childEffects)
|
|
257
|
+
|
|
258
|
+
fragment.appendChild(clone)
|
|
259
|
+
entry = { node: clone, effects: childEffects, context: itemContext }
|
|
260
|
+
nodesToInsert.push({ node: clone, effects: childEffects, context: itemContext, key })
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
nextCache.set(key, entry)
|
|
264
|
+
nextOrder.push(entry.node)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
if (fragment.childNodes.length > 0) {
|
|
268
|
+
parentNode.insertBefore(fragment, currentCursor)
|
|
269
|
+
nodesToInsert.forEach(({ node, effects, context, key }) => {
|
|
270
|
+
nextCache.set(key, { node, effects, context })
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const removedEntries = []
|
|
275
|
+
for (const [_, entry] of cache.entries()) {
|
|
276
|
+
removedEntries.push(entry)
|
|
277
|
+
}
|
|
278
|
+
if (removedEntries.length > 0) {
|
|
279
|
+
const removeFragment = document.createDocumentFragment()
|
|
280
|
+
removedEntries.forEach(entry => {
|
|
281
|
+
for (let i = entry.effects.length - 1; i >= 0; i--) {
|
|
282
|
+
const cleanup = entry.effects[i]
|
|
283
|
+
if (typeof cleanup === 'function') cleanup()
|
|
284
|
+
}
|
|
285
|
+
if (entry.node.parentNode) {
|
|
286
|
+
removeFragment.appendChild(entry.node)
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
cache = nextCache
|
|
292
|
+
previousOrder = nextOrder
|
|
293
|
+
}))
|
|
294
|
+
|
|
295
|
+
effects.push(() => {
|
|
296
|
+
for (const entry of cache.values()) {
|
|
297
|
+
for (let i = entry.effects.length - 1; i >= 0; i--) {
|
|
298
|
+
const cleanup = entry.effects[i]
|
|
299
|
+
if (typeof cleanup === 'function') cleanup()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
cache.clear()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function processIf(el, context, components, effects) {
|
|
309
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-if')) return false
|
|
310
|
+
|
|
311
|
+
const parent = el.parentNode
|
|
312
|
+
if (!parent) return false
|
|
313
|
+
|
|
314
|
+
const group = []
|
|
315
|
+
let curr = el
|
|
316
|
+
|
|
317
|
+
while (curr) {
|
|
318
|
+
if (curr.nodeType === 1) {
|
|
319
|
+
const type = curr.hasAttribute('f-if') ? 'if' :
|
|
320
|
+
curr.hasAttribute('f-elif') ? 'elif' :
|
|
321
|
+
curr.hasAttribute('f-else-if') ? 'elif' :
|
|
322
|
+
curr.hasAttribute('f-else') ? 'else' : null
|
|
323
|
+
|
|
324
|
+
if (type) {
|
|
325
|
+
const expr = type === 'else' ? 'true' : curr.getAttribute(`f-${type === 'elif' && curr.hasAttribute('f-else-if') ? 'else-if' : type}`)
|
|
326
|
+
group.push({ node: curr, expr, type, isElement: true })
|
|
327
|
+
|
|
328
|
+
curr.removeAttribute('f-if')
|
|
329
|
+
curr.removeAttribute('f-elif')
|
|
330
|
+
curr.removeAttribute('f-else-if')
|
|
331
|
+
curr.removeAttribute('f-else')
|
|
332
|
+
|
|
333
|
+
if (type === 'else') break
|
|
334
|
+
} else {
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
} else if (curr.nodeType === 3 && curr.textContent.trim() === '' || curr.nodeType === 8) {
|
|
338
|
+
group.push({ node: curr, isElement: false })
|
|
339
|
+
} else {
|
|
340
|
+
break
|
|
341
|
+
}
|
|
342
|
+
curr = curr.nextSibling
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const templates = group
|
|
346
|
+
.filter(item => item.isElement)
|
|
347
|
+
.map(item => ({
|
|
348
|
+
template: item.node.cloneNode(true),
|
|
349
|
+
expr: item.expr,
|
|
350
|
+
type: item.type
|
|
351
|
+
}))
|
|
352
|
+
|
|
353
|
+
const anchor = document.createComment('f-condition-anchor')
|
|
354
|
+
parent.insertBefore(anchor, el)
|
|
355
|
+
|
|
356
|
+
for (const item of group) item.node.remove()
|
|
357
|
+
|
|
358
|
+
let activeIndex = -1
|
|
359
|
+
let activeNode = null
|
|
360
|
+
let activeCleanup = []
|
|
361
|
+
|
|
362
|
+
effects.push(effect(() => {
|
|
363
|
+
let nextIndex = -1
|
|
364
|
+
for (let i = 0; i < templates.length; i++) {
|
|
365
|
+
const isMatch = templates[i].type === 'else' ? true : evaluateExpression(templates[i].expr, context)
|
|
366
|
+
if (isMatch) {
|
|
367
|
+
nextIndex = i
|
|
368
|
+
break
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (nextIndex !== activeIndex) {
|
|
373
|
+
for (let i = activeCleanup.length - 1; i >= 0; i--) {
|
|
374
|
+
const cleanup = activeCleanup[i]
|
|
375
|
+
if (typeof cleanup === 'function') cleanup()
|
|
376
|
+
}
|
|
377
|
+
activeCleanup = []
|
|
378
|
+
if (activeNode) {
|
|
379
|
+
activeNode.remove()
|
|
380
|
+
activeNode = null
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (nextIndex !== -1) {
|
|
384
|
+
activeNode = templates[nextIndex].template.cloneNode(true)
|
|
385
|
+
compileNode(activeNode, context, components, activeCleanup)
|
|
386
|
+
anchor.parentNode.insertBefore(activeNode, anchor.nextSibling)
|
|
387
|
+
}
|
|
388
|
+
activeIndex = nextIndex
|
|
389
|
+
}
|
|
390
|
+
}))
|
|
391
|
+
|
|
392
|
+
effects.push(() => {
|
|
393
|
+
for (let i = activeCleanup.length - 1; i >= 0; i--) {
|
|
394
|
+
const cleanup = activeCleanup[i]
|
|
395
|
+
if (typeof cleanup === 'function') cleanup()
|
|
396
|
+
}
|
|
397
|
+
activeCleanup = []
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
return true
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function processIs(el, context, components, effects) {
|
|
404
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-is')) return false
|
|
405
|
+
|
|
406
|
+
const expr = el.getAttribute('f-is')
|
|
407
|
+
const keepAlive = processKeepAlive(el)
|
|
408
|
+
|
|
409
|
+
el.removeAttribute('f-is')
|
|
410
|
+
|
|
411
|
+
const anchor = document.createComment('f-keepAlive-anchor')
|
|
412
|
+
el.parentNode.insertBefore(anchor, el)
|
|
413
|
+
const templateNode = el.cloneNode(true)
|
|
414
|
+
el.remove()
|
|
415
|
+
|
|
416
|
+
const cache = new Map()
|
|
417
|
+
let activeKey = null
|
|
418
|
+
let activeCleanup = []
|
|
419
|
+
|
|
420
|
+
effects.push(effect(() => {
|
|
421
|
+
const rawValue = evaluateExpression(expr, context)
|
|
422
|
+
|
|
423
|
+
if (rawValue === activeKey) return
|
|
424
|
+
|
|
425
|
+
if (activeKey !== null) {
|
|
426
|
+
const currentEntry = cache.get(activeKey)
|
|
427
|
+
if (currentEntry) {
|
|
428
|
+
if (keepAlive) {
|
|
429
|
+
currentEntry.node.remove()
|
|
430
|
+
} else {
|
|
431
|
+
for (let i = activeCleanup.length - 1; i >= 0; i--) {
|
|
432
|
+
const cleanup = activeCleanup[i]
|
|
433
|
+
if (typeof cleanup === 'function') cleanup()
|
|
434
|
+
}
|
|
435
|
+
currentEntry.node.remove()
|
|
436
|
+
cache.delete(activeKey)
|
|
437
|
+
activeCleanup = []
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!rawValue) {
|
|
443
|
+
activeKey = null
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let entry = cache.get(rawValue)
|
|
448
|
+
|
|
449
|
+
if (entry && keepAlive) {
|
|
450
|
+
anchor.parentNode.insertBefore(entry.node, anchor.nextSibling)
|
|
451
|
+
activeCleanup = entry.cleanup
|
|
452
|
+
} else {
|
|
453
|
+
let ComponentFn = typeof rawValue === 'string' ? components[rawValue] : rawValue
|
|
454
|
+
|
|
455
|
+
if (typeof ComponentFn !== 'function') {
|
|
456
|
+
console.warn(`[framework] f-is: Component "${rawValue}" not found or invalid.`)
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const compEl = templateNode.cloneNode(true)
|
|
461
|
+
compEl._isDynamicComponent = true
|
|
462
|
+
|
|
463
|
+
const tempName = '__dynamic_component__'
|
|
464
|
+
compEl.setAttribute('data-component', tempName)
|
|
465
|
+
|
|
466
|
+
anchor.parentNode.insertBefore(compEl, anchor.nextSibling)
|
|
467
|
+
|
|
468
|
+
const childCleanup = []
|
|
469
|
+
const dynamicComponents = { ...components, [tempName]: ComponentFn }
|
|
470
|
+
|
|
471
|
+
compileNode(compEl, context, dynamicComponents, childCleanup)
|
|
472
|
+
|
|
473
|
+
entry = { node: compEl, cleanup: childCleanup }
|
|
474
|
+
cache.set(rawValue, entry)
|
|
475
|
+
activeCleanup = childCleanup
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
activeKey = rawValue
|
|
479
|
+
}))
|
|
480
|
+
|
|
481
|
+
effects.push(() => {
|
|
482
|
+
for (const entry of cache.values()) {
|
|
483
|
+
for (let i = entry.cleanup.length - 1; i >= 0; i--) {
|
|
484
|
+
const cleanup = entry.cleanup[i]
|
|
485
|
+
if (typeof cleanup === 'function') cleanup()
|
|
486
|
+
}
|
|
487
|
+
entry.node.remove()
|
|
488
|
+
}
|
|
489
|
+
cache.clear()
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
return true
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function processKeepAlive(el) {
|
|
496
|
+
if (el.nodeType !== 1) return false
|
|
497
|
+
const hasKeepAlive = el.hasAttribute('f-keepAlive')
|
|
498
|
+
if (hasKeepAlive) el.removeAttribute('f-keepAlive')
|
|
499
|
+
return hasKeepAlive
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function processShow(el, context, effects) {
|
|
503
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-show')) return
|
|
504
|
+
|
|
505
|
+
const expr = el.getAttribute('f-show')
|
|
506
|
+
el.removeAttribute('f-show')
|
|
507
|
+
const originalDisplay = el.style.display === 'none' ? '' : el.style.display
|
|
508
|
+
|
|
509
|
+
effects.push(effect(() => {
|
|
510
|
+
const val = evaluateExpression(expr, context)
|
|
511
|
+
el.style.display = val ? originalDisplay : 'none'
|
|
512
|
+
}))
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function processHtml(el, context, effects) {
|
|
516
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-html')) return
|
|
517
|
+
|
|
518
|
+
const expr = el.getAttribute('f-html')
|
|
519
|
+
el.removeAttribute('f-html')
|
|
520
|
+
|
|
521
|
+
const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
|
|
522
|
+
if (isDev) {
|
|
523
|
+
console.warn(`[framework] f-html can expose the site to XSS. Use only with trusted content.`)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
effects.push(effect(() => {
|
|
527
|
+
const val = evaluateExpression(expr, context)
|
|
528
|
+
el.innerHTML = String(val ?? '')
|
|
529
|
+
}))
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function processEvents(el, context, effects) {
|
|
533
|
+
if (el.nodeType !== 1 || !el.attributes) return
|
|
534
|
+
|
|
535
|
+
for (const attr of [...el.attributes]) {
|
|
536
|
+
if (attr.name.startsWith('on:')) {
|
|
537
|
+
const fullEventName = attr.name.slice(3)
|
|
538
|
+
const parts = fullEventName.split('.')
|
|
539
|
+
const eventName = parts[0]
|
|
540
|
+
const modifiers = parts.slice(1)
|
|
541
|
+
const expr = attr.value
|
|
542
|
+
|
|
543
|
+
el.removeAttribute(attr.name)
|
|
544
|
+
|
|
545
|
+
let timeoutId
|
|
546
|
+
let lastRun = 0
|
|
547
|
+
let throttleTimeoutId
|
|
548
|
+
|
|
549
|
+
let debounceMs = 0
|
|
550
|
+
let throttleMs = 0
|
|
551
|
+
|
|
552
|
+
const parseDuration = (idx) => {
|
|
553
|
+
const next = modifiers[idx + 1]
|
|
554
|
+
if (next && /^\d/.test(next)) {
|
|
555
|
+
if (next.endsWith('ms')) return parseInt(next) || 250
|
|
556
|
+
if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
|
|
557
|
+
}
|
|
558
|
+
return 250
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const debounceIdx = modifiers.indexOf('debounce')
|
|
562
|
+
if (debounceIdx > -1) debounceMs = parseDuration(debounceIdx)
|
|
563
|
+
|
|
564
|
+
const throttleIdx = modifiers.indexOf('throttle')
|
|
565
|
+
if (throttleIdx > -1) throttleMs = parseDuration(throttleIdx)
|
|
566
|
+
|
|
567
|
+
const executeLogic = (e) => {
|
|
568
|
+
batch(() => {
|
|
569
|
+
const potentialFn = context[expr]
|
|
570
|
+
if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
|
|
571
|
+
potentialFn(e)
|
|
572
|
+
} else {
|
|
573
|
+
evaluateExpression(expr, context, { $event: e }, false)
|
|
574
|
+
}
|
|
575
|
+
})
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const handler = (e) => {
|
|
579
|
+
if (modifiers.includes('prevent')) e.preventDefault()
|
|
580
|
+
if (modifiers.includes('stop')) e.stopPropagation()
|
|
581
|
+
if (modifiers.includes('self') && e.target !== el) return
|
|
582
|
+
|
|
583
|
+
if (e instanceof KeyboardEvent) {
|
|
584
|
+
const key = e.key.toLowerCase()
|
|
585
|
+
const keyModifiers = modifiers.filter(m => ['enter', 'escape', 'tab', 'space', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(m))
|
|
586
|
+
if (keyModifiers.length > 0) {
|
|
587
|
+
const match = keyModifiers.some(m => {
|
|
588
|
+
if (m === 'escape') return key === 'escape' || key === 'esc'
|
|
589
|
+
if (m === 'space') return key === ' ' || key === 'spacebar'
|
|
590
|
+
return key === m
|
|
591
|
+
})
|
|
592
|
+
if (!match) return
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (throttleMs > 0) {
|
|
597
|
+
const now = Date.now()
|
|
598
|
+
const remaining = throttleMs - (now - lastRun)
|
|
599
|
+
|
|
600
|
+
if (remaining <= 0) {
|
|
601
|
+
clearTimeout(throttleTimeoutId)
|
|
602
|
+
lastRun = now
|
|
603
|
+
executeLogic(e)
|
|
604
|
+
} else {
|
|
605
|
+
clearTimeout(throttleTimeoutId)
|
|
606
|
+
throttleTimeoutId = setTimeout(() => {
|
|
607
|
+
lastRun = Date.now()
|
|
608
|
+
executeLogic(e)
|
|
609
|
+
}, remaining)
|
|
610
|
+
}
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (debounceMs > 0) {
|
|
615
|
+
clearTimeout(timeoutId)
|
|
616
|
+
timeoutId = setTimeout(() => executeLogic(e), debounceMs)
|
|
617
|
+
} else {
|
|
618
|
+
executeLogic(e)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const targetNode = modifiers.includes('window') ? window : (modifiers.includes('document') ? document : el)
|
|
623
|
+
const options = {
|
|
624
|
+
once: modifiers.includes('once'),
|
|
625
|
+
capture: modifiers.includes('capture') || modifiers.includes('away')
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
targetNode.addEventListener(eventName, handler, options)
|
|
629
|
+
if (effects) {
|
|
630
|
+
effects.push(() => {
|
|
631
|
+
targetNode.removeEventListener(eventName, handler, options)
|
|
632
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
633
|
+
if (throttleTimeoutId) clearTimeout(throttleTimeoutId)
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
continue
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (attr.name.startsWith('@')) {
|
|
640
|
+
const fullEventName = attr.name.slice(1)
|
|
641
|
+
const parts = fullEventName.split('.')
|
|
642
|
+
const eventName = parts[0]
|
|
643
|
+
const modifiers = parts.slice(1)
|
|
644
|
+
const expr = attr.value
|
|
645
|
+
|
|
646
|
+
el.removeAttribute(attr.name)
|
|
647
|
+
|
|
648
|
+
const targetNode = modifiers.includes('window') ? window : (modifiers.includes('document') ? document : null)
|
|
649
|
+
|
|
650
|
+
if (targetNode) {
|
|
651
|
+
const handler = createEventHandler(
|
|
652
|
+
typeof context[expr] === 'function' && !context[expr].isSignal
|
|
653
|
+
? context[expr]
|
|
654
|
+
: expr,
|
|
655
|
+
modifiers,
|
|
656
|
+
context,
|
|
657
|
+
expr
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
const options = {
|
|
661
|
+
once: modifiers.includes('once'),
|
|
662
|
+
capture: modifiers.includes('capture') || modifiers.includes('away')
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
targetNode.addEventListener(eventName, handler, options)
|
|
666
|
+
if (effects) {
|
|
667
|
+
effects.push(() => {
|
|
668
|
+
targetNode.removeEventListener(eventName, handler, options)
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
} else if (isDelegatedEvent(eventName) && !modifiers.includes('capture') && !modifiers.includes('away')) {
|
|
672
|
+
const potentialFn = context[expr]
|
|
673
|
+
const isFunction = typeof potentialFn === 'function' && !potentialFn.isSignal
|
|
674
|
+
|
|
675
|
+
const handlerState = {
|
|
676
|
+
timeoutId: null,
|
|
677
|
+
throttleTimeoutId: null,
|
|
678
|
+
lastRun: 0
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const wrappedHandler = createEventHandler(
|
|
682
|
+
isFunction ? potentialFn : expr,
|
|
683
|
+
modifiers,
|
|
684
|
+
context,
|
|
685
|
+
expr,
|
|
686
|
+
handlerState
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
const cleanup = registerDelegatedEvent(el, eventName, wrappedHandler, {
|
|
690
|
+
modifiers,
|
|
691
|
+
context,
|
|
692
|
+
expr,
|
|
693
|
+
handlerState
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
if (effects) {
|
|
697
|
+
effects.push(cleanup)
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
let timeoutId
|
|
701
|
+
let lastRun = 0
|
|
702
|
+
let throttleTimeoutId
|
|
703
|
+
|
|
704
|
+
let debounceMs = 0
|
|
705
|
+
let throttleMs = 0
|
|
706
|
+
|
|
707
|
+
const parseDuration = (idx) => {
|
|
708
|
+
const next = modifiers[idx + 1]
|
|
709
|
+
if (next && /^\d/.test(next)) {
|
|
710
|
+
if (next.endsWith('ms')) return parseInt(next) || 250
|
|
711
|
+
if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
|
|
712
|
+
}
|
|
713
|
+
return 250
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const debounceIdx = modifiers.indexOf('debounce')
|
|
717
|
+
if (debounceIdx > -1) debounceMs = parseDuration(debounceIdx)
|
|
718
|
+
|
|
719
|
+
const throttleIdx = modifiers.indexOf('throttle')
|
|
720
|
+
if (throttleIdx > -1) throttleMs = parseDuration(throttleIdx)
|
|
721
|
+
|
|
722
|
+
const executeLogic = (e) => {
|
|
723
|
+
batch(() => {
|
|
724
|
+
const potentialFn = context[expr]
|
|
725
|
+
if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
|
|
726
|
+
potentialFn(e)
|
|
727
|
+
} else {
|
|
728
|
+
evaluateExpression(expr, context, { $event: e }, false)
|
|
729
|
+
}
|
|
730
|
+
})
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const handler = (e) => {
|
|
734
|
+
if (modifiers.includes('prevent')) e.preventDefault()
|
|
735
|
+
if (modifiers.includes('stop')) e.stopPropagation()
|
|
736
|
+
if (modifiers.includes('self') && e.target !== el) return
|
|
737
|
+
|
|
738
|
+
if (e instanceof KeyboardEvent) {
|
|
739
|
+
const key = e.key.toLowerCase()
|
|
740
|
+
const keyModifiers = modifiers.filter(m => ['enter', 'escape', 'tab', 'space', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(m))
|
|
741
|
+
if (keyModifiers.length > 0) {
|
|
742
|
+
const match = keyModifiers.some(m => {
|
|
743
|
+
if (m === 'escape') return key === 'escape' || key === 'esc'
|
|
744
|
+
if (m === 'space') return key === ' ' || key === 'spacebar'
|
|
745
|
+
return key === m
|
|
746
|
+
})
|
|
747
|
+
if (!match) return
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (throttleMs > 0) {
|
|
752
|
+
const now = Date.now()
|
|
753
|
+
const remaining = throttleMs - (now - lastRun)
|
|
754
|
+
|
|
755
|
+
if (remaining <= 0) {
|
|
756
|
+
clearTimeout(throttleTimeoutId)
|
|
757
|
+
lastRun = now
|
|
758
|
+
executeLogic(e)
|
|
759
|
+
} else {
|
|
760
|
+
clearTimeout(throttleTimeoutId)
|
|
761
|
+
throttleTimeoutId = setTimeout(() => {
|
|
762
|
+
lastRun = Date.now()
|
|
763
|
+
executeLogic(e)
|
|
764
|
+
}, remaining)
|
|
765
|
+
}
|
|
766
|
+
return
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (debounceMs > 0) {
|
|
770
|
+
clearTimeout(timeoutId)
|
|
771
|
+
timeoutId = setTimeout(() => executeLogic(e), debounceMs)
|
|
772
|
+
} else {
|
|
773
|
+
executeLogic(e)
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const options = {
|
|
778
|
+
once: modifiers.includes('once'),
|
|
779
|
+
capture: modifiers.includes('capture') || modifiers.includes('away')
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
el.addEventListener(eventName, handler, options)
|
|
783
|
+
if (effects) {
|
|
784
|
+
effects.push(() => {
|
|
785
|
+
el.removeEventListener(eventName, handler, options)
|
|
786
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
787
|
+
if (throttleTimeoutId) clearTimeout(throttleTimeoutId)
|
|
788
|
+
})
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export function processText(el, context, effects) {
|
|
796
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-text')) return
|
|
797
|
+
const expr = el.getAttribute('f-text')
|
|
798
|
+
el.removeAttribute('f-text')
|
|
799
|
+
effects.push(effect(() => {
|
|
800
|
+
const val = evaluateExpression(expr, context)
|
|
801
|
+
el.textContent = String(val ?? '')
|
|
802
|
+
}))
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function processCloak(el) {
|
|
806
|
+
if (el.nodeType !== 1) return
|
|
807
|
+
if (el.hasAttribute('f-cloak')) el.removeAttribute('f-cloak')
|
|
808
|
+
|
|
809
|
+
const els = el.querySelectorAll('[f-cloak]')
|
|
810
|
+
for (const e of els) e.removeAttribute('f-cloak')
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export function processClassList(el, context, effects) {
|
|
814
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-classList')) return
|
|
815
|
+
|
|
816
|
+
const expr = el.getAttribute('f-classList')
|
|
817
|
+
el.removeAttribute('f-classList')
|
|
818
|
+
|
|
819
|
+
const appliedClasses = new Set()
|
|
820
|
+
|
|
821
|
+
const e = effect(() => {
|
|
822
|
+
const result = evaluateExpression(expr, context)
|
|
823
|
+
|
|
824
|
+
if (!result || typeof result !== 'object') {
|
|
825
|
+
console.warn(`[framework] f-classList must evaluate to an object, got: ${typeof result}`)
|
|
826
|
+
return
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
for (const [className, shouldApply] of Object.entries(result)) {
|
|
830
|
+
const isApplied = appliedClasses.has(className)
|
|
831
|
+
|
|
832
|
+
if (shouldApply && !isApplied) {
|
|
833
|
+
el.classList.add(className)
|
|
834
|
+
appliedClasses.add(className)
|
|
835
|
+
} else if (!shouldApply && isApplied) {
|
|
836
|
+
el.classList.remove(className)
|
|
837
|
+
appliedClasses.delete(className)
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const currentClasses = new Set(Object.keys(result))
|
|
842
|
+
for (const className of [...appliedClasses]) {
|
|
843
|
+
if (!currentClasses.has(className)) {
|
|
844
|
+
el.classList.remove(className)
|
|
845
|
+
appliedClasses.delete(className)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
})
|
|
849
|
+
|
|
850
|
+
effects.push(e)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export function processLinks(el, effects) {
|
|
854
|
+
if (el.nodeType !== 1) return
|
|
855
|
+
|
|
856
|
+
const links = el.hasAttribute('f-link') ? [el] : el.querySelectorAll('[f-link]')
|
|
857
|
+
|
|
858
|
+
for (const link of links) {
|
|
859
|
+
if (link.tagName !== 'A') continue
|
|
860
|
+
|
|
861
|
+
const href = link.getAttribute('href')
|
|
862
|
+
if (!href) continue
|
|
863
|
+
|
|
864
|
+
effects.push(effect(() => {
|
|
865
|
+
const currentPath = currentRoute()
|
|
866
|
+
const isActive = currentPath === href || currentPath.startsWith(href + '/')
|
|
867
|
+
|
|
868
|
+
if (isActive) {
|
|
869
|
+
link.classList.add('active')
|
|
870
|
+
} else {
|
|
871
|
+
link.classList.remove('active')
|
|
872
|
+
}
|
|
873
|
+
}))
|
|
874
|
+
}
|
|
875
|
+
}
|