fusee-framework 1.6.0 → 1.8.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 +145 -123
- 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/CounterSection.js +3 -4
- package/app/main.js +50 -62
- package/app/main.ts +78 -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 +53 -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/async-work.js +65 -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 +137 -0
- package/app/pages/index.ts +87 -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 +79 -0
- package/app/pages/router-example.ts +56 -0
- package/app/pages/scroll-demo.ts +102 -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/providers/ChildComponent.js +8 -2
- package/app/providers/ChildComponent.ts +47 -47
- package/app/providers/GrandchildComponent.js +19 -0
- package/app/providers/ParentComponent.js +5 -1
- 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 +78 -0
- package/app/routes/RouterExample.ts +56 -0
- package/app/routes/TestResource.js +47 -0
- package/app/routes/WildcardDemo.js +35 -0
- package/app/routes/WildcardDemo.ts +35 -0
- package/app/services.js +15 -0
- package/auto-imports.d.ts +9 -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 +13 -9
- package/framework/core/compiler.js +252 -249
- package/framework/core/component.js +279 -260
- package/framework/core/di.js +187 -0
- package/framework/core/directives.js +819 -786
- package/framework/core/evaluator.js +153 -70
- package/framework/core/event-delegation.js +33 -14
- package/framework/core/reconcile.js +151 -0
- package/framework/core/signal.js +679 -403
- package/framework/core/store.js +322 -81
- package/framework/index.js +12 -11
- package/framework/router/file-router.js +177 -0
- package/framework/router/router.js +554 -80
- package/framework/types/component.d.ts +89 -88
- package/framework/types/di.d.ts +43 -0
- package/framework/types/event-delegation.d.ts +0 -58
- package/framework/types/index.d.ts +32 -22
- package/framework/types/reconcile.d.ts +7 -0
- package/framework/types/router.d.ts +42 -15
- package/framework/types/signal.d.ts +147 -78
- package/framework/types/store.d.ts +66 -8
- package/index.html +638 -626
- package/package.json +5 -3
- package/tsconfig.json +1 -0
- package/vite.config.js +7 -1
- /package/{framework → create-fusee-go/internal/assets/embed/framework}/bin/create-fusee.js +0 -0
|
@@ -1,80 +1,554 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
1
|
+
import { signal } from '../core/signal.js'
|
|
2
|
+
|
|
3
|
+
export const currentRoute = signal('/')
|
|
4
|
+
export const routeParams = signal({})
|
|
5
|
+
export const routeQuery = signal({})
|
|
6
|
+
export const matchedRoutes = signal([])
|
|
7
|
+
|
|
8
|
+
let _routes = []
|
|
9
|
+
let _rootOutlet = null
|
|
10
|
+
let _clickHandler = null
|
|
11
|
+
let _activeChain = []
|
|
12
|
+
let _routeCache = new Map()
|
|
13
|
+
let _cacheMaxSize = 100
|
|
14
|
+
let _routerViewTimeout = 10000
|
|
15
|
+
let _scrollBehaviorOptions = null
|
|
16
|
+
let _scrollPositions = new Map()
|
|
17
|
+
let _currentPath = null
|
|
18
|
+
|
|
19
|
+
function _getPath() {
|
|
20
|
+
const pathname = window.location.pathname || '/'
|
|
21
|
+
const search = window.location.search || ''
|
|
22
|
+
const hash = window.location.hash || ''
|
|
23
|
+
return pathname + search + hash
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function _extractQuery(path) {
|
|
27
|
+
const queryStr = path.split('?')[1]?.split('#')[0]
|
|
28
|
+
if (!queryStr) return {}
|
|
29
|
+
const params = new URLSearchParams(queryStr)
|
|
30
|
+
const query = {}
|
|
31
|
+
for (const [key, value] of params.entries()) {
|
|
32
|
+
query[key] = value
|
|
33
|
+
}
|
|
34
|
+
return query
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _getRoutePaths(routePath) {
|
|
38
|
+
return Array.isArray(routePath) ? routePath : [routePath]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _updateRoute() {
|
|
42
|
+
currentRoute(_getPath())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _matchSegments(routePath, urlSegments) {
|
|
46
|
+
const cleanPath = routePath.startsWith('/') ? routePath.slice(1) : routePath
|
|
47
|
+
|
|
48
|
+
if (cleanPath === '') {
|
|
49
|
+
return { matched: true, consumed: 0, params: {} }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (cleanPath === '*') {
|
|
53
|
+
return { matched: true, consumed: urlSegments.length, params: {} }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const routeSegs = cleanPath.split('/').filter(Boolean)
|
|
57
|
+
const hasWildcard = routeSegs[routeSegs.length - 1] === '*'
|
|
58
|
+
const checkSegs = hasWildcard ? routeSegs.slice(0, -1) : routeSegs
|
|
59
|
+
|
|
60
|
+
if (hasWildcard) {
|
|
61
|
+
if (urlSegments.length < checkSegs.length) return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (urlSegments.length < checkSegs.length) return null
|
|
65
|
+
|
|
66
|
+
const params = {}
|
|
67
|
+
for (let i = 0; i < checkSegs.length; i++) {
|
|
68
|
+
const rSeg = checkSegs[i]
|
|
69
|
+
const uSeg = urlSegments[i]
|
|
70
|
+
if (rSeg.startsWith(':')) {
|
|
71
|
+
params[rSeg.slice(1)] = uSeg
|
|
72
|
+
} else if (rSeg !== uSeg) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const consumed = hasWildcard ? urlSegments.length : checkSegs.length
|
|
78
|
+
return { matched: true, consumed, params }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _matchRouteTree(routes, urlSegments) {
|
|
82
|
+
for (const route of routes) {
|
|
83
|
+
const paths = _getRoutePaths(route.path)
|
|
84
|
+
for (const path of paths) {
|
|
85
|
+
const result = _matchSegments(path, urlSegments)
|
|
86
|
+
if (!result) continue
|
|
87
|
+
|
|
88
|
+
const remaining = urlSegments.slice(result.consumed)
|
|
89
|
+
|
|
90
|
+
if (route.children && route.children.length > 0) {
|
|
91
|
+
const childChain = _matchRouteTree(route.children, remaining)
|
|
92
|
+
if (childChain) {
|
|
93
|
+
return [
|
|
94
|
+
{ route, params: result.params, matchedSegments: urlSegments.slice(0, result.consumed), matchedPath: path },
|
|
95
|
+
...childChain
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (remaining.length === 0 || path === '*' || path.endsWith('/*')) {
|
|
102
|
+
return [{ route, params: result.params, matchedSegments: urlSegments.slice(0, result.consumed), matchedPath: path }]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _matchSingleFlat(routePath, actualPath) {
|
|
111
|
+
const actualPathWithoutParams = actualPath.split(/[?#]/)[0]
|
|
112
|
+
const routeParts = routePath.split('/').filter(Boolean)
|
|
113
|
+
const actualParts = actualPathWithoutParams.split('/').filter(Boolean)
|
|
114
|
+
|
|
115
|
+
const hasWildcard = routeParts.length > 0 && routeParts[routeParts.length - 1] === '*'
|
|
116
|
+
|
|
117
|
+
if (hasWildcard) {
|
|
118
|
+
const baseParts = routeParts.slice(0, -1)
|
|
119
|
+
if (actualParts.length < baseParts.length) return false
|
|
120
|
+
return baseParts.every((part, i) => part.startsWith(':') || part === actualParts[i])
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (routeParts.length !== actualParts.length) return false
|
|
124
|
+
return routeParts.every((part, i) => part.startsWith(':') || part === actualParts[i])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _matchFlat(routePath, actualPath) {
|
|
128
|
+
const paths = _getRoutePaths(routePath)
|
|
129
|
+
return paths.some(p => _matchSingleFlat(p, actualPath))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _findMatchingPath(routePath, actualPath) {
|
|
133
|
+
const paths = _getRoutePaths(routePath)
|
|
134
|
+
return paths.find(p => _matchSingleFlat(p, actualPath)) || null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _extractParamsFromPath(singlePath, actualPath) {
|
|
138
|
+
const params = {}
|
|
139
|
+
if (singlePath === '*') return params
|
|
140
|
+
|
|
141
|
+
const actualPathWithoutParams = actualPath.split(/[?#]/)[0]
|
|
142
|
+
const routeParts = singlePath.split('/').filter(Boolean)
|
|
143
|
+
const actualParts = actualPathWithoutParams.split('/').filter(Boolean)
|
|
144
|
+
|
|
145
|
+
const hasWildcard = routeParts.length > 0 && routeParts[routeParts.length - 1] === '*'
|
|
146
|
+
const checkLen = hasWildcard ? routeParts.length - 1 : routeParts.length
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < checkLen; i++) {
|
|
149
|
+
if (routeParts[i] && routeParts[i].startsWith(':')) {
|
|
150
|
+
params[routeParts[i].slice(1)] = actualParts[i] || ''
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return params
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function _extractParamsFlat(routePath, actualPath) {
|
|
157
|
+
const matchingPath = _findMatchingPath(routePath, actualPath)
|
|
158
|
+
if (!matchingPath) return {}
|
|
159
|
+
return _extractParamsFromPath(matchingPath, actualPath)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function _findMatchingChain(path) {
|
|
163
|
+
const pathWithoutParams = path.split(/[?#]/)[0]
|
|
164
|
+
|
|
165
|
+
if (_routeCache.has(pathWithoutParams)) {
|
|
166
|
+
const cached = _routeCache.get(pathWithoutParams)
|
|
167
|
+
_routeCache.delete(pathWithoutParams)
|
|
168
|
+
_routeCache.set(pathWithoutParams, cached)
|
|
169
|
+
return cached
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const urlSegments = pathWithoutParams.split('/').filter(Boolean)
|
|
173
|
+
const hasNested = _routes.some(r => r.children && r.children.length > 0)
|
|
174
|
+
|
|
175
|
+
let chain = null
|
|
176
|
+
|
|
177
|
+
if (hasNested) {
|
|
178
|
+
chain = _matchRouteTree(_routes, urlSegments)
|
|
179
|
+
if (chain) {
|
|
180
|
+
_cacheResult(pathWithoutParams, chain)
|
|
181
|
+
return chain
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const paths = _routes.filter(r => {
|
|
186
|
+
const routePaths = _getRoutePaths(r.path)
|
|
187
|
+
return !routePaths.includes('*') && !routePaths.some(p => p.endsWith('/*')) && !r.children
|
|
188
|
+
})
|
|
189
|
+
const exactMatch = paths.find(r => _matchFlat(r.path, pathWithoutParams))
|
|
190
|
+
if (exactMatch) {
|
|
191
|
+
const matchedPath = _findMatchingPath(exactMatch.path, pathWithoutParams)
|
|
192
|
+
chain = [{ route: exactMatch, params: _extractParamsFlat(exactMatch.path, pathWithoutParams), matchedSegments: urlSegments, matchedPath }]
|
|
193
|
+
_cacheResult(pathWithoutParams, chain)
|
|
194
|
+
return chain
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const wildcardPaths = _routes.filter(r => {
|
|
198
|
+
const routePaths = _getRoutePaths(r.path)
|
|
199
|
+
return !r.children && routePaths.some(p => p.endsWith('/*'))
|
|
200
|
+
})
|
|
201
|
+
const wildcardMatch = wildcardPaths.find(r => _matchFlat(r.path, pathWithoutParams))
|
|
202
|
+
if (wildcardMatch) {
|
|
203
|
+
const matchedPath = _findMatchingPath(wildcardMatch.path, pathWithoutParams)
|
|
204
|
+
chain = [{ route: wildcardMatch, params: _extractParamsFlat(wildcardMatch.path, pathWithoutParams), matchedSegments: urlSegments, matchedPath }]
|
|
205
|
+
_cacheResult(pathWithoutParams, chain)
|
|
206
|
+
return chain
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const catchAll = _routes.find(r => {
|
|
210
|
+
const routePaths = _getRoutePaths(r.path)
|
|
211
|
+
return routePaths.includes('*')
|
|
212
|
+
})
|
|
213
|
+
if (catchAll) {
|
|
214
|
+
chain = [{ route: catchAll, params: {}, matchedSegments: urlSegments, matchedPath: '*' }]
|
|
215
|
+
_cacheResult(pathWithoutParams, chain)
|
|
216
|
+
return chain
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function _cacheResult(path, chain) {
|
|
223
|
+
if (_routeCache.size >= _cacheMaxSize) {
|
|
224
|
+
const firstKey = _routeCache.keys().next().value
|
|
225
|
+
_routeCache.delete(firstKey)
|
|
226
|
+
}
|
|
227
|
+
_routeCache.set(path, chain)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function _clearRouteCache() {
|
|
231
|
+
_routeCache.clear()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function _saveScrollPosition(path) {
|
|
235
|
+
if (!_scrollBehaviorOptions?.saveScrollPosition) return
|
|
236
|
+
_scrollPositions.set(path, {
|
|
237
|
+
left: window.scrollX,
|
|
238
|
+
top: window.scrollY
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function _getSavedScrollPosition(path) {
|
|
243
|
+
return _scrollPositions.get(path) || null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function _applyScrollBehavior(to, from) {
|
|
247
|
+
const options = _scrollBehaviorOptions
|
|
248
|
+
if (!options) return
|
|
249
|
+
|
|
250
|
+
const savedPosition = _getSavedScrollPosition(to)
|
|
251
|
+
|
|
252
|
+
if (options.custom) {
|
|
253
|
+
const result = options.custom(to, from, savedPosition)
|
|
254
|
+
if (result && typeof result.then === 'function') {
|
|
255
|
+
result.then(pos => _performScroll(pos))
|
|
256
|
+
} else {
|
|
257
|
+
_performScroll(result)
|
|
258
|
+
}
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (options.scrollToAnchor && to.includes('#')) {
|
|
263
|
+
const anchor = to.split('#')[1]
|
|
264
|
+
const element = document.getElementById(anchor)
|
|
265
|
+
if (element) {
|
|
266
|
+
element.scrollIntoView({ behavior: 'smooth' })
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (options.scrollToTop !== false) {
|
|
272
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
|
273
|
+
} else if (savedPosition && options.saveScrollPosition) {
|
|
274
|
+
window.scrollTo(savedPosition.left, savedPosition.top)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function _performScroll(position) {
|
|
279
|
+
if (!position || position === false) return
|
|
280
|
+
|
|
281
|
+
if (typeof position === 'object' && 'selector' in position) {
|
|
282
|
+
const element = document.querySelector(position.selector)
|
|
283
|
+
if (element) {
|
|
284
|
+
element.scrollIntoView({ behavior: 'smooth' })
|
|
285
|
+
}
|
|
286
|
+
} else if (typeof position === 'object' && 'left' in position) {
|
|
287
|
+
window.scrollTo({ left: position.left, top: position.top, behavior: 'smooth' })
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function _unmountFromLevel(level) {
|
|
292
|
+
for (let i = _activeChain.length - 1; i >= level; i--) {
|
|
293
|
+
const entry = _activeChain[i]
|
|
294
|
+
if (entry && entry.instance) {
|
|
295
|
+
try { entry.instance.unmount() } catch { }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
_activeChain.length = level
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function _getRoutePathCount(route) {
|
|
302
|
+
return Array.isArray(route.path) ? route.path.length : 1
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function _renderChain(chain) {
|
|
306
|
+
try {
|
|
307
|
+
let reuseUntil = 0
|
|
308
|
+
for (let i = 0; i < Math.min(_activeChain.length, chain.length); i++) {
|
|
309
|
+
const active = _activeChain[i]
|
|
310
|
+
const incoming = chain[i]
|
|
311
|
+
|
|
312
|
+
if (
|
|
313
|
+
active.route === incoming.route &&
|
|
314
|
+
_segmentsEqual(active.matchedSegments, incoming.matchedSegments)
|
|
315
|
+
) {
|
|
316
|
+
reuseUntil = i + 1
|
|
317
|
+
}
|
|
318
|
+
else if (
|
|
319
|
+
active.route === incoming.route &&
|
|
320
|
+
_getRoutePathCount(active.route) > 1
|
|
321
|
+
) {
|
|
322
|
+
reuseUntil = i + 1
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
break
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
_unmountFromLevel(reuseUntil)
|
|
330
|
+
|
|
331
|
+
const allParams = {}
|
|
332
|
+
for (const entry of chain) {
|
|
333
|
+
Object.assign(allParams, entry.params)
|
|
334
|
+
}
|
|
335
|
+
routeParams(allParams)
|
|
336
|
+
matchedRoutes(chain.map(e => e.route))
|
|
337
|
+
_renderFromLevel(chain, reuseUntil)
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error('[framework] Error rendering route chain:', error)
|
|
340
|
+
_unmountFromLevel(0)
|
|
341
|
+
if (_rootOutlet) {
|
|
342
|
+
_rootOutlet.innerHTML = `<p style="color:red">[framework] Error rendering route: ${error.message}</p>`
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function _renderFromLevel(chain, level) {
|
|
348
|
+
if (level >= chain.length) return
|
|
349
|
+
|
|
350
|
+
const entry = chain[level]
|
|
351
|
+
const outlet = _getOutletForLevel(level)
|
|
352
|
+
|
|
353
|
+
if (!outlet) {
|
|
354
|
+
_waitForRouterView(level, () => _renderFromLevel(chain, level))
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
outlet.innerHTML = ''
|
|
359
|
+
|
|
360
|
+
const componentFn = entry.route.component
|
|
361
|
+
const instance = componentFn()
|
|
362
|
+
instance.render(outlet)
|
|
363
|
+
|
|
364
|
+
_activeChain[level] = {
|
|
365
|
+
route: entry.route,
|
|
366
|
+
matchedSegments: entry.matchedSegments,
|
|
367
|
+
params: entry.params,
|
|
368
|
+
instance,
|
|
369
|
+
outlet
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (level + 1 < chain.length) _scheduleNextLevel(chain, level + 1)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function _scheduleNextLevel(chain, level) {
|
|
376
|
+
const outlet = _getOutletForLevel(level)
|
|
377
|
+
if (outlet) {
|
|
378
|
+
_renderFromLevel(chain, level)
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_waitForRouterView(level, () => _renderFromLevel(chain, level))
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function _getOutletForLevel(level) {
|
|
386
|
+
if (level === 0) return _rootOutlet
|
|
387
|
+
|
|
388
|
+
const parentEntry = _activeChain[level - 1]
|
|
389
|
+
if (!parentEntry || !parentEntry.outlet) return null
|
|
390
|
+
|
|
391
|
+
return parentEntry.outlet.querySelector('[data-router-view]')
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function _waitForRouterView(level, callback) {
|
|
395
|
+
const parentEntry = _activeChain[level - 1]
|
|
396
|
+
if (!parentEntry || !parentEntry.outlet) return
|
|
397
|
+
|
|
398
|
+
const parentOutlet = parentEntry.outlet
|
|
399
|
+
|
|
400
|
+
const existing = parentOutlet.querySelector('[data-router-view]')
|
|
401
|
+
if (existing) {
|
|
402
|
+
callback()
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const observer = new MutationObserver(() => {
|
|
407
|
+
const el = parentOutlet.querySelector('[data-router-view]')
|
|
408
|
+
if (el) {
|
|
409
|
+
observer.disconnect()
|
|
410
|
+
callback()
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
observer.observe(parentOutlet, { childList: true, subtree: true })
|
|
415
|
+
|
|
416
|
+
setTimeout(() => observer.disconnect(), _routerViewTimeout)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function _segmentsEqual(a, b) {
|
|
420
|
+
if (a.length !== b.length) return false
|
|
421
|
+
for (let i = 0; i < a.length; i++) {
|
|
422
|
+
if (a[i] !== b[i]) return false
|
|
423
|
+
}
|
|
424
|
+
return true
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function _resolveRoute() {
|
|
428
|
+
if (!_rootOutlet || _routes.length === 0) return
|
|
429
|
+
|
|
430
|
+
const path = _getPath()
|
|
431
|
+
const chain = _findMatchingChain(path)
|
|
432
|
+
|
|
433
|
+
if (!chain) {
|
|
434
|
+
_unmountFromLevel(0)
|
|
435
|
+
_rootOutlet.innerHTML = `<p style="color:red">[framework] No route matched for "${path}"</p>`
|
|
436
|
+
routeParams({})
|
|
437
|
+
matchedRoutes([])
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
_renderChain(chain)
|
|
442
|
+
|
|
443
|
+
const from = _currentPath
|
|
444
|
+
_currentPath = path
|
|
445
|
+
_applyScrollBehavior(path, from)
|
|
446
|
+
routeQuery(_extractQuery(path))
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function navigate(path) {
|
|
450
|
+
const currentPath = _getPath().split(/[?#]/)[0]
|
|
451
|
+
const targetPath = path.split(/[?#]/)[0]
|
|
452
|
+
if (targetPath === currentPath && path === _getPath()) {
|
|
453
|
+
_applyScrollBehavior(path, _getPath())
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
_saveScrollPosition(_getPath())
|
|
457
|
+
window.history.pushState({}, '', path)
|
|
458
|
+
_updateRoute()
|
|
459
|
+
_resolveRoute()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function mountOutlet(el) {
|
|
463
|
+
_rootOutlet = el
|
|
464
|
+
_resolveRoute()
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function _isInternalLink(anchor) {
|
|
468
|
+
const href = anchor.getAttribute('href')
|
|
469
|
+
if (!href) return false
|
|
470
|
+
if (href.startsWith('#')) return false
|
|
471
|
+
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {
|
|
472
|
+
const url = new URL(href, window.location.origin)
|
|
473
|
+
return url.origin === window.location.origin
|
|
474
|
+
}
|
|
475
|
+
return href.startsWith('/')
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function _getAnchorFromEvent(e) {
|
|
479
|
+
if (e.composedPath) {
|
|
480
|
+
return e.composedPath().find(el => el.tagName === 'A')
|
|
481
|
+
}
|
|
482
|
+
return e.target.tagName === 'A' ? e.target : null
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function _handleLinkClick(e) {
|
|
486
|
+
const anchor = _getAnchorFromEvent(e)
|
|
487
|
+
if (!anchor) return
|
|
488
|
+
|
|
489
|
+
if (anchor.hasAttribute('f-link') || _isInternalLink(anchor)) {
|
|
490
|
+
e.preventDefault()
|
|
491
|
+
const href = anchor.getAttribute('href')
|
|
492
|
+
if (href && href !== _getPath()) {
|
|
493
|
+
navigate(href)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function _setupPopstateHandler() {
|
|
499
|
+
const handler = () => {
|
|
500
|
+
_updateRoute()
|
|
501
|
+
_resolveRoute()
|
|
502
|
+
}
|
|
503
|
+
window.addEventListener('popstate', handler)
|
|
504
|
+
return handler
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function _setupClickHandler() {
|
|
508
|
+
_clickHandler = _handleLinkClick
|
|
509
|
+
document.addEventListener('click', _clickHandler)
|
|
510
|
+
return _clickHandler
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function _setupInitialRoute() {
|
|
514
|
+
if (document.readyState === 'loading') {
|
|
515
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
516
|
+
_updateRoute()
|
|
517
|
+
_resolveRoute()
|
|
518
|
+
}, { once: true })
|
|
519
|
+
} else {
|
|
520
|
+
setTimeout(() => {
|
|
521
|
+
if (_activeChain.length === 0) _resolveRoute()
|
|
522
|
+
}, 0)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function createRouter(routes, options = {}) {
|
|
527
|
+
_routes = routes
|
|
528
|
+
_activeChain = []
|
|
529
|
+
_cacheMaxSize = options.cacheSize || 100
|
|
530
|
+
_routerViewTimeout = options.routerViewTimeout || 10000
|
|
531
|
+
_scrollBehaviorOptions = options.scrollBehavior || null
|
|
532
|
+
|
|
533
|
+
_updateRoute()
|
|
534
|
+
|
|
535
|
+
const popstateHandler = _setupPopstateHandler()
|
|
536
|
+
_setupClickHandler()
|
|
537
|
+
_setupInitialRoute()
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
navigate,
|
|
541
|
+
destroy() {
|
|
542
|
+
window.removeEventListener('popstate', popstateHandler)
|
|
543
|
+
document.removeEventListener('click', _clickHandler)
|
|
544
|
+
_unmountFromLevel(0)
|
|
545
|
+
_routes = []
|
|
546
|
+
_rootOutlet = null
|
|
547
|
+
_activeChain = []
|
|
548
|
+
_clearRouteCache()
|
|
549
|
+
_scrollPositions.clear()
|
|
550
|
+
_scrollBehaviorOptions = null
|
|
551
|
+
_currentPath = null
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|