@jsenv/navi 0.29.67 → 0.29.68

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.
@@ -21810,6 +21810,244 @@ const updateDocumentState = (value) => {
21810
21810
  documentStateSignal.value = value;
21811
21811
  };
21812
21812
 
21813
+ /**
21814
+ * The document's rendering, held for the one frame a view transition needs.
21815
+ *
21816
+ * The browser does not take the picture of the page being left when a
21817
+ * transition is ASKED for — it takes it at the next frame, just before running
21818
+ * the update callback. Preact renders sooner than that, in a microtask: so a
21819
+ * change nobody asked for (a tab pressed, the back button) has already reached
21820
+ * the DOM when the picture is taken, and the picture is of the page ARRIVING.
21821
+ * Both sides of the animation then show it, and one watches a page slide onto
21822
+ * itself.
21823
+ *
21824
+ * So what Preact has queued waits until the update callback, which is the
21825
+ * moment the API is built around — the change belongs inside it. The whole
21826
+ * document is held: it is about to be frozen under a picture anyway.
21827
+ *
21828
+ * ONE hold for the whole document, whoever animates. The hold is a wrapper
21829
+ * around Preact's `options.debounceRendering`, and two of them installed
21830
+ * independently restore each other in the wrong order when they let go — every
21831
+ * render queued in between is then handed to a wrapper nobody will ever
21832
+ * release. Everything that photographs a navigation (RouteTravel's box, a
21833
+ * route transition) must therefore hold through this module, never through a
21834
+ * wrapper of its own.
21835
+ */
21836
+
21837
+
21838
+ let renderingHold = null;
21839
+ const holdRendering = () => {
21840
+ if (renderingHold) {
21841
+ return renderingHold.release;
21842
+ }
21843
+ const debounceRenderingBefore = options.debounceRendering;
21844
+ const hold = {
21845
+ render: null,
21846
+ waiting: [],
21847
+ release: () => {
21848
+ // Only the hold that is still standing may be given back: a holder
21849
+ // releasing after another has taken over must not let go of what it
21850
+ // does not hold.
21851
+ if (renderingHold !== hold) {
21852
+ return;
21853
+ }
21854
+ renderingHold = null;
21855
+ options.debounceRendering = debounceRenderingBefore;
21856
+ const { render, waiting } = hold;
21857
+ hold.render = null;
21858
+ hold.waiting = [];
21859
+ if (render) {
21860
+ render();
21861
+ }
21862
+ for (const wait of waiting) {
21863
+ wait();
21864
+ }
21865
+ },
21866
+ };
21867
+ renderingHold = hold;
21868
+ options.debounceRendering = (render) => {
21869
+ hold.render = render;
21870
+ };
21871
+ return hold.release;
21872
+ };
21873
+
21874
+ // Anything else that must not happen before the picture is taken, and the
21875
+ // scroll is the other one: a page one arrives at starts at its top, and the
21876
+ // document put back to its top while the page being left is still on screen is
21877
+ // a page that has ALREADY jumped when the picture is taken. Worse, the browser
21878
+ // paints what the new offset shows and nothing else, so the picture keeps only
21879
+ // the band it had already painted — the page being left is then seen in
21880
+ // fragments, whatever the movement does afterwards.
21881
+ //
21882
+ // Run at once when nobody is photographing anything, which is the common case
21883
+ // and must stay free.
21884
+ const whenRenderingResumes = (callback) => {
21885
+ if (!renderingHold) {
21886
+ callback();
21887
+ return;
21888
+ }
21889
+ renderingHold.waiting.push(callback);
21890
+ };
21891
+
21892
+ // The hold a navigation takes on its way in — from before its first write,
21893
+ // because by the time a route announces that it matches, Preact has already
21894
+ // been told and the render is queued; a hold taken then is a hold taken too
21895
+ // late. Kept here until whoever animates the change takes it over, or the
21896
+ // navigation turns out to be one nobody animates.
21897
+ let routingRenderingHold = null;
21898
+ const holdRenderingForRouting = () => {
21899
+ routingRenderingHold = holdRendering();
21900
+ };
21901
+ // Nobody had a picture to take: a page held for a change it does not animate
21902
+ // is a page that stutters for nothing.
21903
+ const releaseRoutingRenderingHold = () => {
21904
+ const release = routingRenderingHold;
21905
+ routingRenderingHold = null;
21906
+ if (release) {
21907
+ release();
21908
+ }
21909
+ };
21910
+ // An animator takes the navigation's hold as its own — taking another would be
21911
+ // taking a hold on a page that is holding still — or takes a fresh one when
21912
+ // the change it animates is not a navigation.
21913
+ const takeoverRoutingRenderingHold = () => {
21914
+ const release = routingRenderingHold || holdRendering();
21915
+ routingRenderingHold = null;
21916
+ return release;
21917
+ };
21918
+
21919
+ /**
21920
+ * A container has put its page on screen — or as much of it as it can.
21921
+ *
21922
+ * A route matching is a signal changing, and the page it selects reaches the
21923
+ * DOM only once Preact has rendered — an unknown number of passes later, in an
21924
+ * unknown number of microtasks. Anyone who needs the page as it IS rather than
21925
+ * as it has been decided (a travel about to have its picture taken by the
21926
+ * browser, see route_travel.jsx) waits for this instead of counting.
21927
+ *
21928
+ * A page waiting on data is announced too, by the boundary showing its loading
21929
+ * state (see Loading in use_async_data.jsx): what the container could put on
21930
+ * screen is what the browser is about to take a picture of, and a page that
21931
+ * cannot render yet would otherwise be waited on until the transition dies of
21932
+ * it. It lives in a module of its own for that: the async layer says it as much
21933
+ * as the router does, and neither can import the other.
21934
+ */
21935
+ const [publishRouteRender, observeRouteRender] = createPubSub();
21936
+
21937
+ /**
21938
+ * Where a page was left, given back when one comes back to it.
21939
+ *
21940
+ * The browser does this on its own, and gets it wrong here for a reason that
21941
+ * has nothing to do with it: it puts the offset back at the instant the entry
21942
+ * changes, when the document still holds the page being LEFT. A position
21943
+ * further down than that page is tall is clamped to its bottom and lost — so
21944
+ * coming back to a long page from a short one lands short, and the deeper one
21945
+ * was, the more is missing.
21946
+ *
21947
+ * So the browser is told to stop (`scrollRestoration = "manual"`) and the
21948
+ * position is put back once the page one is coming back to is really there —
21949
+ * through the same wait as everything else that must not happen before the
21950
+ * picture of a transition is taken (see rendering_hold.js): restored after the
21951
+ * picture, the page arriving would be photographed at the top and seen jumping
21952
+ * from it.
21953
+ *
21954
+ * Kept per URL rather than per history entry: an entry has no name of its own
21955
+ * that survives a reload, and two entries on the same URL are the same place
21956
+ * to a reader. Kept in the session too, so a reload lands where the browser
21957
+ * would have landed — the flag above is a promise to do the whole job.
21958
+ *
21959
+ * What is NOT covered, and cannot be from here: a page whose height depends on
21960
+ * something still loading. Its content is not there at the moment it is put
21961
+ * back, so a position beyond what has arrived is clamped as before. Only the
21962
+ * page knows when it is whole.
21963
+ */
21964
+
21965
+
21966
+ const STORAGE_KEY = "navi_scroll_positions";
21967
+
21968
+ const positionByUrl = new Map();
21969
+ const readStoredPositions = () => {
21970
+ let stored;
21971
+ try {
21972
+ stored = window.sessionStorage.getItem(STORAGE_KEY);
21973
+ } catch {
21974
+ // A session storage that refuses to answer (a private window, a policy) is
21975
+ // not a reason to lose the positions of THIS session.
21976
+ return;
21977
+ }
21978
+ if (!stored) {
21979
+ return;
21980
+ }
21981
+ try {
21982
+ for (const [url, position] of Object.entries(JSON.parse(stored))) {
21983
+ positionByUrl.set(url, position);
21984
+ }
21985
+ } catch {
21986
+ // Something else wrote there, or it was truncated.
21987
+ }
21988
+ };
21989
+ const storePositions = () => {
21990
+ try {
21991
+ window.sessionStorage.setItem(
21992
+ STORAGE_KEY,
21993
+ JSON.stringify(Object.fromEntries(positionByUrl)),
21994
+ );
21995
+ } catch {
21996
+ // Full, or refused: the session is the only thing lost.
21997
+ }
21998
+ };
21999
+
22000
+ let installed = false;
22001
+ const installScrollRestoration = () => {
22002
+ if (installed) {
22003
+ return;
22004
+ }
22005
+ installed = true;
22006
+ if (!("scrollRestoration" in window.history)) {
22007
+ return;
22008
+ }
22009
+ window.history.scrollRestoration = "manual";
22010
+ readStoredPositions();
22011
+ // Read as it happens rather than when leaving: a traverse changes the url
22012
+ // before anything here is told, so a position read then would be read for
22013
+ // the wrong page.
22014
+ window.addEventListener(
22015
+ "scroll",
22016
+ () => {
22017
+ positionByUrl.set(window.location.href, {
22018
+ x: window.scrollX,
22019
+ y: window.scrollY,
22020
+ });
22021
+ },
22022
+ { passive: true },
22023
+ );
22024
+ window.addEventListener("pagehide", storePositions);
22025
+ // What a reload asks for, now that the browser has been told not to do it.
22026
+ // Once, and at the first render of a route: the position is only meaningful
22027
+ // once there is a page under it.
22028
+ const positionOnLoad = positionByUrl.get(window.location.href);
22029
+ if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
22030
+ const stopListening = observeRouteRender(() => {
22031
+ stopListening();
22032
+ scrollTo(positionOnLoad);
22033
+ });
22034
+ }
22035
+ };
22036
+
22037
+ // Nothing to put back is not the same as putting back the top: a page arrived
22038
+ // at for the first time is startAtTop's business, and this must not step on it.
22039
+ const restoreScrollPosition = (url) => {
22040
+ const position = positionByUrl.get(new URL(url, window.location.href).href);
22041
+ if (!position) {
22042
+ return;
22043
+ }
22044
+ scrollTo(position);
22045
+ };
22046
+
22047
+ const scrollTo = ({ x, y }) => {
22048
+ window.scrollTo({ top: y, left: x, behavior: "instant" });
22049
+ };
22050
+
21813
22051
  /**
21814
22052
  * A navigation is ABOUT to be applied — said before its very first write.
21815
22053
  *
@@ -21978,7 +22216,12 @@ const setupBrowserIntegrationViaHistory = ({
21978
22216
  state,
21979
22217
  });
21980
22218
  if (navigationType === "push") {
21981
- startAtTop(url);
22219
+ whenRenderingResumes(() => startAtTop(url));
22220
+ } else if (navigationType === "traverse") {
22221
+ // Where this entry was left. Waited for like the reset above, and for
22222
+ // the same two reasons: the page has to be there to be scrolled, and a
22223
+ // picture taken before it would be of a page at its top.
22224
+ whenRenderingResumes(() => restoreScrollPosition(url));
21982
22225
  }
21983
22226
  executeWithCleanup(
21984
22227
  () => allResult,
@@ -22056,6 +22299,11 @@ const setupBrowserIntegrationViaHistory = ({
22056
22299
  { capture: true },
22057
22300
  );
22058
22301
 
22302
+ // The browser's own scroll restoration is taken over here rather than left
22303
+ // to whoever navigates: it is a decision about the document, and the entry
22304
+ // being left must be recorded from the first pixel scrolled.
22305
+ installScrollRestoration();
22306
+
22059
22307
  window.addEventListener("popstate", (popstateEvent) => {
22060
22308
  const url = window.location.href;
22061
22309
  const state = popstateEvent.state;
@@ -22140,13 +22388,16 @@ const setupBrowserIntegrationViaHistory = ({
22140
22388
  // route_travel.jsx), and resetting there would throw the reader out of a page
22141
22389
  // they never left.
22142
22390
  //
22143
- // After the routes have been told, and that ordering is the whole subtlety:
22144
- // the routes changing is what sets a travel off, and a travel measures the box
22145
- // it is leaving as it stands. Reset before that and the picture of the page
22146
- // being left is taken at the top of a page the reader was not at the top of —
22147
- // it is then watched jumping back to its first line before it even begins to
22148
- // leave (see holdTravelGeometry in route_travel.jsx). After pushState too, so
22149
- // the entry being left keeps the offset it is at.
22391
+ // After the routes have been told, and after the picture of the page being
22392
+ // left has been taken that ordering is the whole subtlety. The routes
22393
+ // changing is what sets a movement off, and a movement measures the box it is
22394
+ // leaving as it stands; put the document back to its top any earlier and the
22395
+ // picture is of a page at its first line, which the reader was not at. The
22396
+ // browser paints what the new offset shows and nothing else, so what is kept
22397
+ // of the page being left is the band it had already painted, and the movement
22398
+ // carries a fragment (see rendering_hold.js, which is where the waiting
22399
+ // happens). After pushState too, so the entry being left keeps the offset it
22400
+ // is at.
22150
22401
  //
22151
22402
  // The document, because the document is the scrollport in the common case. An
22152
22403
  // app that scrolls an element of its own scrolls it itself.
@@ -36224,24 +36475,6 @@ const TYPE_CONVERTERS = {
36224
36475
  },
36225
36476
  };
36226
36477
 
36227
- /**
36228
- * A container has put its page on screen — or as much of it as it can.
36229
- *
36230
- * A route matching is a signal changing, and the page it selects reaches the
36231
- * DOM only once Preact has rendered — an unknown number of passes later, in an
36232
- * unknown number of microtasks. Anyone who needs the page as it IS rather than
36233
- * as it has been decided (a travel about to have its picture taken by the
36234
- * browser, see route_travel.jsx) waits for this instead of counting.
36235
- *
36236
- * A page waiting on data is announced too, by the boundary showing its loading
36237
- * state (see Loading in use_async_data.jsx): what the container could put on
36238
- * screen is what the browser is about to take a picture of, and a page that
36239
- * cannot render yet would otherwise be waited on until the transition dies of
36240
- * it. It lives in a module of its own for that: the async layer says it as much
36241
- * as the router does, and neither can import the other.
36242
- */
36243
- const [publishRouteRender, observeRouteRender] = createPubSub();
36244
-
36245
36478
  const promiseStateWeakMap = new WeakMap();
36246
36479
  const usePromiseAsyncData = (
36247
36480
  promise,
@@ -38316,89 +38549,6 @@ const RouteUI = ({
38316
38549
  return element;
38317
38550
  };
38318
38551
 
38319
- /**
38320
- * The document's rendering, held for the one frame a view transition needs.
38321
- *
38322
- * The browser does not take the picture of the page being left when a
38323
- * transition is ASKED for — it takes it at the next frame, just before running
38324
- * the update callback. Preact renders sooner than that, in a microtask: so a
38325
- * change nobody asked for (a tab pressed, the back button) has already reached
38326
- * the DOM when the picture is taken, and the picture is of the page ARRIVING.
38327
- * Both sides of the animation then show it, and one watches a page slide onto
38328
- * itself.
38329
- *
38330
- * So what Preact has queued waits until the update callback, which is the
38331
- * moment the API is built around — the change belongs inside it. The whole
38332
- * document is held: it is about to be frozen under a picture anyway.
38333
- *
38334
- * ONE hold for the whole document, whoever animates. The hold is a wrapper
38335
- * around Preact's `options.debounceRendering`, and two of them installed
38336
- * independently restore each other in the wrong order when they let go — every
38337
- * render queued in between is then handed to a wrapper nobody will ever
38338
- * release. Everything that photographs a navigation (RouteTravel's box, a
38339
- * route transition) must therefore hold through this module, never through a
38340
- * wrapper of its own.
38341
- */
38342
-
38343
-
38344
- let renderingHold = null;
38345
- const holdRendering = () => {
38346
- if (renderingHold) {
38347
- return renderingHold.release;
38348
- }
38349
- const debounceRenderingBefore = options.debounceRendering;
38350
- const hold = {
38351
- render: null,
38352
- release: () => {
38353
- // Only the hold that is still standing may be given back: a holder
38354
- // releasing after another has taken over must not let go of what it
38355
- // does not hold.
38356
- if (renderingHold !== hold) {
38357
- return;
38358
- }
38359
- renderingHold = null;
38360
- options.debounceRendering = debounceRenderingBefore;
38361
- const { render } = hold;
38362
- hold.render = null;
38363
- if (render) {
38364
- render();
38365
- }
38366
- },
38367
- };
38368
- renderingHold = hold;
38369
- options.debounceRendering = (render) => {
38370
- hold.render = render;
38371
- };
38372
- return hold.release;
38373
- };
38374
-
38375
- // The hold a navigation takes on its way in — from before its first write,
38376
- // because by the time a route announces that it matches, Preact has already
38377
- // been told and the render is queued; a hold taken then is a hold taken too
38378
- // late. Kept here until whoever animates the change takes it over, or the
38379
- // navigation turns out to be one nobody animates.
38380
- let routingRenderingHold = null;
38381
- const holdRenderingForRouting = () => {
38382
- routingRenderingHold = holdRendering();
38383
- };
38384
- // Nobody had a picture to take: a page held for a change it does not animate
38385
- // is a page that stutters for nothing.
38386
- const releaseRoutingRenderingHold = () => {
38387
- const release = routingRenderingHold;
38388
- routingRenderingHold = null;
38389
- if (release) {
38390
- release();
38391
- }
38392
- };
38393
- // An animator takes the navigation's hold as its own — taking another would be
38394
- // taking a hold on a page that is holding still — or takes a fresh one when
38395
- // the change it animates is not a navigation.
38396
- const takeoverRoutingRenderingHold = () => {
38397
- const release = routingRenderingHold || holdRendering();
38398
- routingRenderingHold = null;
38399
- return release;
38400
- };
38401
-
38402
38552
  /**
38403
38553
  * The window two pages are seen through while one replaces the other, measured
38404
38554
  * once and published for the length of the movement.