@symbo.ls/router 3.14.1 → 3.14.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ # @symbo.ls/router
2
+
3
+ ## 3.14.3
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.6
12
+ - @symbo.ls/utils@3.14.5
13
+
14
+ ## 3.14.1
15
+
16
+ ### Patch Changes
17
+
18
+ - Auto-generated patch release for changed packages.
19
+ - Updated dependencies
20
+ - @symbo.ls/element@3.14.3
21
+ - @symbo.ls/utils@3.14.2
@@ -0,0 +1 @@
1
+ "use strict";var w=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var z=Object.getOwnPropertyNames;var G=Object.prototype.hasOwnProperty;var Q=(e,o)=>{for(var r in o)w(e,r,{get:o[r],enumerable:!0})},D=(e,o,r,s)=>{if(o&&typeof o=="object"||typeof o=="function")for(let t of z(o))!G.call(e,t)&&t!==r&&w(e,t,{get:()=>o[t],enumerable:!(s=$(o,t))||s.enumerable});return e};var H=e=>D(w({},"__esModule",{value:!0}),e);var tt={};Q(tt,{default:()=>Z,getActiveRoute:()=>q,lastLevel:()=>K,lastPathname:()=>C,matchRoute:()=>F,parseQuery:()=>L,parseRoutePattern:()=>B,router:()=>N,runGuards:()=>k});module.exports=H(tt);var i=require("@symbo.ls/utils");const M=/^:(.+)/,A=/^\*$/,U=new Map,B=e=>{const o=U.get(e);if(o)return o;const r=e.replace(/^\//,"").split("/"),s=[];let t=!1;for(let c=0;c<r.length;c++){const n=r[c].match(M);n?s.push({index:c,name:n[1]}):A.test(r[c])&&(t=!0)}const a={segments:r,params:s,hasWildcard:t,pattern:e};return U.set(e,a),a},F=(e,o,r=0)=>{const t=e.replace(/^\//,"").split("/").filter(Boolean).slice(r),a="/"+(t[0]||"");let c=null,n=-1,g={};for(const u in o){if(u==="/*")continue;const x=B(u),v=J(t,x);v>n&&(n=v,c=u,g=V(t,x))}return!c&&o["/*"]&&(c="/*"),{key:c,content:c?o[c]:null,params:g,routePath:a}},J=(e,o)=>{const{segments:r,hasWildcard:s}=o;if(!s&&r.length!==e.length&&r.length!==1&&r.length>e.length)return-1;let t=0;const a=Math.min(r.length,e.length);for(let c=0;c<a;c++)if(r[c]===e[c])t+=3;else if(M.test(r[c]))t+=1;else if(A.test(r[c]))t+=.5;else return-1;return t},V=(e,o)=>{const r={};for(const{index:s,name:t}of o.params)e[s]&&(r[t]=decodeURIComponent(e[s]));return r},L=e=>{if(!e||e==="?")return{};const o={};return new URLSearchParams(e).forEach((s,t)=>{o[t]!==void 0?(Array.isArray(o[t])||(o[t]=[o[t]]),o[t].push(s)):o[t]=s}),o},k=async(e,o)=>{if(!e||!e.length)return!0;for(const r of e){const s=await r(o);if(s===!1)return!1;if(typeof s=="string")return s}return!0},q=(e=0,o)=>{o||(o=typeof i.window<"u"?i.window.location.pathname:"/");const s=o.split("/")[e+1];if(s)return`/${s}`};let C,K=0;const X=e=>!e||e==="srcdoc"||e==="about:srcdoc"?"/":e,Y={level:K,pushState:!0,initialRender:!1,scrollToTop:!0,scrollToNode:!1,scrollNode:i.document&&i.document.documentElement,scrollBody:!1,useFragment:!1,updateState:!0,scrollToOffset:0,contentElementKey:"content",scrollToOptions:{behavior:"smooth"},useParamsMatching:!0},N=async(e,o,r={},s={})=>{const t=o||void 0,a=t?.context?.window||i.window,c=t?.context?.document||i.document,n={...Y,...t.context.routerOptions,...s};K=n.lastLevel;const g=t.__ref;(n.contentElementKey!=="content"&&n.contentElementKey!==g.contentElementKey||!g.contentElementKey)&&(g.contentElementKey=n.contentElementKey||"content");const u=g.contentElementKey||n.contentElementKey||"content",x=a.location.origin!=="null"?a.location.origin:"http://localhost";e=X(e);const v=new a.URL(x+e),{pathname:f,search:R,hash:y}=v,P=L(R),_=t.node,I=y&&y!==a.location.hash.slice(1),W=f!==C;C=f;let T,O,p,b;if(n.useParamsMatching){const l=F(f,t.routes,n.level);T=l.key,O=l.routePath,p=l.content,b=l.params}else T=q(n.level,f),O=T,p=t.routes[T||"/"]||t.routes["/*"],b={};const E=n.scrollToNode?_:n.scrollNode;if(t.state?.root?.debugging){t.state.root.debugging=!1;return}if(!p){n.onNotFound?n.onNotFound({pathname:f,route:T,element:t}):n.silent||console.warn("[smbls/router] no content matched for path",f,"\u2014 available routes:",Object.keys(t.routes||{}));return}if(n.guards&&n.guards.length){const l={pathname:f,route:T,params:b,query:P,hash:y,element:t,state:r},d=await k(n.guards,l);if(d===!1)return;if(typeof d=="string")return N(d,o,r,{...s,guards:[]})}if(n.pushState)try{a.history.pushState(r,null,f+(R||"")+(y||""))}catch{}if(W||!I){const l={route:T,routePath:O,hash:y,debugging:!1};Object.keys(b).length&&(l.params=b),Object.keys(P).length&&(l.query=P),n.updateState&&t.state.update(l,{preventContentUpdate:!0}),u&&n.removeOldElement&&t[u].remove();const d=t.__ref?.origin?.content,S={};if(d)for(const m in d){const h=d[m];m==="__ref"||m==="props"||m==="node"||m==="parent"||m==="key"||(typeof h=="string"||typeof h=="number"||typeof h=="boolean"||typeof h=="object"&&h!==null&&!h.node&&!h.__ref)&&(S[m]=h)}const j={...S,...typeof p=="object"?p:{extends:p}};n.useFragment&&(j.tag="fragment");try{t.set(j,{contentElementKey:u})}catch(m){console.error("[smbls/router] failed to render route content",f,m)}}if(n.scrollToTop&&E?.scrollTo&&E.scrollTo({...n.scrollToOptions||{},top:0,left:0}),n.scrollToNode&&p[u]?.node?.scrollTo&&p[u].node.scrollTo({...n.scrollToOptions||{},top:0,left:0}),y){const l=c.getElementById(y);if(l&&E?.scrollTo){const d=l.getBoundingClientRect().top+_.scrollTop-(n.scrollToOffset||0);E.scrollTo({...n.scrollToOptions||{},top:d,left:0})}}(0,i.triggerEventOn)("routeChanged",t,n)};var Z=N;
@@ -0,0 +1 @@
1
+ {"type": "commonjs"}
@@ -0,0 +1 @@
1
+ import{document as O,window as w,triggerEventOn as L}from"@symbo.ls/utils";const _=/^:(.+)/,S=/^\*$/,j=new Map,k=e=>{const n=j.get(e);if(n)return n;const r=e.replace(/^\//,"").split("/"),s=[];let t=!1;for(let c=0;c<r.length;c++){const o=r[c].match(_);o?s.push({index:c,name:o[1]}):S.test(r[c])&&(t=!0)}const a={segments:r,params:s,hasWildcard:t,pattern:e};return j.set(e,a),a},q=(e,n,r=0)=>{const t=e.replace(/^\//,"").split("/").filter(Boolean).slice(r),a="/"+(t[0]||"");let c=null,o=-1,h={};for(const i in n){if(i==="/*")continue;const b=k(i),x=I(t,b);x>o&&(o=x,c=i,h=W(t,b))}return!c&&n["/*"]&&(c="/*"),{key:c,content:c?n[c]:null,params:h,routePath:a}},I=(e,n)=>{const{segments:r,hasWildcard:s}=n;if(!s&&r.length!==e.length&&r.length!==1&&r.length>e.length)return-1;let t=0;const a=Math.min(r.length,e.length);for(let c=0;c<a;c++)if(r[c]===e[c])t+=3;else if(_.test(r[c]))t+=1;else if(S.test(r[c]))t+=.5;else return-1;return t},W=(e,n)=>{const r={};for(const{index:s,name:t}of n.params)e[s]&&(r[t]=decodeURIComponent(e[s]));return r},$=e=>{if(!e||e==="?")return{};const n={};return new URLSearchParams(e).forEach((s,t)=>{n[t]!==void 0?(Array.isArray(n[t])||(n[t]=[n[t]]),n[t].push(s)):n[t]=s}),n},z=async(e,n)=>{if(!e||!e.length)return!0;for(const r of e){const s=await r(n);if(s===!1)return!1;if(typeof s=="string")return s}return!0},G=(e=0,n)=>{n||(n=typeof w<"u"?w.location.pathname:"/");const s=n.split("/")[e+1];if(s)return`/${s}`};let M,A=0;const Q=e=>!e||e==="srcdoc"||e==="about:srcdoc"?"/":e,D={level:A,pushState:!0,initialRender:!1,scrollToTop:!0,scrollToNode:!1,scrollNode:O&&O.documentElement,scrollBody:!1,useFragment:!1,updateState:!0,scrollToOffset:0,contentElementKey:"content",scrollToOptions:{behavior:"smooth"},useParamsMatching:!0},U=async(e,n,r={},s={})=>{const t=n||void 0,a=t?.context?.window||w,c=t?.context?.document||O,o={...D,...t.context.routerOptions,...s};A=o.lastLevel;const h=t.__ref;(o.contentElementKey!=="content"&&o.contentElementKey!==h.contentElementKey||!h.contentElementKey)&&(h.contentElementKey=o.contentElementKey||"content");const i=h.contentElementKey||o.contentElementKey||"content",b=a.location.origin!=="null"?a.location.origin:"http://localhost";e=Q(e);const x=new a.URL(b+e),{pathname:u,search:C,hash:g}=x,E=$(C),K=t.node,B=g&&g!==a.location.hash.slice(1),F=u!==M;M=u;let y,P,m,T;if(o.useParamsMatching){const l=q(u,t.routes,o.level);y=l.key,P=l.routePath,m=l.content,T=l.params}else y=G(o.level,u),P=y,m=t.routes[y||"/"]||t.routes["/*"],T={};const v=o.scrollToNode?K:o.scrollNode;if(t.state?.root?.debugging){t.state.root.debugging=!1;return}if(!m){o.onNotFound?o.onNotFound({pathname:u,route:y,element:t}):o.silent||console.warn("[smbls/router] no content matched for path",u,"\u2014 available routes:",Object.keys(t.routes||{}));return}if(o.guards&&o.guards.length){const l={pathname:u,route:y,params:T,query:E,hash:g,element:t,state:r},f=await z(o.guards,l);if(f===!1)return;if(typeof f=="string")return U(f,n,r,{...s,guards:[]})}if(o.pushState)try{a.history.pushState(r,null,u+(C||"")+(g||""))}catch{}if(F||!B){const l={route:y,routePath:P,hash:g,debugging:!1};Object.keys(T).length&&(l.params=T),Object.keys(E).length&&(l.query=E),o.updateState&&t.state.update(l,{preventContentUpdate:!0}),i&&o.removeOldElement&&t[i].remove();const f=t.__ref?.origin?.content,N={};if(f)for(const d in f){const p=f[d];d==="__ref"||d==="props"||d==="node"||d==="parent"||d==="key"||(typeof p=="string"||typeof p=="number"||typeof p=="boolean"||typeof p=="object"&&p!==null&&!p.node&&!p.__ref)&&(N[d]=p)}const R={...N,...typeof m=="object"?m:{extends:m}};o.useFragment&&(R.tag="fragment");try{t.set(R,{contentElementKey:i})}catch(d){console.error("[smbls/router] failed to render route content",u,d)}}if(o.scrollToTop&&v?.scrollTo&&v.scrollTo({...o.scrollToOptions||{},top:0,left:0}),o.scrollToNode&&m[i]?.node?.scrollTo&&m[i].node.scrollTo({...o.scrollToOptions||{},top:0,left:0}),g){const l=c.getElementById(g);if(l&&v?.scrollTo){const f=l.getBoundingClientRect().top+K.scrollTop-(o.scrollToOffset||0);v.scrollTo({...o.scrollToOptions||{},top:f,left:0})}}L("routeChanged",t,o)};var J=U;export{J as default,G as getActiveRoute,A as lastLevel,M as lastPathname,q as matchRoute,$ as parseQuery,k as parseRoutePattern,U as router,z as runGuards};
package/package.json CHANGED
@@ -1,32 +1,34 @@
1
1
  {
2
2
  "name": "@symbo.ls/router",
3
- "version": "3.14.1",
3
+ "version": "3.14.3",
4
4
  "license": "CC-BY-NC-4.0",
5
5
  "type": "module",
6
- "module": "./index.js",
7
- "unpkg": "./dist/iife/index.js",
8
- "jsdelivr": "./dist/iife/index.js",
9
- "main": "./index.js",
6
+ "module": "./dist/esm/index.js",
7
+ "main": "./dist/cjs/index.js",
10
8
  "exports": {
11
- ".": "./index.js",
9
+ ".": {
10
+ "development": "./index.js",
11
+ "import": "./dist/esm/index.js",
12
+ "require": "./dist/cjs/index.js",
13
+ "default": "./dist/esm/index.js"
14
+ },
12
15
  "./package.json": "./package.json"
13
16
  },
14
17
  "source": "index.js",
15
18
  "files": [
16
19
  "dist",
17
- "*.js"
20
+ "*.md",
21
+ "LICENSE"
18
22
  ],
19
23
  "scripts": {
20
- "copy:package:cjs": "cp ../../build/package-cjs.json dist/cjs/package.json",
21
- "build:esm": "NODE_ENV=$NODE_ENV esbuild *.js --target=es2020 --format=esm --outdir=dist/esm --define:process.env.NODE_ENV=process.env.NODE_ENV",
22
- "build:cjs": "NODE_ENV=$NODE_ENV esbuild *.js --target=node18 --format=cjs --outdir=dist/cjs --define:process.env.NODE_ENV=process.env.NODE_ENV",
23
- "build:iife": "NODE_ENV=$NODE_ENV esbuild index.js --bundle --target=es2020 --format=iife --global-name=DomqlRouter --outfile=dist/iife/index.js --define:process.env.NODE_ENV=process.env.NODE_ENV",
24
- "build": "node ../../build/build.js",
25
- "prepublish": "npm run build && npm run copy:package:cjs"
24
+ "build:esm": "esbuild $(find . -type f -name \"*.js\" -not -path \"*/dist/*\" -not -path \"*/node_modules/*\" -not -path \"*/__tests__/*\" -not -path \"*/test/*\" -not -path \"*/demo/*\" -not -path \"*/demos/*\" -not -path \"*/examples/*\" -not -path \"*/fixtures/*\" -not -name \"*.test.js\") --target=es2022 --format=esm --outbase=. --outdir=dist/esm --minify --log-level=warning",
25
+ "build:cjs": "esbuild $(find . -type f -name \"*.js\" -not -path \"*/dist/*\" -not -path \"*/node_modules/*\" -not -path \"*/__tests__/*\" -not -path \"*/test/*\" -not -path \"*/demo/*\" -not -path \"*/demos/*\" -not -path \"*/examples/*\" -not -path \"*/fixtures/*\" -not -name \"*.test.js\") --target=node20 --format=cjs --outbase=. --outdir=dist/cjs --minify --log-level=warning",
26
+ "build": "rimraf dist && npm run build:esm && npm run build:cjs && node -e \"require('fs').writeFileSync('dist/cjs/package.json', '{\\\"type\\\": \\\"commonjs\\\"}')\"",
27
+ "prepublishOnly": "npm run build"
26
28
  },
27
29
  "dependencies": {
28
- "@symbo.ls/element": "^3.14.3",
29
- "@symbo.ls/utils": "^3.14.2"
30
+ "@symbo.ls/element": "^3.14.6",
31
+ "@symbo.ls/utils": "^3.14.5"
30
32
  },
31
33
  "gitHead": "9fc1b79b41cdc725ca6b24aec64920a599634681",
32
34
  "devDependencies": {
@@ -35,6 +37,6 @@
35
37
  "browser": "./index.js",
36
38
  "sideEffects": false,
37
39
  "publishConfig": {
38
- "access": "public"
40
+ "access": "restricted"
39
41
  }
40
42
  }
package/index.js DELETED
@@ -1,329 +0,0 @@
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