@symbo.ls/router 3.14.601 → 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 +5 -1
- package/dist/cjs/index.js +1 -1
- package/dist/esm/index.js +1 -1
- package/index.js +32 -9
- package/package.json +1 -1
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` |
|
|
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
|
|
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
|
|
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
|
-
//
|
|
286
|
-
// if the content-render step below no-ops due to an extends/ref
|
|
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.
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
+
scrollNode.scrollTop -
|
|
427
|
+
scrollMarginTop -
|
|
405
428
|
(opts.scrollToOffset || 0)
|
|
406
429
|
scrollNode.scrollTo({
|
|
407
430
|
...(opts.scrollToOptions || {}),
|