@symbo.ls/router 3.14.7 → 3.14.9
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/CHANGELOG.md +22 -0
- package/index.js +329 -0
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @symbo.ls/router
|
|
2
2
|
|
|
3
|
+
## 3.14.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Manual patch bump triggered via workflow_dispatch (scope: @symbo.ls).
|
|
8
|
+
No source change behind this bump — released to refresh dist or
|
|
9
|
+
coordinate a cross-package version line.
|
|
10
|
+
- Updated dependencies
|
|
11
|
+
- @symbo.ls/element@3.14.12
|
|
12
|
+
- @symbo.ls/utils@3.14.11
|
|
13
|
+
|
|
14
|
+
## 3.14.8
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Manual patch bump triggered via workflow_dispatch (scope: @symbo.ls).
|
|
19
|
+
No source change behind this bump — released to refresh dist or
|
|
20
|
+
coordinate a cross-package version line.
|
|
21
|
+
- Updated dependencies
|
|
22
|
+
- @symbo.ls/element@3.14.11
|
|
23
|
+
- @symbo.ls/utils@3.14.10
|
|
24
|
+
|
|
3
25
|
## 3.14.7
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/index.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import { document, window, triggerEventOn } from '@symbo.ls/utils'
|
|
4
|
+
|
|
5
|
+
// --- Route matching utilities ---
|
|
6
|
+
|
|
7
|
+
const paramPattern = /^:(.+)/
|
|
8
|
+
const wildcardPattern = /^\*$/
|
|
9
|
+
|
|
10
|
+
const routeCache = new Map()
|
|
11
|
+
|
|
12
|
+
export const parseRoutePattern = (pattern) => {
|
|
13
|
+
const cached = routeCache.get(pattern)
|
|
14
|
+
if (cached) return cached
|
|
15
|
+
|
|
16
|
+
const segments = pattern.replace(/^\//, '').split('/')
|
|
17
|
+
const params = []
|
|
18
|
+
let hasWildcard = false
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < segments.length; i++) {
|
|
21
|
+
const match = segments[i].match(paramPattern)
|
|
22
|
+
if (match) {
|
|
23
|
+
params.push({ index: i, name: match[1] })
|
|
24
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
25
|
+
hasWildcard = true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = { segments, params, hasWildcard, pattern }
|
|
30
|
+
routeCache.set(pattern, result)
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const matchRoute = (pathname, routes, level = 0) => {
|
|
35
|
+
const pathSegments = pathname.replace(/^\//, '').split('/').filter(Boolean)
|
|
36
|
+
const relevantSegments = pathSegments.slice(level)
|
|
37
|
+
const routePath = '/' + (relevantSegments[0] || '')
|
|
38
|
+
|
|
39
|
+
let bestMatch = null
|
|
40
|
+
let bestScore = -1
|
|
41
|
+
let matchedParams = {}
|
|
42
|
+
|
|
43
|
+
for (const key in routes) {
|
|
44
|
+
if (key === '/*') continue
|
|
45
|
+
|
|
46
|
+
const parsed = parseRoutePattern(key)
|
|
47
|
+
const score = scoreMatch(relevantSegments, parsed)
|
|
48
|
+
|
|
49
|
+
if (score > bestScore) {
|
|
50
|
+
bestScore = score
|
|
51
|
+
bestMatch = key
|
|
52
|
+
matchedParams = extractParams(relevantSegments, parsed)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!bestMatch && routes['/*']) {
|
|
57
|
+
bestMatch = '/*'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
key: bestMatch,
|
|
62
|
+
content: bestMatch ? routes[bestMatch] : null,
|
|
63
|
+
params: matchedParams,
|
|
64
|
+
routePath
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const scoreMatch = (pathSegments, parsed) => {
|
|
69
|
+
const { segments, hasWildcard } = parsed
|
|
70
|
+
|
|
71
|
+
if (!hasWildcard && segments.length !== pathSegments.length &&
|
|
72
|
+
segments.length !== 1) {
|
|
73
|
+
// For single-segment patterns, match just the first segment
|
|
74
|
+
if (segments.length > pathSegments.length) return -1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let score = 0
|
|
78
|
+
const len = Math.min(segments.length, pathSegments.length)
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < len; i++) {
|
|
81
|
+
if (segments[i] === pathSegments[i]) {
|
|
82
|
+
score += 3 // exact match
|
|
83
|
+
} else if (paramPattern.test(segments[i])) {
|
|
84
|
+
score += 1 // param match
|
|
85
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
86
|
+
score += 0.5
|
|
87
|
+
} else {
|
|
88
|
+
return -1 // no match
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return score
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const extractParams = (pathSegments, parsed) => {
|
|
96
|
+
const params = {}
|
|
97
|
+
for (const { index, name } of parsed.params) {
|
|
98
|
+
if (pathSegments[index]) {
|
|
99
|
+
params[name] = decodeURIComponent(pathSegments[index])
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return params
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const parseQuery = (search) => {
|
|
106
|
+
if (!search || search === '?') return {}
|
|
107
|
+
const params = {}
|
|
108
|
+
const searchParams = new URLSearchParams(search)
|
|
109
|
+
searchParams.forEach((value, key) => {
|
|
110
|
+
if (params[key] !== undefined) {
|
|
111
|
+
if (!Array.isArray(params[key])) params[key] = [params[key]]
|
|
112
|
+
params[key].push(value)
|
|
113
|
+
} else {
|
|
114
|
+
params[key] = value
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
return params
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Route guards / middleware ---
|
|
121
|
+
|
|
122
|
+
export const runGuards = async (guards, context) => {
|
|
123
|
+
if (!guards || !guards.length) return true
|
|
124
|
+
for (const guard of guards) {
|
|
125
|
+
const result = await guard(context)
|
|
126
|
+
if (result === false) return false
|
|
127
|
+
if (typeof result === 'string') return result // redirect path
|
|
128
|
+
}
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- Core router ---
|
|
133
|
+
|
|
134
|
+
export const getActiveRoute = (level = 0, route) => {
|
|
135
|
+
if (!route) route = (typeof window !== 'undefined' ? window.location.pathname : '/')
|
|
136
|
+
const routeArray = route.split('/')
|
|
137
|
+
const activeRoute = routeArray[level + 1]
|
|
138
|
+
if (activeRoute) return `/${activeRoute}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export let lastPathname
|
|
142
|
+
export let lastLevel = 0
|
|
143
|
+
|
|
144
|
+
const normalizePath = (p) => (!p || p === 'srcdoc' || p === 'about:srcdoc') ? '/' : p
|
|
145
|
+
|
|
146
|
+
const defaultOptions = {
|
|
147
|
+
level: lastLevel,
|
|
148
|
+
pushState: true,
|
|
149
|
+
initialRender: false,
|
|
150
|
+
scrollToTop: true,
|
|
151
|
+
scrollToNode: false,
|
|
152
|
+
scrollNode: document && document.documentElement,
|
|
153
|
+
scrollBody: false,
|
|
154
|
+
useFragment: false,
|
|
155
|
+
updateState: true,
|
|
156
|
+
scrollToOffset: 0,
|
|
157
|
+
contentElementKey: 'content',
|
|
158
|
+
scrollToOptions: { behavior: 'smooth' },
|
|
159
|
+
useParamsMatching: true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const router = async (path, el, state = {}, options = {}) => {
|
|
163
|
+
const element = el || this
|
|
164
|
+
const win = element?.context?.window || window
|
|
165
|
+
const doc = element?.context?.document || document
|
|
166
|
+
const opts = {
|
|
167
|
+
...defaultOptions,
|
|
168
|
+
...element.context.routerOptions,
|
|
169
|
+
...options
|
|
170
|
+
}
|
|
171
|
+
lastLevel = opts.lastLevel
|
|
172
|
+
|
|
173
|
+
const ref = element.__ref
|
|
174
|
+
|
|
175
|
+
if (
|
|
176
|
+
(opts.contentElementKey !== 'content' &&
|
|
177
|
+
opts.contentElementKey !== ref.contentElementKey) ||
|
|
178
|
+
!ref.contentElementKey
|
|
179
|
+
) {
|
|
180
|
+
ref.contentElementKey = opts.contentElementKey || 'content'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const contentElementKey = ref.contentElementKey || opts.contentElementKey || 'content'
|
|
184
|
+
|
|
185
|
+
const origin = win.location.origin !== 'null' ? win.location.origin : 'http://localhost'
|
|
186
|
+
path = normalizePath(path)
|
|
187
|
+
const urlObj = new win.URL(origin + path)
|
|
188
|
+
const { pathname, search, hash } = urlObj
|
|
189
|
+
|
|
190
|
+
const query = parseQuery(search)
|
|
191
|
+
|
|
192
|
+
const rootNode = element.node
|
|
193
|
+
const hashChanged = hash && hash !== win.location.hash.slice(1)
|
|
194
|
+
const pathChanged = pathname !== lastPathname
|
|
195
|
+
lastPathname = pathname
|
|
196
|
+
|
|
197
|
+
// Route matching - support both simple and param-based
|
|
198
|
+
let route, routePath, content, params
|
|
199
|
+
if (opts.useParamsMatching) {
|
|
200
|
+
const match = matchRoute(pathname, element.routes, opts.level)
|
|
201
|
+
// FRAMEWORK-2: state.route must reflect the matched pattern key
|
|
202
|
+
// (e.g. `/players/:id`), not the literal first URL segment, so
|
|
203
|
+
// consumers and devtools can branch on which route handler fired.
|
|
204
|
+
// The first-segment value is still exposed as `routePath` for the
|
|
205
|
+
// rare consumer that needs the URL piece directly.
|
|
206
|
+
route = match.key
|
|
207
|
+
routePath = match.routePath
|
|
208
|
+
content = match.content
|
|
209
|
+
params = match.params
|
|
210
|
+
} else {
|
|
211
|
+
route = getActiveRoute(opts.level, pathname)
|
|
212
|
+
routePath = route
|
|
213
|
+
content = element.routes[route || '/'] || element.routes['/*']
|
|
214
|
+
params = {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const scrollNode = opts.scrollToNode ? rootNode : opts.scrollNode
|
|
218
|
+
|
|
219
|
+
if (element.state?.root?.debugging) {
|
|
220
|
+
element.state.root.debugging = false
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!content) {
|
|
225
|
+
if (opts.onNotFound) {
|
|
226
|
+
opts.onNotFound({ pathname, route, element })
|
|
227
|
+
} else if (!opts.silent) {
|
|
228
|
+
console.warn('[smbls/router] no content matched for path', pathname, '— available routes:', Object.keys(element.routes || {}))
|
|
229
|
+
}
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Run guards
|
|
234
|
+
if (opts.guards && opts.guards.length) {
|
|
235
|
+
const guardContext = { pathname, route, params, query, hash, element, state }
|
|
236
|
+
const guardResult = await runGuards(opts.guards, guardContext)
|
|
237
|
+
if (guardResult === false) return
|
|
238
|
+
if (typeof guardResult === 'string') {
|
|
239
|
+
// Redirect
|
|
240
|
+
return router(guardResult, el, state, { ...options, guards: [] })
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// pushState always fires before render so the URL bar stays in sync even
|
|
245
|
+
// if the content-render step below no-ops due to an extends/ref mismatch.
|
|
246
|
+
if (opts.pushState) {
|
|
247
|
+
try {
|
|
248
|
+
win.history.pushState(state, null, pathname + (search || '') + (hash || ''))
|
|
249
|
+
} catch (e) {} // expected in sandboxed iframes (e.g. about:srcdoc) where pushState is restricted
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (pathChanged || !hashChanged) {
|
|
253
|
+
const stateUpdate = { route, routePath, hash, debugging: false }
|
|
254
|
+
if (Object.keys(params).length) stateUpdate.params = params
|
|
255
|
+
if (Object.keys(query).length) stateUpdate.query = query
|
|
256
|
+
|
|
257
|
+
if (opts.updateState) {
|
|
258
|
+
element.state.update(
|
|
259
|
+
stateUpdate,
|
|
260
|
+
{ preventContentUpdate: true }
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (contentElementKey && opts.removeOldElement) {
|
|
265
|
+
element[contentElementKey].remove()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Merge parent's original content definition (styles like overflow, maxHeight)
|
|
269
|
+
// with the route's content component
|
|
270
|
+
const originContent = element.__ref?.origin?.content
|
|
271
|
+
const contentStyles = {}
|
|
272
|
+
if (originContent) {
|
|
273
|
+
for (const k in originContent) {
|
|
274
|
+
const v = originContent[k]
|
|
275
|
+
if (k === '__ref' || k === 'props' || k === 'node' || k === 'parent' || k === 'key') continue
|
|
276
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || (typeof v === 'object' && v !== null && !v.node && !v.__ref)) {
|
|
277
|
+
contentStyles[k] = v
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const nextContent = {
|
|
283
|
+
...contentStyles,
|
|
284
|
+
...(typeof content === 'object' ? content : { extends: content })
|
|
285
|
+
}
|
|
286
|
+
if (opts.useFragment) nextContent.tag = 'fragment'
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
element.set(nextContent, { contentElementKey })
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error('[smbls/router] failed to render route content', pathname, err)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (opts.scrollToTop && scrollNode?.scrollTo) {
|
|
296
|
+
scrollNode.scrollTo({
|
|
297
|
+
...(opts.scrollToOptions || {}),
|
|
298
|
+
top: 0,
|
|
299
|
+
left: 0
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
if (opts.scrollToNode && content[contentElementKey]?.node?.scrollTo) {
|
|
303
|
+
content[contentElementKey].node.scrollTo({
|
|
304
|
+
...(opts.scrollToOptions || {}),
|
|
305
|
+
top: 0,
|
|
306
|
+
left: 0
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (hash) {
|
|
311
|
+
const activeNode = doc.getElementById(hash)
|
|
312
|
+
if (activeNode && scrollNode?.scrollTo) {
|
|
313
|
+
const top =
|
|
314
|
+
activeNode.getBoundingClientRect().top +
|
|
315
|
+
rootNode.scrollTop -
|
|
316
|
+
(opts.scrollToOffset || 0)
|
|
317
|
+
scrollNode.scrollTo({
|
|
318
|
+
...(opts.scrollToOptions || {}),
|
|
319
|
+
top,
|
|
320
|
+
left: 0
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// trigger `on.routeChanged`
|
|
326
|
+
triggerEventOn('routeChanged', element, opts)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export default router
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/router",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.9",
|
|
4
4
|
"license": "CC-BY-NC-4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"source": "index.js",
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
+
"*.js",
|
|
20
21
|
"*.md",
|
|
21
22
|
"LICENSE"
|
|
22
23
|
],
|
|
@@ -27,8 +28,8 @@
|
|
|
27
28
|
"prepublishOnly": "npm run build"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
|
-
"@symbo.ls/element": "^3.14.
|
|
31
|
-
"@symbo.ls/utils": "^3.14.
|
|
31
|
+
"@symbo.ls/element": "^3.14.12",
|
|
32
|
+
"@symbo.ls/utils": "^3.14.11"
|
|
32
33
|
},
|
|
33
34
|
"gitHead": "9fc1b79b41cdc725ca6b24aec64920a599634681",
|
|
34
35
|
"devDependencies": {
|