@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.
- package/dist/jsenv_navi.js +259 -109
- package/dist/jsenv_navi.js.map +8 -6
- package/docs/AI_INSTRUCTIONS.md +8 -4
- package/docs/list_refresh.md +33 -2
- package/docs/navigation.md +26 -0
- package/docs/route_transitions.md +31 -1
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -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
|
|
22144
|
-
//
|
|
22145
|
-
//
|
|
22146
|
-
//
|
|
22147
|
-
//
|
|
22148
|
-
//
|
|
22149
|
-
// the
|
|
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.
|