@symbo.ls/router 3.1.2 → 3.3.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/index.js +240 -39
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -25,6 +25,168 @@ export const getLastLocationPath = (
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// --- Dynamic route matching ---
|
|
29
|
+
|
|
30
|
+
const paramPattern = /^:(.+)/
|
|
31
|
+
const routeCache = new Map()
|
|
32
|
+
|
|
33
|
+
export const parseRouteParams = (pattern, pathname) => {
|
|
34
|
+
const patternParts = pattern.replace(/^\//, '').split('/')
|
|
35
|
+
const pathParts = pathname.replace(/^\//, '').split('/').filter(Boolean)
|
|
36
|
+
const params = {}
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
39
|
+
const match = patternParts[i].match(paramPattern)
|
|
40
|
+
if (match && pathParts[i]) {
|
|
41
|
+
params[match[1]] = decodeURIComponent(pathParts[i])
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return params
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const matchRoutes = (routes, pathname, level = 0) => {
|
|
49
|
+
const pathParts = pathname.replace(/^\//, '').split('/').filter(Boolean)
|
|
50
|
+
const relevantParts = pathParts.slice(level)
|
|
51
|
+
const routeKey = '/' + (relevantParts[0] || '')
|
|
52
|
+
|
|
53
|
+
// Fast path: direct match
|
|
54
|
+
if (routes[routeKey]) {
|
|
55
|
+
return { key: routeKey, content: routes[routeKey], params: {} }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Dynamic param matching
|
|
59
|
+
let bestMatch = null
|
|
60
|
+
let bestScore = -1
|
|
61
|
+
let bestParams = {}
|
|
62
|
+
|
|
63
|
+
for (const key in routes) {
|
|
64
|
+
if (key === '/*') continue
|
|
65
|
+
const keyParts = key.replace(/^\//, '').split('/')
|
|
66
|
+
|
|
67
|
+
let score = 0
|
|
68
|
+
let matched = true
|
|
69
|
+
const params = {}
|
|
70
|
+
const len = Math.min(keyParts.length, relevantParts.length)
|
|
71
|
+
|
|
72
|
+
if (keyParts.length > relevantParts.length) continue
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < len; i++) {
|
|
75
|
+
const paramMatch = keyParts[i].match(paramPattern)
|
|
76
|
+
if (keyParts[i] === relevantParts[i]) {
|
|
77
|
+
score += 3
|
|
78
|
+
} else if (paramMatch) {
|
|
79
|
+
params[paramMatch[1]] = decodeURIComponent(relevantParts[i])
|
|
80
|
+
score += 1
|
|
81
|
+
} else {
|
|
82
|
+
matched = false
|
|
83
|
+
break
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (matched && score > bestScore) {
|
|
88
|
+
bestScore = score
|
|
89
|
+
bestMatch = key
|
|
90
|
+
bestParams = params
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (bestMatch) {
|
|
95
|
+
return { key: bestMatch, content: routes[bestMatch], params: bestParams }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fallback to wildcard
|
|
99
|
+
if (routes['/*']) {
|
|
100
|
+
return { key: '/*', content: routes['/*'], params: {} }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { key: null, content: null, params: {} }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Nested route resolution ---
|
|
107
|
+
|
|
108
|
+
export const resolveNestedRoute = (routes, pathname, level = 0) => {
|
|
109
|
+
const segments = pathname.replace(/^\//, '').split('/').filter(Boolean)
|
|
110
|
+
const chain = []
|
|
111
|
+
let currentRoutes = routes
|
|
112
|
+
let currentLevel = level
|
|
113
|
+
|
|
114
|
+
while (currentLevel < segments.length && currentRoutes) {
|
|
115
|
+
const match = matchRoutes(currentRoutes, pathname, currentLevel)
|
|
116
|
+
if (!match.content) break
|
|
117
|
+
|
|
118
|
+
chain.push({
|
|
119
|
+
level: currentLevel,
|
|
120
|
+
key: match.key,
|
|
121
|
+
content: match.content,
|
|
122
|
+
params: match.params
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Check for nested routes
|
|
126
|
+
if (match.content.routes) {
|
|
127
|
+
currentRoutes = match.content.routes
|
|
128
|
+
currentLevel++
|
|
129
|
+
} else {
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return chain
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- Query string parsing ---
|
|
138
|
+
|
|
139
|
+
export const parseQuery = (search) => {
|
|
140
|
+
if (!search || search === '?') return {}
|
|
141
|
+
const params = {}
|
|
142
|
+
const searchParams = new URLSearchParams(search)
|
|
143
|
+
searchParams.forEach((value, key) => {
|
|
144
|
+
if (params[key] !== undefined) {
|
|
145
|
+
if (!Array.isArray(params[key])) params[key] = [params[key]]
|
|
146
|
+
params[key].push(value)
|
|
147
|
+
} else {
|
|
148
|
+
params[key] = value
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
return params
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Guards / Middleware ---
|
|
155
|
+
|
|
156
|
+
export const runGuards = async (guards, context) => {
|
|
157
|
+
if (!guards || !guards.length) return true
|
|
158
|
+
for (const guard of guards) {
|
|
159
|
+
const result = await guard(context)
|
|
160
|
+
if (result === false) return false
|
|
161
|
+
if (typeof result === 'string') return result // redirect path
|
|
162
|
+
}
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Pop state handler ---
|
|
167
|
+
|
|
168
|
+
let popStateFired = false
|
|
169
|
+
export const onPopState = (element) => {
|
|
170
|
+
if (popStateFired) return
|
|
171
|
+
popStateFired = true
|
|
172
|
+
const win = (element && element.context && element.context.window) || globalThis
|
|
173
|
+
win.onpopstate = async (e) => {
|
|
174
|
+
const { pathname, search, hash } = win.location
|
|
175
|
+
const url = pathname + search + hash
|
|
176
|
+
if (element && element.call) {
|
|
177
|
+
await element.call(
|
|
178
|
+
'router',
|
|
179
|
+
url,
|
|
180
|
+
element,
|
|
181
|
+
{},
|
|
182
|
+
{ pushState: false, scrollToTop: false, level: 0, event: e }
|
|
183
|
+
)
|
|
184
|
+
} else {
|
|
185
|
+
await router(url, element, {}, { pushState: false, scrollToTop: false, level: 0, event: e })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
28
190
|
export let lastPathname = null
|
|
29
191
|
export let lastLevel = 0
|
|
30
192
|
|
|
@@ -42,18 +204,21 @@ const defaultOptions = {
|
|
|
42
204
|
updateStateOptions: {
|
|
43
205
|
preventUpdate: ['Canvas'],
|
|
44
206
|
preventUpdateTriggerStateUpdate: true
|
|
45
|
-
// preventContentUpdate: true,
|
|
46
|
-
// preventUpdate: ['MainHeaderCaptions', 'HeaderSubAside']
|
|
47
207
|
},
|
|
48
208
|
preventUpdate: ['Canvas', 'Modal'],
|
|
49
209
|
setOptions: {},
|
|
50
210
|
scrollToOffset: 120,
|
|
51
|
-
scrollToOptions: { behavior: 'smooth' }
|
|
211
|
+
scrollToOptions: { behavior: 'smooth' },
|
|
212
|
+
useParamsMatching: false,
|
|
213
|
+
useNestedRoutes: false
|
|
52
214
|
}
|
|
53
215
|
|
|
216
|
+
const lastElement = {}
|
|
217
|
+
|
|
54
218
|
// eslint-disable-next-line default-param-last
|
|
55
|
-
export async function router
|
|
56
|
-
const
|
|
219
|
+
export async function router(path = '/', el, state = {}, passedOptions = {}) {
|
|
220
|
+
const rootElem = this && this.getRoot()
|
|
221
|
+
const element = el && el.routes ? el : rootElem
|
|
57
222
|
const options = merge(passedOptions, defaultOptions)
|
|
58
223
|
lastLevel = options.level
|
|
59
224
|
|
|
@@ -63,12 +228,15 @@ export async function router (path = '/', el, state = {}, passedOptions = {}) {
|
|
|
63
228
|
options.event.preventDefault()
|
|
64
229
|
options.event.stopPropagation()
|
|
65
230
|
}
|
|
66
|
-
|
|
231
|
+
element.call('closeModal')
|
|
232
|
+
if (options.event) { options.event.preventDefault() }
|
|
67
233
|
}
|
|
68
234
|
|
|
69
235
|
const urlObj = new options.window.URL(options.window.location.origin + path)
|
|
70
236
|
const { pathname, search, hash } = urlObj
|
|
71
237
|
|
|
238
|
+
const query = parseQuery(search)
|
|
239
|
+
|
|
72
240
|
const rootRoute = getLocationPath(0, path)
|
|
73
241
|
let routeArray = pathname.split('/')
|
|
74
242
|
|
|
@@ -80,12 +248,37 @@ export async function router (path = '/', el, state = {}, passedOptions = {}) {
|
|
|
80
248
|
}
|
|
81
249
|
|
|
82
250
|
const route = routeArray.join('/').replace('//', '/')
|
|
83
|
-
const routeInContent = `/${routeArray[lastLevel + 1] || ''}`
|
|
84
251
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
252
|
+
// Route matching - support dynamic params and nested routes
|
|
253
|
+
let routeInContent, content, params
|
|
254
|
+
if (options.useNestedRoutes) {
|
|
255
|
+
const chain = resolveNestedRoute(element.routes, pathname, lastLevel)
|
|
256
|
+
const leaf = chain[chain.length - 1]
|
|
257
|
+
if (leaf) {
|
|
258
|
+
routeInContent = leaf.key
|
|
259
|
+
content = leaf.content
|
|
260
|
+
params = chain.reduce((acc, c) => ({ ...acc, ...c.params }), {})
|
|
261
|
+
}
|
|
262
|
+
} else if (options.useParamsMatching) {
|
|
263
|
+
const match = matchRoutes(element.routes, pathname, lastLevel)
|
|
264
|
+
routeInContent = match.key
|
|
265
|
+
content = match.content
|
|
266
|
+
params = match.params
|
|
267
|
+
} else {
|
|
268
|
+
routeInContent = `/${routeArray[lastLevel + 1] || ''}`
|
|
269
|
+
content = element.routes[routeInContent] || element.routes['/*']
|
|
270
|
+
params = {}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Run custom guards
|
|
274
|
+
if (options.guards && options.guards.length) {
|
|
275
|
+
const guardCtx = { pathname, route, routeInContent, params, query, hash, element, state }
|
|
276
|
+
const guardResult = await runGuards(options.guards, guardCtx)
|
|
277
|
+
if (guardResult === false) return
|
|
278
|
+
if (typeof guardResult === 'string') {
|
|
279
|
+
return router.call(this, guardResult, el, state, { ...passedOptions, guards: [] })
|
|
280
|
+
}
|
|
281
|
+
}
|
|
89
282
|
|
|
90
283
|
const wasModal = element.call(
|
|
91
284
|
'isModalRoute',
|
|
@@ -100,6 +293,9 @@ export async function router (path = '/', el, state = {}, passedOptions = {}) {
|
|
|
100
293
|
!content ||
|
|
101
294
|
(element.getRootState('debugging') && !options.allowRouterWhileInspect)
|
|
102
295
|
) {
|
|
296
|
+
if (options.onNotFound) {
|
|
297
|
+
options.onNotFound({ pathname, route, routeInContent, element })
|
|
298
|
+
}
|
|
103
299
|
return
|
|
104
300
|
}
|
|
105
301
|
if (options.pushState && options.level === 0) {
|
|
@@ -114,11 +310,13 @@ export async function router (path = '/', el, state = {}, passedOptions = {}) {
|
|
|
114
310
|
}
|
|
115
311
|
|
|
116
312
|
if (pathChanged || !hashChanged) {
|
|
117
|
-
|
|
313
|
+
const stateUpdate = { route, hash, routeInContent, debugging: false }
|
|
314
|
+
if (Object.keys(params).length) stateUpdate.params = params
|
|
315
|
+
if (Object.keys(query).length) stateUpdate.query = query
|
|
118
316
|
|
|
119
317
|
if (options.updateState) {
|
|
120
318
|
await element.state.update(
|
|
121
|
-
|
|
319
|
+
stateUpdate,
|
|
122
320
|
{
|
|
123
321
|
preventUpdate: [options.preventUpdate, 'content'],
|
|
124
322
|
updateByRouter: true,
|
|
@@ -127,31 +325,34 @@ export async function router (path = '/', el, state = {}, passedOptions = {}) {
|
|
|
127
325
|
)
|
|
128
326
|
}
|
|
129
327
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
328
|
+
if (
|
|
329
|
+
element.content &&
|
|
330
|
+
(options.forceContentUpdate || (!isInitial && wasModal))
|
|
331
|
+
) {
|
|
332
|
+
await element.content?.update(
|
|
333
|
+
{},
|
|
334
|
+
{
|
|
335
|
+
preventUpdate: options.preventUpdate
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
} else {
|
|
339
|
+
lastElement.content = content
|
|
340
|
+
element.removeContent()
|
|
341
|
+
await element.set(
|
|
342
|
+
{
|
|
343
|
+
tag: options.useFragment && 'fragment',
|
|
344
|
+
extends: content
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
lastElement,
|
|
348
|
+
routerContentElement: content,
|
|
349
|
+
preventUpdate: options.preventUpdate,
|
|
350
|
+
...options.setOptions
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
if (element.content) {
|
|
355
|
+
element.content.__ref.__componentKey = route
|
|
155
356
|
}
|
|
156
357
|
}
|
|
157
358
|
|
|
@@ -180,7 +381,7 @@ export async function router (path = '/', el, state = {}, passedOptions = {}) {
|
|
|
180
381
|
const top =
|
|
181
382
|
activeNode.getBoundingClientRect().top +
|
|
182
383
|
rootNode.scrollTop -
|
|
183
|
-
options.scrollToOffset || 0
|
|
384
|
+
(options.scrollToOffset || 0)
|
|
184
385
|
scrollNode.scrollTo({
|
|
185
386
|
...(options.scrollToOptions || {}),
|
|
186
387
|
top,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/router",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"vpatch": "npm version patch && npm publish --access public"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@domql/utils": "^3.
|
|
11
|
+
"@domql/utils": "^3.2.3"
|
|
12
12
|
},
|
|
13
|
-
"gitHead": "
|
|
13
|
+
"gitHead": "0be12bcdd99c265fdeded6d756a1a3616a943ffc",
|
|
14
14
|
"source": "index.js"
|
|
15
15
|
}
|