stx-router 0.2.111 → 0.2.113
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/client.js +101 -8
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -76,24 +76,65 @@ export function getRouterScript() {
|
|
|
76
76
|
// rather than a Map to keep the diff localized and the runtime
|
|
77
77
|
// hand-minified shape consistent.
|
|
78
78
|
var cacheOrder=[];
|
|
79
|
-
function setCache(key,html,layout,group,title){
|
|
79
|
+
function setCache(key,html,layout,group,title,cattrs){
|
|
80
80
|
if(!(key in cache))cacheOrder.push(key);
|
|
81
81
|
cache[key]=html;
|
|
82
82
|
layoutCache[key]=layout;
|
|
83
83
|
layoutGroupCache[key]=group;
|
|
84
84
|
titleCache[key]=title||'';
|
|
85
|
+
attrsCache[key]=cattrs||'';
|
|
85
86
|
while(cacheOrder.length>o.prefetchCacheMax){
|
|
86
87
|
var oldest=cacheOrder.shift();
|
|
87
88
|
delete cache[oldest];
|
|
88
89
|
delete layoutCache[oldest];
|
|
89
90
|
delete layoutGroupCache[oldest];
|
|
90
91
|
delete titleCache[oldest];
|
|
92
|
+
delete attrsCache[oldest];
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
// Apply a page title captured from the fragment response's X-STX-Title
|
|
94
96
|
// header (URI-encoded) so SPA swaps keep document.title in sync with the
|
|
95
97
|
// page \u2014 full-document swaps set the title from the <title> tag directly.
|
|
96
98
|
function applyTitle(t){if(t){try{document.title=decodeURIComponent(t)}catch(e){document.title=t}}}
|
|
99
|
+
// \u2500\u2500 Container attributes \u2500\u2500
|
|
100
|
+
// A fragment carries only the container's INNER content, so any layout that
|
|
101
|
+
// lives on the destination's <main> itself (e.g. a centered auth page whose
|
|
102
|
+
// <main> carries flex + min-h-[100dvh] + items-center) would be
|
|
103
|
+
// dropped when injected into the persistent container. The server sends the
|
|
104
|
+
// destination container's attributes in X-STX-Container-Attrs (URI-encoded);
|
|
105
|
+
// we apply them here and track what we applied so navigating onward to a page
|
|
106
|
+
// with a bare <main> clears them again. Attributes the router never applied
|
|
107
|
+
// (the app's own id/data-stx-content hooks) are left untouched.
|
|
108
|
+
var pendingContainerAttrs='';
|
|
109
|
+
function parseContainerAttrs(attrStr){
|
|
110
|
+
var map={};
|
|
111
|
+
if(!attrStr)return map;
|
|
112
|
+
var probe=document.createElement('div');
|
|
113
|
+
probe.innerHTML='<i '+attrStr+'></i>';
|
|
114
|
+
var src=probe.firstChild;
|
|
115
|
+
if(src&&src.attributes){
|
|
116
|
+
Array.prototype.forEach.call(src.attributes,function(a){map[a.name]=a.value});
|
|
117
|
+
}
|
|
118
|
+
return map;
|
|
119
|
+
}
|
|
120
|
+
function setContainerAttrs(el,map){
|
|
121
|
+
if(!el)return;
|
|
122
|
+
var prev=(el.getAttribute('data-stx-cattrs')||'').split(',');
|
|
123
|
+
prev.forEach(function(n){if(n&&!(n in map))el.removeAttribute(n)});
|
|
124
|
+
var names=[];
|
|
125
|
+
for(var k in map){
|
|
126
|
+
if(Object.prototype.hasOwnProperty.call(map,k)&&k!=='data-stx-cattrs'){
|
|
127
|
+
el.setAttribute(k,map[k]);names.push(k);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if(names.length)el.setAttribute('data-stx-cattrs',names.join(','));
|
|
131
|
+
else el.removeAttribute('data-stx-cattrs');
|
|
132
|
+
}
|
|
133
|
+
function applyContainerAttrs(el,encoded){
|
|
134
|
+
var attrStr='';
|
|
135
|
+
if(encoded){try{attrStr=decodeURIComponent(encoded)}catch(e){attrStr=encoded}}
|
|
136
|
+
setContainerAttrs(el,parseContainerAttrs(attrStr));
|
|
137
|
+
}
|
|
97
138
|
function touchCache(key){
|
|
98
139
|
var i=cacheOrder.indexOf(key);
|
|
99
140
|
if(i>=0){cacheOrder.splice(i,1);cacheOrder.push(key)}
|
|
@@ -105,6 +146,7 @@ export function getRouterScript() {
|
|
|
105
146
|
delete layoutCache[key];
|
|
106
147
|
delete layoutGroupCache[key];
|
|
107
148
|
delete titleCache[key];
|
|
149
|
+
delete attrsCache[key];
|
|
108
150
|
}
|
|
109
151
|
|
|
110
152
|
// Extract top-level CSS blocks (rules AND @media blocks with nested braces).
|
|
@@ -158,6 +200,7 @@ export function getRouterScript() {
|
|
|
158
200
|
var layoutCache={};
|
|
159
201
|
var layoutGroupCache={};
|
|
160
202
|
var titleCache={};
|
|
203
|
+
var attrsCache={};
|
|
161
204
|
// Track executed script hashes to prevent redeclaration errors on navigation.
|
|
162
205
|
// Layout-level scripts (theme, nav setup) execute on initial page load and
|
|
163
206
|
// should NOT re-execute when navigating to another page with the same layout.
|
|
@@ -172,7 +215,10 @@ export function getRouterScript() {
|
|
|
172
215
|
if(text.trim()&&!s.hasAttribute('src'))executedScriptHashes[hashScript(text)]=1;
|
|
173
216
|
});
|
|
174
217
|
function cacheKey(url){
|
|
175
|
-
|
|
218
|
+
// Base is the CURRENT document, not the origin root: a relative key like
|
|
219
|
+
// '?status=resolved' on /dashboard must cache as /dashboard?status=resolved,
|
|
220
|
+
// not /?status=resolved (#1777).
|
|
221
|
+
var u=new URL(url,location.href);
|
|
176
222
|
return u.pathname+u.search;
|
|
177
223
|
}
|
|
178
224
|
// Keep SPA fetches inside the active locale (/en/... when viewing English).
|
|
@@ -229,7 +275,12 @@ export function getRouterScript() {
|
|
|
229
275
|
if(!force) url=withCurrentLocale(url);
|
|
230
276
|
log('[router] navigate() called:',url,'isNavigating:',isNavigating);
|
|
231
277
|
if(isNavigating)return;
|
|
232
|
-
|
|
278
|
+
// Resolve against the current document URL, matching native <a> semantics.
|
|
279
|
+
// With location.origin as the base, any relative href ('?status=resolved',
|
|
280
|
+
// '#anchor', './sibling') resolved to the SITE ROOT \u2014 so filter tabs,
|
|
281
|
+
// pagination and sort links silently bounced users to the landing page once
|
|
282
|
+
// the SPA interceptor was active (#1777).
|
|
283
|
+
var t=new URL(url,location.href);
|
|
233
284
|
|
|
234
285
|
if(t.origin!==location.origin){location.href=url;return}
|
|
235
286
|
|
|
@@ -277,6 +328,7 @@ export function getRouterScript() {
|
|
|
277
328
|
}).finally(done);
|
|
278
329
|
return;
|
|
279
330
|
}
|
|
331
|
+
pendingContainerAttrs=attrsCache[targetPath]||'';
|
|
280
332
|
swap(cache[targetPath],targetPath,pushState,targetHash);
|
|
281
333
|
applyTitle(titleCache[targetPath]);
|
|
282
334
|
done();
|
|
@@ -293,6 +345,7 @@ else {
|
|
|
293
345
|
var newLayout=r.headers.get('X-STX-Layout')||'';
|
|
294
346
|
var newGroup=r.headers.get('X-STX-Layout-Group')||'';
|
|
295
347
|
var newTitle=r.headers.get('X-STX-Title')||'';
|
|
348
|
+
var newCAttrs=r.headers.get('X-STX-Container-Attrs')||'';
|
|
296
349
|
// Layout change? Fetch the FULL page (no X-STX-Router header) and do full document swap
|
|
297
350
|
if(isFragment&&checkLayoutChange(newLayout,url,newGroup)){
|
|
298
351
|
log('[router] layout change \u2014 fetching full page for document swap');
|
|
@@ -305,11 +358,12 @@ else {
|
|
|
305
358
|
});
|
|
306
359
|
});
|
|
307
360
|
}
|
|
308
|
-
return r.text().then(function(html){return{html:html,isFragment:isFragment,layout:newLayout,layoutGroup:newGroup,title:newTitle}});
|
|
361
|
+
return r.text().then(function(html){return{html:html,isFragment:isFragment,layout:newLayout,layoutGroup:newGroup,title:newTitle,containerAttrs:newCAttrs}});
|
|
309
362
|
}).then(function(result){
|
|
310
363
|
if(!result)return;
|
|
311
364
|
if(result.isFragment)result.html='<!--stx-fragment-->'+result.html;
|
|
312
|
-
if(o.cache)setCache(targetPath,result.html,result.layout,result.layoutGroup,result.title);
|
|
365
|
+
if(o.cache)setCache(targetPath,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
366
|
+
pendingContainerAttrs=result.isFragment?(result.containerAttrs||''):'';
|
|
313
367
|
swap(result.html,targetPath,pushState,targetHash);
|
|
314
368
|
// Fragment swaps carry no <head>; apply the title from the header.
|
|
315
369
|
// Full-document swaps already set document.title from the <title> tag.
|
|
@@ -382,7 +436,10 @@ else {
|
|
|
382
436
|
el.setAttribute('data-stx-page','');
|
|
383
437
|
document.head.appendChild(el);
|
|
384
438
|
});
|
|
385
|
-
// Swap content
|
|
439
|
+
// Swap content \u2014 apply the destination container's own attributes first
|
|
440
|
+
// so the incoming markup lands in a correctly-laid-out container instead
|
|
441
|
+
// of flashing (or sticking) unstyled.
|
|
442
|
+
applyContainerAttrs(currentContent,pendingContainerAttrs);
|
|
386
443
|
currentContent.innerHTML=cleanFrag;
|
|
387
444
|
// Remove old page scripts
|
|
388
445
|
document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
|
|
@@ -493,6 +550,35 @@ else {
|
|
|
493
550
|
|
|
494
551
|
incoming.forEach(function(s){s.removeAttribute('data-stx-incoming')});
|
|
495
552
|
|
|
553
|
+
// \u2500\u2500 Swap <head> stylesheet / font <link>s \u2500\u2500
|
|
554
|
+
// Without this, an SPA navigation to a page whose <link rel="stylesheet">
|
|
555
|
+
// (or font/preconnect <link>) the current page had not already loaded left
|
|
556
|
+
// the swapped-in content completely unstyled (e.g. a page that pulls in its
|
|
557
|
+
// own marketing.css, or Google Fonts). Additive and href-deduped, mirroring
|
|
558
|
+
// the external <head script[src]> reconcile below \u2014 never removes existing
|
|
559
|
+
// links, so stylesheets shared across pages simply persist.
|
|
560
|
+
// Relative asset hrefs resolve against the document that declared them:
|
|
561
|
+
// the CURRENT page for links already in <head>, and the page being
|
|
562
|
+
// navigated TO for links from the fetched document. Against location.origin
|
|
563
|
+
// both collapsed to the site root, so 'assets/app.css' on /docs/intro
|
|
564
|
+
// deduped as /assets/app.css \u2014 the wrong file, and a false cache hit that
|
|
565
|
+
// skipped the real stylesheet (#1777).
|
|
566
|
+
var docBase;
|
|
567
|
+
try{docBase=new URL(url,location.href).href}catch(e){docBase=location.href}
|
|
568
|
+
var linkSel='link[rel="stylesheet"],link[rel="preconnect"]';
|
|
569
|
+
var curLinks={};
|
|
570
|
+
document.querySelectorAll('head '+linkSel).forEach(function(l){var h=l.getAttribute('href');if(h)curLinks[new URL(h,location.href).href]=1});
|
|
571
|
+
doc.querySelectorAll('head '+linkSel).forEach(function(l){
|
|
572
|
+
var href=l.getAttribute('href');
|
|
573
|
+
if(!href)return;
|
|
574
|
+
var abs=new URL(href,docBase).href;
|
|
575
|
+
if(curLinks[abs])return;
|
|
576
|
+
curLinks[abs]=1;
|
|
577
|
+
var nl=document.createElement('link');
|
|
578
|
+
Array.from(l.attributes).forEach(function(a){nl.setAttribute(a.name,a.value)});
|
|
579
|
+
document.head.appendChild(nl);
|
|
580
|
+
});
|
|
581
|
+
|
|
496
582
|
// \u2500\u2500 Swap content \u2500\u2500
|
|
497
583
|
// For layout changes: swap the entire <body> to replace layout chrome (nav, footer, etc.)
|
|
498
584
|
// For same-layout: swap only the container (<main>)
|
|
@@ -526,7 +612,12 @@ else {
|
|
|
526
612
|
// Update container reference for script execution below
|
|
527
613
|
currentContent=document.querySelector(containerSel)||document.querySelector('main')||document.body;
|
|
528
614
|
} else {
|
|
529
|
-
// Same layout \u2014 swap only container content
|
|
615
|
+
// Same layout \u2014 swap only container content. Carry the destination
|
|
616
|
+
// container's attributes across too (same reason as the fragment path:
|
|
617
|
+
// pages whose layout lives on <main> itself would otherwise lose it).
|
|
618
|
+
var attrMap={};
|
|
619
|
+
Array.prototype.forEach.call(newContent.attributes,function(a){attrMap[a.name]=a.value});
|
|
620
|
+
setContainerAttrs(currentContent,attrMap);
|
|
530
621
|
var cleanHTML=newContent.innerHTML.replace(new RegExp('<scr'+'ipt\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/scr'+'ipt\\\\s*>','gi'),'');
|
|
531
622
|
currentContent.innerHTML=cleanHTML;
|
|
532
623
|
}
|
|
@@ -536,7 +627,9 @@ else {
|
|
|
536
627
|
document.querySelectorAll('head script[src]').forEach(function(s){loadedSrcs[s.src]=1});
|
|
537
628
|
var extPromises=[];
|
|
538
629
|
doc.querySelectorAll('head script[src]').forEach(function(s){
|
|
539
|
-
|
|
630
|
+
// Fetched-document script: resolve against the page being navigated to
|
|
631
|
+
// (docBase), not the origin root \u2014 same rationale as the <link> reconcile (#1777).
|
|
632
|
+
var src=new URL(s.getAttribute('src'),docBase).href;
|
|
540
633
|
if(loadedSrcs[src])return;
|
|
541
634
|
loadedSrcs[src]=1;
|
|
542
635
|
extPromises.push(new Promise(function(resolve,reject){
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stx-router",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.113",
|
|
5
5
|
"description": "File-based router for STX — .stx template discovery, nested layouts, typed route params, middleware, and client-side SPA navigation.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
7
7
|
"license": "MIT",
|