what-router 0.12.2 → 0.12.4
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 +106 -15
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,l)=>`*:${l}`).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 a=(r.path.match(/:/g)||[]).length+(r.path.includes("*")?100:0),i=(o.path.match(/:/g)||[]).length+(o.path.includes("*")?100:0);return a-i});for(let r of t){let{regex:o,paramNames:a,catchAll:i}=H(r.path),l=e.match(o);if(l){let p={},u=!1;if(a.forEach((s,d)=>{let m=K(l[d+1],s===i);if(m===null){u=!0;return}p[s]=m}),u)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,a]=r.split("=");if(!o)continue;let i=decodeURIComponent(o),l=a?decodeURIComponent(a):"";Object.prototype.hasOwnProperty.call(n,i)?Array.isArray(n[i])?n[i].push(l):n[i]=[n[i],l]:n[i]=l}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()}},T=[],q=[];function D(e,n){return e.push(n),()=>{let t=e.indexOf(n);t!==-1&&e.splice(t,1)}}function le(e){return D(T,e)}function fe(e){return D(q,e)}async function v(e,n={}){let{replace:t=!1,state:r=null,transition:o=!0,_fromPopstate:a=!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 l=h();if(T.length){let u=!1;try{for(let s of T.slice())if(await s(e,l)===!1){u=!0;break}}catch(s){throw y.set(!1),x.set(null),s}if(u){y.set(!1),x.set(null),a&&typeof history<"u"&&history.pushState(null,"",l);return}}B.set(null);let p=()=>{a||(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 u=document.startViewTransition(p);u.ready&&typeof u.ready.catch=="function"&&u.ready.catch(()=>{}),await u.finished}catch{}else p();if(q.length)for(let u of q.slice())u(e,l)}var V=Symbol.for("what.router.redirect"),Y=Symbol.for("what.navigation.signal");function de(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 a of r){o+="/"+a;let i=n.find(l=>l.layout&&l.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 a=h(),i=a.split("?")[0].split("#")[0],l=a.split("?")[1]?.split("#")[0]||"",p=y(),u=_(i,e);if(u){M(()=>{G.set(u.params),F.set(A(l))});let{route:s,params:d}=u,m=A(l);if(s.middleware&&s.middleware.length>0)for(let N of s.middleware){let P=N({path:i,params:d,query:m,route:s});if(P===!1)return n?c(n,{}):c("div",{class:"what-403"},c("h1",null,"403"),c("p",null,"Access denied"));if(typeof P=="string")return L(P)}let f,E=p?ee(x(),e,s):null;E?f=c(E,{}):s.loading&&p?f=c(s.loading,{}):f=c(s.component,{params:d,query:m,route:s}),s.error&&(f=c(X,{fallback:s.error},f));let j=J(s,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 a=null;try{return r()}catch(i){if(!i||!i[V])throw i;a=i}return L(a.to,a.options)};return t?c(t,{},o):o}function ne({href:e,class:n,className:t,children:r,replace:o,prefetch:a=!0,activeClass:i="active",exactActiveClass:l="exact-active",transition:p=!0,...u}){let s=C(e)?e:"about:blank";!C(e)&&typeof console<"u"&&console.warn("[what-router] Link blocked unsafe href:",e);let d=s.split("?")[0].split("#")[0];return c("a",{href:s,class:()=>{let f=g.path,E=d==="/"?f==="/":f===d||f.startsWith(d+"/");return[n||t,E&&i,f===d&&l].filter(Boolean).join(" ")||void 0},onclick:f=>{f.ctrlKey||f.metaKey||f.shiftKey||f.altKey||f.button!==0||(f.preventDefault(),v(s,{replace:o,transition:p}))},onmouseenter:a?()=>U(s):void 0,...u},...Array.isArray(r)?r:[r])}function pe(e){return ne(e)}function he(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 me(e,n,t={}){let{layout:r,loading:o,error:a}=t;return n.map(i=>({...i,path:re(e,i.path),layout:i.layout||r,loading:i.loading||o,error:i.error||a}))}function ge(e,n,t={}){let{layout:r,middleware:o}=t;return n.map(a=>({...a,_group:e,layout:a.layout||r,middleware:[...a.middleware||[],...o||[]]}))}function ye({to:e}){return v(e,{replace:!0}),null}function we(e,n){return t=>function(o){let a=e(o);return a instanceof Promise?c("div",{class:"what-guard-loading"},"Loading..."):a?c(t,o):typeof n=="string"?(v(n,{replace:!0}),null):c(n,o)}}function xe(e,n={}){let{fallback:t="/login",loading:r=null}=n;return o=>function(i){let l=R("pending"),p=R(null),u=!1;return $(()=>(u=!1,Promise.resolve(e(i)).then(s=>{u||(p.set(s),l.set(s?"allowed":"denied"))}).catch(()=>{u||l.set("denied")}),()=>{u=!0})),()=>{let s=l();return s==="pending"?r?c(r,{}):null:s==="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 Re(){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 ve(e){return{style:{viewTransitionName:e}}}function be(e){typeof document>"u"||(document.documentElement.dataset.transition=e)}function Ee(){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 Se(){return g.params}function _e(){return g.query}function Ae(){return v}function Ce(e){U(e)}function ke({children:e}){return e||null}function Ne({routes:e,layout:n,fallback:t,error:r}){let o=e.map(a=>({path:a.path,component:a.component,layout:a.layout||void 0,loading:a.loading||void 0,error:a.error||r||void 0,_mode:a.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{Ne as FileRouter,ne as Link,pe as NavLink,ke as Outlet,ye as Redirect,te as Router,fe as afterNavigate,xe as asyncGuard,le as beforeNavigate,O as compilePath,he as defineRoutes,Re as enableScrollRestoration,we as guard,C as isSafeUrl,_ as matchRoute,v as navigate,me as nestedRoutes,A as parseQuery,U as prefetch,Ce as prefetchRoute,de as redirect,g as route,ge as routeGroup,be as setViewTransition,Ae as useNavigate,Se as useParams,Ee as useRoute,_e as useSearch,ve as viewTransitionName};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-router",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
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.12.
|
|
38
|
+
"what-core": "^0.12.4"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
package/src/index.js
CHANGED
|
@@ -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
|
}
|
|
@@ -184,7 +186,16 @@ export async function navigate(to, opts = {}) {
|
|
|
184
186
|
// Use View Transitions API if available and enabled
|
|
185
187
|
if (transition && typeof document !== 'undefined' && document.startViewTransition) {
|
|
186
188
|
try {
|
|
187
|
-
|
|
189
|
+
const viewTransition = document.startViewTransition(doNavigation);
|
|
190
|
+
// `.ready` rejects when the transition is skipped, which two navigations
|
|
191
|
+
// in quick succession do routinely. Awaiting only `.finished` left that
|
|
192
|
+
// rejection unhandled, so it surfaced as an uncaught page error
|
|
193
|
+
// ("Transition was skipped") and landed in whatever error reporter the
|
|
194
|
+
// app had installed, for a navigation that actually succeeded.
|
|
195
|
+
if (viewTransition.ready && typeof viewTransition.ready.catch === 'function') {
|
|
196
|
+
viewTransition.ready.catch(() => {});
|
|
197
|
+
}
|
|
198
|
+
await viewTransition.finished;
|
|
188
199
|
} catch (e) {
|
|
189
200
|
// Transition failed, navigation still happened
|
|
190
201
|
}
|
|
@@ -328,6 +339,23 @@ function loopScreen(message) {
|
|
|
328
339
|
// instead would never catch a cycle between two route components, because each
|
|
329
340
|
// hop matches successfully before its component throws the next redirect.
|
|
330
341
|
function handleRedirect(target, options) {
|
|
342
|
+
// A navigation to this exact target is already in flight, so this is a
|
|
343
|
+
// re-match of the same hop, not a second one.
|
|
344
|
+
//
|
|
345
|
+
// It happens on every re-match that occurs before the URL commits. With the
|
|
346
|
+
// View Transitions API, navigate() defers the `_url` write into the
|
|
347
|
+
// transition callback while `_isNavigating` flips immediately, and
|
|
348
|
+
// renderMatch reads `_isNavigating`, so the match re-runs against the
|
|
349
|
+
// still-old URL, the same middleware returns the same target, and the hop was
|
|
350
|
+
// counted twice. One guarded deep link produced 25 false "Redirect cycle
|
|
351
|
+
// detected" errors and a flash of the redirect-loop screen, and the detector
|
|
352
|
+
// then cleared `_isNavigating` / `_pendingUrl` while the real navigation was
|
|
353
|
+
// still in flight, corrupting router state mid-navigation.
|
|
354
|
+
//
|
|
355
|
+
// Chromium-only, because without View Transitions doNavigation() runs
|
|
356
|
+
// synchronously and the URL and the flag change together.
|
|
357
|
+
if (_pendingUrl.peek() === target) return null;
|
|
358
|
+
|
|
331
359
|
_redirectHistory.push(target);
|
|
332
360
|
|
|
333
361
|
if (_redirectHistory.length > MAX_REDIRECTS) {
|
|
@@ -690,34 +718,97 @@ export function prefetch(href) {
|
|
|
690
718
|
|
|
691
719
|
// --- Scroll Restoration ---
|
|
692
720
|
|
|
721
|
+
// Saved viewport offsets. ONE key (the full URL: path + search + hash) and ONE
|
|
722
|
+
// shape ({ x, y }) shared by every writer and by the reader below.
|
|
723
|
+
//
|
|
724
|
+
// They used to disagree on both. navigate() and the popstate handler wrote
|
|
725
|
+
// { x, y } keyed by the full URL, while enableScrollRestoration()'s own
|
|
726
|
+
// beforeunload handler wrote a bare NUMBER keyed by location.pathname, and the
|
|
727
|
+
// restore effect looked the entry up by route.path (the pattern-stripped path,
|
|
728
|
+
// never the query or the hash) and handed it straight to
|
|
729
|
+
// `window.scrollTo(0, savedPosition)`. A hit on a { x, y } record therefore
|
|
730
|
+
// produced `scrollTo(0, NaN)`, which browsers normalize to 0 and which jumps
|
|
731
|
+
// the page to the TOP; a URL carrying a query string or a hash missed the
|
|
732
|
+
// lookup entirely and got the same jump. Restoration could not work at all.
|
|
733
|
+
// If you add a writer, key it by the full URL and store { x, y }.
|
|
693
734
|
const scrollPositions = new Map();
|
|
694
735
|
|
|
695
736
|
export function enableScrollRestoration() {
|
|
696
737
|
if (typeof window === 'undefined') return;
|
|
697
738
|
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
//
|
|
739
|
+
// There is deliberately no beforeunload writer. scrollPositions is an
|
|
740
|
+
// in-memory Map that dies with the document, so anything written while the
|
|
741
|
+
// page is unloading can never be read back. The only case where the write
|
|
742
|
+
// survived is the one where the document does NOT go away (the user cancels
|
|
743
|
+
// the "leave site?" prompt), and there it poisoned the map with an entry of
|
|
744
|
+
// the wrong shape under the wrong key. Restoring scroll across a RELOAD is
|
|
745
|
+
// the browser's job (history.scrollRestoration), and the first-run guard
|
|
746
|
+
// below is what keeps this effect out of its way.
|
|
747
|
+
|
|
748
|
+
// The effect's first run is the URL the document loaded with, not a
|
|
749
|
+
// navigation this router performed: the browser has already positioned the
|
|
750
|
+
// document (top for a fresh load, the previous offset for a reload or a
|
|
751
|
+
// document-recreating back). Scrolling to the top there would undo exactly
|
|
752
|
+
// the restoration the comment above defers to. Nothing else is affected,
|
|
753
|
+
// because scrollPositions is necessarily empty on the first run, so the
|
|
754
|
+
// guard can only ever suppress the scroll-to-top branch.
|
|
755
|
+
let firstRun = true;
|
|
756
|
+
|
|
757
|
+
// Restore the saved position after each committed navigation. Reading
|
|
758
|
+
// route.url subscribes to the URL signal, so this re-runs for query-only and
|
|
759
|
+
// hash-only changes too, which is the granularity the map is keyed at.
|
|
704
760
|
effect(() => {
|
|
705
|
-
const
|
|
706
|
-
const
|
|
761
|
+
const url = route.url;
|
|
762
|
+
const saved = scrollPositions.get(url);
|
|
763
|
+
// Read inside the effect, not inside the frame callback: by the time the
|
|
764
|
+
// frame runs another navigation may already have moved the URL on. Same
|
|
765
|
+
// reason the first-run latch is consumed here rather than in the frame.
|
|
766
|
+
const hash = route.hash;
|
|
767
|
+
const isFirstRun = firstRun;
|
|
768
|
+
firstRun = false;
|
|
707
769
|
|
|
708
770
|
requestAnimationFrame(() => {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
771
|
+
// Hash BEFORE saved, and the order is load-bearing. A URL carrying a
|
|
772
|
+
// hash is a request for a specific ELEMENT, and it outranks any offset
|
|
773
|
+
// saved under that same URL: clicking a "/guide#api" link a second time
|
|
774
|
+
// has to land on #api, not on wherever the user happened to be standing
|
|
775
|
+
// the last time they left /guide#api. Because navigate() keys the map by
|
|
776
|
+
// the full URL (hash included), checking `saved` first made this branch
|
|
777
|
+
// unreachable for every hash URL the user had already visited, and it
|
|
778
|
+
// also fought navigate()'s own same-page hash handling: that path
|
|
779
|
+
// scrollIntoViews the anchor synchronously, and a `saved` hit here
|
|
780
|
+
// clobbered it one frame later.
|
|
781
|
+
if (hash) {
|
|
782
|
+
// Deferred to the frame because an SSR-less deep link renders the
|
|
783
|
+
// target element after the URL commits, so it does not exist yet here.
|
|
784
|
+
const el = queryFragment(hash);
|
|
785
|
+
// No fallback when the anchor is missing: it may simply not have
|
|
786
|
+
// rendered yet, and yanking the page to the top (or to some unrelated
|
|
787
|
+
// saved offset) is the worse answer to "I asked for #api".
|
|
713
788
|
el?.scrollIntoView();
|
|
714
|
-
} else {
|
|
789
|
+
} else if (saved) {
|
|
790
|
+
window.scrollTo(saved.x, saved.y);
|
|
791
|
+
} else if (!isFirstRun) {
|
|
715
792
|
window.scrollTo(0, 0);
|
|
716
793
|
}
|
|
717
794
|
});
|
|
718
795
|
});
|
|
719
796
|
}
|
|
720
797
|
|
|
798
|
+
// Resolve a URL fragment to its element. A fragment is an id, and an id is not
|
|
799
|
+
// necessarily a valid CSS selector: "#1" is legal HTML that browsers navigate
|
|
800
|
+
// to happily, while querySelector('#1') THROWS "Invalid selector". The throw
|
|
801
|
+
// happened inside a requestAnimationFrame callback, where nothing could catch
|
|
802
|
+
// it, so a single numeric anchor took out the frame as an uncaught error.
|
|
803
|
+
function queryFragment(hash) {
|
|
804
|
+
try {
|
|
805
|
+
return document.querySelector(hash);
|
|
806
|
+
} catch {
|
|
807
|
+
// Not selector-parseable, so ask the way the browser itself does.
|
|
808
|
+
return document.getElementById(hash.slice(1));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
721
812
|
// --- View Transition Helpers ---
|
|
722
813
|
|
|
723
814
|
export function viewTransitionName(name) {
|