stx-router 0.2.406 → 0.2.407
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.d.ts +6 -1
- package/dist/client.js +1024 -57
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -40,9 +40,9 @@ function stripRouterLogs(source) {
|
|
|
40
40
|
}
|
|
41
41
|
return out.join("");
|
|
42
42
|
}
|
|
43
|
-
function minifyRouterScript(source) {
|
|
43
|
+
function minifyRouterScript(source, mangle = !1) {
|
|
44
44
|
try {
|
|
45
|
-
return new Bun.Transpiler({ loader: "js", minifyWhitespace: !0 }).transformSync(source).replace(/\}(var |let |const |function )/g, "};$1").trim();
|
|
45
|
+
return new Bun.Transpiler(mangle ? { loader: "js", minify: { whitespace: !0, identifiers: !0 } } : { loader: "js", minifyWhitespace: !0 }).transformSync(source).replace(/\}(var |let |const |function )/g, "};$1").trim();
|
|
46
46
|
} catch {
|
|
47
47
|
return source;
|
|
48
48
|
}
|
|
@@ -55,7 +55,7 @@ function routerSource() {
|
|
|
55
55
|
if(window.__stxRouter&&window.__stxRouter.__rev===ROUTER_REV)return;
|
|
56
56
|
|
|
57
57
|
// \u2500\u2500 Configuration \u2500\u2500
|
|
58
|
-
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};
|
|
58
|
+
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};
|
|
59
59
|
var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{});
|
|
60
60
|
var containerSel=o.container;
|
|
61
61
|
var debug=!!o.debug;
|
|
@@ -91,6 +91,12 @@ function routerSource() {
|
|
|
91
91
|
// (which carries a scroll token too): going back from a pushed entry stays
|
|
92
92
|
// in the app, from the first it leaves it (or, in a phone app, does nothing).
|
|
93
93
|
var PUSHED_MARK='__stxPushed';
|
|
94
|
+
// How far above the entry the app opened with this one sits, and which tab
|
|
95
|
+
// it belongs to. A tab switch rewinds to depth 0 before it writes the next
|
|
96
|
+
// tab's entries, which is what keeps Back from ever crossing tabs.
|
|
97
|
+
var DEPTH='__stxDepth';
|
|
98
|
+
var TAB='__stxTab';
|
|
99
|
+
var histDepth=(history.state&&history.state[DEPTH])|0;
|
|
94
100
|
var scrollSeq=0;
|
|
95
101
|
var pendingScroll=null;
|
|
96
102
|
var restoreScroll=!!o.scrollRestoration;
|
|
@@ -105,11 +111,44 @@ function routerSource() {
|
|
|
105
111
|
return parts.length===2?[parseFloat(parts[0])||0,parseFloat(parts[1])||0]:null;
|
|
106
112
|
}catch(e){return null}
|
|
107
113
|
}
|
|
114
|
+
// Inner scrollers, marked data-stx-scroll: a screen whose list scrolls
|
|
115
|
+
// inside its own box rather than the window lost its place on Back exactly
|
|
116
|
+
// the way the window used to. Keyed by the attribute's value, or by order
|
|
117
|
+
// for an unnamed one.
|
|
118
|
+
function scrollerKey(el,i){return el.getAttribute('data-stx-scroll')||('#'+i)}
|
|
119
|
+
function innerScroll(root){
|
|
120
|
+
var m=null;
|
|
121
|
+
if(root&&root.querySelectorAll)root.querySelectorAll('[data-stx-scroll]').forEach(function(el,i){
|
|
122
|
+
if(el.scrollTop||el.scrollLeft){m=m||{};m[scrollerKey(el,i)]=[el.scrollLeft||0,el.scrollTop||0]}
|
|
123
|
+
});
|
|
124
|
+
return m;
|
|
125
|
+
}
|
|
126
|
+
function applyInner(root,m){
|
|
127
|
+
if(!m||!root||!root.querySelectorAll)return;
|
|
128
|
+
root.querySelectorAll('[data-stx-scroll]').forEach(function(el,i){
|
|
129
|
+
var at=m[scrollerKey(el,i)];
|
|
130
|
+
if(at){el.scrollLeft=at[0];el.scrollTop=at[1]}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
108
133
|
// Called while the outgoing entry is still the current one, so the position
|
|
109
134
|
// read here belongs to the token being written.
|
|
110
135
|
function rememberScroll(){
|
|
136
|
+
if(active&&active.el&&!active.el.hasAttribute(HIDDEN))saveScroll(active);
|
|
111
137
|
if(!restoreScroll)return;
|
|
112
|
-
try{
|
|
138
|
+
try{
|
|
139
|
+
window.sessionStorage.setItem(SCROLL_STORE+scrollToken,(window.pageXOffset||window.scrollX||0)+','+(window.pageYOffset||window.scrollY||0));
|
|
140
|
+
// Taken as the swap began when there is one: by the time the history
|
|
141
|
+
// is written the outgoing page's scrollers have been replaced.
|
|
142
|
+
var inner=innerSnap!==undefined?innerSnap:innerScroll(activeRoot());
|
|
143
|
+
innerSnap=undefined;
|
|
144
|
+
if(inner)window.sessionStorage.setItem(SCROLL_STORE+scrollToken+':i',JSON.stringify(inner));
|
|
145
|
+
else window.sessionStorage.removeItem(SCROLL_STORE+scrollToken+':i');
|
|
146
|
+
}catch(e){}
|
|
147
|
+
}
|
|
148
|
+
var pendingInner=null;
|
|
149
|
+
var innerSnap;
|
|
150
|
+
function readInner(token){
|
|
151
|
+
try{return JSON.parse(window.sessionStorage.getItem(SCROLL_STORE+token+':i')||'null')}catch(e){return null}
|
|
113
152
|
}
|
|
114
153
|
// One place decides where a swap leaves the viewport, so the fragment path
|
|
115
154
|
// and the whole-document path cannot drift apart.
|
|
@@ -118,10 +157,16 @@ function routerSource() {
|
|
|
118
157
|
var at=pendingScroll;
|
|
119
158
|
pendingScroll=null;
|
|
120
159
|
window.scrollTo({left:at[0],top:at[1],behavior:'instant'});
|
|
160
|
+
// Again once the page has bound: a scroller's content is often drawn
|
|
161
|
+
// by a :for that has not run yet, and a box with nothing in it cannot
|
|
162
|
+
// be scrolled anywhere.
|
|
163
|
+
var inner=pendingInner;pendingInner=null;
|
|
164
|
+
if(inner){applyInner(activeRoot(),inner);setTimeout(function(){applyInner(activeRoot(),inner)},60)}
|
|
121
165
|
return;
|
|
122
166
|
}
|
|
167
|
+
pendingInner=null;
|
|
123
168
|
if(o.scrollToTop&&!hash){window.scrollTo({top:0,behavior:'instant'});return}
|
|
124
|
-
if(hash){var el=
|
|
169
|
+
if(hash){var el=findIn(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
|
|
125
170
|
}
|
|
126
171
|
// Keeps whatever another script put in the entry; only adds the token.
|
|
127
172
|
function stampScrollToken(){
|
|
@@ -129,6 +174,7 @@ function routerSource() {
|
|
|
129
174
|
var current=history.state;
|
|
130
175
|
if(current&&typeof current==='object')for(var key in current)state[key]=current[key];
|
|
131
176
|
state[SCROLL_TOKEN]=scrollToken;
|
|
177
|
+
state[DEPTH]=histDepth;
|
|
132
178
|
try{history.replaceState(state,'',location.href)}catch(e){}
|
|
133
179
|
}
|
|
134
180
|
if(restoreScroll){
|
|
@@ -182,9 +228,24 @@ function routerSource() {
|
|
|
182
228
|
function isBuildSkew(incoming){
|
|
183
229
|
return !!(loadedBuild&&incoming&&incoming!==loadedBuild);
|
|
184
230
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
231
|
+
// A navigation the user asked for loads its destination as a document: no
|
|
232
|
+
// fragment of the newer build can be hydrated by this runtime, and a tap is
|
|
233
|
+
// the one moment a page load is expected anyway. Anything else that notices
|
|
234
|
+
// the newer build (a background revalidation, the offline worker) must not
|
|
235
|
+
// reload a screen the user is in the middle of using, so the reload waits
|
|
236
|
+
// until the page is hidden, and a cold start picks the build up regardless.
|
|
237
|
+
var skewDeferred=false;
|
|
238
|
+
function reloadForSkew(url,incoming,background){
|
|
239
|
+
log('[router] build skew: page is',loadedBuild,'server is',incoming,background?'\u2014 reload when hidden':'\u2014 full navigation');
|
|
240
|
+
if(!background){location.href=url;return}
|
|
241
|
+
if(skewDeferred)return;
|
|
242
|
+
skewDeferred=true;
|
|
243
|
+
var reloadHidden=function(){
|
|
244
|
+
if(document.visibilityState!=='hidden')return;
|
|
245
|
+
document.removeEventListener('visibilitychange',reloadHidden);
|
|
246
|
+
location.reload();
|
|
247
|
+
};
|
|
248
|
+
document.addEventListener('visibilitychange',reloadHidden);
|
|
188
249
|
}
|
|
189
250
|
// A link marked data-stx-transition="none" swaps without the cross-fade. A
|
|
190
251
|
// tab bar is the case: iOS switches tabs instantly, and the fade read as a
|
|
@@ -331,6 +392,9 @@ function routerSource() {
|
|
|
331
392
|
}
|
|
332
393
|
|
|
333
394
|
var cache={};
|
|
395
|
+
// When each entry was fetched, so a cache hit knows whether it is worth
|
|
396
|
+
// checking behind the swap (revalidate).
|
|
397
|
+
var cacheAt={};
|
|
334
398
|
var prefetching={};
|
|
335
399
|
var awaitingKey='';
|
|
336
400
|
var isNavigating=false;
|
|
@@ -352,6 +416,7 @@ function routerSource() {
|
|
|
352
416
|
layoutGroupCache[key]=group;
|
|
353
417
|
titleCache[key]=title||'';
|
|
354
418
|
attrsCache[key]=cattrs||'';
|
|
419
|
+
cacheAt[key]=Date.now();
|
|
355
420
|
while(cacheOrder.length>o.prefetchCacheMax){
|
|
356
421
|
var oldest=cacheOrder.shift();
|
|
357
422
|
delete cache[oldest];
|
|
@@ -359,6 +424,7 @@ function routerSource() {
|
|
|
359
424
|
delete layoutGroupCache[oldest];
|
|
360
425
|
delete titleCache[oldest];
|
|
361
426
|
delete attrsCache[oldest];
|
|
427
|
+
delete cacheAt[oldest];
|
|
362
428
|
}
|
|
363
429
|
}
|
|
364
430
|
// Read a prefetch response into the shape setCache stores, or null when this
|
|
@@ -462,6 +528,7 @@ function routerSource() {
|
|
|
462
528
|
delete layoutGroupCache[key];
|
|
463
529
|
delete titleCache[key];
|
|
464
530
|
delete attrsCache[key];
|
|
531
|
+
delete cacheAt[key];
|
|
465
532
|
}
|
|
466
533
|
|
|
467
534
|
// Extract top-level CSS blocks (rules AND @media blocks with nested braces).
|
|
@@ -871,10 +938,17 @@ function routerSource() {
|
|
|
871
938
|
// pushes, and the string 'replace' replaces. Threading a fourth argument
|
|
872
939
|
// through swap() to each of the three history sites would have been the
|
|
873
940
|
// alternative (#1807).
|
|
941
|
+
//
|
|
942
|
+
// 'tab' is a tab's first screen: it takes the place of the entry the app
|
|
943
|
+
// opened with (selectTab has rewound to it), under a token of its own so
|
|
944
|
+
// the tab it replaced keeps its positions.
|
|
945
|
+
var scrollSaved=false;
|
|
874
946
|
function writeHistory(mode,href){
|
|
875
947
|
// The position belongs to the entry being left, and scrollToken still
|
|
876
|
-
// names it here
|
|
877
|
-
|
|
948
|
+
// names it here -- unless the screen swap already saved it, before the
|
|
949
|
+
// outgoing screen was lifted out of the page and the window clamped.
|
|
950
|
+
if(!scrollSaved)rememberScroll();
|
|
951
|
+
scrollSaved=false;
|
|
878
952
|
if(mode==='replace'){
|
|
879
953
|
// A replaced entry is still the one it replaces: pushed by the app or
|
|
880
954
|
// the one the app opened with.
|
|
@@ -884,22 +958,43 @@ function routerSource() {
|
|
|
884
958
|
return;
|
|
885
959
|
}
|
|
886
960
|
scrollToken=newScrollToken();
|
|
961
|
+
if(mode==='tab'){histDepth=0;history.replaceState(tokenState(),'',href);return}
|
|
962
|
+
histDepth++;
|
|
887
963
|
var pushed=tokenState();
|
|
888
964
|
pushed[PUSHED_MARK]=true;
|
|
889
965
|
history.pushState(pushed,'',href);
|
|
890
966
|
}
|
|
891
|
-
function tokenState(){var state={};state[SCROLL_TOKEN]=scrollToken;return state}
|
|
967
|
+
function tokenState(){var state={};state[SCROLL_TOKEN]=scrollToken;state[DEPTH]=histDepth;if(curTab)state[TAB]=curTab;return state}
|
|
892
968
|
|
|
893
969
|
// Second arg accepts the legacy pushState boolean OR an options object
|
|
894
970
|
// { replace, instant }. Callers inside this file still pass the boolean.
|
|
895
971
|
// instant is what data-stx-transition="none" is to a link: no cross-fade.
|
|
972
|
+
//
|
|
973
|
+
// The newest navigation wins. One already on its way when another starts
|
|
974
|
+
// (a second tap, Back while a page is still loading) is aborted and its
|
|
975
|
+
// answer ignored: it used to be the other way round, the later one was
|
|
976
|
+
// dropped, and a Back during a load left the address bar on the popped
|
|
977
|
+
// entry while the screen went on to show the page being left.
|
|
978
|
+
var navSeq=0;
|
|
979
|
+
var navAbort=null;
|
|
980
|
+
// What the next swap needs to know about where it is going: set by the
|
|
981
|
+
// caller right before navigate() (popstate, a tab switch) and carried to
|
|
982
|
+
// swap() through pendingCtx, the same way pendingContainerAttrs is.
|
|
983
|
+
var navCtxNext=null;
|
|
984
|
+
var pendingCtx=null;
|
|
985
|
+
function routerFetch(url,headers){
|
|
986
|
+
var init={headers:headers};
|
|
987
|
+
if(navAbort)init.signal=navAbort.signal;
|
|
988
|
+
return fetch(url,init);
|
|
989
|
+
}
|
|
896
990
|
function navigate(url,pushState,force){
|
|
991
|
+
var ctx=navCtxNext||{};
|
|
992
|
+
navCtxNext=null;
|
|
897
993
|
if(pushState&&typeof pushState==='object'){if(pushState.instant)instantNext=true;pushState=pushState.replace?'replace':true}
|
|
898
994
|
// Lang-picker passes force=true with an already-localized path (/en/...).
|
|
899
995
|
// Re-localizing would map it back to the *current* locale and no-op.
|
|
900
996
|
if(!force) url=withCurrentLocale(url);
|
|
901
997
|
log('[router] navigate() called:',url,'isNavigating:',isNavigating);
|
|
902
|
-
if(isNavigating)return Promise.resolve(false);
|
|
903
998
|
// Resolve against the current document URL, matching native <a> semantics.
|
|
904
999
|
// With location.origin as the base, any relative href ('?status=resolved',
|
|
905
1000
|
// '#anchor', './sibling') resolved to the SITE ROOT \u2014 so filter tabs,
|
|
@@ -909,9 +1004,9 @@ function routerSource() {
|
|
|
909
1004
|
|
|
910
1005
|
if(t.origin!==location.origin){location.href=url;return Promise.resolve(false)}
|
|
911
1006
|
|
|
912
|
-
if(t.pathname===location.pathname&&t.hash){
|
|
1007
|
+
if(t.pathname===location.pathname&&t.hash&&pushState!=='tab'){
|
|
913
1008
|
if(pushState!==false)writeHistory(pushState,t.href);
|
|
914
|
-
var el=
|
|
1009
|
+
var el=findIn(t.hash);
|
|
915
1010
|
if(el)el.scrollIntoView({behavior:'smooth'});
|
|
916
1011
|
return Promise.resolve(true);
|
|
917
1012
|
}
|
|
@@ -922,7 +1017,7 @@ function routerSource() {
|
|
|
922
1017
|
// entry \u2014 without this allowance, hitting back would early-return
|
|
923
1018
|
// and the visible page content would be left frozen on the
|
|
924
1019
|
// forward-navigation page.
|
|
925
|
-
if(pushState!==false&&t.href===location.href&&!t.hash&&!force)return Promise.resolve(false);
|
|
1020
|
+
if(pushState!==false&&pushState!=='tab'&&t.href===location.href&&!t.hash&&!force)return Promise.resolve(false);
|
|
926
1021
|
|
|
927
1022
|
// Its prefetch is on the way: wait for that answer rather than asking the
|
|
928
1023
|
// server a second time. A later tap elsewhere wins.
|
|
@@ -932,11 +1027,17 @@ function routerSource() {
|
|
|
932
1027
|
return prefetching[awaitedKey].then(function(){
|
|
933
1028
|
if(awaitingKey!==awaitedKey)return false;
|
|
934
1029
|
awaitingKey='';
|
|
1030
|
+
navCtxNext=ctx;
|
|
935
1031
|
return navigate(url,pushState,force);
|
|
936
1032
|
});
|
|
937
1033
|
}
|
|
938
1034
|
awaitingKey='';
|
|
939
1035
|
|
|
1036
|
+
if(navAbort){try{navAbort.abort()}catch(e){}}
|
|
1037
|
+
navAbort=typeof AbortController==='function'?new AbortController():null;
|
|
1038
|
+
var id=++navSeq;
|
|
1039
|
+
ctx.id=id;
|
|
1040
|
+
if(!ctx.direction)ctx.direction=pushState==='replace'?'replace':pushState==='tab'?'tab':pushState===false?'pop':'push';
|
|
940
1041
|
isNavigating=true;
|
|
941
1042
|
instantNav=instantNext;
|
|
942
1043
|
instantNext=false;
|
|
@@ -947,7 +1048,22 @@ function routerSource() {
|
|
|
947
1048
|
var targetPath=cacheKey(url);
|
|
948
1049
|
var targetHash=t.hash;
|
|
949
1050
|
|
|
950
|
-
function done(){
|
|
1051
|
+
function done(){
|
|
1052
|
+
if(id!==navSeq)return;
|
|
1053
|
+
isNavigating=false;navAbort=null;
|
|
1054
|
+
document.body.classList.remove(o.loadingClass);finishProgress();
|
|
1055
|
+
settleDirection();
|
|
1056
|
+
}
|
|
1057
|
+
function stale(){return id!==navSeq}
|
|
1058
|
+
// A failed fetch keeps the app: the destination shows a way to try again
|
|
1059
|
+
// in place of its content, where a document load used to be the answer
|
|
1060
|
+
// to everything -- offline that load is the offline page, or nothing.
|
|
1061
|
+
function failed(err){
|
|
1062
|
+
if(stale()||(err&&err.name==='AbortError'))return false;
|
|
1063
|
+
console.error('[router] fetch error:',err);
|
|
1064
|
+
return showNavError(url,err,pushState,targetHash,ctx);
|
|
1065
|
+
}
|
|
1066
|
+
function swapWith(html,key){pendingCtx=ctx;return swap(html,key,pushState,targetHash)}
|
|
951
1067
|
|
|
952
1068
|
if(o.cache&&cache[targetPath]&&!force){
|
|
953
1069
|
// Cache hit \u2192 promote in LRU order so this entry survives eviction
|
|
@@ -956,22 +1072,24 @@ function routerSource() {
|
|
|
956
1072
|
if(checkLayoutChange(layoutCache[targetPath],url,layoutGroupCache[targetPath])){
|
|
957
1073
|
// Layout changed \u2014 fetch full page and do full document swap
|
|
958
1074
|
log('[router] cache hit but layout changed \u2014 fetching full page');
|
|
959
|
-
return
|
|
960
|
-
if(!r.ok)throw
|
|
1075
|
+
return routerFetch(url,{'Accept':'text/html'}).then(function(r){
|
|
1076
|
+
if(!r.ok)throw httpError(r.status);
|
|
961
1077
|
return r.text();
|
|
962
1078
|
}).then(function(html){
|
|
1079
|
+
if(stale())return false;
|
|
963
1080
|
log('[router] full page fetched from cache path, len:',html.length);
|
|
964
1081
|
pendingLayoutDecl={layout:layoutCache[targetPath]||'',group:layoutGroupCache[targetPath]||''};
|
|
965
|
-
return
|
|
966
|
-
}).catch(
|
|
967
|
-
console.error('[router] full page fetch error:',err);
|
|
968
|
-
location.href=url;
|
|
969
|
-
}).finally(done);
|
|
1082
|
+
return swapWith(html,targetPath);
|
|
1083
|
+
}).catch(failed).finally(done);
|
|
970
1084
|
}
|
|
971
1085
|
pendingContainerAttrs=attrsCache[targetPath]||'';
|
|
972
1086
|
pendingLayoutDecl=null;
|
|
973
|
-
return Promise.resolve(
|
|
1087
|
+
return Promise.resolve(swapWith(cache[targetPath],targetPath)).then(function(swapped){
|
|
1088
|
+
if(swapped===false)return false;
|
|
974
1089
|
applyTitle(titleCache[targetPath]);
|
|
1090
|
+
// Shown from the cache at once; asked again behind it, so a page that
|
|
1091
|
+
// changed since says so (stx:updated) instead of staying stale.
|
|
1092
|
+
revalidate(targetPath,url);
|
|
975
1093
|
return true;
|
|
976
1094
|
}).finally(done);
|
|
977
1095
|
}
|
|
@@ -981,8 +1099,9 @@ else {
|
|
|
981
1099
|
// the page content. Custom app-shell containers need full documents so
|
|
982
1100
|
// the router does not inject a <main> fragment into the wrong element.
|
|
983
1101
|
var wantsFragment=shouldUseFragmentResponse();
|
|
984
|
-
return
|
|
985
|
-
if(
|
|
1102
|
+
return routerFetch(url,wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}).then(function(r){
|
|
1103
|
+
if(stale())return null;
|
|
1104
|
+
if(!r.ok)throw httpError(r.status);
|
|
986
1105
|
// A route guard answered with a redirect and fetch followed it
|
|
987
1106
|
// transparently, so r.ok is the DESTINATION's 200 and this markup
|
|
988
1107
|
// belongs to r.url \u2014 not to the path that was requested. Without
|
|
@@ -1013,9 +1132,9 @@ else {
|
|
|
1013
1132
|
// Layout change? Fetch the FULL page (no X-STX-Router header) and do full document swap
|
|
1014
1133
|
if(isFragment&&checkLayoutChange(newLayout,url,newGroup)){
|
|
1015
1134
|
log('[router] layout change \u2014 fetching full page for document swap');
|
|
1016
|
-
return
|
|
1135
|
+
return routerFetch(url,{'Accept':'text/html'}).then(function(fullRes){
|
|
1017
1136
|
log('[router] full page fetched:',fullRes.status,'ok:',fullRes.ok);
|
|
1018
|
-
if(!fullRes.ok)throw
|
|
1137
|
+
if(!fullRes.ok)throw httpError(fullRes.status);
|
|
1019
1138
|
return fullRes.text().then(function(html){
|
|
1020
1139
|
log('[router] full page html length:',html.length);
|
|
1021
1140
|
return{html:html,isFragment:false,layout:newLayout,layoutGroup:newGroup,title:newTitle};
|
|
@@ -1024,23 +1143,51 @@ else {
|
|
|
1024
1143
|
}
|
|
1025
1144
|
return r.text().then(function(html){return{html:html,isFragment:isFragment,layout:newLayout,layoutGroup:newGroup,title:newTitle,containerAttrs:newCAttrs,runtime:newRuntime}});
|
|
1026
1145
|
}).then(function(result){
|
|
1027
|
-
if(!result)return;
|
|
1146
|
+
if(!result||stale())return false;
|
|
1028
1147
|
if(result.isFragment)result.html=fragmentMarker(result.runtime)+result.html;
|
|
1029
1148
|
if(o.cache)setCache(targetPath,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
1030
1149
|
pendingContainerAttrs=result.isFragment?(result.containerAttrs||''):'';
|
|
1031
1150
|
pendingLayoutDecl=result.isFragment?null:{layout:result.layout||'',group:result.layoutGroup||''};
|
|
1032
|
-
return Promise.resolve(
|
|
1151
|
+
return Promise.resolve(swapWith(result.html,targetPath)).then(function(swapped){
|
|
1152
|
+
if(swapped===false)return false;
|
|
1033
1153
|
// Fragment swaps carry no <head>; apply the title from the header.
|
|
1034
1154
|
// Full-document swaps already set document.title from the <title> tag.
|
|
1035
1155
|
if(result.isFragment)applyTitle(result.title);
|
|
1036
1156
|
return true;
|
|
1037
1157
|
});
|
|
1038
|
-
}).catch(
|
|
1039
|
-
console.error('[router] fetch error:',err);
|
|
1040
|
-
location.href=url;
|
|
1041
|
-
}).finally(done);
|
|
1158
|
+
}).catch(failed).finally(done);
|
|
1042
1159
|
}
|
|
1043
1160
|
}
|
|
1161
|
+
function httpError(status){var err=new Error(String(status));err.status=status;return err}
|
|
1162
|
+
|
|
1163
|
+
// \u2500\u2500 A screen that could not be loaded \u2500\u2500
|
|
1164
|
+
// Shown in the routed container (in a screen of its own when screens are
|
|
1165
|
+
// retained, so Back returns to the one before it), with the address bar at
|
|
1166
|
+
// the destination, so "Try again" and coming back online both retry it in
|
|
1167
|
+
// place. A page that wants to draw its own cancels stx:navigate-error.
|
|
1168
|
+
function escAttr(v){return String(v).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<')}
|
|
1169
|
+
function showNavError(url,err,pushState,hash,ctx){
|
|
1170
|
+
var status=err&&err.status||0;
|
|
1171
|
+
var offline=!status&&navigator.onLine===false;
|
|
1172
|
+
var ev=new CustomEvent('stx:navigate-error',{cancelable:true,detail:{url:url,status:status,offline:offline,error:err}});
|
|
1173
|
+
window.dispatchEvent(ev);
|
|
1174
|
+
if(ev.defaultPrevented)return false;
|
|
1175
|
+
var title=offline?'You are offline':'This page could not be loaded';
|
|
1176
|
+
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.';
|
|
1177
|
+
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>';
|
|
1178
|
+
pendingContainerAttrs='';
|
|
1179
|
+
pendingLayoutDecl=null;
|
|
1180
|
+
pendingCtx=ctx;
|
|
1181
|
+
return Promise.resolve(swap(html,cacheKey(url),pushState,hash)).then(function(){return false});
|
|
1182
|
+
}
|
|
1183
|
+
function retryShown(){var root=activeRoot();var el=root&&root.querySelector?root.querySelector('[data-stx-retry]'):null;return el?el.getAttribute('data-stx-retry'):''}
|
|
1184
|
+
document.addEventListener('click',function(e){
|
|
1185
|
+
var b=e.target&&e.target.closest?e.target.closest('[data-stx-retry-button]'):null;
|
|
1186
|
+
if(!b)return;
|
|
1187
|
+
var holder=b.closest('[data-stx-retry]');
|
|
1188
|
+
if(holder)navigate(holder.getAttribute('data-stx-retry'),'replace',true);
|
|
1189
|
+
});
|
|
1190
|
+
window.addEventListener('online',function(){var u=retryShown();if(u)navigate(u,'replace',true)});
|
|
1044
1191
|
|
|
1045
1192
|
// A full document is only safe to partial-swap into the stx shell if it is
|
|
1046
1193
|
// itself stx-rendered. Pages served by another engine mounted on the same
|
|
@@ -1086,7 +1233,7 @@ else {
|
|
|
1086
1233
|
|
|
1087
1234
|
function announceRoute(){
|
|
1088
1235
|
if(o.announceRoute===false)return;
|
|
1089
|
-
var c=
|
|
1236
|
+
var c=activeRoot();
|
|
1090
1237
|
var h=c&&c.querySelector?c.querySelector('h1'):null;
|
|
1091
1238
|
// The heading names the destination better than the title, which often
|
|
1092
1239
|
// carries a site-name suffix. Falls back to the title, then gives up
|
|
@@ -1236,7 +1383,12 @@ else {
|
|
|
1236
1383
|
var declaredRuntime=fragMark&&fragMark[1]?fragMark[1]:'';
|
|
1237
1384
|
if(isFragment)html=html.slice(fragMark[0].length);
|
|
1238
1385
|
if(!isFragment&&!isStxDocument(html)){log('[router] non-stx document \u2014 full navigation to:',url);location.href=url;return Promise.resolve(false)}
|
|
1386
|
+
var ctx=pendingCtx||{direction:pushState==='replace'?'replace':pushState==='tab'?'tab':pushState===false?'pop':'push'};
|
|
1387
|
+
pendingCtx=null;
|
|
1239
1388
|
var currentContent=getContainer();
|
|
1389
|
+
// The routed container. currentContent is where the page is written: the
|
|
1390
|
+
// container itself, or with screens retained, the new screen inside it.
|
|
1391
|
+
var host=currentContent;
|
|
1240
1392
|
log('[router] swap: isFragment='+isFragment+' container='+!!currentContent+' tag='+(currentContent&¤tContent.tagName)+' selector='+containerSel+' htmlLen='+html.length);
|
|
1241
1393
|
if(!currentContent){log('[router] no container \u2014 falling back');location.href=url;return Promise.resolve(false)}
|
|
1242
1394
|
|
|
@@ -1353,14 +1505,16 @@ else {
|
|
|
1353
1505
|
// Swap content \u2014 apply the destination container's own attributes first
|
|
1354
1506
|
// so the incoming markup lands in a correctly-laid-out container instead
|
|
1355
1507
|
// of flashing (or sticking) unstyled.
|
|
1356
|
-
|
|
1508
|
+
if(scr)scr.cattrs=pendingContainerAttrs;
|
|
1509
|
+
applyContainerAttrs(host,pendingContainerAttrs);
|
|
1357
1510
|
currentContent.innerHTML=cleanFrag;
|
|
1358
1511
|
// Remove old page scripts
|
|
1359
1512
|
qsa('script[data-stx-page]').forEach(function(s){s.remove()});
|
|
1360
1513
|
if(pushState!==false)writeHistory(pushState,url+(hash||''));
|
|
1514
|
+
if(scr)screenCommit(scr,url+(hash||''));
|
|
1361
1515
|
refreshCurrentLinks();
|
|
1362
1516
|
applyScroll(hash);
|
|
1363
|
-
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
|
|
1517
|
+
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url,direction:ctx.direction}}));
|
|
1364
1518
|
// Before page scripts run, so a page that focuses its own control on
|
|
1365
1519
|
// mount still wins \u2014 its script executes after this.
|
|
1366
1520
|
focusAfterNavigation(hash);
|
|
@@ -1465,17 +1619,28 @@ else {
|
|
|
1465
1619
|
if(fragPending.length)Promise.all(fragPending).then(runFragScripts).catch(runFragScripts);
|
|
1466
1620
|
else runFragScripts();
|
|
1467
1621
|
}
|
|
1622
|
+
// With screens retained, the outgoing page is kept (or disposed once it
|
|
1623
|
+
// has animated away) by screenBegin rather than cleaned up here.
|
|
1624
|
+
var scr=null;
|
|
1625
|
+
var useScreens=screensOn();
|
|
1468
1626
|
function startFragSwap(){
|
|
1469
|
-
if(
|
|
1627
|
+
if(ctx.id&&ctx.id!==navSeq)return Promise.resolve(false);
|
|
1628
|
+
if(pushState!==false)innerSnap=innerScroll(activeRoot());
|
|
1629
|
+
if(useScreens)settleAnimation();
|
|
1630
|
+
else if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
|
|
1470
1631
|
return new Promise(function(resolve,reject){
|
|
1471
1632
|
function completeFragSwap(){
|
|
1472
1633
|
try{
|
|
1634
|
+
setDirection(ctx.direction);
|
|
1635
|
+
if(useScreens){scr=screenBegin(host,ctx,html,pushState!==false);currentContent=scr.el}
|
|
1473
1636
|
doFragSwap();
|
|
1637
|
+
if(scr)screenEnd(scr);
|
|
1474
1638
|
resolve(true);
|
|
1475
1639
|
}catch(err){reject(err)}
|
|
1476
1640
|
}
|
|
1477
1641
|
if(instantNav&&swapRunsInOneTask(html,url)){runInOneTask(completeFragSwap)}
|
|
1478
1642
|
else if(instantNav&&runInstantSwap(completeFragSwap)){}
|
|
1643
|
+
else if(useScreens){completeFragSwap()}
|
|
1479
1644
|
else if(runViewTransition(completeFragSwap)){}
|
|
1480
1645
|
else if(instantNav){completeFragSwap()}
|
|
1481
1646
|
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)}
|
|
@@ -1527,7 +1692,7 @@ else {
|
|
|
1527
1692
|
function doSwap(){
|
|
1528
1693
|
// \u2500\u2500 Swap <head> styles \u2500\u2500
|
|
1529
1694
|
// Inject new styles FIRST, then remove old to prevent unstyled flash
|
|
1530
|
-
var keepIds={'stx-
|
|
1695
|
+
var keepIds={'stx-r-css':1};
|
|
1531
1696
|
var curStyles=qsa('head style');
|
|
1532
1697
|
var newStyles=doc.querySelectorAll('head style');
|
|
1533
1698
|
|
|
@@ -1861,7 +2026,7 @@ else {
|
|
|
1861
2026
|
else curRoot.removeAttribute('data-stx-html-attrs');
|
|
1862
2027
|
}
|
|
1863
2028
|
|
|
1864
|
-
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
|
|
2029
|
+
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url,direction:ctx.direction}}));
|
|
1865
2030
|
|
|
1866
2031
|
// Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
|
|
1867
2032
|
function execScripts(){
|
|
@@ -1915,6 +2080,13 @@ else {
|
|
|
1915
2080
|
}
|
|
1916
2081
|
|
|
1917
2082
|
function startSwap(){
|
|
2083
|
+
if(ctx.id&&ctx.id!==navSeq)return Promise.resolve(false);
|
|
2084
|
+
if(pushState!==false)innerSnap=innerScroll(activeRoot());
|
|
2085
|
+
setDirection(ctx.direction);
|
|
2086
|
+
// A whole document is written into the container as it is, so retained
|
|
2087
|
+
// screens go first: they would otherwise outlive the page they belong to.
|
|
2088
|
+
collapseScreens();
|
|
2089
|
+
currentContent=getContainer();
|
|
1918
2090
|
// Clean up existing signals/effects
|
|
1919
2091
|
if(window.stx&&window.stx._cleanupContainer){
|
|
1920
2092
|
window.stx._cleanupContainer(currentContent);
|
|
@@ -1950,6 +2122,748 @@ else {
|
|
|
1950
2122
|
return afterCss(docCssReady,url,startSwap);
|
|
1951
2123
|
}
|
|
1952
2124
|
|
|
2125
|
+
// \u2500\u2500 Screens \u2500\u2500
|
|
2126
|
+
// A phone app keeps the screens it has shown. Going back finds the list
|
|
2127
|
+
// exactly where it was left, and switching tabs finds the other tab as it
|
|
2128
|
+
// was: the same DOM, its scroll, its signals still live. Re-rendering each
|
|
2129
|
+
// of them from the server on every visit is what made an stx app read as a
|
|
2130
|
+
// web page in a shell.
|
|
2131
|
+
//
|
|
2132
|
+
// So with screens on, each page is written into a screen of its own,
|
|
2133
|
+
// <div data-stx-screen>, inside the routed container. Leaving one by a push
|
|
2134
|
+
// or a tab switch hides it instead of disposing it; Back and the tab bar
|
|
2135
|
+
// show it again without running anything. Each tab owns a stack of them,
|
|
2136
|
+
// mirrored by the history entries above the one the app opened with.
|
|
2137
|
+
//
|
|
2138
|
+
// The runtime binds whichever element carries data-stx-content before it
|
|
2139
|
+
// falls back to the router's container, so the screen being shown wears
|
|
2140
|
+
// that marker and a navigation binds the new screen alone, leaving the
|
|
2141
|
+
// retained ones as they are. A container that is itself [data-stx-content]
|
|
2142
|
+
// (the app-shell mode) keeps the old behaviour, as does a page without the
|
|
2143
|
+
// signals runtime, which has nothing to retain.
|
|
2144
|
+
//
|
|
2145
|
+
// On when the page has tab links (data-stx-nav="tab"), or always with
|
|
2146
|
+
// screens:true; never with screens:false.
|
|
2147
|
+
var SCREEN='data-stx-screen';
|
|
2148
|
+
var HIDDEN='data-stx-screen-hidden';
|
|
2149
|
+
var TAB_SEL='[data-stx-nav="tab"],[data-native-tab]';
|
|
2150
|
+
var tabs={};
|
|
2151
|
+
var curTab='';
|
|
2152
|
+
var active=null;
|
|
2153
|
+
var shownSeq=0;
|
|
2154
|
+
var screensLive=false;
|
|
2155
|
+
function screensOn(){
|
|
2156
|
+
if(screensLive)return true;
|
|
2157
|
+
if(o.screens===false||o.screens==='false'||!shouldUseFragmentResponse())return false;
|
|
2158
|
+
var stx=window.stx;
|
|
2159
|
+
if(!stx||typeof stx._cleanupContainer!=='function')return false;
|
|
2160
|
+
var host=getContainer();
|
|
2161
|
+
if(!host||(host.closest&&host.closest('[data-stx-content]')))return false;
|
|
2162
|
+
return o.screens===true||!!qs(TAB_SEL);
|
|
2163
|
+
}
|
|
2164
|
+
// Where the current page lives: its screen, or the container.
|
|
2165
|
+
function activeRoot(){return active&&active.el&&active.el.isConnected?active.el:getContainer()}
|
|
2166
|
+
// A selector looked up in the page on screen first: a retained screen
|
|
2167
|
+
// carries the same ids as the page that rendered it.
|
|
2168
|
+
function findIn(sel){
|
|
2169
|
+
var root=activeRoot(),el=null;
|
|
2170
|
+
try{el=root&&root.querySelector?root.querySelector(sel):null;if(!el)el=qs(sel)}catch(e){}
|
|
2171
|
+
return el;
|
|
2172
|
+
}
|
|
2173
|
+
function stackOf(id){return tabs[id]||(tabs[id]=[])}
|
|
2174
|
+
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}}
|
|
2175
|
+
function tabId(href){return cacheKey(withCurrentLocale(href))}
|
|
2176
|
+
function tabLinks(){return qsa(TAB_SEL)}
|
|
2177
|
+
// The tab the page on screen belongs to: the one the tab bar has current,
|
|
2178
|
+
// which the sticky nav keeps lit for a screen opened from it.
|
|
2179
|
+
function currentTabId(){
|
|
2180
|
+
var links=tabLinks(),best='',i;
|
|
2181
|
+
for(i=0;i<links.length;i++)if(links[i].hasAttribute('data-stx-nav-current'))return tabId(links[i].getAttribute('href')||'');
|
|
2182
|
+
for(i=0;i<links.length;i++){
|
|
2183
|
+
var st=linkState(links[i].getAttribute('href')||'',links[i].getAttribute('data-stx-active-match'));
|
|
2184
|
+
if(st&&(st.exact||st.matched))return tabId(links[i].getAttribute('href')||'');
|
|
2185
|
+
if(st&&st.active&&!best)best=tabId(links[i].getAttribute('href')||'');
|
|
2186
|
+
}
|
|
2187
|
+
return best;
|
|
2188
|
+
}
|
|
2189
|
+
// Which tab link the bar shows as current. Set by hand on a switch: the
|
|
2190
|
+
// sticky nav would otherwise keep the old tab lit over the other tab's
|
|
2191
|
+
// pushed screen, since neither tab's own URL is on screen.
|
|
2192
|
+
function markTab(id){
|
|
2193
|
+
tabLinks().forEach(function(a){
|
|
2194
|
+
if(tabId(a.getAttribute('href')||'')===id)a.setAttribute('data-stx-nav-current','');
|
|
2195
|
+
else a.removeAttribute('data-stx-nav-current');
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
function hostAttrString(host){
|
|
2199
|
+
var out=[];
|
|
2200
|
+
if(host&&host.getAttributeNames)host.getAttributeNames().forEach(function(n){
|
|
2201
|
+
if(n==='data-stx-cattrs'||n==='data-stx-route-focus'||n==='data-stx-animating'||(n==='tabindex'&&host.hasAttribute('data-stx-route-focus')))return;
|
|
2202
|
+
out.push(n+'="'+escAttr(host.getAttribute(n))+'"');
|
|
2203
|
+
});
|
|
2204
|
+
return encodeURIComponent(out.join(' '));
|
|
2205
|
+
}
|
|
2206
|
+
// The page the app opened with becomes the first screen the first time a
|
|
2207
|
+
// screen is needed. Moved, not re-rendered: its bindings stay on its nodes.
|
|
2208
|
+
function adoptInitial(host){
|
|
2209
|
+
if(active&&active.el&&active.el.isConnected)return;
|
|
2210
|
+
screensLive=true;
|
|
2211
|
+
var el=ce('div');
|
|
2212
|
+
el.setAttribute(SCREEN,'');
|
|
2213
|
+
while(host.firstChild)el.appendChild(host.firstChild);
|
|
2214
|
+
host.appendChild(el);
|
|
2215
|
+
if(host.__stx_disposers){el.__stx_disposers=host.__stx_disposers;host.__stx_disposers=null}
|
|
2216
|
+
if(!curTab)curTab=currentTabId();
|
|
2217
|
+
active=newEntry(location.pathname+location.search+location.hash,scrollToken,histDepth);
|
|
2218
|
+
active.el=el;
|
|
2219
|
+
active.cattrs=hostAttrString(host);
|
|
2220
|
+
active.shown=++shownSeq;
|
|
2221
|
+
stackOf(curTab)[histDepth]=active;
|
|
2222
|
+
markActive(el);
|
|
2223
|
+
}
|
|
2224
|
+
function markActive(el){
|
|
2225
|
+
var host=getContainer();
|
|
2226
|
+
if(host)Array.prototype.forEach.call(host.querySelectorAll('['+SCREEN+'][data-stx-content]'),function(other){if(other!==el)other.removeAttribute('data-stx-content')});
|
|
2227
|
+
el.setAttribute('data-stx-content','');
|
|
2228
|
+
el.removeAttribute(HIDDEN);
|
|
2229
|
+
}
|
|
2230
|
+
function saveScroll(e){
|
|
2231
|
+
e.sx=window.pageXOffset||window.scrollX||0;
|
|
2232
|
+
e.sy=window.pageYOffset||window.scrollY||0;
|
|
2233
|
+
e.inner=innerScroll(e.el);
|
|
2234
|
+
}
|
|
2235
|
+
// Everything that is global to the page on screen, kept with it while it is
|
|
2236
|
+
// hidden: its page-level onDestroy callbacks (the runtime runs whatever is
|
|
2237
|
+
// queued on the next stx:load, which would tear a retained page down), its
|
|
2238
|
+
// setup, its server data and its route params.
|
|
2239
|
+
function saveEntry(e){
|
|
2240
|
+
var stx=window.stx||{};
|
|
2241
|
+
e.title=document.title;
|
|
2242
|
+
e.setup=stx._latestSetup;
|
|
2243
|
+
e.data=window.__STX_DATA__;
|
|
2244
|
+
e.config=window.__STX_RUNTIME_CONFIG__;
|
|
2245
|
+
e.params=stx._rp||window.__stx_rp||{};
|
|
2246
|
+
if(stx._destroyCallbacks)e.destroys=e.destroys.concat(stx._destroyCallbacks.splice(0));
|
|
2247
|
+
}
|
|
2248
|
+
function restoreEntry(e){
|
|
2249
|
+
var stx=window.stx||{};
|
|
2250
|
+
if(e.title)document.title=e.title;
|
|
2251
|
+
if(e.setup)stx._latestSetup=e.setup;
|
|
2252
|
+
window.__STX_DATA__=e.data||{};
|
|
2253
|
+
window.__STX_RUNTIME_CONFIG__=e.config||{};
|
|
2254
|
+
if(stx._destroyCallbacks&&e.destroys.length)Array.prototype.push.apply(stx._destroyCallbacks,e.destroys);
|
|
2255
|
+
e.destroys=[];
|
|
2256
|
+
applyContainerAttrs(getContainer(),e.cattrs);
|
|
2257
|
+
}
|
|
2258
|
+
// A new page must not share the outgoing page's scope object, or its setup
|
|
2259
|
+
// merges into the one the retained screen's bindings read. Cleaning an
|
|
2260
|
+
// empty element resets that scope and nothing else.
|
|
2261
|
+
function resetScope(){
|
|
2262
|
+
var stx=window.stx;
|
|
2263
|
+
if(!stx||!stx._cleanupContainer)return;
|
|
2264
|
+
var keep=stx._mountCallbacks?stx._mountCallbacks.splice(0):[];
|
|
2265
|
+
stx._cleanupContainer(ce('div'));
|
|
2266
|
+
if(stx._mountCallbacks)Array.prototype.push.apply(stx._mountCallbacks,keep);
|
|
2267
|
+
}
|
|
2268
|
+
function hideScreen(e){
|
|
2269
|
+
if(!e||!e.el)return;
|
|
2270
|
+
unfreeze(e.el);
|
|
2271
|
+
e.el.setAttribute(HIDDEN,'');
|
|
2272
|
+
e.el.removeAttribute('data-stx-content');
|
|
2273
|
+
}
|
|
2274
|
+
// Disposal runs the same paths a navigation always has: the runtime's
|
|
2275
|
+
// container cleanup (effects, scopes, element hooks) and the page-level
|
|
2276
|
+
// onDestroy callbacks that were put aside when the screen was hidden.
|
|
2277
|
+
// Pending mount callbacks belong to whatever is arriving, so they survive.
|
|
2278
|
+
function disposeEntry(e){
|
|
2279
|
+
if(!e||!e.el)return;
|
|
2280
|
+
var el=e.el,stx=window.stx;
|
|
2281
|
+
e.el=null;
|
|
2282
|
+
if(e===active)saveEntry(e);
|
|
2283
|
+
if(stx&&stx._cleanupContainer){
|
|
2284
|
+
var keep=stx._mountCallbacks?stx._mountCallbacks.splice(0):[];
|
|
2285
|
+
try{stx._cleanupContainer(el)}catch(err){console.error('[router] screen cleanup failed:',err)}
|
|
2286
|
+
if(stx._mountCallbacks)Array.prototype.push.apply(stx._mountCallbacks,keep);
|
|
2287
|
+
}
|
|
2288
|
+
var fns=e.destroys;e.destroys=[];
|
|
2289
|
+
fns.forEach(function(fn){try{fn()}catch(err){console.warn('[stx] destroy callback error:',err)}});
|
|
2290
|
+
if(el.parentNode)el.parentNode.removeChild(el);
|
|
2291
|
+
}
|
|
2292
|
+
function eachEntry(fn){for(var id in tabs)tabs[id].forEach(function(e){if(e)fn(e,id)})}
|
|
2293
|
+
// Keep at most screenLimit screens alive, the least recently shown going
|
|
2294
|
+
// first. An evicted screen stays in its stack as an address: Back to it
|
|
2295
|
+
// loads it again.
|
|
2296
|
+
function enforceLimit(){
|
|
2297
|
+
var live=[];
|
|
2298
|
+
eachEntry(function(e){if(e.el&&e!==active&&!e.leaving)live.push(e)});
|
|
2299
|
+
live.sort(function(a,b){return a.shown-b.shown});
|
|
2300
|
+
while(live.length+1>Math.max(1,o.screenLimit|0))disposeEntry(live.shift());
|
|
2301
|
+
}
|
|
2302
|
+
// Everything back to the plain container: before a whole-document swap and
|
|
2303
|
+
// when the screens are given up.
|
|
2304
|
+
// The screen on screen is left in place: the swap cleans the container,
|
|
2305
|
+
// and it with it, as it always has.
|
|
2306
|
+
function collapseScreens(){
|
|
2307
|
+
if(!screensLive)return;
|
|
2308
|
+
settleAnimation();
|
|
2309
|
+
eachEntry(function(e){if(e!==active)disposeEntry(e)});
|
|
2310
|
+
tabs={};active=null;screensLive=false;
|
|
2311
|
+
}
|
|
2312
|
+
// Component scopes are keyed by ids the server derives from the page, so a
|
|
2313
|
+
// second visit to the same page, pushed over a retained first one, brings
|
|
2314
|
+
// the same ids. The retained copies are renamed out of the way, registry
|
|
2315
|
+
// entry and all, so each instance keeps its own and disposing one cannot
|
|
2316
|
+
// reach the other.
|
|
2317
|
+
var retagSeq=0;
|
|
2318
|
+
function retagCollisions(html){
|
|
2319
|
+
var ids={},any=false;
|
|
2320
|
+
html.replace(/data-stx-scope="([^"]+)"/g,function(m,id){ids[id]=1;any=true;return m});
|
|
2321
|
+
if(!any)return;
|
|
2322
|
+
var scopes=window.stx&&window.stx._scopes;
|
|
2323
|
+
eachEntry(function(e){
|
|
2324
|
+
if(!e.el)return;
|
|
2325
|
+
Array.prototype.forEach.call(e.el.querySelectorAll('[data-stx-scope]'),function(node){
|
|
2326
|
+
var id=node.getAttribute('data-stx-scope');
|
|
2327
|
+
if(!ids[id])return;
|
|
2328
|
+
var next=id+'-r'+(++retagSeq);
|
|
2329
|
+
node.setAttribute('data-stx-scope',next);
|
|
2330
|
+
if(scopes&&scopes[id]&&(!scopes[id].__el||scopes[id].__el===node)){scopes[next]=scopes[id];delete scopes[id]}
|
|
2331
|
+
});
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// The swap's half: before the page is written, the outgoing screen is put
|
|
2336
|
+
// aside (lifted out of the page for a slide, or hidden), and a new screen
|
|
2337
|
+
// is made for the page to go into, first in the container so that a lookup
|
|
2338
|
+
// by id finds the page on screen before any retained copy of it.
|
|
2339
|
+
function screenBegin(host,ctx,html,writes){
|
|
2340
|
+
adoptInitial(host);
|
|
2341
|
+
var from=active;
|
|
2342
|
+
retagCollisions(html);
|
|
2343
|
+
if(from&&from.el){
|
|
2344
|
+
// A popstate saved the outgoing position already, under the token it
|
|
2345
|
+
// has since replaced; saving again here would file it under the wrong one.
|
|
2346
|
+
if(writes){rememberScroll();scrollSaved=true}
|
|
2347
|
+
saveEntry(from);
|
|
2348
|
+
}
|
|
2349
|
+
resetScope();
|
|
2350
|
+
var el=ce('div');
|
|
2351
|
+
el.setAttribute(SCREEN,'');
|
|
2352
|
+
var anim=!!(from&&from.el&&canAnimate(ctx.direction));
|
|
2353
|
+
if(anim)freeze(from.el);
|
|
2354
|
+
else if(from&&from.el)hideScreen(from);
|
|
2355
|
+
host.insertBefore(el,host.firstChild);
|
|
2356
|
+
markActive(el);
|
|
2357
|
+
return{el:el,from:from,ctx:ctx,anim:anim,cattrs:''};
|
|
2358
|
+
}
|
|
2359
|
+
// After the history write, so the entry knows its token and depth.
|
|
2360
|
+
function screenCommit(scr,href){
|
|
2361
|
+
var e=scr.ctx.entry||newEntry(href,scrollToken,histDepth);
|
|
2362
|
+
e.url=href;e.key=cacheKey(href);e.token=scrollToken;e.depth=histDepth;e.tab=curTab;
|
|
2363
|
+
e.el=scr.el;e.cattrs=scr.cattrs;e.shown=++shownSeq;e.destroys=[];
|
|
2364
|
+
var stack=stackOf(curTab);
|
|
2365
|
+
// A push drops whatever was forward of it; a pop or a replacement takes
|
|
2366
|
+
// the place of what was at its depth. The outgoing screen is left to the
|
|
2367
|
+
// transition, which disposes it once it is off screen.
|
|
2368
|
+
for(var i=e.depth;i<stack.length;i++){var x=stack[i];if(x&&x!==e&&x!==scr.from)disposeEntry(x)}
|
|
2369
|
+
stack.length=e.depth;
|
|
2370
|
+
stack[e.depth]=e;
|
|
2371
|
+
active=e;
|
|
2372
|
+
scrollSaved=false;
|
|
2373
|
+
}
|
|
2374
|
+
function screenEnd(scr){
|
|
2375
|
+
var from=scr.from,dir=scr.ctx.direction;
|
|
2376
|
+
if(!from||!from.el){finishScreen();return}
|
|
2377
|
+
// Retained: left by a push, or by a tab switch. Popped or replaced: gone.
|
|
2378
|
+
var keep=dir==='push'||dir==='tab';
|
|
2379
|
+
var stack=tabs[from.tab];
|
|
2380
|
+
if(keep&&!(stack&&stack[from.depth]===from))keep=false;
|
|
2381
|
+
from.leaving=true;
|
|
2382
|
+
var after=function(){
|
|
2383
|
+
from.leaving=false;
|
|
2384
|
+
if(keep&&from.el)hideScreen(from);
|
|
2385
|
+
else disposeEntry(from);
|
|
2386
|
+
finishScreen();
|
|
2387
|
+
};
|
|
2388
|
+
if(scr.anim)animateScreens(from.el,scr.el,dir,after);
|
|
2389
|
+
else after();
|
|
2390
|
+
}
|
|
2391
|
+
function finishScreen(){
|
|
2392
|
+
enforceLimit();
|
|
2393
|
+
observeLinks();
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// Show a screen that is already here: Back to one below in the stack, or a
|
|
2397
|
+
// tab's top screen. Nothing is fetched and no page script runs; the page is
|
|
2398
|
+
// told it is on screen again (stx:screen-shown) so it can refresh whatever
|
|
2399
|
+
// may have gone stale while it was hidden.
|
|
2400
|
+
function showRetained(to,dir,opts){
|
|
2401
|
+
opts=opts||{};
|
|
2402
|
+
settleAnimation();
|
|
2403
|
+
var from=active,host=getContainer();
|
|
2404
|
+
if(navAbort){try{navAbort.abort()}catch(e){}navAbort=null}
|
|
2405
|
+
navSeq++;
|
|
2406
|
+
isNavigating=false;
|
|
2407
|
+
document.body.classList.remove(o.loadingClass);
|
|
2408
|
+
if(from&&from.el){if(!opts.settled)saveScroll(from);saveEntry(from)}
|
|
2409
|
+
setDirection(dir);
|
|
2410
|
+
var anim=!opts.settled&&from&&from.el&&from!==to&&canAnimate(dir,true);
|
|
2411
|
+
if(anim)freeze(from.el);
|
|
2412
|
+
else if(from&&from.el&&from!==to&&!opts.settled)hideScreen(from);
|
|
2413
|
+
unfreeze(to.el);
|
|
2414
|
+
markActive(to.el);
|
|
2415
|
+
active=to;
|
|
2416
|
+
to.shown=++shownSeq;
|
|
2417
|
+
restoreEntry(to);
|
|
2418
|
+
pendingScroll=null;pendingInner=null;
|
|
2419
|
+
window.scrollTo({left:to.sx,top:to.sy,behavior:'instant'});
|
|
2420
|
+
applyInner(to.el,to.inner);
|
|
2421
|
+
if(opts.settled&&from&&from!==to&&from.el)hideScreen(from);
|
|
2422
|
+
refreshCurrentLinks();
|
|
2423
|
+
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:to.url,direction:dir,retained:true,params:to.params||{}}}));
|
|
2424
|
+
focusAfterNavigation('');
|
|
2425
|
+
setTimeout(announceRoute,0);
|
|
2426
|
+
window.dispatchEvent(new CustomEvent('stx:screen-shown',{detail:{url:to.url,direction:dir}}));
|
|
2427
|
+
revalidate(to.key,to.url);
|
|
2428
|
+
var keep=dir==='push'||dir==='tab';
|
|
2429
|
+
var after=function(){
|
|
2430
|
+
if(from&&from!==to){
|
|
2431
|
+
if(dir==='pop'){
|
|
2432
|
+
// Everything above the screen shown is gone from the stack.
|
|
2433
|
+
var stack=tabs[to.tab]||[];
|
|
2434
|
+
for(var i=to.depth+1;i<stack.length;i++)if(stack[i]&&stack[i]!==from)disposeEntry(stack[i]);
|
|
2435
|
+
stack.length=to.depth+1;
|
|
2436
|
+
}
|
|
2437
|
+
if(keep&&from.el)hideScreen(from);
|
|
2438
|
+
else if(!keep)disposeEntry(from);
|
|
2439
|
+
}
|
|
2440
|
+
finishScreen();
|
|
2441
|
+
settleDirection();
|
|
2442
|
+
};
|
|
2443
|
+
if(anim){from.leaving=true;animateScreens(from.el,to.el,dir,function(){from.leaving=false;after()})}
|
|
2444
|
+
else after();
|
|
2445
|
+
return true;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// \u2500\u2500 Direction and motion \u2500\u2500
|
|
2449
|
+
// data-nav-direction on <html> says what kind of move is under way --
|
|
2450
|
+
// push, pop, tab or replace -- for as long as it is, so CSS (and a page's
|
|
2451
|
+
// own transitions) can tell a step forward from a step back.
|
|
2452
|
+
var animEnd=null;
|
|
2453
|
+
function setDirection(d){if(d)document.documentElement.setAttribute('data-nav-direction',d)}
|
|
2454
|
+
function settleDirection(){
|
|
2455
|
+
if(isNavigating||animEnd)return;
|
|
2456
|
+
document.documentElement.removeAttribute('data-nav-direction');
|
|
2457
|
+
}
|
|
2458
|
+
function reducedMotion(){
|
|
2459
|
+
try{return !!(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches)}catch(e){return false}
|
|
2460
|
+
}
|
|
2461
|
+
// instantNav belongs to a navigation that fetched; a screen shown from
|
|
2462
|
+
// the stack (Back) is not one, and must not inherit the last tab tap's.
|
|
2463
|
+
function canAnimate(dir,shown){
|
|
2464
|
+
return (dir==='push'||dir==='pop')&&(shown||!instantNav)&&o.navDuration>0&&typeof window.getComputedStyle==='function';
|
|
2465
|
+
}
|
|
2466
|
+
// The first background behind the container that is not transparent: the
|
|
2467
|
+
// screens are transparent, and two of them sliding over each other need
|
|
2468
|
+
// the page's colour to hide the one underneath.
|
|
2469
|
+
function screenBackground(host){
|
|
2470
|
+
for(var n=host;n&&n.nodeType===1;n=n.parentNode){
|
|
2471
|
+
var c='';
|
|
2472
|
+
try{c=getComputedStyle(n).backgroundColor}catch(e){}
|
|
2473
|
+
if(c&&c!=='transparent'&&c!=='rgba(0, 0, 0, 0)')return c;
|
|
2474
|
+
}
|
|
2475
|
+
return '';
|
|
2476
|
+
}
|
|
2477
|
+
// Lifted out of the flow and pinned where it is on screen, as a box the
|
|
2478
|
+
// height of the viewport scrolled to the same place: the page can scroll
|
|
2479
|
+
// to wherever the next screen wants, the outgoing one does not move, and a
|
|
2480
|
+
// sticky header inside it stays stuck.
|
|
2481
|
+
function freeze(el,top){
|
|
2482
|
+
var r=el.getBoundingClientRect();
|
|
2483
|
+
var vh=window.innerHeight||document.documentElement.clientHeight||0;
|
|
2484
|
+
var at=top===undefined?r.top:top;
|
|
2485
|
+
var y=Math.max(at,0);
|
|
2486
|
+
var s=el.style;
|
|
2487
|
+
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';
|
|
2488
|
+
el.scrollTop=y-at;
|
|
2489
|
+
}
|
|
2490
|
+
function unfreeze(el){
|
|
2491
|
+
if(!el)return;
|
|
2492
|
+
var s=el.style;
|
|
2493
|
+
['position','top','left','width','height','overflow','margin','transform','transition','zIndex','backgroundColor','pointerEvents'].forEach(function(k){s[k]=''});
|
|
2494
|
+
el.removeAttribute('data-stx-screen-enter');
|
|
2495
|
+
el.removeAttribute('data-stx-screen-leave');
|
|
2496
|
+
el.scrollTop=0;
|
|
2497
|
+
}
|
|
2498
|
+
function parseDuration(v){
|
|
2499
|
+
v=String(v||'').split(',')[0].trim();
|
|
2500
|
+
var n=parseFloat(v);
|
|
2501
|
+
if(!(n>=0))return -1;
|
|
2502
|
+
return v.indexOf('ms')>0?n:n*1000;
|
|
2503
|
+
}
|
|
2504
|
+
function dimLayer(host){
|
|
2505
|
+
var r=host.getBoundingClientRect();
|
|
2506
|
+
var dim=ce('div');
|
|
2507
|
+
dim.setAttribute('data-stx-screen-dim','');
|
|
2508
|
+
dim.style.left=r.left+'px';
|
|
2509
|
+
dim.style.width=r.width+'px';
|
|
2510
|
+
return dim;
|
|
2511
|
+
}
|
|
2512
|
+
// Two real screens, animated by the shipped CSS: the one arriving and the
|
|
2513
|
+
// one leaving, and a dim between them over the one underneath.
|
|
2514
|
+
function animateScreens(fromEl,toEl,dir,done){
|
|
2515
|
+
var host=getContainer();
|
|
2516
|
+
var bg=screenBackground(host);
|
|
2517
|
+
var dim=dimLayer(host);
|
|
2518
|
+
host.setAttribute('data-stx-animating','');
|
|
2519
|
+
fromEl.style.backgroundColor=bg;toEl.style.backgroundColor=bg;
|
|
2520
|
+
fromEl.style.pointerEvents='none';
|
|
2521
|
+
fromEl.setAttribute('data-stx-screen-leave','');
|
|
2522
|
+
toEl.setAttribute('data-stx-screen-enter','');
|
|
2523
|
+
host.appendChild(dim);
|
|
2524
|
+
var ms=-1;
|
|
2525
|
+
try{ms=parseDuration(getComputedStyle(toEl).animationDuration)}catch(e){}
|
|
2526
|
+
if(!(ms>0))ms=o.navDuration;
|
|
2527
|
+
var timer=setTimeout(end,ms+120);
|
|
2528
|
+
function onEnd(e){if(e.target===toEl)end()}
|
|
2529
|
+
toEl.addEventListener('animationend',onEnd);
|
|
2530
|
+
function end(){
|
|
2531
|
+
if(animEnd!==end)return;
|
|
2532
|
+
animEnd=null;
|
|
2533
|
+
clearTimeout(timer);
|
|
2534
|
+
toEl.removeEventListener('animationend',onEnd);
|
|
2535
|
+
if(dim.parentNode)dim.parentNode.removeChild(dim);
|
|
2536
|
+
host.removeAttribute('data-stx-animating');
|
|
2537
|
+
toEl.removeAttribute('data-stx-screen-enter');
|
|
2538
|
+
toEl.style.backgroundColor='';
|
|
2539
|
+
done();
|
|
2540
|
+
settleDirection();
|
|
2541
|
+
}
|
|
2542
|
+
animEnd=end;
|
|
2543
|
+
}
|
|
2544
|
+
// A navigation that starts while a slide is still running finishes the
|
|
2545
|
+
// slide first, rather than lifting a screen that is halfway across.
|
|
2546
|
+
function settleAnimation(){if(animEnd)animEnd()}
|
|
2547
|
+
|
|
2548
|
+
// \u2500\u2500 Tabs \u2500\u2500
|
|
2549
|
+
// Back never crosses tabs: the history above the entry the app opened with
|
|
2550
|
+
// is always the current tab's stack. A switch rewinds to that entry, writes
|
|
2551
|
+
// the target tab's stack in its place, and shows its top screen exactly as
|
|
2552
|
+
// it was left.
|
|
2553
|
+
var rewinding=null;
|
|
2554
|
+
function rewind(){
|
|
2555
|
+
if(histDepth<=0)return Promise.resolve();
|
|
2556
|
+
return new Promise(function(resolve){
|
|
2557
|
+
var n=histDepth;
|
|
2558
|
+
var timer=setTimeout(finish,700);
|
|
2559
|
+
function finish(){
|
|
2560
|
+
clearTimeout(timer);
|
|
2561
|
+
if(rewinding===finish)rewinding=null;
|
|
2562
|
+
histDepth=(history.state&&history.state[DEPTH])|0;
|
|
2563
|
+
var token=history.state&&history.state[SCROLL_TOKEN];
|
|
2564
|
+
if(token)scrollToken=token;
|
|
2565
|
+
resolve();
|
|
2566
|
+
}
|
|
2567
|
+
rewinding=finish;
|
|
2568
|
+
history.go(-n);
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
function entryState(e,pushed){
|
|
2572
|
+
var st={};
|
|
2573
|
+
st[SCROLL_TOKEN]=e.token;st[DEPTH]=e.depth;st[TAB]=e.tab;
|
|
2574
|
+
if(pushed)st[PUSHED_MARK]=true;
|
|
2575
|
+
return st;
|
|
2576
|
+
}
|
|
2577
|
+
function selectTab(href){
|
|
2578
|
+
if(!href)return Promise.resolve(false);
|
|
2579
|
+
var id=tabId(href);
|
|
2580
|
+
var on=screensOn();
|
|
2581
|
+
if(on&&!screensLive)adoptInitial(getContainer());
|
|
2582
|
+
if(!curTab)curTab=on&&active?active.tab:currentTabId();
|
|
2583
|
+
if(id===curTab)return reselectTab(href);
|
|
2584
|
+
if(active&&active.el){rememberScroll();saveScroll(active)}
|
|
2585
|
+
settleAnimation();
|
|
2586
|
+
if(navAbort){try{navAbort.abort()}catch(e){}navAbort=null}
|
|
2587
|
+
var seq=++navSeq;
|
|
2588
|
+
return rewind().then(function(){
|
|
2589
|
+
if(seq!==navSeq)return false;
|
|
2590
|
+
curTab=id;
|
|
2591
|
+
markTab(id);
|
|
2592
|
+
var stack=on?(tabs[id]||[]).filter(Boolean):[];
|
|
2593
|
+
if(!stack.length){
|
|
2594
|
+
if(on)tabs[id]=[];
|
|
2595
|
+
navCtxNext={direction:'tab'};
|
|
2596
|
+
instantNext=true;
|
|
2597
|
+
return navigate(href,'tab');
|
|
2598
|
+
}
|
|
2599
|
+
// Rewritten in order, so Back walks this tab's own stack.
|
|
2600
|
+
stack.forEach(function(e,i){
|
|
2601
|
+
e.depth=i;e.tab=id;
|
|
2602
|
+
if(i===0)history.replaceState(entryState(e,false),'',e.url);
|
|
2603
|
+
else history.pushState(entryState(e,true),'',e.url);
|
|
2604
|
+
});
|
|
2605
|
+
tabs[id]=stack;
|
|
2606
|
+
var top=stack[stack.length-1];
|
|
2607
|
+
histDepth=top.depth;
|
|
2608
|
+
scrollToken=top.token;
|
|
2609
|
+
if(top.el)return showRetained(top,'tab');
|
|
2610
|
+
// Its top screen was let go: load it into the same entry.
|
|
2611
|
+
pendingScroll=[top.sx,top.sy];
|
|
2612
|
+
navCtxNext={direction:'tab',entry:top};
|
|
2613
|
+
instantNext=true;
|
|
2614
|
+
return navigate(top.url,false);
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
// The tab that is already current, selected again: back to the top of its
|
|
2618
|
+
// screen, and when it is there already, back to the tab's first screen.
|
|
2619
|
+
// stx:tabreselect says which, and a page that cancels it keeps its own.
|
|
2620
|
+
function reselectTab(href){
|
|
2621
|
+
var root=activeRoot();
|
|
2622
|
+
var y=window.pageYOffset||window.scrollY||0;
|
|
2623
|
+
var inner=innerScroll(root);
|
|
2624
|
+
var atRoot=active?active.depth===0&&active.key===tabId(href):cacheKey(location.href)===tabId(href);
|
|
2625
|
+
var action=(y>0||inner)?'top':atRoot?'none':'root';
|
|
2626
|
+
var ev=new CustomEvent('stx:tabreselect',{cancelable:true,detail:{href:href,action:action}});
|
|
2627
|
+
window.dispatchEvent(ev);
|
|
2628
|
+
if(ev.defaultPrevented||action==='none')return Promise.resolve(false);
|
|
2629
|
+
if(action==='top'){
|
|
2630
|
+
window.scrollTo({top:0,left:0,behavior:reducedMotion()?'instant':'smooth'});
|
|
2631
|
+
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});
|
|
2632
|
+
return Promise.resolve(true);
|
|
2633
|
+
}
|
|
2634
|
+
var stack=tabs[curTab]||[];
|
|
2635
|
+
var first=stack.filter(Boolean)[0];
|
|
2636
|
+
if(first&&first.el&&first.key===tabId(href)&&histDepth>0){
|
|
2637
|
+
var target=first;
|
|
2638
|
+
return new Promise(function(resolve){
|
|
2639
|
+
history.go(target.depth-histDepth);
|
|
2640
|
+
setTimeout(function(){resolve(true)},0);
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
// The tab's first screen is not here (opened on a deep link, or let
|
|
2644
|
+
// go): load the tab's own page in its place.
|
|
2645
|
+
return rewind().then(function(){
|
|
2646
|
+
navCtxNext={direction:'pop'};
|
|
2647
|
+
return navigate(href,'tab');
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// \u2500\u2500 Swipe back \u2500\u2500
|
|
2652
|
+
// A pan from the left edge drags the screen on top away and shows the
|
|
2653
|
+
// retained one underneath, the way iOS does: the one underneath moves from
|
|
2654
|
+
// -30% and brightens as the top one goes. Released past half way, or
|
|
2655
|
+
// flicked, it completes as a Back (history.back(), shown without a second
|
|
2656
|
+
// animation); otherwise it springs back. Only where there is a screen to
|
|
2657
|
+
// go back to in this tab, never from inside data-stx-no-swipe, and never
|
|
2658
|
+
// over something that pans sideways on its own (touch-action none, pan-x,
|
|
2659
|
+
// pan-y, or a horizontal scroller that is not at its start).
|
|
2660
|
+
var swipe=null;
|
|
2661
|
+
var swipeSettled=null;
|
|
2662
|
+
function swipePrev(){
|
|
2663
|
+
if(o.swipeBack===false||!screensLive||!active||!active.el||isNavigating||animEnd)return null;
|
|
2664
|
+
var prev=(tabs[curTab]||[])[active.depth-1];
|
|
2665
|
+
return prev&&prev.el?prev:null;
|
|
2666
|
+
}
|
|
2667
|
+
function swipeBlocked(target){
|
|
2668
|
+
var host=getContainer();
|
|
2669
|
+
for(var n=target;n&&n.nodeType===1&&n!==host;n=n.parentNode){
|
|
2670
|
+
if(n.hasAttribute('data-stx-no-swipe'))return true;
|
|
2671
|
+
var cs=null;
|
|
2672
|
+
try{cs=getComputedStyle(n)}catch(e){}
|
|
2673
|
+
if(!cs)continue;
|
|
2674
|
+
var ta=String(cs.touchAction||'').split(' ');
|
|
2675
|
+
var has=function(v){return ta.indexOf(v)!==-1};
|
|
2676
|
+
if(has('none')||(has('pan-x')||has('pan-left')||has('pan-right'))!==(has('pan-y')||has('pan-up')||has('pan-down')))return true;
|
|
2677
|
+
var ox=String(cs.overflowX||'');
|
|
2678
|
+
if((ox==='auto'||ox==='scroll')&&n.scrollWidth>n.clientWidth&&n.scrollLeft>0)return true;
|
|
2679
|
+
}
|
|
2680
|
+
return false;
|
|
2681
|
+
}
|
|
2682
|
+
function now(){return window.performance&&performance.now?performance.now():Date.now()}
|
|
2683
|
+
function touchOf(e){return (e.touches&&e.touches[0])||(e.changedTouches&&e.changedTouches[0])||null}
|
|
2684
|
+
function swipeStart(e){
|
|
2685
|
+
if(swipe||!e.touches||e.touches.length!==1)return;
|
|
2686
|
+
var t=e.touches[0];
|
|
2687
|
+
if(t.clientX>o.swipeEdge)return;
|
|
2688
|
+
var prev=swipePrev();
|
|
2689
|
+
if(!prev||swipeBlocked(e.target))return;
|
|
2690
|
+
swipe={prev:prev,x0:t.clientX,y0:t.clientY,x:t.clientX,t:now(),v:0,on:false};
|
|
2691
|
+
}
|
|
2692
|
+
function swipeBegin(){
|
|
2693
|
+
var cur=active,prev=swipe.prev,host=getContainer();
|
|
2694
|
+
var w=host.getBoundingClientRect().width||window.innerWidth||1;
|
|
2695
|
+
saveScroll(cur);
|
|
2696
|
+
var docTop=cur.el.getBoundingClientRect().top+(window.pageYOffset||window.scrollY||0);
|
|
2697
|
+
swipe.w=w;
|
|
2698
|
+
swipe.cur=cur;
|
|
2699
|
+
swipe.bg=screenBackground(host);
|
|
2700
|
+
swipe.dim=dimLayer(host);
|
|
2701
|
+
swipe.dim.setAttribute('data-stx-drag','');
|
|
2702
|
+
// Shown before it is measured: a hidden screen has no box to pin.
|
|
2703
|
+
prev.el.removeAttribute(HIDDEN);
|
|
2704
|
+
freeze(prev.el,docTop-prev.sy);
|
|
2705
|
+
applyInner(prev.el,prev.inner);
|
|
2706
|
+
prev.el.style.zIndex='1';
|
|
2707
|
+
prev.el.style.backgroundColor=swipe.bg;
|
|
2708
|
+
cur.el.style.position='relative';
|
|
2709
|
+
cur.el.style.zIndex='3';
|
|
2710
|
+
cur.el.style.backgroundColor=swipe.bg;
|
|
2711
|
+
host.setAttribute('data-stx-animating','');
|
|
2712
|
+
host.appendChild(swipe.dim);
|
|
2713
|
+
setDirection('pop');
|
|
2714
|
+
swipe.on=true;
|
|
2715
|
+
}
|
|
2716
|
+
function swipeDraw(dx){
|
|
2717
|
+
var p=Math.min(Math.max(dx/swipe.w,0),1);
|
|
2718
|
+
swipe.p=p;
|
|
2719
|
+
swipe.cur.el.style.transform='translateX('+(p*swipe.w)+'px)';
|
|
2720
|
+
swipe.prev.el.style.transform='translateX('+(-0.3*swipe.w*(1-p))+'px)';
|
|
2721
|
+
swipe.dim.style.opacity=String(1-p);
|
|
2722
|
+
}
|
|
2723
|
+
function swipeMove(e){
|
|
2724
|
+
if(!swipe)return;
|
|
2725
|
+
var t=touchOf(e);
|
|
2726
|
+
if(!t)return;
|
|
2727
|
+
var dx=t.clientX-swipe.x0,dy=t.clientY-swipe.y0;
|
|
2728
|
+
if(!swipe.on){
|
|
2729
|
+
if(Math.abs(dx)<8&&Math.abs(dy)<8)return;
|
|
2730
|
+
// Vertical first, or leftwards: a scroll, not a Back.
|
|
2731
|
+
if(dx<=0||Math.abs(dy)>=Math.abs(dx)){swipe=null;return}
|
|
2732
|
+
swipeBegin();
|
|
2733
|
+
}
|
|
2734
|
+
if(e.cancelable)e.preventDefault();
|
|
2735
|
+
var n=now(),dt=n-swipe.t;
|
|
2736
|
+
if(dt>0)swipe.v=swipe.v*0.2+((t.clientX-swipe.x)/dt)*0.8;
|
|
2737
|
+
swipe.x=t.clientX;swipe.t=n;
|
|
2738
|
+
swipeDraw(dx);
|
|
2739
|
+
}
|
|
2740
|
+
function swipeEnd(e){
|
|
2741
|
+
if(!swipe)return;
|
|
2742
|
+
var s=swipe;
|
|
2743
|
+
swipe=null;
|
|
2744
|
+
if(!s.on)return;
|
|
2745
|
+
// A finger held still before lifting has no speed left.
|
|
2746
|
+
if(now()-s.t>80)s.v=0;
|
|
2747
|
+
var commit=e.type!=='touchcancel'&&(s.p>0.5||s.v>0.5);
|
|
2748
|
+
var remaining=commit?(1-s.p)*s.w:s.p*s.w;
|
|
2749
|
+
var ms=Math.round(Math.min(o.navDuration,Math.max(120,remaining/Math.max(Math.abs(s.v),0.6))));
|
|
2750
|
+
var ease='cubic-bezier(0.2, 0.9, 0.3, 1)';
|
|
2751
|
+
[s.cur.el,s.prev.el].forEach(function(el){el.style.transition='transform '+ms+'ms '+ease});
|
|
2752
|
+
s.dim.style.transition='opacity '+ms+'ms '+ease;
|
|
2753
|
+
s.cur.el.style.transform=commit?'translateX('+s.w+'px)':'translateX(0px)';
|
|
2754
|
+
s.prev.el.style.transform=commit?'translateX(0px)':'translateX('+(-0.3*s.w)+'px)';
|
|
2755
|
+
s.dim.style.opacity=commit?'0':'1';
|
|
2756
|
+
var finished=false;
|
|
2757
|
+
var finish=function(){
|
|
2758
|
+
if(finished)return;
|
|
2759
|
+
finished=true;
|
|
2760
|
+
animEnd=null;
|
|
2761
|
+
if(s.dim.parentNode)s.dim.parentNode.removeChild(s.dim);
|
|
2762
|
+
getContainer().removeAttribute('data-stx-animating');
|
|
2763
|
+
if(commit){
|
|
2764
|
+
// Already where Back would put it: the popstate that follows shows
|
|
2765
|
+
// the screen underneath as it is, with no second animation.
|
|
2766
|
+
s.cur.el.style.visibility='hidden';
|
|
2767
|
+
swipeSettled=s.prev;
|
|
2768
|
+
history.back();
|
|
2769
|
+
}
|
|
2770
|
+
else{
|
|
2771
|
+
hideScreen(s.prev);
|
|
2772
|
+
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='';
|
|
2773
|
+
settleDirection();
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
animEnd=finish;
|
|
2777
|
+
setTimeout(finish,ms+40);
|
|
2778
|
+
}
|
|
2779
|
+
document.addEventListener('touchstart',swipeStart,{capture:true,passive:true});
|
|
2780
|
+
document.addEventListener('touchmove',swipeMove,{capture:true,passive:false});
|
|
2781
|
+
document.addEventListener('touchend',swipeEnd,{capture:true,passive:true});
|
|
2782
|
+
document.addEventListener('touchcancel',swipeEnd,{capture:true,passive:true});
|
|
2783
|
+
// The system is short of memory: let go of every hidden screen except the
|
|
2784
|
+
// one a swipe back would show.
|
|
2785
|
+
window.addEventListener('craftMemoryWarning',function(){
|
|
2786
|
+
var keep=active?(tabs[curTab]||[])[active.depth-1]:null;
|
|
2787
|
+
eachEntry(function(e){if(e!==active&&e!==keep&&!e.leaving)disposeEntry(e)});
|
|
2788
|
+
});
|
|
2789
|
+
|
|
2790
|
+
// \u2500\u2500 Revalidation and updates \u2500\u2500
|
|
2791
|
+
// A page served from the router's cache, or a screen shown again, is asked
|
|
2792
|
+
// for once more behind the swap. If it changed, the cache takes the new copy
|
|
2793
|
+
// and stx:updated says so, so the page can refresh what it shows; the
|
|
2794
|
+
// screen is never swapped out from under the user.
|
|
2795
|
+
function revalidate(key,url){
|
|
2796
|
+
if(!o.revalidate||!o.cache||!key||prefetching[key])return;
|
|
2797
|
+
if(Date.now()-(cacheAt[key]||0)<o.revalidateAfter)return;
|
|
2798
|
+
var wantsFragment=shouldUseFragmentResponse();
|
|
2799
|
+
var before=cache[key];
|
|
2800
|
+
prefetching[key]=fetch(url,{headers:wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}}).then(function(r){
|
|
2801
|
+
var incoming=r.headers.get('X-STX-Build')||'';
|
|
2802
|
+
if(isBuildSkew(incoming)){reloadForSkew(url,incoming,true);return null}
|
|
2803
|
+
return readPrefetchResponse(r,wantsFragment);
|
|
2804
|
+
}).then(function(result){
|
|
2805
|
+
if(!result)return;
|
|
2806
|
+
if(o.cache)setCache(key,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
2807
|
+
if(before!==undefined&&before!==result.html&&cacheKey(location.href)===key)emitUpdated(url,'page');
|
|
2808
|
+
}).catch(function(){}).finally(function(){delete prefetching[key]});
|
|
2809
|
+
}
|
|
2810
|
+
// The offline worker answered from its cache and found something newer
|
|
2811
|
+
// behind it. It posts { type: 'stx:updated', url, kind }, which its own
|
|
2812
|
+
// register script turns into a window event; the router does that only
|
|
2813
|
+
// where that script is not on the page, so the event is heard once. Either
|
|
2814
|
+
// way a page the router has cached is dropped, so the next visit gets the
|
|
2815
|
+
// new one.
|
|
2816
|
+
var ownUpdate=false;
|
|
2817
|
+
function emitUpdated(url,kind){
|
|
2818
|
+
ownUpdate=true;
|
|
2819
|
+
try{window.dispatchEvent(new CustomEvent('stx:updated',{detail:{url:url,kind:kind||'page'}}))}finally{ownUpdate=false}
|
|
2820
|
+
}
|
|
2821
|
+
window.addEventListener('stx:updated',function(e){
|
|
2822
|
+
var d=e&&e.detail;
|
|
2823
|
+
if(ownUpdate||!d||!d.url||d.kind==='api')return;
|
|
2824
|
+
try{evictCache(cacheKey(d.url))}catch(err){}
|
|
2825
|
+
});
|
|
2826
|
+
try{
|
|
2827
|
+
var sw=navigator.serviceWorker;
|
|
2828
|
+
if(sw&&sw.addEventListener)sw.addEventListener('message',function(e){
|
|
2829
|
+
var d=e&&e.data;
|
|
2830
|
+
if(!d||d.type!=='stx:updated'||window.stxOffline)return;
|
|
2831
|
+
window.dispatchEvent(new CustomEvent('stx:updated',{detail:{url:d.url,kind:d.kind||'page'}}));
|
|
2832
|
+
});
|
|
2833
|
+
}catch(e){}
|
|
2834
|
+
|
|
2835
|
+
// \u2500\u2500 Prefetch on sight \u2500\u2500
|
|
2836
|
+
// Links are fetched as they scroll into view, a few per screen, so most
|
|
2837
|
+
// taps find their page already here. Not on a connection that asked to
|
|
2838
|
+
// save data, and not for a link marked data-stx-prefetch="false" or
|
|
2839
|
+
// "hover" (hover and touch still prefetch everything).
|
|
2840
|
+
var sightObserver=null;
|
|
2841
|
+
var sightCount=0;
|
|
2842
|
+
function observeLinks(){
|
|
2843
|
+
if(!o.prefetch||!o.cache||o.prefetchVisible===false||typeof IntersectionObserver!=='function')return;
|
|
2844
|
+
var c=navigator.connection;
|
|
2845
|
+
if(c&&(c.saveData||/2g/.test(c.effectiveType||'')))return;
|
|
2846
|
+
if(!sightObserver)sightObserver=new IntersectionObserver(function(entries){
|
|
2847
|
+
entries.forEach(function(en){
|
|
2848
|
+
if(!en.isIntersecting)return;
|
|
2849
|
+
sightObserver.unobserve(en.target);
|
|
2850
|
+
if(sightCount>=o.prefetchVisibleMax)return;
|
|
2851
|
+
sightCount++;
|
|
2852
|
+
prefetchLink(en.target);
|
|
2853
|
+
});
|
|
2854
|
+
});
|
|
2855
|
+
sightCount=0;
|
|
2856
|
+
var root=activeRoot()||document.body;
|
|
2857
|
+
if(!root||!root.querySelectorAll)return;
|
|
2858
|
+
root.querySelectorAll('[data-stx-link][href]').forEach(function(a){
|
|
2859
|
+
if(a.__stxSeen)return;
|
|
2860
|
+
var p=a.getAttribute('data-stx-prefetch');
|
|
2861
|
+
if(p==='false'||p==='hover'||p==='none')return;
|
|
2862
|
+
a.__stxSeen=1;
|
|
2863
|
+
sightObserver.observe(a);
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
|
|
1953
2867
|
// \u2500\u2500 Link interception \u2500\u2500
|
|
1954
2868
|
// Links the router must never touch, however it found them. This is the
|
|
1955
2869
|
// opt-out set, and it is deliberately separate from shouldIntercept's
|
|
@@ -2052,6 +2966,8 @@ else {
|
|
|
2052
2966
|
e.preventDefault();
|
|
2053
2967
|
e.stopPropagation();
|
|
2054
2968
|
log('[router] navigating to:',href);
|
|
2969
|
+
// A tab: its own stack, never a back entry (selectTab).
|
|
2970
|
+
if(link.getAttribute('data-stx-nav')==='tab'||link.hasAttribute('data-native-tab')){selectTab(href);return}
|
|
2055
2971
|
instantNext=link.getAttribute('data-stx-transition')==='none';
|
|
2056
2972
|
navigate(withCurrentLocale(href));
|
|
2057
2973
|
});
|
|
@@ -2193,16 +3109,33 @@ else {
|
|
|
2193
3109
|
|
|
2194
3110
|
// \u2500\u2500 Back/forward \u2500\u2500
|
|
2195
3111
|
window.addEventListener('popstate',function(){
|
|
3112
|
+
// A tab switch rewinding to the first entry: not a navigation of its own.
|
|
3113
|
+
if(rewinding){rewinding();return}
|
|
3114
|
+
var settled=swipeSettled;
|
|
3115
|
+
swipeSettled=null;
|
|
2196
3116
|
// Nothing has scrolled yet (restoration is manual), so the viewport still
|
|
2197
3117
|
// shows the entry being left: save it under its own token before adopting
|
|
2198
3118
|
// the popped entry's, and hand the swap the position to land on.
|
|
2199
|
-
rememberScroll();
|
|
3119
|
+
if(!settled)rememberScroll();
|
|
3120
|
+
var st=history.state;
|
|
3121
|
+
// An entry the router did not write (a page's own pushState) keeps the
|
|
3122
|
+
// depth it was written at; a lower one is a step back.
|
|
3123
|
+
var depth=st&&typeof st[DEPTH]==='number'?st[DEPTH]:histDepth;
|
|
3124
|
+
var dir=depth<histDepth?'pop':depth>histDepth?'push':'replace';
|
|
3125
|
+
histDepth=depth;
|
|
3126
|
+
var popped=st&&st[SCROLL_TOKEN];
|
|
3127
|
+
scrollToken=popped||newScrollToken();
|
|
2200
3128
|
if(restoreScroll){
|
|
2201
|
-
var popped=history.state&&history.state[SCROLL_TOKEN];
|
|
2202
|
-
scrollToken=popped||newScrollToken();
|
|
2203
3129
|
if(!popped)stampScrollToken();
|
|
2204
3130
|
pendingScroll=readScroll(scrollToken);
|
|
3131
|
+
pendingInner=readInner(scrollToken);
|
|
3132
|
+
}
|
|
3133
|
+
if(screensLive){
|
|
3134
|
+
if(st&&st[TAB]!==undefined&&st[TAB]!==curTab&&tabs[st[TAB]]){curTab=st[TAB];markTab(curTab)}
|
|
3135
|
+
var target=(tabs[curTab]||[])[depth];
|
|
3136
|
+
if(target&&target.el&&target.token===scrollToken&&target!==active){showRetained(target,dir==='replace'?'pop':dir,{settled:settled===target});return}
|
|
2205
3137
|
}
|
|
3138
|
+
navCtxNext={direction:dir};
|
|
2206
3139
|
navigate(location.pathname+location.search+location.hash,false);
|
|
2207
3140
|
});
|
|
2208
3141
|
|
|
@@ -2246,7 +3179,13 @@ else {
|
|
|
2246
3179
|
setCache(key,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
2247
3180
|
if(eager)keepPage(key,result);
|
|
2248
3181
|
}
|
|
2249
|
-
if(result)
|
|
3182
|
+
if(result){
|
|
3183
|
+
loadModuleRegistries(result.html,href);
|
|
3184
|
+
// Its stylesheet too, so the tap never waits on cssLoadTimeout for a
|
|
3185
|
+
// sheet it could have had all along. Added unapplied (media=print),
|
|
3186
|
+
// as a navigation adds it; the swap makes it live.
|
|
3187
|
+
if(result.html.indexOf('<!--stx-fragment')===0)preloadGeneratedCss(fragmentCssHrefs(result.html),location.href);
|
|
3188
|
+
}
|
|
2250
3189
|
}).catch(function(){}).finally(function(){delete prefetching[key]});
|
|
2251
3190
|
}
|
|
2252
3191
|
|
|
@@ -2342,6 +3281,7 @@ else {
|
|
|
2342
3281
|
document.addEventListener('mouseover',onIntent,true);
|
|
2343
3282
|
document.addEventListener('touchstart',onIntent,{capture:true,passive:true});
|
|
2344
3283
|
window.addEventListener('stx:load',prefetchEager);
|
|
3284
|
+
window.addEventListener('stx:load',function(){setTimeout(observeLinks,0)});
|
|
2345
3285
|
}
|
|
2346
3286
|
|
|
2347
3287
|
// \u2500\u2500 Active link management \u2500\u2500
|
|
@@ -2408,7 +3348,7 @@ else {
|
|
|
2408
3348
|
// updateActiveLinks has already rewritten the classes by now.
|
|
2409
3349
|
function updateNav(){
|
|
2410
3350
|
var done=new Set();
|
|
2411
|
-
qsa('nav, #mobileNav, [data-stx-nav]').forEach(function(nav){
|
|
3351
|
+
qsa('nav, #mobileNav, [data-stx-nav]:not(a)').forEach(function(nav){
|
|
2412
3352
|
var states=[];
|
|
2413
3353
|
Array.prototype.forEach.call(nav.querySelectorAll('a[href]'),function(a){
|
|
2414
3354
|
if(done.has(a)||!a.hasAttribute('data-stx-link'))return;
|
|
@@ -2504,16 +3444,40 @@ else {
|
|
|
2504
3444
|
// The container the router focuses after a navigation is not a
|
|
2505
3445
|
// control, so it draws no focus ring: Safari drew one around the whole
|
|
2506
3446
|
// page, a blue line along its bottom edge on every screen.
|
|
2507
|
-
|
|
3447
|
+
// Links stay live while a page loads: a tap on another one is the newest
|
|
3448
|
+
// navigation and wins (navigate), where pointer-events:none used to
|
|
3449
|
+
// swallow it.
|
|
3450
|
+
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}';
|
|
3451
|
+
// One View Transitions block. There used to be two, and the second
|
|
3452
|
+
// (injected after this one) silently overrode the first: the page stays
|
|
3453
|
+
// put and only the routed content fades, which is what this now says
|
|
3454
|
+
// once. The retained screen inside it is not named, so the container
|
|
3455
|
+
// and the screen never carry the same name.
|
|
2508
3456
|
if(o.viewTransitions&&'startViewTransition' in document){
|
|
2509
3457
|
var dur=(o.viewTransitionDuration||220)+'ms';
|
|
2510
3458
|
var ease=o.viewTransitionEasing||'cubic-bezier(0.16, 1, 0.3, 1)';
|
|
2511
|
-
css+='::view-transition-old(root),::view-transition-new(root){animation
|
|
2512
|
-
css+='
|
|
3459
|
+
css+='::view-transition-old(root),::view-transition-new(root){animation:none}::view-transition{background:transparent}';
|
|
3460
|
+
css+='main,#app-content,[data-stx-content]:not([data-stx-screen]){view-transition-name:stx-content}::view-transition-group(stx-content){overflow:hidden}';
|
|
3461
|
+
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}';
|
|
2513
3462
|
css+='@keyframes stx-r-fade-out{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-4px)}}';
|
|
2514
3463
|
css+='@keyframes stx-r-fade-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}';
|
|
2515
|
-
css+='@media (prefers-reduced-motion: reduce){::view-transition-old(
|
|
3464
|
+
css+='@media (prefers-reduced-motion: reduce){::view-transition-old(stx-content),::view-transition-new(stx-content){animation-duration:0s;animation-name:none}}';
|
|
2516
3465
|
}
|
|
3466
|
+
// Screens: a hidden one takes no room, and a screen lays its page out
|
|
3467
|
+
// the way the container would have. Push and pop are iOS's: the new
|
|
3468
|
+
// screen slides in from the right edge while the one under it moves a
|
|
3469
|
+
// third of the way left and dims, over --stx-nav-duration on a spring
|
|
3470
|
+
// (linear() where the browser has it). Reduced motion cross-fades.
|
|
3471
|
+
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}';
|
|
3472
|
+
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)}}';
|
|
3473
|
+
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}';
|
|
3474
|
+
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}';
|
|
3475
|
+
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)}';
|
|
3476
|
+
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}';
|
|
3477
|
+
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}}';
|
|
3478
|
+
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}}';
|
|
3479
|
+
// The screen a failed load leaves behind (showNavError).
|
|
3480
|
+
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}';
|
|
2517
3481
|
s.textContent=css;
|
|
2518
3482
|
dhead().appendChild(s);
|
|
2519
3483
|
}
|
|
@@ -2533,12 +3497,6 @@ else {
|
|
|
2533
3497
|
}
|
|
2534
3498
|
}
|
|
2535
3499
|
|
|
2536
|
-
function injectViewTransitionCSS(){
|
|
2537
|
-
if(document.getElementById('stx-view-transitions'))return;
|
|
2538
|
-
var s=ce('style');s.id='stx-view-transitions';
|
|
2539
|
-
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}';
|
|
2540
|
-
(document.head||document.documentElement).appendChild(s);
|
|
2541
|
-
}
|
|
2542
3500
|
|
|
2543
3501
|
// \u2500\u2500 Public API \u2500\u2500
|
|
2544
3502
|
var router={
|
|
@@ -2573,6 +3531,12 @@ else {
|
|
|
2573
3531
|
evictCache(cacheKey(url||(location.pathname+location.search)));
|
|
2574
3532
|
},
|
|
2575
3533
|
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},
|
|
3534
|
+
// Switch to the tab whose link points at href, as a tap on it would:
|
|
3535
|
+
// its retained screens and its own back stack (selectTab).
|
|
3536
|
+
selectTab:selectTab,
|
|
3537
|
+
back:function(){return history.back()},
|
|
3538
|
+
// The screens kept alive, for a page or a test that wants to look.
|
|
3539
|
+
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},
|
|
2576
3540
|
cache:cache,
|
|
2577
3541
|
swap:swap,
|
|
2578
3542
|
updateNav:updateNav
|
|
@@ -2586,10 +3550,13 @@ else {
|
|
|
2586
3550
|
// \u2500\u2500 Initialize \u2500\u2500
|
|
2587
3551
|
function init(){
|
|
2588
3552
|
injectStyles();
|
|
2589
|
-
injectViewTransitionCSS();
|
|
2590
3553
|
refreshCurrentLinks();
|
|
3554
|
+
// The first screen, from the start, so the page never changes shape on
|
|
3555
|
+
// its first navigation.
|
|
3556
|
+
if(screensOn()){var host=getContainer();if(host)adoptInitial(host)}
|
|
2591
3557
|
restoreKeptPages();
|
|
2592
3558
|
prefetchEager();
|
|
3559
|
+
observeLinks();
|
|
2593
3560
|
}
|
|
2594
3561
|
|
|
2595
3562
|
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
|
|
@@ -2599,7 +3566,7 @@ else {
|
|
|
2599
3566
|
export function getRouterScript() {
|
|
2600
3567
|
if (cachedRouterScript)
|
|
2601
3568
|
return cachedRouterScript;
|
|
2602
|
-
cachedRouterScript = minifyRouterScript(stripRouterLogs(routerSource()));
|
|
3569
|
+
cachedRouterScript = minifyRouterScript(stripRouterLogs(routerSource()), !0);
|
|
2603
3570
|
return cachedRouterScript;
|
|
2604
3571
|
}
|
|
2605
3572
|
export function getRouterScriptDev() {
|