stx-router 0.2.338 → 0.2.340

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.
Files changed (2) hide show
  1. package/dist/client.js +98 -7
  2. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -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};
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};
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;
@@ -72,6 +72,84 @@ function routerSource() {
72
72
  function ce(tag){return document.createElement(tag)}
73
73
  function dhead(){return document.head}
74
74
 
75
+ // \u2500\u2500 Scroll position \u2500\u2500
76
+ // Forward navigation goes to the top, or to the hash. Back and forward return
77
+ // to where the entry was left -- the browser cannot do that for us, because
78
+ // by the time it would restore, the content of the entry has not been fetched
79
+ // yet, so it clamps to whatever height the outgoing page happened to have.
80
+ // history.scrollRestoration therefore goes to manual and the positions are
81
+ // kept here, keyed by a token written into each history entry (two entries
82
+ // for the same URL keep their own positions, which is the point).
83
+ //
84
+ // Manual restoration also takes the browser's reload behaviour away, so the
85
+ // positions are mirrored into sessionStorage: a reload, or a return to the
86
+ // tab, finds the entry's token already in history.state and restores from
87
+ // there. sessionStorage is per tab and dies with it, same as the history.
88
+ var SCROLL_TOKEN='__stxScroll';
89
+ var SCROLL_STORE='stx:scroll:';
90
+ var scrollSeq=0;
91
+ var pendingScroll=null;
92
+ var restoreScroll=!!o.scrollRestoration;
93
+ function newScrollToken(){return 's'+(++scrollSeq)+'-'+Date.now()}
94
+ var scrollToken=(history.state&&history.state[SCROLL_TOKEN])||newScrollToken();
95
+ // sessionStorage is the only store: it is synchronous, it is per tab, it dies
96
+ // with the tab exactly as the history does, and it is the one that survives a
97
+ // reload. A private window that refuses it degrades to scrolling to the top.
98
+ function readScroll(token){
99
+ try{
100
+ var parts=(window.sessionStorage.getItem(SCROLL_STORE+token)||'').split(',');
101
+ return parts.length===2?[parseFloat(parts[0])||0,parseFloat(parts[1])||0]:null;
102
+ }catch(e){return null}
103
+ }
104
+ // Called while the outgoing entry is still the current one, so the position
105
+ // read here belongs to the token being written.
106
+ function rememberScroll(){
107
+ if(!restoreScroll)return;
108
+ try{window.sessionStorage.setItem(SCROLL_STORE+scrollToken,(window.pageXOffset||window.scrollX||0)+','+(window.pageYOffset||window.scrollY||0))}catch(e){}
109
+ }
110
+ // One place decides where a swap leaves the viewport, so the fragment path
111
+ // and the whole-document path cannot drift apart.
112
+ function applyScroll(hash){
113
+ if(pendingScroll){
114
+ var at=pendingScroll;
115
+ pendingScroll=null;
116
+ window.scrollTo({left:at[0],top:at[1],behavior:'instant'});
117
+ return;
118
+ }
119
+ if(o.scrollToTop&&!hash){window.scrollTo({top:0,behavior:'instant'});return}
120
+ if(hash){var el=qs(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
121
+ }
122
+ // Keeps whatever another script put in the entry; only adds the token.
123
+ function stampScrollToken(){
124
+ var state={};
125
+ var current=history.state;
126
+ if(current&&typeof current==='object')for(var key in current)state[key]=current[key];
127
+ state[SCROLL_TOKEN]=scrollToken;
128
+ try{history.replaceState(state,'',location.href)}catch(e){}
129
+ }
130
+ if(restoreScroll){
131
+ try{history.scrollRestoration='manual'}catch(e){}
132
+ // A token already in the entry means this document is a reload or a
133
+ // restore of an entry visited before, so its position is worth asking for.
134
+ var known=history.state&&history.state[SCROLL_TOKEN];
135
+ if(known){
136
+ var saved=readScroll(scrollToken);
137
+ if(saved&&(saved[0]||saved[1])){
138
+ var restoreAt=function(){window.scrollTo({left:saved[0],top:saved[1],behavior:'instant'})};
139
+ if(document.readyState==='complete')restoreAt();
140
+ else window.addEventListener('load',restoreAt,{once:true});
141
+ }
142
+ }
143
+ else{
144
+ stampScrollToken();
145
+ }
146
+ // A reload or a link out of the app never calls writeHistory, so this is
147
+ // the only chance to record where the page was left. pagehide fires for
148
+ // both, and unlike beforeunload it does not keep the page out of the
149
+ // back/forward cache.
150
+ window.addEventListener('pagehide',rememberScroll);
151
+ }
152
+
75
153
  // Replace, never merge: absent payloads must forget the outgoing page's data.
76
154
  // Run at the committed swap, not during prefetch or a superseded navigation.
77
155
  function hydrateServerData(html){
@@ -662,9 +740,14 @@ function routerSource() {
662
740
  // through swap() to each of the three history sites would have been the
663
741
  // alternative (#1807).
664
742
  function writeHistory(mode,href){
665
- if(mode==='replace')history.replaceState({},'',href);
666
- else history.pushState({},'',href);
743
+ // The position belongs to the entry being left, and scrollToken still
744
+ // names it here.
745
+ rememberScroll();
746
+ if(mode==='replace'){history.replaceState(tokenState(),'',href);return}
747
+ scrollToken=newScrollToken();
748
+ history.pushState(tokenState(),'',href);
667
749
  }
750
+ function tokenState(){var state={};state[SCROLL_TOKEN]=scrollToken;return state}
668
751
 
669
752
  // Second arg accepts the legacy pushState boolean OR an options object
670
753
  // { replace }. Callers inside this file still pass the boolean.
@@ -1119,8 +1202,7 @@ else {
1119
1202
  qsa('script[data-stx-page]').forEach(function(s){s.remove()});
1120
1203
  if(pushState!==false)writeHistory(pushState,url+(hash||''));
1121
1204
  refreshCurrentLinks();
1122
- if(o.scrollToTop&&!hash)window.scrollTo({top:0,behavior:'instant'});
1123
- else if(hash){var el=qs(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
1205
+ applyScroll(hash);
1124
1206
  window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
1125
1207
  // Before page scripts run, so a page that focuses its own control on
1126
1208
  // mount still wins \u2014 its script executes after this.
@@ -1578,8 +1660,7 @@ else {
1578
1660
  refreshCurrentLinks();
1579
1661
 
1580
1662
  // Scroll
1581
- if(o.scrollToTop&&!hash)window.scrollTo({top:0,behavior:'instant'});
1582
- else if(hash){var el=qs(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
1663
+ applyScroll(hash);
1583
1664
 
1584
1665
  // Update title
1585
1666
  var newTitle=doc.querySelector('title');
@@ -1945,6 +2026,16 @@ else {
1945
2026
 
1946
2027
  // \u2500\u2500 Back/forward \u2500\u2500
1947
2028
  window.addEventListener('popstate',function(){
2029
+ // Nothing has scrolled yet (restoration is manual), so the viewport still
2030
+ // shows the entry being left: save it under its own token before adopting
2031
+ // the popped entry's, and hand the swap the position to land on.
2032
+ rememberScroll();
2033
+ if(restoreScroll){
2034
+ var popped=history.state&&history.state[SCROLL_TOKEN];
2035
+ scrollToken=popped||newScrollToken();
2036
+ if(!popped)stampScrollToken();
2037
+ pendingScroll=readScroll(scrollToken);
2038
+ }
1948
2039
  navigate(location.pathname+location.search+location.hash,false);
1949
2040
  });
1950
2041
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stx-router",
3
3
  "type": "module",
4
- "version": "0.2.338",
4
+ "version": "0.2.340",
5
5
  "description": "File-based router for STX — .stx template discovery, nested layouts, typed route params, middleware, and client-side SPA navigation.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",