bun-plugin-stx 0.2.406 → 0.2.408
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/{chunk-9z4zav9s.js → chunk-6svt7a27.js} +1024 -57
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +16 -6
- package/package.json +3 -3
|
@@ -696,9 +696,9 @@ function stripRouterLogs(source) {
|
|
|
696
696
|
}
|
|
697
697
|
return out.join("");
|
|
698
698
|
}
|
|
699
|
-
function minifyRouterScript(source) {
|
|
699
|
+
function minifyRouterScript(source, mangle = false) {
|
|
700
700
|
try {
|
|
701
|
-
const transpiler = new Bun.Transpiler({ loader: "js", minifyWhitespace: true });
|
|
701
|
+
const transpiler = new Bun.Transpiler(mangle ? { loader: "js", minify: { whitespace: true, identifiers: true } } : { loader: "js", minifyWhitespace: true });
|
|
702
702
|
return transpiler.transformSync(source).replace(/\}(var |let |const |function )/g, "};$1").trim();
|
|
703
703
|
} catch {
|
|
704
704
|
return source;
|
|
@@ -712,7 +712,7 @@ function routerSource() {
|
|
|
712
712
|
if(window.__stxRouter&&window.__stxRouter.__rev===ROUTER_REV)return;
|
|
713
713
|
|
|
714
714
|
// \u2500\u2500 Configuration \u2500\u2500
|
|
715
|
-
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,scrollRestoration:true};
|
|
715
|
+
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,scrollRestoration:true,screens:'auto',screenLimit:8,navDuration:350,swipeBack:true,swipeEdge:20,prefetchVisible:true,prefetchVisibleMax:12,revalidate:true,revalidateAfter:3000};
|
|
716
716
|
var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{});
|
|
717
717
|
var containerSel=o.container;
|
|
718
718
|
var debug=!!o.debug;
|
|
@@ -748,6 +748,12 @@ function routerSource() {
|
|
|
748
748
|
// (which carries a scroll token too): going back from a pushed entry stays
|
|
749
749
|
// in the app, from the first it leaves it (or, in a phone app, does nothing).
|
|
750
750
|
var PUSHED_MARK='__stxPushed';
|
|
751
|
+
// How far above the entry the app opened with this one sits, and which tab
|
|
752
|
+
// it belongs to. A tab switch rewinds to depth 0 before it writes the next
|
|
753
|
+
// tab's entries, which is what keeps Back from ever crossing tabs.
|
|
754
|
+
var DEPTH='__stxDepth';
|
|
755
|
+
var TAB='__stxTab';
|
|
756
|
+
var histDepth=(history.state&&history.state[DEPTH])|0;
|
|
751
757
|
var scrollSeq=0;
|
|
752
758
|
var pendingScroll=null;
|
|
753
759
|
var restoreScroll=!!o.scrollRestoration;
|
|
@@ -762,11 +768,44 @@ function routerSource() {
|
|
|
762
768
|
return parts.length===2?[parseFloat(parts[0])||0,parseFloat(parts[1])||0]:null;
|
|
763
769
|
}catch(e){return null}
|
|
764
770
|
}
|
|
771
|
+
// Inner scrollers, marked data-stx-scroll: a screen whose list scrolls
|
|
772
|
+
// inside its own box rather than the window lost its place on Back exactly
|
|
773
|
+
// the way the window used to. Keyed by the attribute's value, or by order
|
|
774
|
+
// for an unnamed one.
|
|
775
|
+
function scrollerKey(el,i){return el.getAttribute('data-stx-scroll')||('#'+i)}
|
|
776
|
+
function innerScroll(root){
|
|
777
|
+
var m=null;
|
|
778
|
+
if(root&&root.querySelectorAll)root.querySelectorAll('[data-stx-scroll]').forEach(function(el,i){
|
|
779
|
+
if(el.scrollTop||el.scrollLeft){m=m||{};m[scrollerKey(el,i)]=[el.scrollLeft||0,el.scrollTop||0]}
|
|
780
|
+
});
|
|
781
|
+
return m;
|
|
782
|
+
}
|
|
783
|
+
function applyInner(root,m){
|
|
784
|
+
if(!m||!root||!root.querySelectorAll)return;
|
|
785
|
+
root.querySelectorAll('[data-stx-scroll]').forEach(function(el,i){
|
|
786
|
+
var at=m[scrollerKey(el,i)];
|
|
787
|
+
if(at){el.scrollLeft=at[0];el.scrollTop=at[1]}
|
|
788
|
+
});
|
|
789
|
+
}
|
|
765
790
|
// Called while the outgoing entry is still the current one, so the position
|
|
766
791
|
// read here belongs to the token being written.
|
|
767
792
|
function rememberScroll(){
|
|
793
|
+
if(active&&active.el&&!active.el.hasAttribute(HIDDEN))saveScroll(active);
|
|
768
794
|
if(!restoreScroll)return;
|
|
769
|
-
try{
|
|
795
|
+
try{
|
|
796
|
+
window.sessionStorage.setItem(SCROLL_STORE+scrollToken,(window.pageXOffset||window.scrollX||0)+','+(window.pageYOffset||window.scrollY||0));
|
|
797
|
+
// Taken as the swap began when there is one: by the time the history
|
|
798
|
+
// is written the outgoing page's scrollers have been replaced.
|
|
799
|
+
var inner=innerSnap!==undefined?innerSnap:innerScroll(activeRoot());
|
|
800
|
+
innerSnap=undefined;
|
|
801
|
+
if(inner)window.sessionStorage.setItem(SCROLL_STORE+scrollToken+':i',JSON.stringify(inner));
|
|
802
|
+
else window.sessionStorage.removeItem(SCROLL_STORE+scrollToken+':i');
|
|
803
|
+
}catch(e){}
|
|
804
|
+
}
|
|
805
|
+
var pendingInner=null;
|
|
806
|
+
var innerSnap;
|
|
807
|
+
function readInner(token){
|
|
808
|
+
try{return JSON.parse(window.sessionStorage.getItem(SCROLL_STORE+token+':i')||'null')}catch(e){return null}
|
|
770
809
|
}
|
|
771
810
|
// One place decides where a swap leaves the viewport, so the fragment path
|
|
772
811
|
// and the whole-document path cannot drift apart.
|
|
@@ -775,10 +814,16 @@ function routerSource() {
|
|
|
775
814
|
var at=pendingScroll;
|
|
776
815
|
pendingScroll=null;
|
|
777
816
|
window.scrollTo({left:at[0],top:at[1],behavior:'instant'});
|
|
817
|
+
// Again once the page has bound: a scroller's content is often drawn
|
|
818
|
+
// by a :for that has not run yet, and a box with nothing in it cannot
|
|
819
|
+
// be scrolled anywhere.
|
|
820
|
+
var inner=pendingInner;pendingInner=null;
|
|
821
|
+
if(inner){applyInner(activeRoot(),inner);setTimeout(function(){applyInner(activeRoot(),inner)},60)}
|
|
778
822
|
return;
|
|
779
823
|
}
|
|
824
|
+
pendingInner=null;
|
|
780
825
|
if(o.scrollToTop&&!hash){window.scrollTo({top:0,behavior:'instant'});return}
|
|
781
|
-
if(hash){var el=
|
|
826
|
+
if(hash){var el=findIn(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
|
|
782
827
|
}
|
|
783
828
|
// Keeps whatever another script put in the entry; only adds the token.
|
|
784
829
|
function stampScrollToken(){
|
|
@@ -786,6 +831,7 @@ function routerSource() {
|
|
|
786
831
|
var current=history.state;
|
|
787
832
|
if(current&&typeof current==='object')for(var key in current)state[key]=current[key];
|
|
788
833
|
state[SCROLL_TOKEN]=scrollToken;
|
|
834
|
+
state[DEPTH]=histDepth;
|
|
789
835
|
try{history.replaceState(state,'',location.href)}catch(e){}
|
|
790
836
|
}
|
|
791
837
|
if(restoreScroll){
|
|
@@ -839,9 +885,24 @@ function routerSource() {
|
|
|
839
885
|
function isBuildSkew(incoming){
|
|
840
886
|
return !!(loadedBuild&&incoming&&incoming!==loadedBuild);
|
|
841
887
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
888
|
+
// A navigation the user asked for loads its destination as a document: no
|
|
889
|
+
// fragment of the newer build can be hydrated by this runtime, and a tap is
|
|
890
|
+
// the one moment a page load is expected anyway. Anything else that notices
|
|
891
|
+
// the newer build (a background revalidation, the offline worker) must not
|
|
892
|
+
// reload a screen the user is in the middle of using, so the reload waits
|
|
893
|
+
// until the page is hidden, and a cold start picks the build up regardless.
|
|
894
|
+
var skewDeferred=false;
|
|
895
|
+
function reloadForSkew(url,incoming,background){
|
|
896
|
+
log('[router] build skew: page is',loadedBuild,'server is',incoming,background?'\u2014 reload when hidden':'\u2014 full navigation');
|
|
897
|
+
if(!background){location.href=url;return}
|
|
898
|
+
if(skewDeferred)return;
|
|
899
|
+
skewDeferred=true;
|
|
900
|
+
var reloadHidden=function(){
|
|
901
|
+
if(document.visibilityState!=='hidden')return;
|
|
902
|
+
document.removeEventListener('visibilitychange',reloadHidden);
|
|
903
|
+
location.reload();
|
|
904
|
+
};
|
|
905
|
+
document.addEventListener('visibilitychange',reloadHidden);
|
|
845
906
|
}
|
|
846
907
|
// A link marked data-stx-transition="none" swaps without the cross-fade. A
|
|
847
908
|
// tab bar is the case: iOS switches tabs instantly, and the fade read as a
|
|
@@ -988,6 +1049,9 @@ function routerSource() {
|
|
|
988
1049
|
}
|
|
989
1050
|
|
|
990
1051
|
var cache={};
|
|
1052
|
+
// When each entry was fetched, so a cache hit knows whether it is worth
|
|
1053
|
+
// checking behind the swap (revalidate).
|
|
1054
|
+
var cacheAt={};
|
|
991
1055
|
var prefetching={};
|
|
992
1056
|
var awaitingKey='';
|
|
993
1057
|
var isNavigating=false;
|
|
@@ -1009,6 +1073,7 @@ function routerSource() {
|
|
|
1009
1073
|
layoutGroupCache[key]=group;
|
|
1010
1074
|
titleCache[key]=title||'';
|
|
1011
1075
|
attrsCache[key]=cattrs||'';
|
|
1076
|
+
cacheAt[key]=Date.now();
|
|
1012
1077
|
while(cacheOrder.length>o.prefetchCacheMax){
|
|
1013
1078
|
var oldest=cacheOrder.shift();
|
|
1014
1079
|
delete cache[oldest];
|
|
@@ -1016,6 +1081,7 @@ function routerSource() {
|
|
|
1016
1081
|
delete layoutGroupCache[oldest];
|
|
1017
1082
|
delete titleCache[oldest];
|
|
1018
1083
|
delete attrsCache[oldest];
|
|
1084
|
+
delete cacheAt[oldest];
|
|
1019
1085
|
}
|
|
1020
1086
|
}
|
|
1021
1087
|
// Read a prefetch response into the shape setCache stores, or null when this
|
|
@@ -1119,6 +1185,7 @@ function routerSource() {
|
|
|
1119
1185
|
delete layoutGroupCache[key];
|
|
1120
1186
|
delete titleCache[key];
|
|
1121
1187
|
delete attrsCache[key];
|
|
1188
|
+
delete cacheAt[key];
|
|
1122
1189
|
}
|
|
1123
1190
|
|
|
1124
1191
|
// Extract top-level CSS blocks (rules AND @media blocks with nested braces).
|
|
@@ -1528,10 +1595,17 @@ function routerSource() {
|
|
|
1528
1595
|
// pushes, and the string 'replace' replaces. Threading a fourth argument
|
|
1529
1596
|
// through swap() to each of the three history sites would have been the
|
|
1530
1597
|
// alternative (#1807).
|
|
1598
|
+
//
|
|
1599
|
+
// 'tab' is a tab's first screen: it takes the place of the entry the app
|
|
1600
|
+
// opened with (selectTab has rewound to it), under a token of its own so
|
|
1601
|
+
// the tab it replaced keeps its positions.
|
|
1602
|
+
var scrollSaved=false;
|
|
1531
1603
|
function writeHistory(mode,href){
|
|
1532
1604
|
// The position belongs to the entry being left, and scrollToken still
|
|
1533
|
-
// names it here
|
|
1534
|
-
|
|
1605
|
+
// names it here -- unless the screen swap already saved it, before the
|
|
1606
|
+
// outgoing screen was lifted out of the page and the window clamped.
|
|
1607
|
+
if(!scrollSaved)rememberScroll();
|
|
1608
|
+
scrollSaved=false;
|
|
1535
1609
|
if(mode==='replace'){
|
|
1536
1610
|
// A replaced entry is still the one it replaces: pushed by the app or
|
|
1537
1611
|
// the one the app opened with.
|
|
@@ -1541,22 +1615,43 @@ function routerSource() {
|
|
|
1541
1615
|
return;
|
|
1542
1616
|
}
|
|
1543
1617
|
scrollToken=newScrollToken();
|
|
1618
|
+
if(mode==='tab'){histDepth=0;history.replaceState(tokenState(),'',href);return}
|
|
1619
|
+
histDepth++;
|
|
1544
1620
|
var pushed=tokenState();
|
|
1545
1621
|
pushed[PUSHED_MARK]=true;
|
|
1546
1622
|
history.pushState(pushed,'',href);
|
|
1547
1623
|
}
|
|
1548
|
-
function tokenState(){var state={};state[SCROLL_TOKEN]=scrollToken;return state}
|
|
1624
|
+
function tokenState(){var state={};state[SCROLL_TOKEN]=scrollToken;state[DEPTH]=histDepth;if(curTab)state[TAB]=curTab;return state}
|
|
1549
1625
|
|
|
1550
1626
|
// Second arg accepts the legacy pushState boolean OR an options object
|
|
1551
1627
|
// { replace, instant }. Callers inside this file still pass the boolean.
|
|
1552
1628
|
// instant is what data-stx-transition="none" is to a link: no cross-fade.
|
|
1629
|
+
//
|
|
1630
|
+
// The newest navigation wins. One already on its way when another starts
|
|
1631
|
+
// (a second tap, Back while a page is still loading) is aborted and its
|
|
1632
|
+
// answer ignored: it used to be the other way round, the later one was
|
|
1633
|
+
// dropped, and a Back during a load left the address bar on the popped
|
|
1634
|
+
// entry while the screen went on to show the page being left.
|
|
1635
|
+
var navSeq=0;
|
|
1636
|
+
var navAbort=null;
|
|
1637
|
+
// What the next swap needs to know about where it is going: set by the
|
|
1638
|
+
// caller right before navigate() (popstate, a tab switch) and carried to
|
|
1639
|
+
// swap() through pendingCtx, the same way pendingContainerAttrs is.
|
|
1640
|
+
var navCtxNext=null;
|
|
1641
|
+
var pendingCtx=null;
|
|
1642
|
+
function routerFetch(url,headers){
|
|
1643
|
+
var init={headers:headers};
|
|
1644
|
+
if(navAbort)init.signal=navAbort.signal;
|
|
1645
|
+
return fetch(url,init);
|
|
1646
|
+
}
|
|
1553
1647
|
function navigate(url,pushState,force){
|
|
1648
|
+
var ctx=navCtxNext||{};
|
|
1649
|
+
navCtxNext=null;
|
|
1554
1650
|
if(pushState&&typeof pushState==='object'){if(pushState.instant)instantNext=true;pushState=pushState.replace?'replace':true}
|
|
1555
1651
|
// Lang-picker passes force=true with an already-localized path (/en/...).
|
|
1556
1652
|
// Re-localizing would map it back to the *current* locale and no-op.
|
|
1557
1653
|
if(!force) url=withCurrentLocale(url);
|
|
1558
1654
|
log('[router] navigate() called:',url,'isNavigating:',isNavigating);
|
|
1559
|
-
if(isNavigating)return Promise.resolve(false);
|
|
1560
1655
|
// Resolve against the current document URL, matching native <a> semantics.
|
|
1561
1656
|
// With location.origin as the base, any relative href ('?status=resolved',
|
|
1562
1657
|
// '#anchor', './sibling') resolved to the SITE ROOT \u2014 so filter tabs,
|
|
@@ -1566,9 +1661,9 @@ function routerSource() {
|
|
|
1566
1661
|
|
|
1567
1662
|
if(t.origin!==location.origin){location.href=url;return Promise.resolve(false)}
|
|
1568
1663
|
|
|
1569
|
-
if(t.pathname===location.pathname&&t.hash){
|
|
1664
|
+
if(t.pathname===location.pathname&&t.hash&&pushState!=='tab'){
|
|
1570
1665
|
if(pushState!==false)writeHistory(pushState,t.href);
|
|
1571
|
-
var el=
|
|
1666
|
+
var el=findIn(t.hash);
|
|
1572
1667
|
if(el)el.scrollIntoView({behavior:'smooth'});
|
|
1573
1668
|
return Promise.resolve(true);
|
|
1574
1669
|
}
|
|
@@ -1579,7 +1674,7 @@ function routerSource() {
|
|
|
1579
1674
|
// entry \u2014 without this allowance, hitting back would early-return
|
|
1580
1675
|
// and the visible page content would be left frozen on the
|
|
1581
1676
|
// forward-navigation page.
|
|
1582
|
-
if(pushState!==false&&t.href===location.href&&!t.hash&&!force)return Promise.resolve(false);
|
|
1677
|
+
if(pushState!==false&&pushState!=='tab'&&t.href===location.href&&!t.hash&&!force)return Promise.resolve(false);
|
|
1583
1678
|
|
|
1584
1679
|
// Its prefetch is on the way: wait for that answer rather than asking the
|
|
1585
1680
|
// server a second time. A later tap elsewhere wins.
|
|
@@ -1589,11 +1684,17 @@ function routerSource() {
|
|
|
1589
1684
|
return prefetching[awaitedKey].then(function(){
|
|
1590
1685
|
if(awaitingKey!==awaitedKey)return false;
|
|
1591
1686
|
awaitingKey='';
|
|
1687
|
+
navCtxNext=ctx;
|
|
1592
1688
|
return navigate(url,pushState,force);
|
|
1593
1689
|
});
|
|
1594
1690
|
}
|
|
1595
1691
|
awaitingKey='';
|
|
1596
1692
|
|
|
1693
|
+
if(navAbort){try{navAbort.abort()}catch(e){}}
|
|
1694
|
+
navAbort=typeof AbortController==='function'?new AbortController():null;
|
|
1695
|
+
var id=++navSeq;
|
|
1696
|
+
ctx.id=id;
|
|
1697
|
+
if(!ctx.direction)ctx.direction=pushState==='replace'?'replace':pushState==='tab'?'tab':pushState===false?'pop':'push';
|
|
1597
1698
|
isNavigating=true;
|
|
1598
1699
|
instantNav=instantNext;
|
|
1599
1700
|
instantNext=false;
|
|
@@ -1604,7 +1705,22 @@ function routerSource() {
|
|
|
1604
1705
|
var targetPath=cacheKey(url);
|
|
1605
1706
|
var targetHash=t.hash;
|
|
1606
1707
|
|
|
1607
|
-
function done(){
|
|
1708
|
+
function done(){
|
|
1709
|
+
if(id!==navSeq)return;
|
|
1710
|
+
isNavigating=false;navAbort=null;
|
|
1711
|
+
document.body.classList.remove(o.loadingClass);finishProgress();
|
|
1712
|
+
settleDirection();
|
|
1713
|
+
}
|
|
1714
|
+
function stale(){return id!==navSeq}
|
|
1715
|
+
// A failed fetch keeps the app: the destination shows a way to try again
|
|
1716
|
+
// in place of its content, where a document load used to be the answer
|
|
1717
|
+
// to everything -- offline that load is the offline page, or nothing.
|
|
1718
|
+
function failed(err){
|
|
1719
|
+
if(stale()||(err&&err.name==='AbortError'))return false;
|
|
1720
|
+
console.error('[router] fetch error:',err);
|
|
1721
|
+
return showNavError(url,err,pushState,targetHash,ctx);
|
|
1722
|
+
}
|
|
1723
|
+
function swapWith(html,key){pendingCtx=ctx;return swap(html,key,pushState,targetHash)}
|
|
1608
1724
|
|
|
1609
1725
|
if(o.cache&&cache[targetPath]&&!force){
|
|
1610
1726
|
// Cache hit \u2192 promote in LRU order so this entry survives eviction
|
|
@@ -1613,22 +1729,24 @@ function routerSource() {
|
|
|
1613
1729
|
if(checkLayoutChange(layoutCache[targetPath],url,layoutGroupCache[targetPath])){
|
|
1614
1730
|
// Layout changed \u2014 fetch full page and do full document swap
|
|
1615
1731
|
log('[router] cache hit but layout changed \u2014 fetching full page');
|
|
1616
|
-
return
|
|
1617
|
-
if(!r.ok)throw
|
|
1732
|
+
return routerFetch(url,{'Accept':'text/html'}).then(function(r){
|
|
1733
|
+
if(!r.ok)throw httpError(r.status);
|
|
1618
1734
|
return r.text();
|
|
1619
1735
|
}).then(function(html){
|
|
1736
|
+
if(stale())return false;
|
|
1620
1737
|
log('[router] full page fetched from cache path, len:',html.length);
|
|
1621
1738
|
pendingLayoutDecl={layout:layoutCache[targetPath]||'',group:layoutGroupCache[targetPath]||''};
|
|
1622
|
-
return
|
|
1623
|
-
}).catch(
|
|
1624
|
-
console.error('[router] full page fetch error:',err);
|
|
1625
|
-
location.href=url;
|
|
1626
|
-
}).finally(done);
|
|
1739
|
+
return swapWith(html,targetPath);
|
|
1740
|
+
}).catch(failed).finally(done);
|
|
1627
1741
|
}
|
|
1628
1742
|
pendingContainerAttrs=attrsCache[targetPath]||'';
|
|
1629
1743
|
pendingLayoutDecl=null;
|
|
1630
|
-
return Promise.resolve(
|
|
1744
|
+
return Promise.resolve(swapWith(cache[targetPath],targetPath)).then(function(swapped){
|
|
1745
|
+
if(swapped===false)return false;
|
|
1631
1746
|
applyTitle(titleCache[targetPath]);
|
|
1747
|
+
// Shown from the cache at once; asked again behind it, so a page that
|
|
1748
|
+
// changed since says so (stx:updated) instead of staying stale.
|
|
1749
|
+
revalidate(targetPath,url);
|
|
1632
1750
|
return true;
|
|
1633
1751
|
}).finally(done);
|
|
1634
1752
|
}
|
|
@@ -1638,8 +1756,9 @@ else {
|
|
|
1638
1756
|
// the page content. Custom app-shell containers need full documents so
|
|
1639
1757
|
// the router does not inject a <main> fragment into the wrong element.
|
|
1640
1758
|
var wantsFragment=shouldUseFragmentResponse();
|
|
1641
|
-
return
|
|
1642
|
-
if(
|
|
1759
|
+
return routerFetch(url,wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}).then(function(r){
|
|
1760
|
+
if(stale())return null;
|
|
1761
|
+
if(!r.ok)throw httpError(r.status);
|
|
1643
1762
|
// A route guard answered with a redirect and fetch followed it
|
|
1644
1763
|
// transparently, so r.ok is the DESTINATION's 200 and this markup
|
|
1645
1764
|
// belongs to r.url \u2014 not to the path that was requested. Without
|
|
@@ -1670,9 +1789,9 @@ else {
|
|
|
1670
1789
|
// Layout change? Fetch the FULL page (no X-STX-Router header) and do full document swap
|
|
1671
1790
|
if(isFragment&&checkLayoutChange(newLayout,url,newGroup)){
|
|
1672
1791
|
log('[router] layout change \u2014 fetching full page for document swap');
|
|
1673
|
-
return
|
|
1792
|
+
return routerFetch(url,{'Accept':'text/html'}).then(function(fullRes){
|
|
1674
1793
|
log('[router] full page fetched:',fullRes.status,'ok:',fullRes.ok);
|
|
1675
|
-
if(!fullRes.ok)throw
|
|
1794
|
+
if(!fullRes.ok)throw httpError(fullRes.status);
|
|
1676
1795
|
return fullRes.text().then(function(html){
|
|
1677
1796
|
log('[router] full page html length:',html.length);
|
|
1678
1797
|
return{html:html,isFragment:false,layout:newLayout,layoutGroup:newGroup,title:newTitle};
|
|
@@ -1681,23 +1800,51 @@ else {
|
|
|
1681
1800
|
}
|
|
1682
1801
|
return r.text().then(function(html){return{html:html,isFragment:isFragment,layout:newLayout,layoutGroup:newGroup,title:newTitle,containerAttrs:newCAttrs,runtime:newRuntime}});
|
|
1683
1802
|
}).then(function(result){
|
|
1684
|
-
if(!result)return;
|
|
1803
|
+
if(!result||stale())return false;
|
|
1685
1804
|
if(result.isFragment)result.html=fragmentMarker(result.runtime)+result.html;
|
|
1686
1805
|
if(o.cache)setCache(targetPath,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
1687
1806
|
pendingContainerAttrs=result.isFragment?(result.containerAttrs||''):'';
|
|
1688
1807
|
pendingLayoutDecl=result.isFragment?null:{layout:result.layout||'',group:result.layoutGroup||''};
|
|
1689
|
-
return Promise.resolve(
|
|
1808
|
+
return Promise.resolve(swapWith(result.html,targetPath)).then(function(swapped){
|
|
1809
|
+
if(swapped===false)return false;
|
|
1690
1810
|
// Fragment swaps carry no <head>; apply the title from the header.
|
|
1691
1811
|
// Full-document swaps already set document.title from the <title> tag.
|
|
1692
1812
|
if(result.isFragment)applyTitle(result.title);
|
|
1693
1813
|
return true;
|
|
1694
1814
|
});
|
|
1695
|
-
}).catch(
|
|
1696
|
-
console.error('[router] fetch error:',err);
|
|
1697
|
-
location.href=url;
|
|
1698
|
-
}).finally(done);
|
|
1815
|
+
}).catch(failed).finally(done);
|
|
1699
1816
|
}
|
|
1700
1817
|
}
|
|
1818
|
+
function httpError(status){var err=new Error(String(status));err.status=status;return err}
|
|
1819
|
+
|
|
1820
|
+
// \u2500\u2500 A screen that could not be loaded \u2500\u2500
|
|
1821
|
+
// Shown in the routed container (in a screen of its own when screens are
|
|
1822
|
+
// retained, so Back returns to the one before it), with the address bar at
|
|
1823
|
+
// the destination, so "Try again" and coming back online both retry it in
|
|
1824
|
+
// place. A page that wants to draw its own cancels stx:navigate-error.
|
|
1825
|
+
function escAttr(v){return String(v).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<')}
|
|
1826
|
+
function showNavError(url,err,pushState,hash,ctx){
|
|
1827
|
+
var status=err&&err.status||0;
|
|
1828
|
+
var offline=!status&&navigator.onLine===false;
|
|
1829
|
+
var ev=new CustomEvent('stx:navigate-error',{cancelable:true,detail:{url:url,status:status,offline:offline,error:err}});
|
|
1830
|
+
window.dispatchEvent(ev);
|
|
1831
|
+
if(ev.defaultPrevented)return false;
|
|
1832
|
+
var title=offline?'You are offline':'This page could not be loaded';
|
|
1833
|
+
var text=offline?'It will load as soon as you are back online.':status?'Something went wrong on the way. Try again in a moment.':'Check your connection and try again.';
|
|
1834
|
+
var html='<!--stx-fragment rt=0--><div class="stx-retry" role="alert" data-stx-retry="'+escAttr(url)+'"><p class="stx-retry-title">'+title+'</p><p class="stx-retry-text">'+text+'</p><button type="button" class="stx-retry-button" data-stx-retry-button>Try again</button></div>';
|
|
1835
|
+
pendingContainerAttrs='';
|
|
1836
|
+
pendingLayoutDecl=null;
|
|
1837
|
+
pendingCtx=ctx;
|
|
1838
|
+
return Promise.resolve(swap(html,cacheKey(url),pushState,hash)).then(function(){return false});
|
|
1839
|
+
}
|
|
1840
|
+
function retryShown(){var root=activeRoot();var el=root&&root.querySelector?root.querySelector('[data-stx-retry]'):null;return el?el.getAttribute('data-stx-retry'):''}
|
|
1841
|
+
document.addEventListener('click',function(e){
|
|
1842
|
+
var b=e.target&&e.target.closest?e.target.closest('[data-stx-retry-button]'):null;
|
|
1843
|
+
if(!b)return;
|
|
1844
|
+
var holder=b.closest('[data-stx-retry]');
|
|
1845
|
+
if(holder)navigate(holder.getAttribute('data-stx-retry'),'replace',true);
|
|
1846
|
+
});
|
|
1847
|
+
window.addEventListener('online',function(){var u=retryShown();if(u)navigate(u,'replace',true)});
|
|
1701
1848
|
|
|
1702
1849
|
// A full document is only safe to partial-swap into the stx shell if it is
|
|
1703
1850
|
// itself stx-rendered. Pages served by another engine mounted on the same
|
|
@@ -1743,7 +1890,7 @@ else {
|
|
|
1743
1890
|
|
|
1744
1891
|
function announceRoute(){
|
|
1745
1892
|
if(o.announceRoute===false)return;
|
|
1746
|
-
var c=
|
|
1893
|
+
var c=activeRoot();
|
|
1747
1894
|
var h=c&&c.querySelector?c.querySelector('h1'):null;
|
|
1748
1895
|
// The heading names the destination better than the title, which often
|
|
1749
1896
|
// carries a site-name suffix. Falls back to the title, then gives up
|
|
@@ -1893,7 +2040,12 @@ else {
|
|
|
1893
2040
|
var declaredRuntime=fragMark&&fragMark[1]?fragMark[1]:'';
|
|
1894
2041
|
if(isFragment)html=html.slice(fragMark[0].length);
|
|
1895
2042
|
if(!isFragment&&!isStxDocument(html)){log('[router] non-stx document \u2014 full navigation to:',url);location.href=url;return Promise.resolve(false)}
|
|
2043
|
+
var ctx=pendingCtx||{direction:pushState==='replace'?'replace':pushState==='tab'?'tab':pushState===false?'pop':'push'};
|
|
2044
|
+
pendingCtx=null;
|
|
1896
2045
|
var currentContent=getContainer();
|
|
2046
|
+
// The routed container. currentContent is where the page is written: the
|
|
2047
|
+
// container itself, or with screens retained, the new screen inside it.
|
|
2048
|
+
var host=currentContent;
|
|
1897
2049
|
log('[router] swap: isFragment='+isFragment+' container='+!!currentContent+' tag='+(currentContent&¤tContent.tagName)+' selector='+containerSel+' htmlLen='+html.length);
|
|
1898
2050
|
if(!currentContent){log('[router] no container \u2014 falling back');location.href=url;return Promise.resolve(false)}
|
|
1899
2051
|
|
|
@@ -2010,14 +2162,16 @@ else {
|
|
|
2010
2162
|
// Swap content \u2014 apply the destination container's own attributes first
|
|
2011
2163
|
// so the incoming markup lands in a correctly-laid-out container instead
|
|
2012
2164
|
// of flashing (or sticking) unstyled.
|
|
2013
|
-
|
|
2165
|
+
if(scr)scr.cattrs=pendingContainerAttrs;
|
|
2166
|
+
applyContainerAttrs(host,pendingContainerAttrs);
|
|
2014
2167
|
currentContent.innerHTML=cleanFrag;
|
|
2015
2168
|
// Remove old page scripts
|
|
2016
2169
|
qsa('script[data-stx-page]').forEach(function(s){s.remove()});
|
|
2017
2170
|
if(pushState!==false)writeHistory(pushState,url+(hash||''));
|
|
2171
|
+
if(scr)screenCommit(scr,url+(hash||''));
|
|
2018
2172
|
refreshCurrentLinks();
|
|
2019
2173
|
applyScroll(hash);
|
|
2020
|
-
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
|
|
2174
|
+
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url,direction:ctx.direction}}));
|
|
2021
2175
|
// Before page scripts run, so a page that focuses its own control on
|
|
2022
2176
|
// mount still wins \u2014 its script executes after this.
|
|
2023
2177
|
focusAfterNavigation(hash);
|
|
@@ -2122,17 +2276,28 @@ else {
|
|
|
2122
2276
|
if(fragPending.length)Promise.all(fragPending).then(runFragScripts).catch(runFragScripts);
|
|
2123
2277
|
else runFragScripts();
|
|
2124
2278
|
}
|
|
2279
|
+
// With screens retained, the outgoing page is kept (or disposed once it
|
|
2280
|
+
// has animated away) by screenBegin rather than cleaned up here.
|
|
2281
|
+
var scr=null;
|
|
2282
|
+
var useScreens=screensOn();
|
|
2125
2283
|
function startFragSwap(){
|
|
2126
|
-
if(
|
|
2284
|
+
if(ctx.id&&ctx.id!==navSeq)return Promise.resolve(false);
|
|
2285
|
+
if(pushState!==false)innerSnap=innerScroll(activeRoot());
|
|
2286
|
+
if(useScreens)settleAnimation();
|
|
2287
|
+
else if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
|
|
2127
2288
|
return new Promise(function(resolve,reject){
|
|
2128
2289
|
function completeFragSwap(){
|
|
2129
2290
|
try{
|
|
2291
|
+
setDirection(ctx.direction);
|
|
2292
|
+
if(useScreens){scr=screenBegin(host,ctx,html,pushState!==false);currentContent=scr.el}
|
|
2130
2293
|
doFragSwap();
|
|
2294
|
+
if(scr)screenEnd(scr);
|
|
2131
2295
|
resolve(true);
|
|
2132
2296
|
}catch(err){reject(err)}
|
|
2133
2297
|
}
|
|
2134
2298
|
if(instantNav&&swapRunsInOneTask(html,url)){runInOneTask(completeFragSwap)}
|
|
2135
2299
|
else if(instantNav&&runInstantSwap(completeFragSwap)){}
|
|
2300
|
+
else if(useScreens){completeFragSwap()}
|
|
2136
2301
|
else if(runViewTransition(completeFragSwap)){}
|
|
2137
2302
|
else if(instantNav){completeFragSwap()}
|
|
2138
2303
|
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)}
|
|
@@ -2184,7 +2349,7 @@ else {
|
|
|
2184
2349
|
function doSwap(){
|
|
2185
2350
|
// \u2500\u2500 Swap <head> styles \u2500\u2500
|
|
2186
2351
|
// Inject new styles FIRST, then remove old to prevent unstyled flash
|
|
2187
|
-
var keepIds={'stx-
|
|
2352
|
+
var keepIds={'stx-r-css':1};
|
|
2188
2353
|
var curStyles=qsa('head style');
|
|
2189
2354
|
var newStyles=doc.querySelectorAll('head style');
|
|
2190
2355
|
|
|
@@ -2518,7 +2683,7 @@ else {
|
|
|
2518
2683
|
else curRoot.removeAttribute('data-stx-html-attrs');
|
|
2519
2684
|
}
|
|
2520
2685
|
|
|
2521
|
-
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
|
|
2686
|
+
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url,direction:ctx.direction}}));
|
|
2522
2687
|
|
|
2523
2688
|
// Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
|
|
2524
2689
|
function execScripts(){
|
|
@@ -2572,6 +2737,13 @@ else {
|
|
|
2572
2737
|
}
|
|
2573
2738
|
|
|
2574
2739
|
function startSwap(){
|
|
2740
|
+
if(ctx.id&&ctx.id!==navSeq)return Promise.resolve(false);
|
|
2741
|
+
if(pushState!==false)innerSnap=innerScroll(activeRoot());
|
|
2742
|
+
setDirection(ctx.direction);
|
|
2743
|
+
// A whole document is written into the container as it is, so retained
|
|
2744
|
+
// screens go first: they would otherwise outlive the page they belong to.
|
|
2745
|
+
collapseScreens();
|
|
2746
|
+
currentContent=getContainer();
|
|
2575
2747
|
// Clean up existing signals/effects
|
|
2576
2748
|
if(window.stx&&window.stx._cleanupContainer){
|
|
2577
2749
|
window.stx._cleanupContainer(currentContent);
|
|
@@ -2607,6 +2779,748 @@ else {
|
|
|
2607
2779
|
return afterCss(docCssReady,url,startSwap);
|
|
2608
2780
|
}
|
|
2609
2781
|
|
|
2782
|
+
// \u2500\u2500 Screens \u2500\u2500
|
|
2783
|
+
// A phone app keeps the screens it has shown. Going back finds the list
|
|
2784
|
+
// exactly where it was left, and switching tabs finds the other tab as it
|
|
2785
|
+
// was: the same DOM, its scroll, its signals still live. Re-rendering each
|
|
2786
|
+
// of them from the server on every visit is what made an stx app read as a
|
|
2787
|
+
// web page in a shell.
|
|
2788
|
+
//
|
|
2789
|
+
// So with screens on, each page is written into a screen of its own,
|
|
2790
|
+
// <div data-stx-screen>, inside the routed container. Leaving one by a push
|
|
2791
|
+
// or a tab switch hides it instead of disposing it; Back and the tab bar
|
|
2792
|
+
// show it again without running anything. Each tab owns a stack of them,
|
|
2793
|
+
// mirrored by the history entries above the one the app opened with.
|
|
2794
|
+
//
|
|
2795
|
+
// The runtime binds whichever element carries data-stx-content before it
|
|
2796
|
+
// falls back to the router's container, so the screen being shown wears
|
|
2797
|
+
// that marker and a navigation binds the new screen alone, leaving the
|
|
2798
|
+
// retained ones as they are. A container that is itself [data-stx-content]
|
|
2799
|
+
// (the app-shell mode) keeps the old behaviour, as does a page without the
|
|
2800
|
+
// signals runtime, which has nothing to retain.
|
|
2801
|
+
//
|
|
2802
|
+
// On when the page has tab links (data-stx-nav="tab"), or always with
|
|
2803
|
+
// screens:true; never with screens:false.
|
|
2804
|
+
var SCREEN='data-stx-screen';
|
|
2805
|
+
var HIDDEN='data-stx-screen-hidden';
|
|
2806
|
+
var TAB_SEL='[data-stx-nav="tab"],[data-native-tab]';
|
|
2807
|
+
var tabs={};
|
|
2808
|
+
var curTab='';
|
|
2809
|
+
var active=null;
|
|
2810
|
+
var shownSeq=0;
|
|
2811
|
+
var screensLive=false;
|
|
2812
|
+
function screensOn(){
|
|
2813
|
+
if(screensLive)return true;
|
|
2814
|
+
if(o.screens===false||o.screens==='false'||!shouldUseFragmentResponse())return false;
|
|
2815
|
+
var stx=window.stx;
|
|
2816
|
+
if(!stx||typeof stx._cleanupContainer!=='function')return false;
|
|
2817
|
+
var host=getContainer();
|
|
2818
|
+
if(!host||(host.closest&&host.closest('[data-stx-content]')))return false;
|
|
2819
|
+
return o.screens===true||!!qs(TAB_SEL);
|
|
2820
|
+
}
|
|
2821
|
+
// Where the current page lives: its screen, or the container.
|
|
2822
|
+
function activeRoot(){return active&&active.el&&active.el.isConnected?active.el:getContainer()}
|
|
2823
|
+
// A selector looked up in the page on screen first: a retained screen
|
|
2824
|
+
// carries the same ids as the page that rendered it.
|
|
2825
|
+
function findIn(sel){
|
|
2826
|
+
var root=activeRoot(),el=null;
|
|
2827
|
+
try{el=root&&root.querySelector?root.querySelector(sel):null;if(!el)el=qs(sel)}catch(e){}
|
|
2828
|
+
return el;
|
|
2829
|
+
}
|
|
2830
|
+
function stackOf(id){return tabs[id]||(tabs[id]=[])}
|
|
2831
|
+
function newEntry(url,token,depth){return{url:url,key:cacheKey(url),token:token,depth:depth,tab:curTab,el:null,sx:0,sy:0,inner:null,destroys:[],cattrs:'',shown:0}}
|
|
2832
|
+
function tabId(href){return cacheKey(withCurrentLocale(href))}
|
|
2833
|
+
function tabLinks(){return qsa(TAB_SEL)}
|
|
2834
|
+
// The tab the page on screen belongs to: the one the tab bar has current,
|
|
2835
|
+
// which the sticky nav keeps lit for a screen opened from it.
|
|
2836
|
+
function currentTabId(){
|
|
2837
|
+
var links=tabLinks(),best='',i;
|
|
2838
|
+
for(i=0;i<links.length;i++)if(links[i].hasAttribute('data-stx-nav-current'))return tabId(links[i].getAttribute('href')||'');
|
|
2839
|
+
for(i=0;i<links.length;i++){
|
|
2840
|
+
var st=linkState(links[i].getAttribute('href')||'',links[i].getAttribute('data-stx-active-match'));
|
|
2841
|
+
if(st&&(st.exact||st.matched))return tabId(links[i].getAttribute('href')||'');
|
|
2842
|
+
if(st&&st.active&&!best)best=tabId(links[i].getAttribute('href')||'');
|
|
2843
|
+
}
|
|
2844
|
+
return best;
|
|
2845
|
+
}
|
|
2846
|
+
// Which tab link the bar shows as current. Set by hand on a switch: the
|
|
2847
|
+
// sticky nav would otherwise keep the old tab lit over the other tab's
|
|
2848
|
+
// pushed screen, since neither tab's own URL is on screen.
|
|
2849
|
+
function markTab(id){
|
|
2850
|
+
tabLinks().forEach(function(a){
|
|
2851
|
+
if(tabId(a.getAttribute('href')||'')===id)a.setAttribute('data-stx-nav-current','');
|
|
2852
|
+
else a.removeAttribute('data-stx-nav-current');
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
function hostAttrString(host){
|
|
2856
|
+
var out=[];
|
|
2857
|
+
if(host&&host.getAttributeNames)host.getAttributeNames().forEach(function(n){
|
|
2858
|
+
if(n==='data-stx-cattrs'||n==='data-stx-route-focus'||n==='data-stx-animating'||(n==='tabindex'&&host.hasAttribute('data-stx-route-focus')))return;
|
|
2859
|
+
out.push(n+'="'+escAttr(host.getAttribute(n))+'"');
|
|
2860
|
+
});
|
|
2861
|
+
return encodeURIComponent(out.join(' '));
|
|
2862
|
+
}
|
|
2863
|
+
// The page the app opened with becomes the first screen the first time a
|
|
2864
|
+
// screen is needed. Moved, not re-rendered: its bindings stay on its nodes.
|
|
2865
|
+
function adoptInitial(host){
|
|
2866
|
+
if(active&&active.el&&active.el.isConnected)return;
|
|
2867
|
+
screensLive=true;
|
|
2868
|
+
var el=ce('div');
|
|
2869
|
+
el.setAttribute(SCREEN,'');
|
|
2870
|
+
while(host.firstChild)el.appendChild(host.firstChild);
|
|
2871
|
+
host.appendChild(el);
|
|
2872
|
+
if(host.__stx_disposers){el.__stx_disposers=host.__stx_disposers;host.__stx_disposers=null}
|
|
2873
|
+
if(!curTab)curTab=currentTabId();
|
|
2874
|
+
active=newEntry(location.pathname+location.search+location.hash,scrollToken,histDepth);
|
|
2875
|
+
active.el=el;
|
|
2876
|
+
active.cattrs=hostAttrString(host);
|
|
2877
|
+
active.shown=++shownSeq;
|
|
2878
|
+
stackOf(curTab)[histDepth]=active;
|
|
2879
|
+
markActive(el);
|
|
2880
|
+
}
|
|
2881
|
+
function markActive(el){
|
|
2882
|
+
var host=getContainer();
|
|
2883
|
+
if(host)Array.prototype.forEach.call(host.querySelectorAll('['+SCREEN+'][data-stx-content]'),function(other){if(other!==el)other.removeAttribute('data-stx-content')});
|
|
2884
|
+
el.setAttribute('data-stx-content','');
|
|
2885
|
+
el.removeAttribute(HIDDEN);
|
|
2886
|
+
}
|
|
2887
|
+
function saveScroll(e){
|
|
2888
|
+
e.sx=window.pageXOffset||window.scrollX||0;
|
|
2889
|
+
e.sy=window.pageYOffset||window.scrollY||0;
|
|
2890
|
+
e.inner=innerScroll(e.el);
|
|
2891
|
+
}
|
|
2892
|
+
// Everything that is global to the page on screen, kept with it while it is
|
|
2893
|
+
// hidden: its page-level onDestroy callbacks (the runtime runs whatever is
|
|
2894
|
+
// queued on the next stx:load, which would tear a retained page down), its
|
|
2895
|
+
// setup, its server data and its route params.
|
|
2896
|
+
function saveEntry(e){
|
|
2897
|
+
var stx=window.stx||{};
|
|
2898
|
+
e.title=document.title;
|
|
2899
|
+
e.setup=stx._latestSetup;
|
|
2900
|
+
e.data=window.__STX_DATA__;
|
|
2901
|
+
e.config=window.__STX_RUNTIME_CONFIG__;
|
|
2902
|
+
e.params=stx._rp||window.__stx_rp||{};
|
|
2903
|
+
if(stx._destroyCallbacks)e.destroys=e.destroys.concat(stx._destroyCallbacks.splice(0));
|
|
2904
|
+
}
|
|
2905
|
+
function restoreEntry(e){
|
|
2906
|
+
var stx=window.stx||{};
|
|
2907
|
+
if(e.title)document.title=e.title;
|
|
2908
|
+
if(e.setup)stx._latestSetup=e.setup;
|
|
2909
|
+
window.__STX_DATA__=e.data||{};
|
|
2910
|
+
window.__STX_RUNTIME_CONFIG__=e.config||{};
|
|
2911
|
+
if(stx._destroyCallbacks&&e.destroys.length)Array.prototype.push.apply(stx._destroyCallbacks,e.destroys);
|
|
2912
|
+
e.destroys=[];
|
|
2913
|
+
applyContainerAttrs(getContainer(),e.cattrs);
|
|
2914
|
+
}
|
|
2915
|
+
// A new page must not share the outgoing page's scope object, or its setup
|
|
2916
|
+
// merges into the one the retained screen's bindings read. Cleaning an
|
|
2917
|
+
// empty element resets that scope and nothing else.
|
|
2918
|
+
function resetScope(){
|
|
2919
|
+
var stx=window.stx;
|
|
2920
|
+
if(!stx||!stx._cleanupContainer)return;
|
|
2921
|
+
var keep=stx._mountCallbacks?stx._mountCallbacks.splice(0):[];
|
|
2922
|
+
stx._cleanupContainer(ce('div'));
|
|
2923
|
+
if(stx._mountCallbacks)Array.prototype.push.apply(stx._mountCallbacks,keep);
|
|
2924
|
+
}
|
|
2925
|
+
function hideScreen(e){
|
|
2926
|
+
if(!e||!e.el)return;
|
|
2927
|
+
unfreeze(e.el);
|
|
2928
|
+
e.el.setAttribute(HIDDEN,'');
|
|
2929
|
+
e.el.removeAttribute('data-stx-content');
|
|
2930
|
+
}
|
|
2931
|
+
// Disposal runs the same paths a navigation always has: the runtime's
|
|
2932
|
+
// container cleanup (effects, scopes, element hooks) and the page-level
|
|
2933
|
+
// onDestroy callbacks that were put aside when the screen was hidden.
|
|
2934
|
+
// Pending mount callbacks belong to whatever is arriving, so they survive.
|
|
2935
|
+
function disposeEntry(e){
|
|
2936
|
+
if(!e||!e.el)return;
|
|
2937
|
+
var el=e.el,stx=window.stx;
|
|
2938
|
+
e.el=null;
|
|
2939
|
+
if(e===active)saveEntry(e);
|
|
2940
|
+
if(stx&&stx._cleanupContainer){
|
|
2941
|
+
var keep=stx._mountCallbacks?stx._mountCallbacks.splice(0):[];
|
|
2942
|
+
try{stx._cleanupContainer(el)}catch(err){console.error('[router] screen cleanup failed:',err)}
|
|
2943
|
+
if(stx._mountCallbacks)Array.prototype.push.apply(stx._mountCallbacks,keep);
|
|
2944
|
+
}
|
|
2945
|
+
var fns=e.destroys;e.destroys=[];
|
|
2946
|
+
fns.forEach(function(fn){try{fn()}catch(err){console.warn('[stx] destroy callback error:',err)}});
|
|
2947
|
+
if(el.parentNode)el.parentNode.removeChild(el);
|
|
2948
|
+
}
|
|
2949
|
+
function eachEntry(fn){for(var id in tabs)tabs[id].forEach(function(e){if(e)fn(e,id)})}
|
|
2950
|
+
// Keep at most screenLimit screens alive, the least recently shown going
|
|
2951
|
+
// first. An evicted screen stays in its stack as an address: Back to it
|
|
2952
|
+
// loads it again.
|
|
2953
|
+
function enforceLimit(){
|
|
2954
|
+
var live=[];
|
|
2955
|
+
eachEntry(function(e){if(e.el&&e!==active&&!e.leaving)live.push(e)});
|
|
2956
|
+
live.sort(function(a,b){return a.shown-b.shown});
|
|
2957
|
+
while(live.length+1>Math.max(1,o.screenLimit|0))disposeEntry(live.shift());
|
|
2958
|
+
}
|
|
2959
|
+
// Everything back to the plain container: before a whole-document swap and
|
|
2960
|
+
// when the screens are given up.
|
|
2961
|
+
// The screen on screen is left in place: the swap cleans the container,
|
|
2962
|
+
// and it with it, as it always has.
|
|
2963
|
+
function collapseScreens(){
|
|
2964
|
+
if(!screensLive)return;
|
|
2965
|
+
settleAnimation();
|
|
2966
|
+
eachEntry(function(e){if(e!==active)disposeEntry(e)});
|
|
2967
|
+
tabs={};active=null;screensLive=false;
|
|
2968
|
+
}
|
|
2969
|
+
// Component scopes are keyed by ids the server derives from the page, so a
|
|
2970
|
+
// second visit to the same page, pushed over a retained first one, brings
|
|
2971
|
+
// the same ids. The retained copies are renamed out of the way, registry
|
|
2972
|
+
// entry and all, so each instance keeps its own and disposing one cannot
|
|
2973
|
+
// reach the other.
|
|
2974
|
+
var retagSeq=0;
|
|
2975
|
+
function retagCollisions(html){
|
|
2976
|
+
var ids={},any=false;
|
|
2977
|
+
html.replace(/data-stx-scope="([^"]+)"/g,function(m,id){ids[id]=1;any=true;return m});
|
|
2978
|
+
if(!any)return;
|
|
2979
|
+
var scopes=window.stx&&window.stx._scopes;
|
|
2980
|
+
eachEntry(function(e){
|
|
2981
|
+
if(!e.el)return;
|
|
2982
|
+
Array.prototype.forEach.call(e.el.querySelectorAll('[data-stx-scope]'),function(node){
|
|
2983
|
+
var id=node.getAttribute('data-stx-scope');
|
|
2984
|
+
if(!ids[id])return;
|
|
2985
|
+
var next=id+'-r'+(++retagSeq);
|
|
2986
|
+
node.setAttribute('data-stx-scope',next);
|
|
2987
|
+
if(scopes&&scopes[id]&&(!scopes[id].__el||scopes[id].__el===node)){scopes[next]=scopes[id];delete scopes[id]}
|
|
2988
|
+
});
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
// The swap's half: before the page is written, the outgoing screen is put
|
|
2993
|
+
// aside (lifted out of the page for a slide, or hidden), and a new screen
|
|
2994
|
+
// is made for the page to go into, first in the container so that a lookup
|
|
2995
|
+
// by id finds the page on screen before any retained copy of it.
|
|
2996
|
+
function screenBegin(host,ctx,html,writes){
|
|
2997
|
+
adoptInitial(host);
|
|
2998
|
+
var from=active;
|
|
2999
|
+
retagCollisions(html);
|
|
3000
|
+
if(from&&from.el){
|
|
3001
|
+
// A popstate saved the outgoing position already, under the token it
|
|
3002
|
+
// has since replaced; saving again here would file it under the wrong one.
|
|
3003
|
+
if(writes){rememberScroll();scrollSaved=true}
|
|
3004
|
+
saveEntry(from);
|
|
3005
|
+
}
|
|
3006
|
+
resetScope();
|
|
3007
|
+
var el=ce('div');
|
|
3008
|
+
el.setAttribute(SCREEN,'');
|
|
3009
|
+
var anim=!!(from&&from.el&&canAnimate(ctx.direction));
|
|
3010
|
+
if(anim)freeze(from.el);
|
|
3011
|
+
else if(from&&from.el)hideScreen(from);
|
|
3012
|
+
host.insertBefore(el,host.firstChild);
|
|
3013
|
+
markActive(el);
|
|
3014
|
+
return{el:el,from:from,ctx:ctx,anim:anim,cattrs:''};
|
|
3015
|
+
}
|
|
3016
|
+
// After the history write, so the entry knows its token and depth.
|
|
3017
|
+
function screenCommit(scr,href){
|
|
3018
|
+
var e=scr.ctx.entry||newEntry(href,scrollToken,histDepth);
|
|
3019
|
+
e.url=href;e.key=cacheKey(href);e.token=scrollToken;e.depth=histDepth;e.tab=curTab;
|
|
3020
|
+
e.el=scr.el;e.cattrs=scr.cattrs;e.shown=++shownSeq;e.destroys=[];
|
|
3021
|
+
var stack=stackOf(curTab);
|
|
3022
|
+
// A push drops whatever was forward of it; a pop or a replacement takes
|
|
3023
|
+
// the place of what was at its depth. The outgoing screen is left to the
|
|
3024
|
+
// transition, which disposes it once it is off screen.
|
|
3025
|
+
for(var i=e.depth;i<stack.length;i++){var x=stack[i];if(x&&x!==e&&x!==scr.from)disposeEntry(x)}
|
|
3026
|
+
stack.length=e.depth;
|
|
3027
|
+
stack[e.depth]=e;
|
|
3028
|
+
active=e;
|
|
3029
|
+
scrollSaved=false;
|
|
3030
|
+
}
|
|
3031
|
+
function screenEnd(scr){
|
|
3032
|
+
var from=scr.from,dir=scr.ctx.direction;
|
|
3033
|
+
if(!from||!from.el){finishScreen();return}
|
|
3034
|
+
// Retained: left by a push, or by a tab switch. Popped or replaced: gone.
|
|
3035
|
+
var keep=dir==='push'||dir==='tab';
|
|
3036
|
+
var stack=tabs[from.tab];
|
|
3037
|
+
if(keep&&!(stack&&stack[from.depth]===from))keep=false;
|
|
3038
|
+
from.leaving=true;
|
|
3039
|
+
var after=function(){
|
|
3040
|
+
from.leaving=false;
|
|
3041
|
+
if(keep&&from.el)hideScreen(from);
|
|
3042
|
+
else disposeEntry(from);
|
|
3043
|
+
finishScreen();
|
|
3044
|
+
};
|
|
3045
|
+
if(scr.anim)animateScreens(from.el,scr.el,dir,after);
|
|
3046
|
+
else after();
|
|
3047
|
+
}
|
|
3048
|
+
function finishScreen(){
|
|
3049
|
+
enforceLimit();
|
|
3050
|
+
observeLinks();
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// Show a screen that is already here: Back to one below in the stack, or a
|
|
3054
|
+
// tab's top screen. Nothing is fetched and no page script runs; the page is
|
|
3055
|
+
// told it is on screen again (stx:screen-shown) so it can refresh whatever
|
|
3056
|
+
// may have gone stale while it was hidden.
|
|
3057
|
+
function showRetained(to,dir,opts){
|
|
3058
|
+
opts=opts||{};
|
|
3059
|
+
settleAnimation();
|
|
3060
|
+
var from=active,host=getContainer();
|
|
3061
|
+
if(navAbort){try{navAbort.abort()}catch(e){}navAbort=null}
|
|
3062
|
+
navSeq++;
|
|
3063
|
+
isNavigating=false;
|
|
3064
|
+
document.body.classList.remove(o.loadingClass);
|
|
3065
|
+
if(from&&from.el){if(!opts.settled)saveScroll(from);saveEntry(from)}
|
|
3066
|
+
setDirection(dir);
|
|
3067
|
+
var anim=!opts.settled&&from&&from.el&&from!==to&&canAnimate(dir,true);
|
|
3068
|
+
if(anim)freeze(from.el);
|
|
3069
|
+
else if(from&&from.el&&from!==to&&!opts.settled)hideScreen(from);
|
|
3070
|
+
unfreeze(to.el);
|
|
3071
|
+
markActive(to.el);
|
|
3072
|
+
active=to;
|
|
3073
|
+
to.shown=++shownSeq;
|
|
3074
|
+
restoreEntry(to);
|
|
3075
|
+
pendingScroll=null;pendingInner=null;
|
|
3076
|
+
window.scrollTo({left:to.sx,top:to.sy,behavior:'instant'});
|
|
3077
|
+
applyInner(to.el,to.inner);
|
|
3078
|
+
if(opts.settled&&from&&from!==to&&from.el)hideScreen(from);
|
|
3079
|
+
refreshCurrentLinks();
|
|
3080
|
+
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:to.url,direction:dir,retained:true,params:to.params||{}}}));
|
|
3081
|
+
focusAfterNavigation('');
|
|
3082
|
+
setTimeout(announceRoute,0);
|
|
3083
|
+
window.dispatchEvent(new CustomEvent('stx:screen-shown',{detail:{url:to.url,direction:dir}}));
|
|
3084
|
+
revalidate(to.key,to.url);
|
|
3085
|
+
var keep=dir==='push'||dir==='tab';
|
|
3086
|
+
var after=function(){
|
|
3087
|
+
if(from&&from!==to){
|
|
3088
|
+
if(dir==='pop'){
|
|
3089
|
+
// Everything above the screen shown is gone from the stack.
|
|
3090
|
+
var stack=tabs[to.tab]||[];
|
|
3091
|
+
for(var i=to.depth+1;i<stack.length;i++)if(stack[i]&&stack[i]!==from)disposeEntry(stack[i]);
|
|
3092
|
+
stack.length=to.depth+1;
|
|
3093
|
+
}
|
|
3094
|
+
if(keep&&from.el)hideScreen(from);
|
|
3095
|
+
else if(!keep)disposeEntry(from);
|
|
3096
|
+
}
|
|
3097
|
+
finishScreen();
|
|
3098
|
+
settleDirection();
|
|
3099
|
+
};
|
|
3100
|
+
if(anim){from.leaving=true;animateScreens(from.el,to.el,dir,function(){from.leaving=false;after()})}
|
|
3101
|
+
else after();
|
|
3102
|
+
return true;
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
// \u2500\u2500 Direction and motion \u2500\u2500
|
|
3106
|
+
// data-nav-direction on <html> says what kind of move is under way --
|
|
3107
|
+
// push, pop, tab or replace -- for as long as it is, so CSS (and a page's
|
|
3108
|
+
// own transitions) can tell a step forward from a step back.
|
|
3109
|
+
var animEnd=null;
|
|
3110
|
+
function setDirection(d){if(d)document.documentElement.setAttribute('data-nav-direction',d)}
|
|
3111
|
+
function settleDirection(){
|
|
3112
|
+
if(isNavigating||animEnd)return;
|
|
3113
|
+
document.documentElement.removeAttribute('data-nav-direction');
|
|
3114
|
+
}
|
|
3115
|
+
function reducedMotion(){
|
|
3116
|
+
try{return !!(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches)}catch(e){return false}
|
|
3117
|
+
}
|
|
3118
|
+
// instantNav belongs to a navigation that fetched; a screen shown from
|
|
3119
|
+
// the stack (Back) is not one, and must not inherit the last tab tap's.
|
|
3120
|
+
function canAnimate(dir,shown){
|
|
3121
|
+
return (dir==='push'||dir==='pop')&&(shown||!instantNav)&&o.navDuration>0&&typeof window.getComputedStyle==='function';
|
|
3122
|
+
}
|
|
3123
|
+
// The first background behind the container that is not transparent: the
|
|
3124
|
+
// screens are transparent, and two of them sliding over each other need
|
|
3125
|
+
// the page's colour to hide the one underneath.
|
|
3126
|
+
function screenBackground(host){
|
|
3127
|
+
for(var n=host;n&&n.nodeType===1;n=n.parentNode){
|
|
3128
|
+
var c='';
|
|
3129
|
+
try{c=getComputedStyle(n).backgroundColor}catch(e){}
|
|
3130
|
+
if(c&&c!=='transparent'&&c!=='rgba(0, 0, 0, 0)')return c;
|
|
3131
|
+
}
|
|
3132
|
+
return '';
|
|
3133
|
+
}
|
|
3134
|
+
// Lifted out of the flow and pinned where it is on screen, as a box the
|
|
3135
|
+
// height of the viewport scrolled to the same place: the page can scroll
|
|
3136
|
+
// to wherever the next screen wants, the outgoing one does not move, and a
|
|
3137
|
+
// sticky header inside it stays stuck.
|
|
3138
|
+
function freeze(el,top){
|
|
3139
|
+
var r=el.getBoundingClientRect();
|
|
3140
|
+
var vh=window.innerHeight||document.documentElement.clientHeight||0;
|
|
3141
|
+
var at=top===undefined?r.top:top;
|
|
3142
|
+
var y=Math.max(at,0);
|
|
3143
|
+
var s=el.style;
|
|
3144
|
+
s.position='fixed';s.top=y+'px';s.left=r.left+'px';s.width=r.width+'px';s.height=Math.max(vh-y,0)+'px';s.overflow='hidden';s.margin='0';
|
|
3145
|
+
el.scrollTop=y-at;
|
|
3146
|
+
}
|
|
3147
|
+
function unfreeze(el){
|
|
3148
|
+
if(!el)return;
|
|
3149
|
+
var s=el.style;
|
|
3150
|
+
['position','top','left','width','height','overflow','margin','transform','transition','zIndex','backgroundColor','pointerEvents'].forEach(function(k){s[k]=''});
|
|
3151
|
+
el.removeAttribute('data-stx-screen-enter');
|
|
3152
|
+
el.removeAttribute('data-stx-screen-leave');
|
|
3153
|
+
el.scrollTop=0;
|
|
3154
|
+
}
|
|
3155
|
+
function parseDuration(v){
|
|
3156
|
+
v=String(v||'').split(',')[0].trim();
|
|
3157
|
+
var n=parseFloat(v);
|
|
3158
|
+
if(!(n>=0))return -1;
|
|
3159
|
+
return v.indexOf('ms')>0?n:n*1000;
|
|
3160
|
+
}
|
|
3161
|
+
function dimLayer(host){
|
|
3162
|
+
var r=host.getBoundingClientRect();
|
|
3163
|
+
var dim=ce('div');
|
|
3164
|
+
dim.setAttribute('data-stx-screen-dim','');
|
|
3165
|
+
dim.style.left=r.left+'px';
|
|
3166
|
+
dim.style.width=r.width+'px';
|
|
3167
|
+
return dim;
|
|
3168
|
+
}
|
|
3169
|
+
// Two real screens, animated by the shipped CSS: the one arriving and the
|
|
3170
|
+
// one leaving, and a dim between them over the one underneath.
|
|
3171
|
+
function animateScreens(fromEl,toEl,dir,done){
|
|
3172
|
+
var host=getContainer();
|
|
3173
|
+
var bg=screenBackground(host);
|
|
3174
|
+
var dim=dimLayer(host);
|
|
3175
|
+
host.setAttribute('data-stx-animating','');
|
|
3176
|
+
fromEl.style.backgroundColor=bg;toEl.style.backgroundColor=bg;
|
|
3177
|
+
fromEl.style.pointerEvents='none';
|
|
3178
|
+
fromEl.setAttribute('data-stx-screen-leave','');
|
|
3179
|
+
toEl.setAttribute('data-stx-screen-enter','');
|
|
3180
|
+
host.appendChild(dim);
|
|
3181
|
+
var ms=-1;
|
|
3182
|
+
try{ms=parseDuration(getComputedStyle(toEl).animationDuration)}catch(e){}
|
|
3183
|
+
if(!(ms>0))ms=o.navDuration;
|
|
3184
|
+
var timer=setTimeout(end,ms+120);
|
|
3185
|
+
function onEnd(e){if(e.target===toEl)end()}
|
|
3186
|
+
toEl.addEventListener('animationend',onEnd);
|
|
3187
|
+
function end(){
|
|
3188
|
+
if(animEnd!==end)return;
|
|
3189
|
+
animEnd=null;
|
|
3190
|
+
clearTimeout(timer);
|
|
3191
|
+
toEl.removeEventListener('animationend',onEnd);
|
|
3192
|
+
if(dim.parentNode)dim.parentNode.removeChild(dim);
|
|
3193
|
+
host.removeAttribute('data-stx-animating');
|
|
3194
|
+
toEl.removeAttribute('data-stx-screen-enter');
|
|
3195
|
+
toEl.style.backgroundColor='';
|
|
3196
|
+
done();
|
|
3197
|
+
settleDirection();
|
|
3198
|
+
}
|
|
3199
|
+
animEnd=end;
|
|
3200
|
+
}
|
|
3201
|
+
// A navigation that starts while a slide is still running finishes the
|
|
3202
|
+
// slide first, rather than lifting a screen that is halfway across.
|
|
3203
|
+
function settleAnimation(){if(animEnd)animEnd()}
|
|
3204
|
+
|
|
3205
|
+
// \u2500\u2500 Tabs \u2500\u2500
|
|
3206
|
+
// Back never crosses tabs: the history above the entry the app opened with
|
|
3207
|
+
// is always the current tab's stack. A switch rewinds to that entry, writes
|
|
3208
|
+
// the target tab's stack in its place, and shows its top screen exactly as
|
|
3209
|
+
// it was left.
|
|
3210
|
+
var rewinding=null;
|
|
3211
|
+
function rewind(){
|
|
3212
|
+
if(histDepth<=0)return Promise.resolve();
|
|
3213
|
+
return new Promise(function(resolve){
|
|
3214
|
+
var n=histDepth;
|
|
3215
|
+
var timer=setTimeout(finish,700);
|
|
3216
|
+
function finish(){
|
|
3217
|
+
clearTimeout(timer);
|
|
3218
|
+
if(rewinding===finish)rewinding=null;
|
|
3219
|
+
histDepth=(history.state&&history.state[DEPTH])|0;
|
|
3220
|
+
var token=history.state&&history.state[SCROLL_TOKEN];
|
|
3221
|
+
if(token)scrollToken=token;
|
|
3222
|
+
resolve();
|
|
3223
|
+
}
|
|
3224
|
+
rewinding=finish;
|
|
3225
|
+
history.go(-n);
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
function entryState(e,pushed){
|
|
3229
|
+
var st={};
|
|
3230
|
+
st[SCROLL_TOKEN]=e.token;st[DEPTH]=e.depth;st[TAB]=e.tab;
|
|
3231
|
+
if(pushed)st[PUSHED_MARK]=true;
|
|
3232
|
+
return st;
|
|
3233
|
+
}
|
|
3234
|
+
function selectTab(href){
|
|
3235
|
+
if(!href)return Promise.resolve(false);
|
|
3236
|
+
var id=tabId(href);
|
|
3237
|
+
var on=screensOn();
|
|
3238
|
+
if(on&&!screensLive)adoptInitial(getContainer());
|
|
3239
|
+
if(!curTab)curTab=on&&active?active.tab:currentTabId();
|
|
3240
|
+
if(id===curTab)return reselectTab(href);
|
|
3241
|
+
if(active&&active.el){rememberScroll();saveScroll(active)}
|
|
3242
|
+
settleAnimation();
|
|
3243
|
+
if(navAbort){try{navAbort.abort()}catch(e){}navAbort=null}
|
|
3244
|
+
var seq=++navSeq;
|
|
3245
|
+
return rewind().then(function(){
|
|
3246
|
+
if(seq!==navSeq)return false;
|
|
3247
|
+
curTab=id;
|
|
3248
|
+
markTab(id);
|
|
3249
|
+
var stack=on?(tabs[id]||[]).filter(Boolean):[];
|
|
3250
|
+
if(!stack.length){
|
|
3251
|
+
if(on)tabs[id]=[];
|
|
3252
|
+
navCtxNext={direction:'tab'};
|
|
3253
|
+
instantNext=true;
|
|
3254
|
+
return navigate(href,'tab');
|
|
3255
|
+
}
|
|
3256
|
+
// Rewritten in order, so Back walks this tab's own stack.
|
|
3257
|
+
stack.forEach(function(e,i){
|
|
3258
|
+
e.depth=i;e.tab=id;
|
|
3259
|
+
if(i===0)history.replaceState(entryState(e,false),'',e.url);
|
|
3260
|
+
else history.pushState(entryState(e,true),'',e.url);
|
|
3261
|
+
});
|
|
3262
|
+
tabs[id]=stack;
|
|
3263
|
+
var top=stack[stack.length-1];
|
|
3264
|
+
histDepth=top.depth;
|
|
3265
|
+
scrollToken=top.token;
|
|
3266
|
+
if(top.el)return showRetained(top,'tab');
|
|
3267
|
+
// Its top screen was let go: load it into the same entry.
|
|
3268
|
+
pendingScroll=[top.sx,top.sy];
|
|
3269
|
+
navCtxNext={direction:'tab',entry:top};
|
|
3270
|
+
instantNext=true;
|
|
3271
|
+
return navigate(top.url,false);
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
// The tab that is already current, selected again: back to the top of its
|
|
3275
|
+
// screen, and when it is there already, back to the tab's first screen.
|
|
3276
|
+
// stx:tabreselect says which, and a page that cancels it keeps its own.
|
|
3277
|
+
function reselectTab(href){
|
|
3278
|
+
var root=activeRoot();
|
|
3279
|
+
var y=window.pageYOffset||window.scrollY||0;
|
|
3280
|
+
var inner=innerScroll(root);
|
|
3281
|
+
var atRoot=active?active.depth===0&&active.key===tabId(href):cacheKey(location.href)===tabId(href);
|
|
3282
|
+
var action=(y>0||inner)?'top':atRoot?'none':'root';
|
|
3283
|
+
var ev=new CustomEvent('stx:tabreselect',{cancelable:true,detail:{href:href,action:action}});
|
|
3284
|
+
window.dispatchEvent(ev);
|
|
3285
|
+
if(ev.defaultPrevented||action==='none')return Promise.resolve(false);
|
|
3286
|
+
if(action==='top'){
|
|
3287
|
+
window.scrollTo({top:0,left:0,behavior:reducedMotion()?'instant':'smooth'});
|
|
3288
|
+
if(root&&root.querySelectorAll)root.querySelectorAll('[data-stx-scroll]').forEach(function(el){if(el.scrollTo)el.scrollTo({top:0,left:0,behavior:'smooth'});else el.scrollTop=0});
|
|
3289
|
+
return Promise.resolve(true);
|
|
3290
|
+
}
|
|
3291
|
+
var stack=tabs[curTab]||[];
|
|
3292
|
+
var first=stack.filter(Boolean)[0];
|
|
3293
|
+
if(first&&first.el&&first.key===tabId(href)&&histDepth>0){
|
|
3294
|
+
var target=first;
|
|
3295
|
+
return new Promise(function(resolve){
|
|
3296
|
+
history.go(target.depth-histDepth);
|
|
3297
|
+
setTimeout(function(){resolve(true)},0);
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
// The tab's first screen is not here (opened on a deep link, or let
|
|
3301
|
+
// go): load the tab's own page in its place.
|
|
3302
|
+
return rewind().then(function(){
|
|
3303
|
+
navCtxNext={direction:'pop'};
|
|
3304
|
+
return navigate(href,'tab');
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
// \u2500\u2500 Swipe back \u2500\u2500
|
|
3309
|
+
// A pan from the left edge drags the screen on top away and shows the
|
|
3310
|
+
// retained one underneath, the way iOS does: the one underneath moves from
|
|
3311
|
+
// -30% and brightens as the top one goes. Released past half way, or
|
|
3312
|
+
// flicked, it completes as a Back (history.back(), shown without a second
|
|
3313
|
+
// animation); otherwise it springs back. Only where there is a screen to
|
|
3314
|
+
// go back to in this tab, never from inside data-stx-no-swipe, and never
|
|
3315
|
+
// over something that pans sideways on its own (touch-action none, pan-x,
|
|
3316
|
+
// pan-y, or a horizontal scroller that is not at its start).
|
|
3317
|
+
var swipe=null;
|
|
3318
|
+
var swipeSettled=null;
|
|
3319
|
+
function swipePrev(){
|
|
3320
|
+
if(o.swipeBack===false||!screensLive||!active||!active.el||isNavigating||animEnd)return null;
|
|
3321
|
+
var prev=(tabs[curTab]||[])[active.depth-1];
|
|
3322
|
+
return prev&&prev.el?prev:null;
|
|
3323
|
+
}
|
|
3324
|
+
function swipeBlocked(target){
|
|
3325
|
+
var host=getContainer();
|
|
3326
|
+
for(var n=target;n&&n.nodeType===1&&n!==host;n=n.parentNode){
|
|
3327
|
+
if(n.hasAttribute('data-stx-no-swipe'))return true;
|
|
3328
|
+
var cs=null;
|
|
3329
|
+
try{cs=getComputedStyle(n)}catch(e){}
|
|
3330
|
+
if(!cs)continue;
|
|
3331
|
+
var ta=String(cs.touchAction||'').split(' ');
|
|
3332
|
+
var has=function(v){return ta.indexOf(v)!==-1};
|
|
3333
|
+
if(has('none')||(has('pan-x')||has('pan-left')||has('pan-right'))!==(has('pan-y')||has('pan-up')||has('pan-down')))return true;
|
|
3334
|
+
var ox=String(cs.overflowX||'');
|
|
3335
|
+
if((ox==='auto'||ox==='scroll')&&n.scrollWidth>n.clientWidth&&n.scrollLeft>0)return true;
|
|
3336
|
+
}
|
|
3337
|
+
return false;
|
|
3338
|
+
}
|
|
3339
|
+
function now(){return window.performance&&performance.now?performance.now():Date.now()}
|
|
3340
|
+
function touchOf(e){return (e.touches&&e.touches[0])||(e.changedTouches&&e.changedTouches[0])||null}
|
|
3341
|
+
function swipeStart(e){
|
|
3342
|
+
if(swipe||!e.touches||e.touches.length!==1)return;
|
|
3343
|
+
var t=e.touches[0];
|
|
3344
|
+
if(t.clientX>o.swipeEdge)return;
|
|
3345
|
+
var prev=swipePrev();
|
|
3346
|
+
if(!prev||swipeBlocked(e.target))return;
|
|
3347
|
+
swipe={prev:prev,x0:t.clientX,y0:t.clientY,x:t.clientX,t:now(),v:0,on:false};
|
|
3348
|
+
}
|
|
3349
|
+
function swipeBegin(){
|
|
3350
|
+
var cur=active,prev=swipe.prev,host=getContainer();
|
|
3351
|
+
var w=host.getBoundingClientRect().width||window.innerWidth||1;
|
|
3352
|
+
saveScroll(cur);
|
|
3353
|
+
var docTop=cur.el.getBoundingClientRect().top+(window.pageYOffset||window.scrollY||0);
|
|
3354
|
+
swipe.w=w;
|
|
3355
|
+
swipe.cur=cur;
|
|
3356
|
+
swipe.bg=screenBackground(host);
|
|
3357
|
+
swipe.dim=dimLayer(host);
|
|
3358
|
+
swipe.dim.setAttribute('data-stx-drag','');
|
|
3359
|
+
// Shown before it is measured: a hidden screen has no box to pin.
|
|
3360
|
+
prev.el.removeAttribute(HIDDEN);
|
|
3361
|
+
freeze(prev.el,docTop-prev.sy);
|
|
3362
|
+
applyInner(prev.el,prev.inner);
|
|
3363
|
+
prev.el.style.zIndex='1';
|
|
3364
|
+
prev.el.style.backgroundColor=swipe.bg;
|
|
3365
|
+
cur.el.style.position='relative';
|
|
3366
|
+
cur.el.style.zIndex='3';
|
|
3367
|
+
cur.el.style.backgroundColor=swipe.bg;
|
|
3368
|
+
host.setAttribute('data-stx-animating','');
|
|
3369
|
+
host.appendChild(swipe.dim);
|
|
3370
|
+
setDirection('pop');
|
|
3371
|
+
swipe.on=true;
|
|
3372
|
+
}
|
|
3373
|
+
function swipeDraw(dx){
|
|
3374
|
+
var p=Math.min(Math.max(dx/swipe.w,0),1);
|
|
3375
|
+
swipe.p=p;
|
|
3376
|
+
swipe.cur.el.style.transform='translateX('+(p*swipe.w)+'px)';
|
|
3377
|
+
swipe.prev.el.style.transform='translateX('+(-0.3*swipe.w*(1-p))+'px)';
|
|
3378
|
+
swipe.dim.style.opacity=String(1-p);
|
|
3379
|
+
}
|
|
3380
|
+
function swipeMove(e){
|
|
3381
|
+
if(!swipe)return;
|
|
3382
|
+
var t=touchOf(e);
|
|
3383
|
+
if(!t)return;
|
|
3384
|
+
var dx=t.clientX-swipe.x0,dy=t.clientY-swipe.y0;
|
|
3385
|
+
if(!swipe.on){
|
|
3386
|
+
if(Math.abs(dx)<8&&Math.abs(dy)<8)return;
|
|
3387
|
+
// Vertical first, or leftwards: a scroll, not a Back.
|
|
3388
|
+
if(dx<=0||Math.abs(dy)>=Math.abs(dx)){swipe=null;return}
|
|
3389
|
+
swipeBegin();
|
|
3390
|
+
}
|
|
3391
|
+
if(e.cancelable)e.preventDefault();
|
|
3392
|
+
var n=now(),dt=n-swipe.t;
|
|
3393
|
+
if(dt>0)swipe.v=swipe.v*0.2+((t.clientX-swipe.x)/dt)*0.8;
|
|
3394
|
+
swipe.x=t.clientX;swipe.t=n;
|
|
3395
|
+
swipeDraw(dx);
|
|
3396
|
+
}
|
|
3397
|
+
function swipeEnd(e){
|
|
3398
|
+
if(!swipe)return;
|
|
3399
|
+
var s=swipe;
|
|
3400
|
+
swipe=null;
|
|
3401
|
+
if(!s.on)return;
|
|
3402
|
+
// A finger held still before lifting has no speed left.
|
|
3403
|
+
if(now()-s.t>80)s.v=0;
|
|
3404
|
+
var commit=e.type!=='touchcancel'&&(s.p>0.5||s.v>0.5);
|
|
3405
|
+
var remaining=commit?(1-s.p)*s.w:s.p*s.w;
|
|
3406
|
+
var ms=Math.round(Math.min(o.navDuration,Math.max(120,remaining/Math.max(Math.abs(s.v),0.6))));
|
|
3407
|
+
var ease='cubic-bezier(0.2, 0.9, 0.3, 1)';
|
|
3408
|
+
[s.cur.el,s.prev.el].forEach(function(el){el.style.transition='transform '+ms+'ms '+ease});
|
|
3409
|
+
s.dim.style.transition='opacity '+ms+'ms '+ease;
|
|
3410
|
+
s.cur.el.style.transform=commit?'translateX('+s.w+'px)':'translateX(0px)';
|
|
3411
|
+
s.prev.el.style.transform=commit?'translateX(0px)':'translateX('+(-0.3*s.w)+'px)';
|
|
3412
|
+
s.dim.style.opacity=commit?'0':'1';
|
|
3413
|
+
var finished=false;
|
|
3414
|
+
var finish=function(){
|
|
3415
|
+
if(finished)return;
|
|
3416
|
+
finished=true;
|
|
3417
|
+
animEnd=null;
|
|
3418
|
+
if(s.dim.parentNode)s.dim.parentNode.removeChild(s.dim);
|
|
3419
|
+
getContainer().removeAttribute('data-stx-animating');
|
|
3420
|
+
if(commit){
|
|
3421
|
+
// Already where Back would put it: the popstate that follows shows
|
|
3422
|
+
// the screen underneath as it is, with no second animation.
|
|
3423
|
+
s.cur.el.style.visibility='hidden';
|
|
3424
|
+
swipeSettled=s.prev;
|
|
3425
|
+
history.back();
|
|
3426
|
+
}
|
|
3427
|
+
else{
|
|
3428
|
+
hideScreen(s.prev);
|
|
3429
|
+
s.cur.el.style.transform='';s.cur.el.style.transition='';s.cur.el.style.position='';s.cur.el.style.zIndex='';s.cur.el.style.backgroundColor='';
|
|
3430
|
+
settleDirection();
|
|
3431
|
+
}
|
|
3432
|
+
};
|
|
3433
|
+
animEnd=finish;
|
|
3434
|
+
setTimeout(finish,ms+40);
|
|
3435
|
+
}
|
|
3436
|
+
document.addEventListener('touchstart',swipeStart,{capture:true,passive:true});
|
|
3437
|
+
document.addEventListener('touchmove',swipeMove,{capture:true,passive:false});
|
|
3438
|
+
document.addEventListener('touchend',swipeEnd,{capture:true,passive:true});
|
|
3439
|
+
document.addEventListener('touchcancel',swipeEnd,{capture:true,passive:true});
|
|
3440
|
+
// The system is short of memory: let go of every hidden screen except the
|
|
3441
|
+
// one a swipe back would show.
|
|
3442
|
+
window.addEventListener('craftMemoryWarning',function(){
|
|
3443
|
+
var keep=active?(tabs[curTab]||[])[active.depth-1]:null;
|
|
3444
|
+
eachEntry(function(e){if(e!==active&&e!==keep&&!e.leaving)disposeEntry(e)});
|
|
3445
|
+
});
|
|
3446
|
+
|
|
3447
|
+
// \u2500\u2500 Revalidation and updates \u2500\u2500
|
|
3448
|
+
// A page served from the router's cache, or a screen shown again, is asked
|
|
3449
|
+
// for once more behind the swap. If it changed, the cache takes the new copy
|
|
3450
|
+
// and stx:updated says so, so the page can refresh what it shows; the
|
|
3451
|
+
// screen is never swapped out from under the user.
|
|
3452
|
+
function revalidate(key,url){
|
|
3453
|
+
if(!o.revalidate||!o.cache||!key||prefetching[key])return;
|
|
3454
|
+
if(Date.now()-(cacheAt[key]||0)<o.revalidateAfter)return;
|
|
3455
|
+
var wantsFragment=shouldUseFragmentResponse();
|
|
3456
|
+
var before=cache[key];
|
|
3457
|
+
prefetching[key]=fetch(url,{headers:wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}}).then(function(r){
|
|
3458
|
+
var incoming=r.headers.get('X-STX-Build')||'';
|
|
3459
|
+
if(isBuildSkew(incoming)){reloadForSkew(url,incoming,true);return null}
|
|
3460
|
+
return readPrefetchResponse(r,wantsFragment);
|
|
3461
|
+
}).then(function(result){
|
|
3462
|
+
if(!result)return;
|
|
3463
|
+
if(o.cache)setCache(key,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
3464
|
+
if(before!==undefined&&before!==result.html&&cacheKey(location.href)===key)emitUpdated(url,'page');
|
|
3465
|
+
}).catch(function(){}).finally(function(){delete prefetching[key]});
|
|
3466
|
+
}
|
|
3467
|
+
// The offline worker answered from its cache and found something newer
|
|
3468
|
+
// behind it. It posts { type: 'stx:updated', url, kind }, which its own
|
|
3469
|
+
// register script turns into a window event; the router does that only
|
|
3470
|
+
// where that script is not on the page, so the event is heard once. Either
|
|
3471
|
+
// way a page the router has cached is dropped, so the next visit gets the
|
|
3472
|
+
// new one.
|
|
3473
|
+
var ownUpdate=false;
|
|
3474
|
+
function emitUpdated(url,kind){
|
|
3475
|
+
ownUpdate=true;
|
|
3476
|
+
try{window.dispatchEvent(new CustomEvent('stx:updated',{detail:{url:url,kind:kind||'page'}}))}finally{ownUpdate=false}
|
|
3477
|
+
}
|
|
3478
|
+
window.addEventListener('stx:updated',function(e){
|
|
3479
|
+
var d=e&&e.detail;
|
|
3480
|
+
if(ownUpdate||!d||!d.url||d.kind==='api')return;
|
|
3481
|
+
try{evictCache(cacheKey(d.url))}catch(err){}
|
|
3482
|
+
});
|
|
3483
|
+
try{
|
|
3484
|
+
var sw=navigator.serviceWorker;
|
|
3485
|
+
if(sw&&sw.addEventListener)sw.addEventListener('message',function(e){
|
|
3486
|
+
var d=e&&e.data;
|
|
3487
|
+
if(!d||d.type!=='stx:updated'||window.stxOffline)return;
|
|
3488
|
+
window.dispatchEvent(new CustomEvent('stx:updated',{detail:{url:d.url,kind:d.kind||'page'}}));
|
|
3489
|
+
});
|
|
3490
|
+
}catch(e){}
|
|
3491
|
+
|
|
3492
|
+
// \u2500\u2500 Prefetch on sight \u2500\u2500
|
|
3493
|
+
// Links are fetched as they scroll into view, a few per screen, so most
|
|
3494
|
+
// taps find their page already here. Not on a connection that asked to
|
|
3495
|
+
// save data, and not for a link marked data-stx-prefetch="false" or
|
|
3496
|
+
// "hover" (hover and touch still prefetch everything).
|
|
3497
|
+
var sightObserver=null;
|
|
3498
|
+
var sightCount=0;
|
|
3499
|
+
function observeLinks(){
|
|
3500
|
+
if(!o.prefetch||!o.cache||o.prefetchVisible===false||typeof IntersectionObserver!=='function')return;
|
|
3501
|
+
var c=navigator.connection;
|
|
3502
|
+
if(c&&(c.saveData||/2g/.test(c.effectiveType||'')))return;
|
|
3503
|
+
if(!sightObserver)sightObserver=new IntersectionObserver(function(entries){
|
|
3504
|
+
entries.forEach(function(en){
|
|
3505
|
+
if(!en.isIntersecting)return;
|
|
3506
|
+
sightObserver.unobserve(en.target);
|
|
3507
|
+
if(sightCount>=o.prefetchVisibleMax)return;
|
|
3508
|
+
sightCount++;
|
|
3509
|
+
prefetchLink(en.target);
|
|
3510
|
+
});
|
|
3511
|
+
});
|
|
3512
|
+
sightCount=0;
|
|
3513
|
+
var root=activeRoot()||document.body;
|
|
3514
|
+
if(!root||!root.querySelectorAll)return;
|
|
3515
|
+
root.querySelectorAll('[data-stx-link][href]').forEach(function(a){
|
|
3516
|
+
if(a.__stxSeen)return;
|
|
3517
|
+
var p=a.getAttribute('data-stx-prefetch');
|
|
3518
|
+
if(p==='false'||p==='hover'||p==='none')return;
|
|
3519
|
+
a.__stxSeen=1;
|
|
3520
|
+
sightObserver.observe(a);
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
|
|
2610
3524
|
// \u2500\u2500 Link interception \u2500\u2500
|
|
2611
3525
|
// Links the router must never touch, however it found them. This is the
|
|
2612
3526
|
// opt-out set, and it is deliberately separate from shouldIntercept's
|
|
@@ -2709,6 +3623,8 @@ else {
|
|
|
2709
3623
|
e.preventDefault();
|
|
2710
3624
|
e.stopPropagation();
|
|
2711
3625
|
log('[router] navigating to:',href);
|
|
3626
|
+
// A tab: its own stack, never a back entry (selectTab).
|
|
3627
|
+
if(link.getAttribute('data-stx-nav')==='tab'||link.hasAttribute('data-native-tab')){selectTab(href);return}
|
|
2712
3628
|
instantNext=link.getAttribute('data-stx-transition')==='none';
|
|
2713
3629
|
navigate(withCurrentLocale(href));
|
|
2714
3630
|
});
|
|
@@ -2850,16 +3766,33 @@ else {
|
|
|
2850
3766
|
|
|
2851
3767
|
// \u2500\u2500 Back/forward \u2500\u2500
|
|
2852
3768
|
window.addEventListener('popstate',function(){
|
|
3769
|
+
// A tab switch rewinding to the first entry: not a navigation of its own.
|
|
3770
|
+
if(rewinding){rewinding();return}
|
|
3771
|
+
var settled=swipeSettled;
|
|
3772
|
+
swipeSettled=null;
|
|
2853
3773
|
// Nothing has scrolled yet (restoration is manual), so the viewport still
|
|
2854
3774
|
// shows the entry being left: save it under its own token before adopting
|
|
2855
3775
|
// the popped entry's, and hand the swap the position to land on.
|
|
2856
|
-
rememberScroll();
|
|
3776
|
+
if(!settled)rememberScroll();
|
|
3777
|
+
var st=history.state;
|
|
3778
|
+
// An entry the router did not write (a page's own pushState) keeps the
|
|
3779
|
+
// depth it was written at; a lower one is a step back.
|
|
3780
|
+
var depth=st&&typeof st[DEPTH]==='number'?st[DEPTH]:histDepth;
|
|
3781
|
+
var dir=depth<histDepth?'pop':depth>histDepth?'push':'replace';
|
|
3782
|
+
histDepth=depth;
|
|
3783
|
+
var popped=st&&st[SCROLL_TOKEN];
|
|
3784
|
+
scrollToken=popped||newScrollToken();
|
|
2857
3785
|
if(restoreScroll){
|
|
2858
|
-
var popped=history.state&&history.state[SCROLL_TOKEN];
|
|
2859
|
-
scrollToken=popped||newScrollToken();
|
|
2860
3786
|
if(!popped)stampScrollToken();
|
|
2861
3787
|
pendingScroll=readScroll(scrollToken);
|
|
3788
|
+
pendingInner=readInner(scrollToken);
|
|
2862
3789
|
}
|
|
3790
|
+
if(screensLive){
|
|
3791
|
+
if(st&&st[TAB]!==undefined&&st[TAB]!==curTab&&tabs[st[TAB]]){curTab=st[TAB];markTab(curTab)}
|
|
3792
|
+
var target=(tabs[curTab]||[])[depth];
|
|
3793
|
+
if(target&&target.el&&target.token===scrollToken&&target!==active){showRetained(target,dir==='replace'?'pop':dir,{settled:settled===target});return}
|
|
3794
|
+
}
|
|
3795
|
+
navCtxNext={direction:dir};
|
|
2863
3796
|
navigate(location.pathname+location.search+location.hash,false);
|
|
2864
3797
|
});
|
|
2865
3798
|
|
|
@@ -2903,7 +3836,13 @@ else {
|
|
|
2903
3836
|
setCache(key,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
2904
3837
|
if(eager)keepPage(key,result);
|
|
2905
3838
|
}
|
|
2906
|
-
if(result)
|
|
3839
|
+
if(result){
|
|
3840
|
+
loadModuleRegistries(result.html,href);
|
|
3841
|
+
// Its stylesheet too, so the tap never waits on cssLoadTimeout for a
|
|
3842
|
+
// sheet it could have had all along. Added unapplied (media=print),
|
|
3843
|
+
// as a navigation adds it; the swap makes it live.
|
|
3844
|
+
if(result.html.indexOf('<!--stx-fragment')===0)preloadGeneratedCss(fragmentCssHrefs(result.html),location.href);
|
|
3845
|
+
}
|
|
2907
3846
|
}).catch(function(){}).finally(function(){delete prefetching[key]});
|
|
2908
3847
|
}
|
|
2909
3848
|
|
|
@@ -2999,6 +3938,7 @@ else {
|
|
|
2999
3938
|
document.addEventListener('mouseover',onIntent,true);
|
|
3000
3939
|
document.addEventListener('touchstart',onIntent,{capture:true,passive:true});
|
|
3001
3940
|
window.addEventListener('stx:load',prefetchEager);
|
|
3941
|
+
window.addEventListener('stx:load',function(){setTimeout(observeLinks,0)});
|
|
3002
3942
|
}
|
|
3003
3943
|
|
|
3004
3944
|
// \u2500\u2500 Active link management \u2500\u2500
|
|
@@ -3065,7 +4005,7 @@ else {
|
|
|
3065
4005
|
// updateActiveLinks has already rewritten the classes by now.
|
|
3066
4006
|
function updateNav(){
|
|
3067
4007
|
var done=new Set();
|
|
3068
|
-
qsa('nav, #mobileNav, [data-stx-nav]').forEach(function(nav){
|
|
4008
|
+
qsa('nav, #mobileNav, [data-stx-nav]:not(a)').forEach(function(nav){
|
|
3069
4009
|
var states=[];
|
|
3070
4010
|
Array.prototype.forEach.call(nav.querySelectorAll('a[href]'),function(a){
|
|
3071
4011
|
if(done.has(a)||!a.hasAttribute('data-stx-link'))return;
|
|
@@ -3161,16 +4101,40 @@ else {
|
|
|
3161
4101
|
// The container the router focuses after a navigation is not a
|
|
3162
4102
|
// control, so it draws no focus ring: Safari drew one around the whole
|
|
3163
4103
|
// page, a blue line along its bottom edge on every screen.
|
|
3164
|
-
|
|
4104
|
+
// Links stay live while a page loads: a tap on another one is the newest
|
|
4105
|
+
// navigation and wins (navigate), where pointer-events:none used to
|
|
4106
|
+
// swallow it.
|
|
4107
|
+
var css='[data-stx-route-focus]:focus{outline:none}.stx-navigating{cursor:progress}#stx-router-progress{position:fixed;top:0;left:0;right:0;height:'+ph+';background:'+pc+';box-shadow:0 0 8px '+pc+',0 0 4px '+pc+';transform:scaleX(0);transform-origin:left;transition:transform .18s ease-out,opacity .26s ease;opacity:0;pointer-events:none;z-index:999999}html.stx-instant::view-transition-group(*),html.stx-instant::view-transition-old(*),html.stx-instant::view-transition-new(*){animation:none!important}';
|
|
4108
|
+
// One View Transitions block. There used to be two, and the second
|
|
4109
|
+
// (injected after this one) silently overrode the first: the page stays
|
|
4110
|
+
// put and only the routed content fades, which is what this now says
|
|
4111
|
+
// once. The retained screen inside it is not named, so the container
|
|
4112
|
+
// and the screen never carry the same name.
|
|
3165
4113
|
if(o.viewTransitions&&'startViewTransition' in document){
|
|
3166
4114
|
var dur=(o.viewTransitionDuration||220)+'ms';
|
|
3167
4115
|
var ease=o.viewTransitionEasing||'cubic-bezier(0.16, 1, 0.3, 1)';
|
|
3168
|
-
css+='::view-transition-old(root),::view-transition-new(root){animation
|
|
3169
|
-
css+='
|
|
4116
|
+
css+='::view-transition-old(root),::view-transition-new(root){animation:none}::view-transition{background:transparent}';
|
|
4117
|
+
css+='main,#app-content,[data-stx-content]:not([data-stx-screen]){view-transition-name:stx-content}::view-transition-group(stx-content){overflow:hidden}';
|
|
4118
|
+
css+='::view-transition-old(stx-content){animation:stx-r-fade-out '+dur+' '+ease+' both}::view-transition-new(stx-content){animation:stx-r-fade-in '+dur+' '+ease+' both}';
|
|
3170
4119
|
css+='@keyframes stx-r-fade-out{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-4px)}}';
|
|
3171
4120
|
css+='@keyframes stx-r-fade-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}';
|
|
3172
|
-
css+='@media (prefers-reduced-motion: reduce){::view-transition-old(
|
|
4121
|
+
css+='@media (prefers-reduced-motion: reduce){::view-transition-old(stx-content),::view-transition-new(stx-content){animation-duration:0s;animation-name:none}}';
|
|
3173
4122
|
}
|
|
4123
|
+
// Screens: a hidden one takes no room, and a screen lays its page out
|
|
4124
|
+
// the way the container would have. Push and pop are iOS's: the new
|
|
4125
|
+
// screen slides in from the right edge while the one under it moves a
|
|
4126
|
+
// third of the way left and dims, over --stx-nav-duration on a spring
|
|
4127
|
+
// (linear() where the browser has it). Reduced motion cross-fades.
|
|
4128
|
+
css+='[data-stx-screen]{display:inherit;flex-direction:inherit;flex-wrap:inherit;gap:inherit;align-items:inherit;justify-content:inherit;grid-template-columns:inherit;grid-template-rows:inherit;grid-column:1/-1;flex:1 1 auto;align-self:stretch;min-width:0}['+HIDDEN+']{display:none!important}';
|
|
4129
|
+
css+=':root{--stx-nav-duration:'+(o.navDuration|0)+'ms;--stx-nav-ease:cubic-bezier(0.32,0.72,0,1)}@supports (animation-timing-function:linear(0,1)){:root{--stx-nav-ease:linear(0,.079,.236,.403,.551,.671,.764,.833,.883,.919,.944,.962,.974,.983,.988,.992,.995,.997,.998,.999,1)}}';
|
|
4130
|
+
css+='[data-stx-animating]{overflow-x:clip}[data-stx-screen-enter],[data-stx-screen-leave]{position:relative;will-change:transform;animation-duration:var(--stx-nav-duration);animation-timing-function:var(--stx-nav-ease);animation-fill-mode:both}';
|
|
4131
|
+
css+='html[data-nav-direction=push] [data-stx-screen-enter]{animation-name:stx-nav-in;z-index:3;box-shadow:0 0 24px rgb(0 0 0 / .12)}html[data-nav-direction=push] [data-stx-screen-leave]{animation-name:stx-nav-out;z-index:1}';
|
|
4132
|
+
css+='html[data-nav-direction=pop] [data-stx-screen-enter]{animation-name:stx-nav-back-in;z-index:1}html[data-nav-direction=pop] [data-stx-screen-leave]{animation-name:stx-nav-back-out;z-index:3;box-shadow:0 0 24px rgb(0 0 0 / .12)}';
|
|
4133
|
+
css+='[data-stx-screen-dim]{position:fixed;top:0;bottom:0;z-index:2;background:rgb(0 0 0 / .1);pointer-events:none;animation:stx-nav-dim var(--stx-nav-duration) var(--stx-nav-ease) both}html[data-nav-direction=pop] [data-stx-screen-dim]{animation-direction:reverse}[data-stx-screen-dim][data-stx-drag]{animation:none}';
|
|
4134
|
+
css+='@keyframes stx-nav-in{from{transform:translateX(100%)}to{transform:none}}@keyframes stx-nav-out{from{transform:none}to{transform:translateX(-30%)}}@keyframes stx-nav-back-in{from{transform:translateX(-30%)}to{transform:none}}@keyframes stx-nav-back-out{from{transform:none}to{transform:translateX(100%)}}@keyframes stx-nav-dim{from{opacity:0}to{opacity:1}}@keyframes stx-nav-fade-in{from{opacity:0}}@keyframes stx-nav-fade-out{to{opacity:0}}';
|
|
4135
|
+
css+='@media (prefers-reduced-motion: reduce){html[data-nav-direction] [data-stx-screen-enter]{animation-name:stx-nav-fade-in;animation-duration:200ms;box-shadow:none}html[data-nav-direction] [data-stx-screen-leave]{animation-name:stx-nav-fade-out;animation-duration:200ms;box-shadow:none}[data-stx-screen-dim]{display:none}}';
|
|
4136
|
+
// The screen a failed load leaves behind (showNavError).
|
|
4137
|
+
css+='.stx-retry{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:4rem 1.5rem;text-align:center}.stx-retry-title{margin:0;font-weight:600;font-size:1.0625rem}.stx-retry-text{margin:0;opacity:.7;max-width:20rem}.stx-retry-button{margin-top:.75rem;padding:.5rem 1.25rem;border:0;border-radius:999px;background:var(--native-accent,#2563eb);color:#fff;font:inherit;font-weight:600}';
|
|
3174
4138
|
s.textContent=css;
|
|
3175
4139
|
dhead().appendChild(s);
|
|
3176
4140
|
}
|
|
@@ -3190,12 +4154,6 @@ else {
|
|
|
3190
4154
|
}
|
|
3191
4155
|
}
|
|
3192
4156
|
|
|
3193
|
-
function injectViewTransitionCSS(){
|
|
3194
|
-
if(document.getElementById('stx-view-transitions'))return;
|
|
3195
|
-
var s=ce('style');s.id='stx-view-transitions';
|
|
3196
|
-
s.textContent='::view-transition-group(root){animation:none}::view-transition-old(root){animation:none}::view-transition-new(root){animation:none}main,#app-content,[data-stx-content]{view-transition-name:stx-content}::view-transition-old(stx-content){animation:stx-fade-out .15s ease-out both}::view-transition-new(stx-content){animation:stx-fade-in .15s ease-in .1s both}@keyframes stx-fade-out{from{opacity:1}to{opacity:0}}@keyframes stx-fade-in{from{opacity:0}to{opacity:1}}::view-transition{background:transparent}::view-transition-group(stx-content){background:inherit;overflow:hidden}';
|
|
3197
|
-
(document.head||document.documentElement).appendChild(s);
|
|
3198
|
-
}
|
|
3199
4157
|
|
|
3200
4158
|
// \u2500\u2500 Public API \u2500\u2500
|
|
3201
4159
|
var router={
|
|
@@ -3230,6 +4188,12 @@ else {
|
|
|
3230
4188
|
evictCache(cacheKey(url||(location.pathname+location.search)));
|
|
3231
4189
|
},
|
|
3232
4190
|
clearCache:function(){for(var k in cache)delete cache[k];for(var lk in layoutCache)delete layoutCache[lk];for(var gk in layoutGroupCache)delete layoutGroupCache[gk];cacheOrder.length=0},
|
|
4191
|
+
// Switch to the tab whose link points at href, as a tap on it would:
|
|
4192
|
+
// its retained screens and its own back stack (selectTab).
|
|
4193
|
+
selectTab:selectTab,
|
|
4194
|
+
back:function(){return history.back()},
|
|
4195
|
+
// The screens kept alive, for a page or a test that wants to look.
|
|
4196
|
+
screens:function(){var out=[];eachEntry(function(e,id){out.push({url:e.url,tab:id,depth:e.depth,live:!!e.el,active:e===active})});return out},
|
|
3233
4197
|
cache:cache,
|
|
3234
4198
|
swap:swap,
|
|
3235
4199
|
updateNav:updateNav
|
|
@@ -3243,10 +4207,13 @@ else {
|
|
|
3243
4207
|
// \u2500\u2500 Initialize \u2500\u2500
|
|
3244
4208
|
function init(){
|
|
3245
4209
|
injectStyles();
|
|
3246
|
-
injectViewTransitionCSS();
|
|
3247
4210
|
refreshCurrentLinks();
|
|
4211
|
+
// The first screen, from the start, so the page never changes shape on
|
|
4212
|
+
// its first navigation.
|
|
4213
|
+
if(screensOn()){var host=getContainer();if(host)adoptInitial(host)}
|
|
3248
4214
|
restoreKeptPages();
|
|
3249
4215
|
prefetchEager();
|
|
4216
|
+
observeLinks();
|
|
3250
4217
|
}
|
|
3251
4218
|
|
|
3252
4219
|
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
|
|
@@ -3256,7 +4223,7 @@ else {
|
|
|
3256
4223
|
function getRouterScript() {
|
|
3257
4224
|
if (cachedRouterScript)
|
|
3258
4225
|
return cachedRouterScript;
|
|
3259
|
-
cachedRouterScript = minifyRouterScript(stripRouterLogs(routerSource()));
|
|
4226
|
+
cachedRouterScript = minifyRouterScript(stripRouterLogs(routerSource()), true);
|
|
3260
4227
|
return cachedRouterScript;
|
|
3261
4228
|
}
|
|
3262
4229
|
function getRouterScriptDev() {
|