what-router 0.12.3 → 0.13.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/dist/index.min.js +3 -3
- package/package.json +2 -2
- package/src/index.js +83 -16
package/dist/index.min.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{signal as
|
|
1
|
+
import{signal as R,effect as $,computed as b,batch as M,h as c,ErrorBoundary as X}from"what-core";function O(e){let n=e.replace(/\([\w-]+\)\//g,"").replace(/\[\.\.\.(\w+)\]/g,(i,u)=>`*:${u}`).replace(/\[(\w+)\]/g,":$1"),t=[],r=null,o=n.split("/").map(i=>i.startsWith("*:")?(r=i.slice(2),t.push(r),"(.+)"):i==="*"?(r="rest",t.push("rest"),"(.+)"):i.startsWith(":")?(t.push(i.slice(1)),"([^/]+)"):i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("/");return{regex:new RegExp(`^${o}$`),paramNames:t,catchAll:r}}var S=new Map,z=1e3;function H(e){let n=S.get(e);return n||(n=O(e),S.size>=z&&S.clear(),S.set(e,n)),n}function K(e,n){let t;try{t=decodeURIComponent(e)}catch{return null}let r=t.split(/[/\\]/);return!n&&r.length>1||r.includes("..")?null:t}function _(e,n){let t=n.filter(r=>r.path);t.sort((r,o)=>{let s=(r.path.match(/:/g)||[]).length+(r.path.includes("*")?100:0),i=(o.path.match(/:/g)||[]).length+(o.path.includes("*")?100:0);return s-i});for(let r of t){let{regex:o,paramNames:s,catchAll:i}=H(r.path),u=e.match(o);if(u){let p={},l=!1;if(s.forEach((a,d)=>{let m=K(u[d+1],a===i);if(m===null){l=!0;return}p[a]=m}),l)continue;return{route:r,params:p}}}return null}function A(e){let n=Object.create(null);if(!e)return n;let t=e.startsWith("?")?e.slice(1):e;for(let r of t.split("&")){let[o,s]=r.split("=");if(!o)continue;let i=decodeURIComponent(o),u=s?decodeURIComponent(s):"";Object.prototype.hasOwnProperty.call(n,i)?Array.isArray(n[i])?n[i].push(u):n[i]=[n[i],u]:n[i]=u}return n}var Q=new Set(["http:","https:","mailto:","tel:"]);function C(e){if(typeof e!="string")return!1;let t=e.trim().replace(/[\s\x00-\x1f]/g,"").toLowerCase();if(t.startsWith("javascript:")||t.startsWith("data:")||t.startsWith("vbscript:")||/^[/\\]{2}/.test(t))return!1;let r=t.match(/^([a-z][a-z0-9+.-]*:)/);return r?Q.has(r[1]):!t.includes("\\")}var h=R(typeof location<"u"?location.pathname+location.search+location.hash:"/"),G=R({}),F=R({}),y=R(!1),x=R(null),B=R(null),g={get url(){return h()},get path(){return h().split("?")[0].split("#")[0]},get params(){return G()},get query(){return F()},get hash(){let e=h().split("#")[1];return e?"#"+e:""},get isNavigating(){return y()},get error(){return B()}},q=[],P=[];function D(e,n){return e.push(n),()=>{let t=e.indexOf(n);t!==-1&&e.splice(t,1)}}function ue(e){return D(q,e)}function le(e){return D(P,e)}async function v(e,n={}){let{replace:t=!1,state:r=null,transition:o=!0,_fromPopstate:s=!1,_redirectChain:i=!1}=n;if(i||(w.length=0),!C(e)){typeof console<"u"&&console.warn("[what-router] Blocked navigation to unsafe URL:",e);return}if(typeof window<"u"&&e.startsWith("#")){let d=h().split("#")[0]+e;history.replaceState(r,"",d),h.set(d);let m=document.querySelector(e);m&&m.scrollIntoView({behavior:"smooth"});return}if(e===h()||y.peek())return;y.set(!0),x.set(e);let u=h();if(q.length){let l=!1;try{for(let a of q.slice())if(await a(e,u)===!1){l=!0;break}}catch(a){throw y.set(!1),x.set(null),a}if(l){y.set(!1),x.set(null),s&&typeof history<"u"&&history.pushState(null,"",u);return}}B.set(null);let p=()=>{s||(typeof window<"u"&&k.set(h(),{x:window.scrollX,y:window.scrollY}),t?history.replaceState(r,"",e):history.pushState(r,"",e)),h.set(e),y.set(!1),x.set(null)};if(o&&typeof document<"u"&&document.startViewTransition)try{let l=document.startViewTransition(p);l.ready&&typeof l.ready.catch=="function"&&l.ready.catch(()=>{}),await l.finished}catch{}else p();if(P.length)for(let l of P.slice())l(e,u)}var V=Symbol.for("what.router.redirect"),Y=Symbol.for("what.navigation.signal");function fe(e,n={}){if(!C(e)){let r=typeof e=="string"?e:Object.prototype.toString.call(e),o=new Error(`[what-router] redirect() refused an unsafe target: ${r}`);throw o.code="ERR_UNSAFE_REDIRECT",o.suggestion='redirect() accepts same-origin paths and http:, https:, mailto: or tel: URLs only. Protocol-relative ("//host"), backslash-smuggled and javascript:/data: targets are open-redirect vectors. Check a user-supplied target against an allowlist first.',o.codeExample=`// Bad - a user-controlled target can leave your origin:
|
|
2
2
|
redirect(query.next);
|
|
3
3
|
|
|
4
4
|
// Good - allowlist the target first:
|
|
5
|
-
redirect(ALLOWED.has(query.next) ? query.next : '/');`,o}let
|
|
5
|
+
redirect(ALLOWED.has(query.next) ? query.next : '/');`,o}let t=new Error(`[what-router] redirect to ${e}`);throw t.name="RouterRedirect",t.code="ERR_REDIRECT_NOT_CAUGHT",t.suggestion="Seeing this signal in your console means nothing caught it. redirect() works from route middleware and from a component body, where the Router catches it. From an event handler, a promise callback or a timer, call navigate(to) instead. A try/catch around the redirect() call also swallows it.",t.codeExample=`// Bad - an event handler runs long after the render the Router caught:
|
|
6
6
|
<button onclick={() => redirect('/login')}>Sign in</button>
|
|
7
7
|
|
|
8
8
|
// Good - navigate() from a handler:
|
|
@@ -12,4 +12,4 @@ redirect(ALLOWED.has(query.next) ? query.next : '/');`,o}let n=new Error(`[what-
|
|
|
12
12
|
function Private() {
|
|
13
13
|
if (!user()) redirect('/login');
|
|
14
14
|
return <Secret />;
|
|
15
|
-
}`,
|
|
15
|
+
}`,t[V]=!0,t[Y]=()=>{L(e,n)},t.to=e,t.options=n,t}typeof window<"u"&&window.addEventListener("popstate",()=>{k.set(h(),{x:window.scrollX,y:window.scrollY});let e=location.pathname+location.search+location.hash;v(e,{replace:!0,_fromPopstate:!0,transition:!1}).then(()=>{let n=k.get(e);n&&requestAnimationFrame(()=>window.scrollTo(n.x,n.y))})});function J(e,n){let t=[];if(!e.path)return t;let r=e.path.split("/").filter(Boolean),o="";for(let s of r){o+="/"+s;let i=n.find(u=>u.layout&&u.path===o+"/_layout");i&&t.push(i.layout)}return e.layout&&t.push(e.layout),t}var w=[],Z=10;function I(e){return c("div",{class:"what-redirect-loop"},c("h1",null,"Redirect Loop"),c("p",null,e))}function L(e,n){if(x.peek()===e)return null;if(w.push(e),w.length>Z){let o=w.slice(-5).join(" \u2192 ");return w.length=0,console.error(`[what-router] Redirect loop detected: ${o}`),y.set(!1),x.set(null),I("Too many redirects. Check your middleware configuration.")}let t=new Set,r=!1;for(let o of w){if(t.has(o)){r=!0;break}t.add(o)}if(r){let o=w.join(" \u2192 ");return w.length=0,console.error(`[what-router] Redirect cycle detected: ${o}`),y.set(!1),x.set(null),I("Circular redirect detected. Check your middleware configuration.")}return v(e,{replace:!0,...n,_redirectChain:!0}),null}function ee(e,n,t){if(!e)return null;let r=e.split("?")[0].split("#")[0],o=_(r,n);return!o||o.route===t?null:o.route.loading||null}function te({routes:e,fallback:n,globalLayout:t}){let r=()=>{let s=h(),i=s.split("?")[0].split("#")[0],u=s.split("?")[1]?.split("#")[0]||"",p=y(),l=_(i,e);if(l){M(()=>{G.set(l.params),F.set(A(u))});let{route:a,params:d}=l,m=A(u);if(a.middleware&&a.middleware.length>0)for(let N of a.middleware){let T=N({path:i,params:d,query:m,route:a});if(T===!1)return n?c(n,{}):c("div",{class:"what-403"},c("h1",null,"403"),c("p",null,"Access denied"));if(typeof T=="string")return L(T)}let f,E=p?ee(x(),e,a):null;E?f=c(E,{}):a.loading&&p?f=c(a.loading,{}):f=c(a.component,{params:d,query:m,route:a}),a.error&&(f=c(X,{fallback:a.error},f));let j=J(a,e);for(let N of j.reverse())f=c(N,{params:d,query:m},f);return f}return n?c(n,{}):c("div",{class:"what-404"},c("h1",null,"404"),c("p",null,"Page not found"))},o=()=>{let s=null;try{return r()}catch(i){if(!i||!i[V])throw i;s=i}return L(s.to,s.options)};return t?c(t,{},o):o}function ne({href:e,class:n,className:t,children:r,replace:o,prefetch:s=!0,activeClass:i="active",exactActiveClass:u="exact-active",transition:p=!0,...l}){let a=C(e)?e:"about:blank";!C(e)&&typeof console<"u"&&console.warn("[what-router] Link blocked unsafe href:",e);let d=a.split("?")[0].split("#")[0];return c("a",{href:a,class:()=>{let f=g.path,E=d==="/"?f==="/":f===d||f.startsWith(d+"/");return[n||t,E&&i,f===d&&u].filter(Boolean).join(" ")||void 0},onclick:f=>{f.ctrlKey||f.metaKey||f.shiftKey||f.altKey||f.button!==0||(f.preventDefault(),v(a,{replace:o,transition:p}))},onmouseenter:s?()=>U(a):void 0,...l},...Array.isArray(r)?r:[r])}function de(e){return ne(e)}function pe(e){return Object.entries(e).map(([n,t])=>typeof t=="function"?{path:n,component:t}:{path:n,...t})}function re(e,n){let t=e.endsWith("/")?e.slice(0,-1):e,r=!n||n==="/"?"/":n.startsWith("/")?n:`/${n}`;return r==="/"?t||"/":t+r}function he(e,n,t={}){let{layout:r,loading:o,error:s}=t;return n.map(i=>({...i,path:re(e,i.path),layout:i.layout||r,loading:i.loading||o,error:i.error||s}))}function me(e,n,t={}){let{layout:r,middleware:o}=t;return n.map(s=>({...s,_group:e,layout:s.layout||r,middleware:[...s.middleware||[],...o||[]]}))}function ge({to:e}){return v(e,{replace:!0}),null}function ye(e,n){return t=>function(o){let s=e(o);return s instanceof Promise?c("div",{class:"what-guard-loading"},"Loading..."):s?c(t,o):typeof n=="string"?(v(n,{replace:!0}),null):c(n,o)}}function we(e,n={}){let{fallback:t="/login",loading:r=null}=n;return o=>function(i){let u=R("pending"),p=R(null),l=!1;return $(()=>(l=!1,Promise.resolve(e(i)).then(a=>{l||(p.set(a),u.set(a?"allowed":"denied"))}).catch(()=>{l||u.set("denied")}),()=>{l=!0})),()=>{let a=u();return a==="pending"?r?c(r,{}):null:a==="allowed"?c(o,i):typeof t=="string"?(v(t,{replace:!0}),null):c(t,i)}}}var W=new Set;function U(e){if(typeof document>"u"||W.has(e))return;W.add(e);let n=document.createElement("link");n.rel="prefetch",n.href=e,document.head.appendChild(n)}var k=new Map;function xe(){if(typeof window>"u")return;let e=!0;$(()=>{let n=g.url,t=k.get(n),r=g.hash,o=e;e=!1,requestAnimationFrame(()=>{r?oe(r)?.scrollIntoView():t?window.scrollTo(t.x,t.y):o||window.scrollTo(0,0)})})}function oe(e){try{return document.querySelector(e)}catch{return document.getElementById(e.slice(1))}}function Re(e){return{style:{viewTransitionName:e}}}function ve(e){typeof document>"u"||(document.documentElement.dataset.transition=e)}function be(){return{path:b(()=>g.path),params:b(()=>g.params),query:b(()=>g.query),hash:b(()=>g.hash),isNavigating:b(()=>g.isNavigating),navigate:v,prefetch:U}}function Ee(){return g.params}function Se(){return g.query}function _e(){return v}function Ae(e){U(e)}function Ce({children:e}){return e||null}function ke({routes:e,layout:n,fallback:t,error:r}){let o=e.map(s=>({path:s.path,component:s.component,layout:s.layout||void 0,loading:s.loading||void 0,error:s.error||r||void 0,_mode:s.mode||"client"}));return te({routes:o,globalLayout:n,fallback:t||ie})}function ie(){return c("div",{style:"text-align:center;padding:60px 20px"},c("h1",{style:"font-size:48px;margin-bottom:8px"},"404"),c("p",{style:"color:#64748b"},"Page not found"))}export{ke as FileRouter,ne as Link,de as NavLink,Ce as Outlet,ge as Redirect,te as Router,le as afterNavigate,we as asyncGuard,ue as beforeNavigate,O as compilePath,pe as defineRoutes,xe as enableScrollRestoration,ye as guard,C as isSafeUrl,_ as matchRoute,v as navigate,he as nestedRoutes,A as parseQuery,U as prefetch,Ae as prefetchRoute,fe as redirect,g as route,me as routeGroup,ve as setViewTransition,_e as useNavigate,Ee as useParams,be as useRoute,Se as useSearch,Re as viewTransitionName};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "What Framework - File-based & programmatic router with View Transitions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"dependencies": {},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"what-core": "^0.
|
|
38
|
+
"what-core": "^0.13.0"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// route groups, view transitions, and middleware.
|
|
4
4
|
|
|
5
5
|
import { signal, effect, computed, batch, h, ErrorBoundary } from 'what-core';
|
|
6
|
-
import {
|
|
6
|
+
import { matchRoute, parseQuery } from './match.js';
|
|
7
7
|
|
|
8
8
|
// --- URL Sanitization ---
|
|
9
9
|
// Rejects javascript:, data:, vbscript: protocols (case-insensitive, trimmed),
|
|
@@ -166,7 +166,9 @@ export async function navigate(to, opts = {}) {
|
|
|
166
166
|
const doNavigation = () => {
|
|
167
167
|
// Skip history manipulation on popstate (browser already updated the URL)
|
|
168
168
|
if (!_fromPopstate) {
|
|
169
|
-
// Save scroll position for current URL before navigating away
|
|
169
|
+
// Save scroll position for current URL before navigating away.
|
|
170
|
+
// Full URL as the key, { x, y } as the value: see scrollPositions below,
|
|
171
|
+
// every writer and the reader must agree on both.
|
|
170
172
|
if (typeof window !== 'undefined') {
|
|
171
173
|
scrollPositions.set(_url(), { x: window.scrollX, y: window.scrollY });
|
|
172
174
|
}
|
|
@@ -194,7 +196,7 @@ export async function navigate(to, opts = {}) {
|
|
|
194
196
|
viewTransition.ready.catch(() => {});
|
|
195
197
|
}
|
|
196
198
|
await viewTransition.finished;
|
|
197
|
-
} catch
|
|
199
|
+
} catch {
|
|
198
200
|
// Transition failed, navigation still happened
|
|
199
201
|
}
|
|
200
202
|
} else {
|
|
@@ -227,6 +229,7 @@ const NAV_SIGNAL = Symbol.for('what.navigation.signal');
|
|
|
227
229
|
export function redirect(to, options = {}) {
|
|
228
230
|
if (!isSafeUrl(to)) {
|
|
229
231
|
const target = typeof to === 'string' ? to : Object.prototype.toString.call(to);
|
|
232
|
+
/** @type {Error & { code?: string, suggestion?: string, codeExample?: string }} */
|
|
230
233
|
const err = new Error(`[what-router] redirect() refused an unsafe target: ${target}`);
|
|
231
234
|
err.code = 'ERR_UNSAFE_REDIRECT';
|
|
232
235
|
err.suggestion = 'redirect() accepts same-origin paths and http:, https:, mailto: or tel: URLs only. Protocol-relative ("//host"), backslash-smuggled and javascript:/data: targets are open-redirect vectors. Check a user-supplied target against an allowlist first.';
|
|
@@ -238,6 +241,7 @@ redirect(ALLOWED.has(query.next) ? query.next : '/');`;
|
|
|
238
241
|
throw err;
|
|
239
242
|
}
|
|
240
243
|
|
|
244
|
+
/** @type {Error & { code?: string, suggestion?: string, codeExample?: string, to?: any, options?: any }} */
|
|
241
245
|
const sig = new Error(`[what-router] redirect to ${to}`);
|
|
242
246
|
sig.name = 'RouterRedirect';
|
|
243
247
|
sig.code = 'ERR_REDIRECT_NOT_CAUGHT';
|
|
@@ -716,34 +720,97 @@ export function prefetch(href) {
|
|
|
716
720
|
|
|
717
721
|
// --- Scroll Restoration ---
|
|
718
722
|
|
|
723
|
+
// Saved viewport offsets. ONE key (the full URL: path + search + hash) and ONE
|
|
724
|
+
// shape ({ x, y }) shared by every writer and by the reader below.
|
|
725
|
+
//
|
|
726
|
+
// They used to disagree on both. navigate() and the popstate handler wrote
|
|
727
|
+
// { x, y } keyed by the full URL, while enableScrollRestoration()'s own
|
|
728
|
+
// beforeunload handler wrote a bare NUMBER keyed by location.pathname, and the
|
|
729
|
+
// restore effect looked the entry up by route.path (the pattern-stripped path,
|
|
730
|
+
// never the query or the hash) and handed it straight to
|
|
731
|
+
// `window.scrollTo(0, savedPosition)`. A hit on a { x, y } record therefore
|
|
732
|
+
// produced `scrollTo(0, NaN)`, which browsers normalize to 0 and which jumps
|
|
733
|
+
// the page to the TOP; a URL carrying a query string or a hash missed the
|
|
734
|
+
// lookup entirely and got the same jump. Restoration could not work at all.
|
|
735
|
+
// If you add a writer, key it by the full URL and store { x, y }.
|
|
719
736
|
const scrollPositions = new Map();
|
|
720
737
|
|
|
721
738
|
export function enableScrollRestoration() {
|
|
722
739
|
if (typeof window === 'undefined') return;
|
|
723
740
|
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
//
|
|
741
|
+
// There is deliberately no beforeunload writer. scrollPositions is an
|
|
742
|
+
// in-memory Map that dies with the document, so anything written while the
|
|
743
|
+
// page is unloading can never be read back. The only case where the write
|
|
744
|
+
// survived is the one where the document does NOT go away (the user cancels
|
|
745
|
+
// the "leave site?" prompt), and there it poisoned the map with an entry of
|
|
746
|
+
// the wrong shape under the wrong key. Restoring scroll across a RELOAD is
|
|
747
|
+
// the browser's job (history.scrollRestoration), and the first-run guard
|
|
748
|
+
// below is what keeps this effect out of its way.
|
|
749
|
+
|
|
750
|
+
// The effect's first run is the URL the document loaded with, not a
|
|
751
|
+
// navigation this router performed: the browser has already positioned the
|
|
752
|
+
// document (top for a fresh load, the previous offset for a reload or a
|
|
753
|
+
// document-recreating back). Scrolling to the top there would undo exactly
|
|
754
|
+
// the restoration the comment above defers to. Nothing else is affected,
|
|
755
|
+
// because scrollPositions is necessarily empty on the first run, so the
|
|
756
|
+
// guard can only ever suppress the scroll-to-top branch.
|
|
757
|
+
let firstRun = true;
|
|
758
|
+
|
|
759
|
+
// Restore the saved position after each committed navigation. Reading
|
|
760
|
+
// route.url subscribes to the URL signal, so this re-runs for query-only and
|
|
761
|
+
// hash-only changes too, which is the granularity the map is keyed at.
|
|
730
762
|
effect(() => {
|
|
731
|
-
const
|
|
732
|
-
const
|
|
763
|
+
const url = route.url;
|
|
764
|
+
const saved = scrollPositions.get(url);
|
|
765
|
+
// Read inside the effect, not inside the frame callback: by the time the
|
|
766
|
+
// frame runs another navigation may already have moved the URL on. Same
|
|
767
|
+
// reason the first-run latch is consumed here rather than in the frame.
|
|
768
|
+
const hash = route.hash;
|
|
769
|
+
const isFirstRun = firstRun;
|
|
770
|
+
firstRun = false;
|
|
733
771
|
|
|
734
772
|
requestAnimationFrame(() => {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
773
|
+
// Hash BEFORE saved, and the order is load-bearing. A URL carrying a
|
|
774
|
+
// hash is a request for a specific ELEMENT, and it outranks any offset
|
|
775
|
+
// saved under that same URL: clicking a "/guide#api" link a second time
|
|
776
|
+
// has to land on #api, not on wherever the user happened to be standing
|
|
777
|
+
// the last time they left /guide#api. Because navigate() keys the map by
|
|
778
|
+
// the full URL (hash included), checking `saved` first made this branch
|
|
779
|
+
// unreachable for every hash URL the user had already visited, and it
|
|
780
|
+
// also fought navigate()'s own same-page hash handling: that path
|
|
781
|
+
// scrollIntoViews the anchor synchronously, and a `saved` hit here
|
|
782
|
+
// clobbered it one frame later.
|
|
783
|
+
if (hash) {
|
|
784
|
+
// Deferred to the frame because an SSR-less deep link renders the
|
|
785
|
+
// target element after the URL commits, so it does not exist yet here.
|
|
786
|
+
const el = queryFragment(hash);
|
|
787
|
+
// No fallback when the anchor is missing: it may simply not have
|
|
788
|
+
// rendered yet, and yanking the page to the top (or to some unrelated
|
|
789
|
+
// saved offset) is the worse answer to "I asked for #api".
|
|
739
790
|
el?.scrollIntoView();
|
|
740
|
-
} else {
|
|
791
|
+
} else if (saved) {
|
|
792
|
+
window.scrollTo(saved.x, saved.y);
|
|
793
|
+
} else if (!isFirstRun) {
|
|
741
794
|
window.scrollTo(0, 0);
|
|
742
795
|
}
|
|
743
796
|
});
|
|
744
797
|
});
|
|
745
798
|
}
|
|
746
799
|
|
|
800
|
+
// Resolve a URL fragment to its element. A fragment is an id, and an id is not
|
|
801
|
+
// necessarily a valid CSS selector: "#1" is legal HTML that browsers navigate
|
|
802
|
+
// to happily, while querySelector('#1') THROWS "Invalid selector". The throw
|
|
803
|
+
// happened inside a requestAnimationFrame callback, where nothing could catch
|
|
804
|
+
// it, so a single numeric anchor took out the frame as an uncaught error.
|
|
805
|
+
function queryFragment(hash) {
|
|
806
|
+
try {
|
|
807
|
+
return document.querySelector(hash);
|
|
808
|
+
} catch {
|
|
809
|
+
// Not selector-parseable, so ask the way the browser itself does.
|
|
810
|
+
return document.getElementById(hash.slice(1));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
747
814
|
// --- View Transition Helpers ---
|
|
748
815
|
|
|
749
816
|
export function viewTransitionName(name) {
|