stx-router 0.2.291 → 0.2.293
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 +156 -48
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -16,7 +16,7 @@ export function getRouterScript() {
|
|
|
16
16
|
if(window.__stxRouter&&window.__stxRouter.__rev===ROUTER_REV)return;
|
|
17
17
|
|
|
18
18
|
// \u2500\u2500 Configuration \u2500\u2500
|
|
19
|
-
var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true,progress:true,progressColor:'#78dce8',progressHeight:'2px',interceptAllLinks:false,prefetchCacheMax:50,routeFocus:true,announceRoute:true,interceptForms:false};
|
|
19
|
+
var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true,progress:true,progressColor:'#78dce8',progressHeight:'2px',interceptAllLinks:false,prefetchCacheMax:50,routeFocus:true,announceRoute:true,interceptForms:false,cssLoadTimeout:1500};
|
|
20
20
|
var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{});
|
|
21
21
|
var containerSel=o.container;
|
|
22
22
|
var debug=!!o.debug;
|
|
@@ -780,6 +780,99 @@ else {
|
|
|
780
780
|
try{target.focus({preventScroll:true})}catch(e){try{target.focus()}catch(e2){}}
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
+
// \u2500\u2500 Generated utility stylesheets \u2500\u2500
|
|
784
|
+
// Every page ships its own content-addressed utility sheet as
|
|
785
|
+
// <link data-css="generated">, and none of them is ever removed on a
|
|
786
|
+
// navigation, so they pile up in <head>. Two of them can define the same
|
|
787
|
+
// class at the same specificity, which makes their ORDER decide the cascade:
|
|
788
|
+
// the later sheet wins. Deduping alone left that order as "first seen", so
|
|
789
|
+
// after Records \u2192 Feed \u2192 Records the Feed sheet still sat after the Records
|
|
790
|
+
// one and its .flex-col beat Records' own responsive sm:flex-row \u2014 the header
|
|
791
|
+
// stacked into a column on desktop. The current page's complete sheet has to
|
|
792
|
+
// be the last one, every navigation, not just the first time it is seen.
|
|
793
|
+
function findGeneratedCss(abs){
|
|
794
|
+
var found=null;
|
|
795
|
+
document.querySelectorAll('head link[data-css][href]').forEach(function(link){
|
|
796
|
+
if(new URL(link.getAttribute('href'),location.href).href!==abs)return;
|
|
797
|
+
// Collapse duplicates, so moving one copy to the end can never leave an
|
|
798
|
+
// older copy of the same sheet behind to fight it.
|
|
799
|
+
if(found){link.parentNode.removeChild(link);return}
|
|
800
|
+
found=link;
|
|
801
|
+
});
|
|
802
|
+
return found;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Before the swap: add each sheet the page does not have yet, as
|
|
806
|
+
// media="print" so it downloads without restyling the page still on screen.
|
|
807
|
+
// Returns a promise for them to finish (load OR error, capped at
|
|
808
|
+
// cssLoadTimeout so a slow or missing sheet never stalls navigation), or null
|
|
809
|
+
// when nothing new is needed and the swap can go ahead in the same turn.
|
|
810
|
+
function preloadGeneratedCss(hrefs,base){
|
|
811
|
+
var pending=[];
|
|
812
|
+
hrefs.forEach(function(href){
|
|
813
|
+
var abs;
|
|
814
|
+
try{abs=new URL(href,base).href}catch(e){return}
|
|
815
|
+
var link=findGeneratedCss(abs);
|
|
816
|
+
if(link){
|
|
817
|
+
// Still downloading for a navigation that never swapped \u2014 wait on it
|
|
818
|
+
// rather than on nothing.
|
|
819
|
+
if(link.__stxCssReady)pending.push(link.__stxCssReady);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
link=document.createElement('link');
|
|
823
|
+
link.setAttribute('data-css','generated');
|
|
824
|
+
link.setAttribute('rel','stylesheet');
|
|
825
|
+
link.setAttribute('href',new URL(href,location.href).href===abs?href:abs);
|
|
826
|
+
link.setAttribute('media','print');
|
|
827
|
+
link.setAttribute('data-stx-css-pending','');
|
|
828
|
+
link.__stxCssReady=new Promise(function(resolve){
|
|
829
|
+
var timer=setTimeout(finish,o.cssLoadTimeout);
|
|
830
|
+
function finish(){clearTimeout(timer);link.onload=null;link.onerror=null;link.__stxCssReady=null;resolve()}
|
|
831
|
+
link.onload=finish;
|
|
832
|
+
link.onerror=finish;
|
|
833
|
+
});
|
|
834
|
+
pending.push(link.__stxCssReady);
|
|
835
|
+
document.head.appendChild(link);
|
|
836
|
+
});
|
|
837
|
+
return pending.length?Promise.all(pending):null;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// At the swap: make the destination's sheets live and move them to the end
|
|
841
|
+
// of <head>. appendChild on a node that is already there MOVES it \u2014 no
|
|
842
|
+
// duplicate, and the browser keeps the parsed sheet rather than refetching.
|
|
843
|
+
// Only generated sheets are reordered; app and font stylesheets keep the
|
|
844
|
+
// order they were declared in.
|
|
845
|
+
function promoteGeneratedCss(hrefs,base){
|
|
846
|
+
hrefs.forEach(function(href){
|
|
847
|
+
var abs;
|
|
848
|
+
try{abs=new URL(href,base).href}catch(e){return}
|
|
849
|
+
var link=findGeneratedCss(abs);
|
|
850
|
+
if(!link){
|
|
851
|
+
preloadGeneratedCss([href],base);
|
|
852
|
+
link=findGeneratedCss(abs);
|
|
853
|
+
if(!link)return;
|
|
854
|
+
}
|
|
855
|
+
if(link.hasAttribute('data-stx-css-pending')){
|
|
856
|
+
link.removeAttribute('data-stx-css-pending');
|
|
857
|
+
link.removeAttribute('media');
|
|
858
|
+
}
|
|
859
|
+
document.head.appendChild(link);
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// The generated-sheet hrefs a fragment carries. Scripts are skipped so a
|
|
864
|
+
// <link> written inside a string in page code is not mistaken for one.
|
|
865
|
+
function fragmentCssHrefs(html){
|
|
866
|
+
var hrefs=[];
|
|
867
|
+
html.replace(new RegExp('<scr'+'ipt\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/scr'+'ipt>','gi'),'').replace(new RegExp('<link\\\\b([^>]*)>','gi'),function(m,attrs){
|
|
868
|
+
if(attrs.indexOf('data-css')===-1)return m;
|
|
869
|
+
var hrefMatch=attrs.match(/\\bhref=(["'])(.*?)\\1/i);
|
|
870
|
+
if(hrefMatch&&hrefMatch[2])hrefs.push(hrefMatch[2]);
|
|
871
|
+
return m;
|
|
872
|
+
});
|
|
873
|
+
return hrefs;
|
|
874
|
+
}
|
|
875
|
+
|
|
783
876
|
function swap(html,url,pushState,hash){
|
|
784
877
|
var fragMark=/^<!--stx-fragment(?: rt=([01]))?-->/.exec(html);
|
|
785
878
|
var isFragment=!!fragMark;
|
|
@@ -800,7 +893,6 @@ else {
|
|
|
800
893
|
location.href=url;
|
|
801
894
|
return Promise.resolve(false);
|
|
802
895
|
}
|
|
803
|
-
if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
|
|
804
896
|
function doFragSwap(){
|
|
805
897
|
// Extract scripts from fragment before injecting HTML
|
|
806
898
|
var fragScripts=[];
|
|
@@ -877,19 +969,9 @@ else {
|
|
|
877
969
|
document.head.appendChild(cw);
|
|
878
970
|
}
|
|
879
971
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
document.querySelectorAll('head link[data-css][href]').forEach(function(link){
|
|
884
|
-
if(new URL(link.getAttribute('href'),location.href).href===absolute)exists=true;
|
|
885
|
-
});
|
|
886
|
-
if(exists)return;
|
|
887
|
-
var link=document.createElement('link');
|
|
888
|
-
link.setAttribute('data-css','generated');
|
|
889
|
-
link.setAttribute('rel','stylesheet');
|
|
890
|
-
link.setAttribute('href',href);
|
|
891
|
-
document.head.appendChild(link);
|
|
892
|
-
});
|
|
972
|
+
// Already present ones are MOVED last, not skipped \u2014 see
|
|
973
|
+
// promoteGeneratedCss for why their order is the whole bug.
|
|
974
|
+
promoteGeneratedCss(fragCssHrefs,location.href);
|
|
893
975
|
// Add new page styles
|
|
894
976
|
fragStyles.forEach(function(s){
|
|
895
977
|
var el=document.createElement('style');
|
|
@@ -1008,16 +1090,25 @@ else {
|
|
|
1008
1090
|
if(fragPending.length)Promise.all(fragPending).then(runFragScripts).catch(runFragScripts);
|
|
1009
1091
|
else runFragScripts();
|
|
1010
1092
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1093
|
+
function startFragSwap(){
|
|
1094
|
+
if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
|
|
1095
|
+
return new Promise(function(resolve,reject){
|
|
1096
|
+
function completeFragSwap(){
|
|
1097
|
+
try{
|
|
1098
|
+
doFragSwap();
|
|
1099
|
+
resolve(true);
|
|
1100
|
+
}catch(err){reject(err)}
|
|
1101
|
+
}
|
|
1102
|
+
if(runViewTransition(completeFragSwap)){}
|
|
1103
|
+
else{currentContent.style.transition='opacity 0.12s ease-out';currentContent.style.opacity='0';setTimeout(function(){completeFragSwap();currentContent.style.opacity='1';setTimeout(function(){currentContent.style.transition=''},150)},120)}
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
// A stylesheet this page has never loaded is fetched BEFORE the content
|
|
1107
|
+
// swaps, or the new markup paints once without its utilities and then
|
|
1108
|
+
// jumps. Cleanup waits with it so the outgoing page stays live meanwhile.
|
|
1109
|
+
// With nothing new to load this runs in the same turn, as it always did.
|
|
1110
|
+
var fragCssReady=preloadGeneratedCss(fragmentCssHrefs(html),location.href);
|
|
1111
|
+
return fragCssReady?fragCssReady.then(startFragSwap):startFragSwap();
|
|
1021
1112
|
}
|
|
1022
1113
|
|
|
1023
1114
|
// Full document mode: parse with DOMParser and extract container content
|
|
@@ -1047,10 +1138,13 @@ else {
|
|
|
1047
1138
|
var newContent=doc.querySelector(containerSel)||doc.querySelector('[data-stx-content]')||doc.querySelector('main');
|
|
1048
1139
|
if(!newContent){location.href=url;return Promise.resolve(false)}
|
|
1049
1140
|
|
|
1050
|
-
//
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1141
|
+
// The destination's generated utility sheets, resolved against the page
|
|
1142
|
+
// being navigated to. Loaded before the swap and moved last during it \u2014
|
|
1143
|
+
// same reasons as the fragment path (promoteGeneratedCss).
|
|
1144
|
+
var docCssBase;
|
|
1145
|
+
try{docCssBase=new URL(url,location.href).href}catch(e){docCssBase=location.href}
|
|
1146
|
+
var docCssHrefs=[];
|
|
1147
|
+
doc.querySelectorAll('head link[data-css][href]').forEach(function(l){docCssHrefs.push(l.getAttribute('href'))});
|
|
1054
1148
|
|
|
1055
1149
|
function doSwap(){
|
|
1056
1150
|
// \u2500\u2500 Swap <head> styles \u2500\u2500
|
|
@@ -1117,6 +1211,9 @@ else {
|
|
|
1117
1211
|
doc.querySelectorAll('head '+linkSel).forEach(function(l){
|
|
1118
1212
|
var href=l.getAttribute('href');
|
|
1119
1213
|
if(!href)return;
|
|
1214
|
+
// Generated utility sheets are not additive-only: promoteGeneratedCss
|
|
1215
|
+
// below also reorders them.
|
|
1216
|
+
if(l.hasAttribute('data-css'))return;
|
|
1120
1217
|
var abs=new URL(href,docBase).href;
|
|
1121
1218
|
if(curLinks[abs])return;
|
|
1122
1219
|
curLinks[abs]=1;
|
|
@@ -1124,6 +1221,9 @@ else {
|
|
|
1124
1221
|
Array.from(l.attributes).forEach(function(a){nl.setAttribute(a.name,a.value)});
|
|
1125
1222
|
document.head.appendChild(nl);
|
|
1126
1223
|
});
|
|
1224
|
+
// After every other stylesheet link, so the destination's complete
|
|
1225
|
+
// utility sheet is last in the cascade.
|
|
1226
|
+
promoteGeneratedCss(docCssHrefs,docBase);
|
|
1127
1227
|
|
|
1128
1228
|
// \u2500\u2500 Swap content \u2500\u2500
|
|
1129
1229
|
// For layout changes: swap the entire <body> to replace layout chrome (nav, footer, etc.)
|
|
@@ -1417,26 +1517,34 @@ else {
|
|
|
1417
1517
|
}
|
|
1418
1518
|
}
|
|
1419
1519
|
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
resolve(true);
|
|
1425
|
-
}catch(err){reject(err)}
|
|
1426
|
-
}
|
|
1427
|
-
if(runViewTransition(completeSwap)){
|
|
1428
|
-
}
|
|
1429
|
-
else {
|
|
1430
|
-
// Fallback fade for browsers without View Transitions API
|
|
1431
|
-
currentContent.style.transition='opacity 0.12s ease-out';
|
|
1432
|
-
currentContent.style.opacity='0';
|
|
1433
|
-
setTimeout(function(){
|
|
1434
|
-
completeSwap();
|
|
1435
|
-
currentContent.style.opacity='1';
|
|
1436
|
-
setTimeout(function(){currentContent.style.transition=''},150);
|
|
1437
|
-
},120);
|
|
1520
|
+
function startSwap(){
|
|
1521
|
+
// Clean up existing signals/effects
|
|
1522
|
+
if(window.stx&&window.stx._cleanupContainer){
|
|
1523
|
+
window.stx._cleanupContainer(currentContent);
|
|
1438
1524
|
}
|
|
1439
|
-
|
|
1525
|
+
return new Promise(function(resolve,reject){
|
|
1526
|
+
function completeSwap(){
|
|
1527
|
+
try{
|
|
1528
|
+
doSwap();
|
|
1529
|
+
resolve(true);
|
|
1530
|
+
}catch(err){reject(err)}
|
|
1531
|
+
}
|
|
1532
|
+
if(runViewTransition(completeSwap)){
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
// Fallback fade for browsers without View Transitions API
|
|
1536
|
+
currentContent.style.transition='opacity 0.12s ease-out';
|
|
1537
|
+
currentContent.style.opacity='0';
|
|
1538
|
+
setTimeout(function(){
|
|
1539
|
+
completeSwap();
|
|
1540
|
+
currentContent.style.opacity='1';
|
|
1541
|
+
setTimeout(function(){currentContent.style.transition=''},150);
|
|
1542
|
+
},120);
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
var docCssReady=preloadGeneratedCss(docCssHrefs,docCssBase);
|
|
1547
|
+
return docCssReady?docCssReady.then(startSwap):startSwap();
|
|
1440
1548
|
}
|
|
1441
1549
|
|
|
1442
1550
|
// \u2500\u2500 Link interception \u2500\u2500
|
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.293",
|
|
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",
|