@symbo.ls/router 3.14.600 → 3.14.602

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 CHANGED
@@ -18,6 +18,9 @@ router('/about', element)
18
18
 
19
19
  // With state and options
20
20
  router('/dashboard', element, { userId: 1 }, { scrollToTop: true })
21
+
22
+ // Replace the current history entry instead of pushing a new one
23
+ router('/login', element, {}, { replace: true })
21
24
  ```
22
25
 
23
26
  Define routes on your DOMQL element:
@@ -158,7 +161,8 @@ export const team = {
158
161
  | Option | Type | Default | Description |
159
162
  |---|---|---|---|
160
163
  | `level` | `number` | `0` | Route nesting level (which path segment to match) |
161
- | `pushState` | `boolean` | `true` | Push to browser history |
164
+ | `pushState` | `boolean` | `true` | Write the navigation to browser history (`false` writes nothing) |
165
+ | `replace` | `boolean` | `false` | Rewrite the current history entry (`history.replaceState`) instead of pushing a new one — redirects, canonical-URL rewrites, filter/query changes that must not stack Back entries. Ignored when `pushState` is `false` |
162
166
  | `initialRender` | `boolean` | `false` | Whether this is the initial page render |
163
167
  | `scrollToTop` | `boolean` | `true` | Scroll to top after navigation |
164
168
  | `scrollToNode` | `boolean` | `false` | Scroll within the element node |
package/dist/cjs/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var O=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var Q=Object.getOwnPropertyNames;var D=Object.prototype.hasOwnProperty;var H=(e,o)=>{for(var r in o)O(e,r,{get:o[r],enumerable:!0})},J=(e,o,r,s)=>{if(o&&typeof o=="object"||typeof o=="function")for(let t of Q(o))!D.call(e,t)&&t!==r&&O(e,t,{get:()=>o[t],enumerable:!(s=G(o,t))||s.enumerable});return e};var V=e=>J(O({},"__esModule",{value:!0}),e);var ot={};H(ot,{default:()=>et,getActiveRoute:()=>I,lastLevel:()=>_,lastPathname:()=>C,matchRoute:()=>B,parseQuery:()=>F,parseRoutePattern:()=>k,router:()=>K,runGuards:()=>q});module.exports=V(ot);var i=require("@symbo.ls/utils");const j=/^:(.+)/,M=/^\*$/,A=new Map,k=e=>{const o=A.get(e);if(o)return o;const r=e.replace(/^\//,"").split("/"),s=[];let t=!1;for(let l=0;l<r.length;l++){const n=r[l].match(j);n?s.push({index:l,name:n[1]}):M.test(r[l])&&(t=!0)}const a={segments:r,params:s,hasWildcard:t,pattern:e};return A.set(e,a),a},B=(e,o,r=0)=>{const t=e.replace(/^\//,"").split("/").filter(Boolean).slice(r),a="/"+(t[0]||"");if(!o)return{key:null,content:null,params:{},routePath:a};let l=null,n=-1,f={};for(const d in o){if(d==="/*")continue;const x=k(d),R=X(t,x);R>n&&(n=R,l=d,f=Y(t,x))}return!l&&o["/*"]&&(l="/*"),{key:l,content:l?o[l]:null,params:f,routePath:a}},X=(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 l=0;l<a;l++)if(r[l]===e[l])t+=3;else if(j.test(r[l]))t+=1;else if(M.test(r[l]))t+=.5;else return-1;return t},Y=(e,o)=>{const r={};for(const{index:s,name:t}of o.params)e[s]&&(r[t]=decodeURIComponent(e[s]));return r},F=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},q=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},I=(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,_=0;const Z=e=>!e||e==="srcdoc"||e==="about:srcdoc"?"/":e,tt={level:_,pushState:!0,initialRender:!1,scrollToTop:!0,scrollToNode:!1,get scrollNode(){return typeof i.document<"u"&&(i.document.scrollingElement||i.document.documentElement)||null},scrollBody:!1,useFragment:!1,updateState:!0,scrollToOffset:0,contentElementKey:"content",scrollToOptions:{behavior:"smooth"},useParamsMatching:!0},K=async(e,o,r={},s={})=>{const t=o||void 0,a=t?.context?.window||i.window,l=t?.context?.document||i.document,n={...tt,...t.context.routerOptions,...s};_=n.lastLevel;const f=t.__ref;(n.contentElementKey!=="content"&&n.contentElementKey!==f.contentElementKey||!f.contentElementKey)&&(f.contentElementKey=n.contentElementKey||"content");const d=f.contentElementKey||n.contentElementKey||"content",x=a.location.origin!=="null"?a.location.origin:"http://localhost";e=Z(e);const R=typeof a.URL=="function"?a.URL:typeof globalThis<"u"&&globalThis.URL||URL,W=new R(x+e),{pathname:p,search:N,hash:T}=W,v=F(N),S=t.node,$=T&&T!==a.location.hash.slice(1),U=p!==C;C=p;const P=f.__routerRendered;let h,w,u,b;if(n.useParamsMatching){const c=B(p,t.routes,n.level);h=c.key,w=c.routePath,u=c.content,b=c.params}else{h=I(n.level,p),w=h;const c=t.routes;u=c?c[h||"/"]||c["/*"]:null,b={}}const E=n.scrollToNode?S:n.scrollNode;if(t.state?.root?.debugging){t.state.root.debugging=!1;return}if(!u){n.onNotFound?n.onNotFound({pathname:p,route:h,element:t}):n.silent||console.warn("[smbls/router] no content matched for path",p,"\u2014 available routes:",Object.keys(t.routes||{}));return}if(n.guards&&n.guards.length){const c={pathname:p,route:h,params:b,query:v,hash:T,element:t,state:r},g=await q(n.guards,c);if(g===!1)return;if(typeof g=="string")return K(g,o,r,{...s,guards:[]})}if(n.pushState)try{a.history.pushState(r,null,p+(N||"")+(T||""))}catch{}if(U||!$){const c={route:h,routePath:w,hash:T,debugging:!1};Object.keys(b).length&&(c.params=b),Object.keys(v).length&&(c.query=v),n.updateState&&t.state.update(c,{preventContentUpdate:!0})}const z=!P||P.content!==u||P.route!==h;if(U||z||n.force===!0){d&&n.removeOldElement&&t[d].remove();const c=t.__ref?.origin?.content,g={};if(c)for(const m in c){const y=c[m];m==="__ref"||m==="props"||m==="node"||m==="parent"||m==="key"||(typeof y=="string"||typeof y=="number"||typeof y=="boolean"||typeof y=="object"&&y!==null&&!y.node&&!y.__ref)&&(g[m]=y)}const L={...g,...typeof u=="object"?u:{extends:u}};n.useFragment&&(L.tag="fragment");try{if(typeof t?.set!="function")return;t.set(L,{contentElementKey:d}),f.__routerRendered={content:u,route:h}}catch(m){console.error("[smbls/router] failed to render route content",p,m)}}if(n.scrollToTop&&E?.scrollTo&&E.scrollTo({...n.scrollToOptions||{},top:0,left:0}),n.scrollToNode&&u[d]?.node?.scrollTo&&u[d].node.scrollTo({...n.scrollToOptions||{},top:0,left:0}),T){const c=l.getElementById(T);if(c&&E?.scrollTo){const g=c.getBoundingClientRect().top+S.scrollTop-(n.scrollToOffset||0);E.scrollTo({...n.scrollToOptions||{},top:g,left:0})}}(0,i.triggerEventOn)("routeChanged",t,n)};var et=K;
1
+ "use strict";var O=Object.defineProperty;var Q=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var H=Object.prototype.hasOwnProperty;var J=(e,o)=>{for(var r in o)O(e,r,{get:o[r],enumerable:!0})},V=(e,o,r,s)=>{if(o&&typeof o=="object"||typeof o=="function")for(let t of D(o))!H.call(e,t)&&t!==r&&O(e,t,{get:()=>o[t],enumerable:!(s=Q(o,t))||s.enumerable});return e};var X=e=>V(O({},"__esModule",{value:!0}),e);var nt={};J(nt,{default:()=>ot,getActiveRoute:()=>W,lastLevel:()=>_,lastPathname:()=>S,matchRoute:()=>B,parseQuery:()=>k,parseRoutePattern:()=>j,router:()=>N,runGuards:()=>I});module.exports=X(nt);var u=require("@symbo.ls/utils");const M=/^:(.+)/,A=/^\*$/,F=new Map,j=e=>{const o=F.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 F.set(e,a),a},B=(e,o,r=0)=>{const t=e.replace(/^\//,"").split("/").filter(Boolean).slice(r),a="/"+(t[0]||"");if(!o)return{key:null,content:null,params:{},routePath:a};let c=null,n=-1,p={};for(const h in o){if(h==="/*")continue;const x=j(h),R=Y(t,x);R>n&&(n=R,c=h,p=Z(t,x))}return!c&&o["/*"]&&(c="/*"),{key:c,content:c?o[c]:null,params:p,routePath:a}},Y=(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},Z=(e,o)=>{const r={};for(const{index:s,name:t}of o.params)e[s]&&(r[t]=decodeURIComponent(e[s]));return r},k=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},I=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},W=(e=0,o)=>{o||(o=typeof u.window<"u"?u.window.location.pathname:"/");const s=o.split("/")[e+1];if(s)return`/${s}`};let S,_=0;const tt=e=>!e||e==="srcdoc"||e==="about:srcdoc"?"/":e,et={level:_,pushState:!0,replace:!1,initialRender:!1,scrollToTop:!0,scrollToNode:!1,get scrollNode(){return typeof u.document<"u"&&(u.document.scrollingElement||u.document.documentElement)||null},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||u.window,c=t?.context?.document||u.document,n={...et,...t.context.routerOptions,...s};_=n.lastLevel;const p=t.__ref;(n.contentElementKey!=="content"&&n.contentElementKey!==p.contentElementKey||!p.contentElementKey)&&(p.contentElementKey=n.contentElementKey||"content");const h=p.contentElementKey||n.contentElementKey||"content",x=a.location.origin!=="null"?a.location.origin:"http://localhost";e=tt(e);const R=typeof a.URL=="function"?a.URL:typeof globalThis<"u"&&globalThis.URL||URL,$=new R(x+e),{pathname:m,search:K,hash:T}=$,U=k(K),q=t.node,L=T&&T!==a.location.hash.slice(1),P=m!==S;S=m;const w=p.__routerRendered;let g,C,f,v;if(n.useParamsMatching){const l=B(m,t.routes,n.level);g=l.key,C=l.routePath,f=l.content,v=l.params}else{g=W(n.level,m),C=g;const l=t.routes;f=l?l[g||"/"]||l["/*"]:null,v={}}const b=n.scrollToNode?q:n.scrollNode;if(t.state?.root?.debugging){t.state.root.debugging=!1;return}if(!f){n.onNotFound?n.onNotFound({pathname:m,route:g,element:t}):n.silent||console.warn("[smbls/router] no content matched for path",m,"\u2014 available routes:",Object.keys(t.routes||{}));return}if(n.guards&&n.guards.length){const l={pathname:m,route:g,params:v,query:U,hash:T,element:t,state:r},d=await I(n.guards,l);if(d===!1)return;if(typeof d=="string")return N(d,o,r,{...s,guards:[]})}if(n.pushState){const l=m+(K||"")+(T||"");try{n.replace?a.history.replaceState(r,null,l):a.history.pushState(r,null,l)}catch{}}if(P||!L){const l={route:g,routePath:C,hash:T,params:v,query:U,debugging:!1};n.updateState&&t.state.update(l,{preventContentUpdate:!0})}const z=!w||w.content!==f||w.route!==g;if(P||z||n.force===!0){h&&n.removeOldElement&&t[h].remove();const l=t.__ref?.origin?.content,d={};if(l)for(const i in l){const y=l[i];i==="__ref"||i==="props"||i==="node"||i==="parent"||i==="key"||(typeof y=="string"||typeof y=="number"||typeof y=="boolean"||typeof y=="object"&&y!==null&&!y.node&&!y.__ref)&&(d[i]=y)}const E={...d,...typeof f=="object"?f:{extends:f}};n.useFragment&&(E.tag="fragment");try{if(typeof t?.set!="function")return;t.set(E,{contentElementKey:h}),p.__routerRendered={content:f,route:g}}catch(i){console.error("[smbls/router] failed to render route content",m,i)}}const G=!P&&L;if(n.scrollToTop&&!G&&b?.scrollTo&&b.scrollTo({...n.scrollToOptions||{},top:0,left:0}),n.scrollToNode&&f[h]?.node?.scrollTo&&f[h].node.scrollTo({...n.scrollToOptions||{},top:0,left:0}),T){const l=c.getElementById(T.slice(1));if(l&&b?.scrollTo){const d=a.getComputedStyle?a.getComputedStyle(l):null,E=d&&parseFloat(d.scrollMarginTop)||0,i=l.getBoundingClientRect().top+b.scrollTop-E-(n.scrollToOffset||0);b.scrollTo({...n.scrollToOptions||{},top:i,left:0})}}(0,u.triggerEventOn)("routeChanged",t,n)};var ot=N;
package/dist/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{document as E,window as O,triggerEventOn as q}from"@symbo.ls/utils";const S=/^:(.+)/,U=/^\*$/,L=new Map,I=o=>{const n=L.get(o);if(n)return n;const r=o.replace(/^\//,"").split("/"),c=[];let t=!1;for(let l=0;l<r.length;l++){const e=r[l].match(S);e?c.push({index:l,name:e[1]}):U.test(r[l])&&(t=!0)}const a={segments:r,params:c,hasWildcard:t,pattern:o};return L.set(o,a),a},W=(o,n,r=0)=>{const t=o.replace(/^\//,"").split("/").filter(Boolean).slice(r),a="/"+(t[0]||"");if(!n)return{key:null,content:null,params:{},routePath:a};let l=null,e=-1,u={};for(const f in n){if(f==="/*")continue;const b=I(f),x=$(t,b);x>e&&(e=x,l=f,u=z(t,b))}return!l&&n["/*"]&&(l="/*"),{key:l,content:l?n[l]:null,params:u,routePath:a}},$=(o,n)=>{const{segments:r,hasWildcard:c}=n;if(!c&&r.length!==o.length&&r.length!==1&&r.length>o.length)return-1;let t=0;const a=Math.min(r.length,o.length);for(let l=0;l<a;l++)if(r[l]===o[l])t+=3;else if(S.test(r[l]))t+=1;else if(U.test(r[l]))t+=.5;else return-1;return t},z=(o,n)=>{const r={};for(const{index:c,name:t}of n.params)o[c]&&(r[t]=decodeURIComponent(o[c]));return r},G=o=>{if(!o||o==="?")return{};const n={};return new URLSearchParams(o).forEach((c,t)=>{n[t]!==void 0?(Array.isArray(n[t])||(n[t]=[n[t]]),n[t].push(c)):n[t]=c}),n},Q=async(o,n)=>{if(!o||!o.length)return!0;for(const r of o){const c=await r(n);if(c===!1)return!1;if(typeof c=="string")return c}return!0},D=(o=0,n)=>{n||(n=typeof O<"u"?O.location.pathname:"/");const c=n.split("/")[o+1];if(c)return`/${c}`};let j,M=0;const H=o=>!o||o==="srcdoc"||o==="about:srcdoc"?"/":o,J={level:M,pushState:!0,initialRender:!1,scrollToTop:!0,scrollToNode:!1,get scrollNode(){return typeof E<"u"&&(E.scrollingElement||E.documentElement)||null},scrollBody:!1,useFragment:!1,updateState:!0,scrollToOffset:0,contentElementKey:"content",scrollToOptions:{behavior:"smooth"},useParamsMatching:!0},A=async(o,n,r={},c={})=>{const t=n||void 0,a=t?.context?.window||O,l=t?.context?.document||E,e={...J,...t.context.routerOptions,...c};M=e.lastLevel;const u=t.__ref;(e.contentElementKey!=="content"&&e.contentElementKey!==u.contentElementKey||!u.contentElementKey)&&(u.contentElementKey=e.contentElementKey||"content");const f=u.contentElementKey||e.contentElementKey||"content",b=a.location.origin!=="null"?a.location.origin:"http://localhost";o=H(o);const x=typeof a.URL=="function"?a.URL:typeof globalThis<"u"&&globalThis.URL||URL,k=new x(b+o),{pathname:d,search:C,hash:y}=k,v=G(C),_=t.node,B=y&&y!==a.location.hash.slice(1),K=d!==j;j=d;const P=u.__routerRendered;let p,w,i,T;if(e.useParamsMatching){const s=W(d,t.routes,e.level);p=s.key,w=s.routePath,i=s.content,T=s.params}else{p=D(e.level,d),w=p;const s=t.routes;i=s?s[p||"/"]||s["/*"]:null,T={}}const R=e.scrollToNode?_:e.scrollNode;if(t.state?.root?.debugging){t.state.root.debugging=!1;return}if(!i){e.onNotFound?e.onNotFound({pathname:d,route:p,element:t}):e.silent||console.warn("[smbls/router] no content matched for path",d,"\u2014 available routes:",Object.keys(t.routes||{}));return}if(e.guards&&e.guards.length){const s={pathname:d,route:p,params:T,query:v,hash:y,element:t,state:r},m=await Q(e.guards,s);if(m===!1)return;if(typeof m=="string")return A(m,n,r,{...c,guards:[]})}if(e.pushState)try{a.history.pushState(r,null,d+(C||"")+(y||""))}catch{}if(K||!B){const s={route:p,routePath:w,hash:y,debugging:!1};Object.keys(T).length&&(s.params=T),Object.keys(v).length&&(s.query=v),e.updateState&&t.state.update(s,{preventContentUpdate:!0})}const F=!P||P.content!==i||P.route!==p;if(K||F||e.force===!0){f&&e.removeOldElement&&t[f].remove();const s=t.__ref?.origin?.content,m={};if(s)for(const h in s){const g=s[h];h==="__ref"||h==="props"||h==="node"||h==="parent"||h==="key"||(typeof g=="string"||typeof g=="number"||typeof g=="boolean"||typeof g=="object"&&g!==null&&!g.node&&!g.__ref)&&(m[h]=g)}const N={...m,...typeof i=="object"?i:{extends:i}};e.useFragment&&(N.tag="fragment");try{if(typeof t?.set!="function")return;t.set(N,{contentElementKey:f}),u.__routerRendered={content:i,route:p}}catch(h){console.error("[smbls/router] failed to render route content",d,h)}}if(e.scrollToTop&&R?.scrollTo&&R.scrollTo({...e.scrollToOptions||{},top:0,left:0}),e.scrollToNode&&i[f]?.node?.scrollTo&&i[f].node.scrollTo({...e.scrollToOptions||{},top:0,left:0}),y){const s=l.getElementById(y);if(s&&R?.scrollTo){const m=s.getBoundingClientRect().top+_.scrollTop-(e.scrollToOffset||0);R.scrollTo({...e.scrollToOptions||{},top:m,left:0})}}q("routeChanged",t,e)};var Y=A;export{Y as default,D as getActiveRoute,M as lastLevel,j as lastPathname,W as matchRoute,G as parseQuery,I as parseRoutePattern,A as router,Q as runGuards};
1
+ import{document as E,window as O,triggerEventOn as W}from"@symbo.ls/utils";const K=/^:(.+)/,U=/^\*$/,L=new Map,$=o=>{const n=L.get(o);if(n)return n;const r=o.replace(/^\//,"").split("/"),l=[];let t=!1;for(let c=0;c<r.length;c++){const e=r[c].match(K);e?l.push({index:c,name:e[1]}):U.test(r[c])&&(t=!0)}const a={segments:r,params:l,hasWildcard:t,pattern:o};return L.set(o,a),a},q=(o,n,r=0)=>{const t=o.replace(/^\//,"").split("/").filter(Boolean).slice(r),a="/"+(t[0]||"");if(!n)return{key:null,content:null,params:{},routePath:a};let c=null,e=-1,d={};for(const p in n){if(p==="/*")continue;const b=$(p),x=z(t,b);x>e&&(e=x,c=p,d=G(t,b))}return!c&&n["/*"]&&(c="/*"),{key:c,content:c?n[c]:null,params:d,routePath:a}},z=(o,n)=>{const{segments:r,hasWildcard:l}=n;if(!l&&r.length!==o.length&&r.length!==1&&r.length>o.length)return-1;let t=0;const a=Math.min(r.length,o.length);for(let c=0;c<a;c++)if(r[c]===o[c])t+=3;else if(K.test(r[c]))t+=1;else if(U.test(r[c]))t+=.5;else return-1;return t},G=(o,n)=>{const r={};for(const{index:l,name:t}of n.params)o[l]&&(r[t]=decodeURIComponent(o[l]));return r},Q=o=>{if(!o||o==="?")return{};const n={};return new URLSearchParams(o).forEach((l,t)=>{n[t]!==void 0?(Array.isArray(n[t])||(n[t]=[n[t]]),n[t].push(l)):n[t]=l}),n},D=async(o,n)=>{if(!o||!o.length)return!0;for(const r of o){const l=await r(n);if(l===!1)return!1;if(typeof l=="string")return l}return!0},H=(o=0,n)=>{n||(n=typeof O<"u"?O.location.pathname:"/");const l=n.split("/")[o+1];if(l)return`/${l}`};let M,A=0;const J=o=>!o||o==="srcdoc"||o==="about:srcdoc"?"/":o,V={level:A,pushState:!0,replace:!1,initialRender:!1,scrollToTop:!0,scrollToNode:!1,get scrollNode(){return typeof E<"u"&&(E.scrollingElement||E.documentElement)||null},scrollBody:!1,useFragment:!1,updateState:!0,scrollToOffset:0,contentElementKey:"content",scrollToOptions:{behavior:"smooth"},useParamsMatching:!0},F=async(o,n,r={},l={})=>{const t=n||void 0,a=t?.context?.window||O,c=t?.context?.document||E,e={...V,...t.context.routerOptions,...l};A=e.lastLevel;const d=t.__ref;(e.contentElementKey!=="content"&&e.contentElementKey!==d.contentElementKey||!d.contentElementKey)&&(d.contentElementKey=e.contentElementKey||"content");const p=d.contentElementKey||e.contentElementKey||"content",b=a.location.origin!=="null"?a.location.origin:"http://localhost";o=J(o);const x=typeof a.URL=="function"?a.URL:typeof globalThis<"u"&&globalThis.URL||URL,j=new x(b+o),{pathname:h,search:S,hash:y}=j,_=Q(S),B=t.node,N=y&&y!==a.location.hash.slice(1),P=h!==M;M=h;const w=d.__routerRendered;let m,C,u,R;if(e.useParamsMatching){const s=q(h,t.routes,e.level);m=s.key,C=s.routePath,u=s.content,R=s.params}else{m=H(e.level,h),C=m;const s=t.routes;u=s?s[m||"/"]||s["/*"]:null,R={}}const T=e.scrollToNode?B:e.scrollNode;if(t.state?.root?.debugging){t.state.root.debugging=!1;return}if(!u){e.onNotFound?e.onNotFound({pathname:h,route:m,element:t}):e.silent||console.warn("[smbls/router] no content matched for path",h,"\u2014 available routes:",Object.keys(t.routes||{}));return}if(e.guards&&e.guards.length){const s={pathname:h,route:m,params:R,query:_,hash:y,element:t,state:r},f=await D(e.guards,s);if(f===!1)return;if(typeof f=="string")return F(f,n,r,{...l,guards:[]})}if(e.pushState){const s=h+(S||"")+(y||"");try{e.replace?a.history.replaceState(r,null,s):a.history.pushState(r,null,s)}catch{}}if(P||!N){const s={route:m,routePath:C,hash:y,params:R,query:_,debugging:!1};e.updateState&&t.state.update(s,{preventContentUpdate:!0})}const k=!w||w.content!==u||w.route!==m;if(P||k||e.force===!0){p&&e.removeOldElement&&t[p].remove();const s=t.__ref?.origin?.content,f={};if(s)for(const i in s){const g=s[i];i==="__ref"||i==="props"||i==="node"||i==="parent"||i==="key"||(typeof g=="string"||typeof g=="number"||typeof g=="boolean"||typeof g=="object"&&g!==null&&!g.node&&!g.__ref)&&(f[i]=g)}const v={...f,...typeof u=="object"?u:{extends:u}};e.useFragment&&(v.tag="fragment");try{if(typeof t?.set!="function")return;t.set(v,{contentElementKey:p}),d.__routerRendered={content:u,route:m}}catch(i){console.error("[smbls/router] failed to render route content",h,i)}}const I=!P&&N;if(e.scrollToTop&&!I&&T?.scrollTo&&T.scrollTo({...e.scrollToOptions||{},top:0,left:0}),e.scrollToNode&&u[p]?.node?.scrollTo&&u[p].node.scrollTo({...e.scrollToOptions||{},top:0,left:0}),y){const s=c.getElementById(y.slice(1));if(s&&T?.scrollTo){const f=a.getComputedStyle?a.getComputedStyle(s):null,v=f&&parseFloat(f.scrollMarginTop)||0,i=s.getBoundingClientRect().top+T.scrollTop-v-(e.scrollToOffset||0);T.scrollTo({...e.scrollToOptions||{},top:i,left:0})}}W("routeChanged",t,e)};var Z=F;export{Z as default,H as getActiveRoute,A as lastLevel,M as lastPathname,q as matchRoute,Q as parseQuery,$ as parseRoutePattern,F as router,D as runGuards};
package/index.js CHANGED
@@ -159,6 +159,11 @@ const normalizePath = (p) => (!p || p === 'srcdoc' || p === 'about:srcdoc') ? '/
159
159
  const defaultOptions = {
160
160
  level: lastLevel,
161
161
  pushState: true,
162
+ // `replace: true` REWRITES the current history entry (replaceState)
163
+ // instead of adding one — a redirect, a canonical-URL rewrite, or a
164
+ // filter/query change that must not stack Back-button entries. It only
165
+ // chooses how the entry is written: `pushState: false` still writes none.
166
+ replace: false,
162
167
  initialRender: false,
163
168
  scrollToTop: true,
164
169
  scrollToNode: false,
@@ -282,18 +287,25 @@ export const router = async (path, el, state = {}, options = {}) => {
282
287
  }
283
288
  }
284
289
 
285
- // pushState always fires before render so the URL bar stays in sync even
286
- // if the content-render step below no-ops due to an extends/ref mismatch.
290
+ // The history write always fires before render so the URL bar stays in
291
+ // sync even if the content-render step below no-ops due to an extends/ref
292
+ // mismatch. `replace` rewrites the current entry instead of pushing one.
287
293
  if (opts.pushState) {
294
+ const url = pathname + (search || '') + (hash || '')
288
295
  try {
289
- win.history.pushState(state, null, pathname + (search || '') + (hash || ''))
296
+ if (opts.replace) win.history.replaceState(state, null, url)
297
+ else win.history.pushState(state, null, url)
290
298
  } catch (e) {} // expected in sandboxed iframes (e.g. about:srcdoc) where pushState is restricted
291
299
  }
292
300
 
293
301
  if (pathChanged || !hashChanged) {
294
- const stateUpdate = { route, routePath, hash, debugging: false }
295
- if (Object.keys(params).length) stateUpdate.params = params
296
- if (Object.keys(query).length) stateUpdate.query = query
302
+ // ROUTER-STALE-PARAMS-1: always write params and query, empty or not.
303
+ // Guarding them on `Object.keys(x).length` left the PREVIOUS route's
304
+ // values in state whenever the new route had none — e.g. /cocktails/:id
305
+ // → an explicit /bars/<id> key kept params.id = the cocktail, so a page
306
+ // reading params resolved the wrong item (404); /cart?order=… → /cart
307
+ // kept query.order. Nested state updates replace, so `{}` clears them.
308
+ const stateUpdate = { route, routePath, hash, params, query, debugging: false }
297
309
 
298
310
  if (opts.updateState) {
299
311
  element.state.update(
@@ -381,7 +393,13 @@ export const router = async (path, el, state = {}, options = {}) => {
381
393
  }
382
394
  }
383
395
 
384
- if (opts.scrollToTop && scrollNode?.scrollTo) {
396
+ // FW-ROUTER-HASH-PUSH-SCROLLS-TO-TOP-1: a push that only changes the hash
397
+ // (pathname unchanged) must land on the hash target, not the top — the
398
+ // unconditional scrollToTop below used to cancel a project's own
399
+ // scroll-into-view on every hash-only push.
400
+ const hashOnlyNav = !pathChanged && hashChanged
401
+
402
+ if (opts.scrollToTop && !hashOnlyNav && scrollNode?.scrollTo) {
385
403
  scrollNode.scrollTo({
386
404
  ...(opts.scrollToOptions || {}),
387
405
  top: 0,
@@ -397,11 +415,16 @@ export const router = async (path, el, state = {}, options = {}) => {
397
415
  }
398
416
 
399
417
  if (hash) {
400
- const activeNode = doc.getElementById(hash)
418
+ // `hash` comes from `URL.hash` and always includes the leading `#`;
419
+ // `getElementById` never does — this lookup used to always miss.
420
+ const activeNode = doc.getElementById(hash.slice(1))
401
421
  if (activeNode && scrollNode?.scrollTo) {
422
+ const style = win.getComputedStyle ? win.getComputedStyle(activeNode) : null
423
+ const scrollMarginTop = (style && parseFloat(style.scrollMarginTop)) || 0
402
424
  const top =
403
425
  activeNode.getBoundingClientRect().top +
404
- rootNode.scrollTop -
426
+ scrollNode.scrollTop -
427
+ scrollMarginTop -
405
428
  (opts.scrollToOffset || 0)
406
429
  scrollNode.scrollTo({
407
430
  ...(opts.scrollToOptions || {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbo.ls/router",
3
- "version": "3.14.600",
3
+ "version": "3.14.602",
4
4
  "license": "CC-BY-NC-4.0",
5
5
  "type": "module",
6
6
  "module": "./dist/esm/index.js",
@@ -38,6 +38,6 @@
38
38
  "browser": "./index.js",
39
39
  "sideEffects": false,
40
40
  "publishConfig": {
41
- "access": "restricted"
41
+ "access": "public"
42
42
  }
43
43
  }