@jsenv/navi 0.29.67 → 0.29.69
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 +606 -185
- package/dist/jsenv_navi.js.map +38 -27
- package/docs/AI_INSTRUCTIONS.md +13 -7
- package/docs/list_refresh.md +86 -2
- package/docs/navigation.md +36 -0
- package/docs/route_transitions.md +126 -3
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -6273,12 +6273,16 @@ const route = (pattern, { searchParams } = {}) => {
|
|
|
6273
6273
|
const routeUrl = resolveRouteUrl(routeRelativeUrl);
|
|
6274
6274
|
return routeUrl;
|
|
6275
6275
|
};
|
|
6276
|
-
|
|
6276
|
+
// Options travel as they do to navTo() itself — `routeTransition` is the
|
|
6277
|
+
// one that matters here: what this one navigation asks of a route
|
|
6278
|
+
// transition (see route_transition.jsx), which is the programmatic half of
|
|
6279
|
+
// what a <Link routeTransition> says.
|
|
6280
|
+
route.navTo = (params, options) => {
|
|
6277
6281
|
if (!integration) {
|
|
6278
6282
|
return Promise.resolve();
|
|
6279
6283
|
}
|
|
6280
6284
|
const routeUrl = route.buildUrl(params);
|
|
6281
|
-
return integration.navTo(routeUrl);
|
|
6285
|
+
return integration.navTo(routeUrl, options);
|
|
6282
6286
|
};
|
|
6283
6287
|
route.redirectTo = (params, { callReason } = {}) => {
|
|
6284
6288
|
if (!integration) {
|
|
@@ -9250,8 +9254,7 @@ const createCalloutManager = (
|
|
|
9250
9254
|
callout.update(message, calloutOptions);
|
|
9251
9255
|
return;
|
|
9252
9256
|
}
|
|
9253
|
-
const resolvedAnchorElement =
|
|
9254
|
-
anchorElement || controller.ref.current;
|
|
9257
|
+
const resolvedAnchorElement = anchorElement || controller.ref.current;
|
|
9255
9258
|
const removeCloseOnCleanup = addTeardown?.(() => {
|
|
9256
9259
|
requestCloseCallout(new CustomEvent("cleanup"), "cleanup");
|
|
9257
9260
|
});
|
|
@@ -14998,7 +15001,8 @@ const ONE_OF_CONSTRAINT = {
|
|
|
14998
15001
|
);
|
|
14999
15002
|
const isNoMatch = visibleOptions.length === 0;
|
|
15000
15003
|
const message = field.controlHostProps["data-one-of-message"];
|
|
15001
|
-
const noMatchMessage =
|
|
15004
|
+
const noMatchMessage =
|
|
15005
|
+
field.controlHostProps["data-one-of-no-match-message"];
|
|
15002
15006
|
if (isNoMatch) {
|
|
15003
15007
|
return noMatchMessage || naviI18n("constraint.one_of.no_match");
|
|
15004
15008
|
}
|
|
@@ -15575,7 +15579,11 @@ const useDebugFocus = () => {
|
|
|
15575
15579
|
const debug = useContext(DebugFocusContext);
|
|
15576
15580
|
return debug || debugNoop;
|
|
15577
15581
|
};
|
|
15578
|
-
/**
|
|
15582
|
+
/**
|
|
15583
|
+
* Logger for virtual scroll / wheel motion (drag, momentum, glide) and for what
|
|
15584
|
+
* a virtualized list does about it — the render window moving, and the rows the
|
|
15585
|
+
* run asks for or decides not to ask for. Or a no-op.
|
|
15586
|
+
*/
|
|
15579
15587
|
const useDebugScroll = () => {
|
|
15580
15588
|
const debug = useContext(DebugScrollContext);
|
|
15581
15589
|
return debug || debugNoop;
|
|
@@ -15618,7 +15626,9 @@ const useDebugUIState = () => {
|
|
|
15618
15626
|
* @param {boolean|Function} [props.debugCommand] - navi command dispatch (`--navi-*`).
|
|
15619
15627
|
* @param {boolean|Function} [props.debugInteraction] - Gated interactions; also implies focus/scroll/popup.
|
|
15620
15628
|
* @param {boolean|Function} [props.debugFocus] - Focus moves and focus-visible decisions.
|
|
15621
|
-
* @param {boolean|Function} [props.debugScroll] - Virtual scroll / wheel motion
|
|
15629
|
+
* @param {boolean|Function} [props.debugScroll] - Virtual scroll / wheel motion,
|
|
15630
|
+
* the render window of a virtualized list, and every pass of its run — what it
|
|
15631
|
+
* asked for, or why it asked for nothing (see docs/list_refresh.md).
|
|
15622
15632
|
* @param {boolean|Function} [props.debugPopup] - Popover/dialog open/close/positioning.
|
|
15623
15633
|
* @param {boolean|Function} [props.debugAction] - Action lifecycle.
|
|
15624
15634
|
* @param {boolean|Function} [props.debugUIState] - UI-state transitions and validation.
|
|
@@ -21810,6 +21820,244 @@ const updateDocumentState = (value) => {
|
|
|
21810
21820
|
documentStateSignal.value = value;
|
|
21811
21821
|
};
|
|
21812
21822
|
|
|
21823
|
+
/**
|
|
21824
|
+
* The document's rendering, held for the one frame a view transition needs.
|
|
21825
|
+
*
|
|
21826
|
+
* The browser does not take the picture of the page being left when a
|
|
21827
|
+
* transition is ASKED for — it takes it at the next frame, just before running
|
|
21828
|
+
* the update callback. Preact renders sooner than that, in a microtask: so a
|
|
21829
|
+
* change nobody asked for (a tab pressed, the back button) has already reached
|
|
21830
|
+
* the DOM when the picture is taken, and the picture is of the page ARRIVING.
|
|
21831
|
+
* Both sides of the animation then show it, and one watches a page slide onto
|
|
21832
|
+
* itself.
|
|
21833
|
+
*
|
|
21834
|
+
* So what Preact has queued waits until the update callback, which is the
|
|
21835
|
+
* moment the API is built around — the change belongs inside it. The whole
|
|
21836
|
+
* document is held: it is about to be frozen under a picture anyway.
|
|
21837
|
+
*
|
|
21838
|
+
* ONE hold for the whole document, whoever animates. The hold is a wrapper
|
|
21839
|
+
* around Preact's `options.debounceRendering`, and two of them installed
|
|
21840
|
+
* independently restore each other in the wrong order when they let go — every
|
|
21841
|
+
* render queued in between is then handed to a wrapper nobody will ever
|
|
21842
|
+
* release. Everything that photographs a navigation (RouteTravel's box, a
|
|
21843
|
+
* route transition) must therefore hold through this module, never through a
|
|
21844
|
+
* wrapper of its own.
|
|
21845
|
+
*/
|
|
21846
|
+
|
|
21847
|
+
|
|
21848
|
+
let renderingHold = null;
|
|
21849
|
+
const holdRendering = () => {
|
|
21850
|
+
if (renderingHold) {
|
|
21851
|
+
return renderingHold.release;
|
|
21852
|
+
}
|
|
21853
|
+
const debounceRenderingBefore = options.debounceRendering;
|
|
21854
|
+
const hold = {
|
|
21855
|
+
render: null,
|
|
21856
|
+
waiting: [],
|
|
21857
|
+
release: () => {
|
|
21858
|
+
// Only the hold that is still standing may be given back: a holder
|
|
21859
|
+
// releasing after another has taken over must not let go of what it
|
|
21860
|
+
// does not hold.
|
|
21861
|
+
if (renderingHold !== hold) {
|
|
21862
|
+
return;
|
|
21863
|
+
}
|
|
21864
|
+
renderingHold = null;
|
|
21865
|
+
options.debounceRendering = debounceRenderingBefore;
|
|
21866
|
+
const { render, waiting } = hold;
|
|
21867
|
+
hold.render = null;
|
|
21868
|
+
hold.waiting = [];
|
|
21869
|
+
if (render) {
|
|
21870
|
+
render();
|
|
21871
|
+
}
|
|
21872
|
+
for (const wait of waiting) {
|
|
21873
|
+
wait();
|
|
21874
|
+
}
|
|
21875
|
+
},
|
|
21876
|
+
};
|
|
21877
|
+
renderingHold = hold;
|
|
21878
|
+
options.debounceRendering = (render) => {
|
|
21879
|
+
hold.render = render;
|
|
21880
|
+
};
|
|
21881
|
+
return hold.release;
|
|
21882
|
+
};
|
|
21883
|
+
|
|
21884
|
+
// Anything else that must not happen before the picture is taken, and the
|
|
21885
|
+
// scroll is the other one: a page one arrives at starts at its top, and the
|
|
21886
|
+
// document put back to its top while the page being left is still on screen is
|
|
21887
|
+
// a page that has ALREADY jumped when the picture is taken. Worse, the browser
|
|
21888
|
+
// paints what the new offset shows and nothing else, so the picture keeps only
|
|
21889
|
+
// the band it had already painted — the page being left is then seen in
|
|
21890
|
+
// fragments, whatever the movement does afterwards.
|
|
21891
|
+
//
|
|
21892
|
+
// Run at once when nobody is photographing anything, which is the common case
|
|
21893
|
+
// and must stay free.
|
|
21894
|
+
const whenRenderingResumes = (callback) => {
|
|
21895
|
+
if (!renderingHold) {
|
|
21896
|
+
callback();
|
|
21897
|
+
return;
|
|
21898
|
+
}
|
|
21899
|
+
renderingHold.waiting.push(callback);
|
|
21900
|
+
};
|
|
21901
|
+
|
|
21902
|
+
// The hold a navigation takes on its way in — from before its first write,
|
|
21903
|
+
// because by the time a route announces that it matches, Preact has already
|
|
21904
|
+
// been told and the render is queued; a hold taken then is a hold taken too
|
|
21905
|
+
// late. Kept here until whoever animates the change takes it over, or the
|
|
21906
|
+
// navigation turns out to be one nobody animates.
|
|
21907
|
+
let routingRenderingHold = null;
|
|
21908
|
+
const holdRenderingForRouting = () => {
|
|
21909
|
+
routingRenderingHold = holdRendering();
|
|
21910
|
+
};
|
|
21911
|
+
// Nobody had a picture to take: a page held for a change it does not animate
|
|
21912
|
+
// is a page that stutters for nothing.
|
|
21913
|
+
const releaseRoutingRenderingHold = () => {
|
|
21914
|
+
const release = routingRenderingHold;
|
|
21915
|
+
routingRenderingHold = null;
|
|
21916
|
+
if (release) {
|
|
21917
|
+
release();
|
|
21918
|
+
}
|
|
21919
|
+
};
|
|
21920
|
+
// An animator takes the navigation's hold as its own — taking another would be
|
|
21921
|
+
// taking a hold on a page that is holding still — or takes a fresh one when
|
|
21922
|
+
// the change it animates is not a navigation.
|
|
21923
|
+
const takeoverRoutingRenderingHold = () => {
|
|
21924
|
+
const release = routingRenderingHold || holdRendering();
|
|
21925
|
+
routingRenderingHold = null;
|
|
21926
|
+
return release;
|
|
21927
|
+
};
|
|
21928
|
+
|
|
21929
|
+
/**
|
|
21930
|
+
* A container has put its page on screen — or as much of it as it can.
|
|
21931
|
+
*
|
|
21932
|
+
* A route matching is a signal changing, and the page it selects reaches the
|
|
21933
|
+
* DOM only once Preact has rendered — an unknown number of passes later, in an
|
|
21934
|
+
* unknown number of microtasks. Anyone who needs the page as it IS rather than
|
|
21935
|
+
* as it has been decided (a travel about to have its picture taken by the
|
|
21936
|
+
* browser, see route_travel.jsx) waits for this instead of counting.
|
|
21937
|
+
*
|
|
21938
|
+
* A page waiting on data is announced too, by the boundary showing its loading
|
|
21939
|
+
* state (see Loading in use_async_data.jsx): what the container could put on
|
|
21940
|
+
* screen is what the browser is about to take a picture of, and a page that
|
|
21941
|
+
* cannot render yet would otherwise be waited on until the transition dies of
|
|
21942
|
+
* it. It lives in a module of its own for that: the async layer says it as much
|
|
21943
|
+
* as the router does, and neither can import the other.
|
|
21944
|
+
*/
|
|
21945
|
+
const [publishRouteRender, observeRouteRender] = createPubSub();
|
|
21946
|
+
|
|
21947
|
+
/**
|
|
21948
|
+
* Where a page was left, given back when one comes back to it.
|
|
21949
|
+
*
|
|
21950
|
+
* The browser does this on its own, and gets it wrong here for a reason that
|
|
21951
|
+
* has nothing to do with it: it puts the offset back at the instant the entry
|
|
21952
|
+
* changes, when the document still holds the page being LEFT. A position
|
|
21953
|
+
* further down than that page is tall is clamped to its bottom and lost — so
|
|
21954
|
+
* coming back to a long page from a short one lands short, and the deeper one
|
|
21955
|
+
* was, the more is missing.
|
|
21956
|
+
*
|
|
21957
|
+
* So the browser is told to stop (`scrollRestoration = "manual"`) and the
|
|
21958
|
+
* position is put back once the page one is coming back to is really there —
|
|
21959
|
+
* through the same wait as everything else that must not happen before the
|
|
21960
|
+
* picture of a transition is taken (see rendering_hold.js): restored after the
|
|
21961
|
+
* picture, the page arriving would be photographed at the top and seen jumping
|
|
21962
|
+
* from it.
|
|
21963
|
+
*
|
|
21964
|
+
* Kept per URL rather than per history entry: an entry has no name of its own
|
|
21965
|
+
* that survives a reload, and two entries on the same URL are the same place
|
|
21966
|
+
* to a reader. Kept in the session too, so a reload lands where the browser
|
|
21967
|
+
* would have landed — the flag above is a promise to do the whole job.
|
|
21968
|
+
*
|
|
21969
|
+
* What is NOT covered, and cannot be from here: a page whose height depends on
|
|
21970
|
+
* something still loading. Its content is not there at the moment it is put
|
|
21971
|
+
* back, so a position beyond what has arrived is clamped as before. Only the
|
|
21972
|
+
* page knows when it is whole.
|
|
21973
|
+
*/
|
|
21974
|
+
|
|
21975
|
+
|
|
21976
|
+
const STORAGE_KEY = "navi_scroll_positions";
|
|
21977
|
+
|
|
21978
|
+
const positionByUrl = new Map();
|
|
21979
|
+
const readStoredPositions = () => {
|
|
21980
|
+
let stored;
|
|
21981
|
+
try {
|
|
21982
|
+
stored = window.sessionStorage.getItem(STORAGE_KEY);
|
|
21983
|
+
} catch {
|
|
21984
|
+
// A session storage that refuses to answer (a private window, a policy) is
|
|
21985
|
+
// not a reason to lose the positions of THIS session.
|
|
21986
|
+
return;
|
|
21987
|
+
}
|
|
21988
|
+
if (!stored) {
|
|
21989
|
+
return;
|
|
21990
|
+
}
|
|
21991
|
+
try {
|
|
21992
|
+
for (const [url, position] of Object.entries(JSON.parse(stored))) {
|
|
21993
|
+
positionByUrl.set(url, position);
|
|
21994
|
+
}
|
|
21995
|
+
} catch {
|
|
21996
|
+
// Something else wrote there, or it was truncated.
|
|
21997
|
+
}
|
|
21998
|
+
};
|
|
21999
|
+
const storePositions = () => {
|
|
22000
|
+
try {
|
|
22001
|
+
window.sessionStorage.setItem(
|
|
22002
|
+
STORAGE_KEY,
|
|
22003
|
+
JSON.stringify(Object.fromEntries(positionByUrl)),
|
|
22004
|
+
);
|
|
22005
|
+
} catch {
|
|
22006
|
+
// Full, or refused: the session is the only thing lost.
|
|
22007
|
+
}
|
|
22008
|
+
};
|
|
22009
|
+
|
|
22010
|
+
let installed = false;
|
|
22011
|
+
const installScrollRestoration = () => {
|
|
22012
|
+
if (installed) {
|
|
22013
|
+
return;
|
|
22014
|
+
}
|
|
22015
|
+
installed = true;
|
|
22016
|
+
if (!("scrollRestoration" in window.history)) {
|
|
22017
|
+
return;
|
|
22018
|
+
}
|
|
22019
|
+
window.history.scrollRestoration = "manual";
|
|
22020
|
+
readStoredPositions();
|
|
22021
|
+
// Read as it happens rather than when leaving: a traverse changes the url
|
|
22022
|
+
// before anything here is told, so a position read then would be read for
|
|
22023
|
+
// the wrong page.
|
|
22024
|
+
window.addEventListener(
|
|
22025
|
+
"scroll",
|
|
22026
|
+
() => {
|
|
22027
|
+
positionByUrl.set(window.location.href, {
|
|
22028
|
+
x: window.scrollX,
|
|
22029
|
+
y: window.scrollY,
|
|
22030
|
+
});
|
|
22031
|
+
},
|
|
22032
|
+
{ passive: true },
|
|
22033
|
+
);
|
|
22034
|
+
window.addEventListener("pagehide", storePositions);
|
|
22035
|
+
// What a reload asks for, now that the browser has been told not to do it.
|
|
22036
|
+
// Once, and at the first render of a route: the position is only meaningful
|
|
22037
|
+
// once there is a page under it.
|
|
22038
|
+
const positionOnLoad = positionByUrl.get(window.location.href);
|
|
22039
|
+
if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
|
|
22040
|
+
const stopListening = observeRouteRender(() => {
|
|
22041
|
+
stopListening();
|
|
22042
|
+
scrollTo(positionOnLoad);
|
|
22043
|
+
});
|
|
22044
|
+
}
|
|
22045
|
+
};
|
|
22046
|
+
|
|
22047
|
+
// Nothing to put back is not the same as putting back the top: a page arrived
|
|
22048
|
+
// at for the first time is startAtTop's business, and this must not step on it.
|
|
22049
|
+
const restoreScrollPosition = (url) => {
|
|
22050
|
+
const position = positionByUrl.get(new URL(url, window.location.href).href);
|
|
22051
|
+
if (!position) {
|
|
22052
|
+
return;
|
|
22053
|
+
}
|
|
22054
|
+
scrollTo(position);
|
|
22055
|
+
};
|
|
22056
|
+
|
|
22057
|
+
const scrollTo = ({ x, y }) => {
|
|
22058
|
+
window.scrollTo({ top: y, left: x, behavior: "instant" });
|
|
22059
|
+
};
|
|
22060
|
+
|
|
21813
22061
|
/**
|
|
21814
22062
|
* A navigation is ABOUT to be applied — said before its very first write.
|
|
21815
22063
|
*
|
|
@@ -21882,8 +22130,67 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
21882
22130
|
visitedUrlsSignal.value++;
|
|
21883
22131
|
};
|
|
21884
22132
|
|
|
22133
|
+
// The one thing the History API cannot say and the Navigation API can: what
|
|
22134
|
+
// stands NEXT to the current entry. A link to the page one just came from is
|
|
22135
|
+
// morally a back — pushed, it grows the stack (A, B, A, B…) and lands at the
|
|
22136
|
+
// top; traversed, the stack stays what the reader thinks it is and the page
|
|
22137
|
+
// comes back where they left it. So a push whose destination is the entry
|
|
22138
|
+
// right behind (or right ahead) is turned into a traversal, and the whole
|
|
22139
|
+
// traverse machinery (routing, scroll, movement) answers it as if the
|
|
22140
|
+
// browser's own button had been pressed.
|
|
22141
|
+
//
|
|
22142
|
+
// Only where the browser exposes the stack (window.navigation — everywhere
|
|
22143
|
+
// but Firefox today; without it a push stays a push, which is what this
|
|
22144
|
+
// whole file already does). And only towards entries of THIS document: a
|
|
22145
|
+
// traversal to another document is a full page load, which no press on a
|
|
22146
|
+
// link asked for — the entries that are ours are recorded as they are
|
|
22147
|
+
// created, starting with the one this document was loaded into.
|
|
22148
|
+
const sameDocumentEntryKeys = new Set();
|
|
22149
|
+
const rememberEntryIsOfThisDocument = () => {
|
|
22150
|
+
if (window.navigation) {
|
|
22151
|
+
sameDocumentEntryKeys.add(window.navigation.currentEntry.key);
|
|
22152
|
+
}
|
|
22153
|
+
};
|
|
22154
|
+
rememberEntryIsOfThisDocument();
|
|
22155
|
+
const adjacentEntryDelta = (url) => {
|
|
22156
|
+
const { navigation } = window;
|
|
22157
|
+
if (!navigation) {
|
|
22158
|
+
return 0;
|
|
22159
|
+
}
|
|
22160
|
+
const entries = navigation.entries();
|
|
22161
|
+
const index = navigation.currentEntry.index;
|
|
22162
|
+
// Behind first: when the same page stands on both sides (A, B, A and one
|
|
22163
|
+
// is on B), a link to it reads as going back.
|
|
22164
|
+
for (const delta of [-1, 1]) {
|
|
22165
|
+
const entry = entries[index + delta];
|
|
22166
|
+
if (entry && entry.url === url && sameDocumentEntryKeys.has(entry.key)) {
|
|
22167
|
+
return delta;
|
|
22168
|
+
}
|
|
22169
|
+
}
|
|
22170
|
+
return 0;
|
|
22171
|
+
};
|
|
22172
|
+
|
|
21885
22173
|
let abortController = null;
|
|
21886
22174
|
const handleRoutingTask = (url, options) => {
|
|
22175
|
+
// Decided before anything is announced: an elided push IS the traversal it
|
|
22176
|
+
// becomes, and the traversal will make its own announcements when the
|
|
22177
|
+
// browser answers — a before/after cycle here would be about a navigation
|
|
22178
|
+
// that never happens.
|
|
22179
|
+
if (
|
|
22180
|
+
options.navigationType === "push" &&
|
|
22181
|
+
options.state === undefined &&
|
|
22182
|
+
url !== window.location.href
|
|
22183
|
+
) {
|
|
22184
|
+
const delta = adjacentEntryDelta(url);
|
|
22185
|
+
if (delta === -1) {
|
|
22186
|
+
window.history.back();
|
|
22187
|
+
return undefined;
|
|
22188
|
+
}
|
|
22189
|
+
if (delta === 1) {
|
|
22190
|
+
window.history.forward();
|
|
22191
|
+
return undefined;
|
|
22192
|
+
}
|
|
22193
|
+
}
|
|
21887
22194
|
// Before anything is written: the visited set, the URL and every route are
|
|
21888
22195
|
// about to change, and this is the last moment the page still stands as it
|
|
21889
22196
|
// was. And after, whichever way the change went out — so that whoever took
|
|
@@ -21943,6 +22250,7 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
21943
22250
|
} else {
|
|
21944
22251
|
window.history.replaceState(effectiveState, null, url);
|
|
21945
22252
|
}
|
|
22253
|
+
rememberEntryIsOfThisDocument();
|
|
21946
22254
|
updateDocumentUrl(url);
|
|
21947
22255
|
updateDocumentState(effectiveState);
|
|
21948
22256
|
} else {
|
|
@@ -21978,7 +22286,12 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
21978
22286
|
state,
|
|
21979
22287
|
});
|
|
21980
22288
|
if (navigationType === "push") {
|
|
21981
|
-
startAtTop(url);
|
|
22289
|
+
whenRenderingResumes(() => startAtTop(url));
|
|
22290
|
+
} else if (navigationType === "traverse") {
|
|
22291
|
+
// Where this entry was left. Waited for like the reset above, and for
|
|
22292
|
+
// the same two reasons: the page has to be there to be scrolled, and a
|
|
22293
|
+
// picture taken before it would be of a page at its top.
|
|
22294
|
+
whenRenderingResumes(() => restoreScrollPosition(url));
|
|
21982
22295
|
}
|
|
21983
22296
|
executeWithCleanup(
|
|
21984
22297
|
() => allResult,
|
|
@@ -22042,6 +22355,12 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
22042
22355
|
handleRoutingTask(href, {
|
|
22043
22356
|
reason: `"click" on a[href="${href}"]`,
|
|
22044
22357
|
navigationType: "push",
|
|
22358
|
+
// Who started it. Announced with the navigation because a press
|
|
22359
|
+
// carries things the url does not: what a link asks of a route
|
|
22360
|
+
// transition is the first of them (see route_transition.jsx). Read by
|
|
22361
|
+
// whoever knows what to do with it, and it is the anchor itself —
|
|
22362
|
+
// resolved here, where it already is.
|
|
22363
|
+
element: linkElement,
|
|
22045
22364
|
});
|
|
22046
22365
|
},
|
|
22047
22366
|
{ capture: true },
|
|
@@ -22056,6 +22375,11 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
22056
22375
|
{ capture: true },
|
|
22057
22376
|
);
|
|
22058
22377
|
|
|
22378
|
+
// The browser's own scroll restoration is taken over here rather than left
|
|
22379
|
+
// to whoever navigates: it is a decision about the document, and the entry
|
|
22380
|
+
// being left must be recorded from the first pixel scrolled.
|
|
22381
|
+
installScrollRestoration();
|
|
22382
|
+
|
|
22059
22383
|
window.addEventListener("popstate", (popstateEvent) => {
|
|
22060
22384
|
const url = window.location.href;
|
|
22061
22385
|
const state = popstateEvent.state;
|
|
@@ -22074,11 +22398,15 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
22074
22398
|
updateDocumentUrl(window.location.href);
|
|
22075
22399
|
});
|
|
22076
22400
|
|
|
22077
|
-
const navTo = async (url, { replace, state } = {}) => {
|
|
22401
|
+
const navTo = async (url, { replace, state, routeTransition } = {}) => {
|
|
22078
22402
|
handleRoutingTask(url, {
|
|
22079
22403
|
reason: `navTo called with "${url}"`,
|
|
22080
22404
|
navigationType: replace ? "replace" : "push",
|
|
22081
22405
|
state,
|
|
22406
|
+
// What this one navigation asks of a route transition, said by the call
|
|
22407
|
+
// that starts it rather than by an element — the programmatic half of
|
|
22408
|
+
// what a <Link routeTransition> says (see route_transition.jsx).
|
|
22409
|
+
routeTransition,
|
|
22082
22410
|
});
|
|
22083
22411
|
};
|
|
22084
22412
|
|
|
@@ -22140,13 +22468,16 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
22140
22468
|
// route_travel.jsx), and resetting there would throw the reader out of a page
|
|
22141
22469
|
// they never left.
|
|
22142
22470
|
//
|
|
22143
|
-
// After the routes have been told, and
|
|
22144
|
-
//
|
|
22145
|
-
//
|
|
22146
|
-
//
|
|
22147
|
-
//
|
|
22148
|
-
//
|
|
22149
|
-
// the
|
|
22471
|
+
// After the routes have been told, and after the picture of the page being
|
|
22472
|
+
// left has been taken — that ordering is the whole subtlety. The routes
|
|
22473
|
+
// changing is what sets a movement off, and a movement measures the box it is
|
|
22474
|
+
// leaving as it stands; put the document back to its top any earlier and the
|
|
22475
|
+
// picture is of a page at its first line, which the reader was not at. The
|
|
22476
|
+
// browser paints what the new offset shows and nothing else, so what is kept
|
|
22477
|
+
// of the page being left is the band it had already painted, and the movement
|
|
22478
|
+
// carries a fragment (see rendering_hold.js, which is where the waiting
|
|
22479
|
+
// happens). After pushState too, so the entry being left keeps the offset it
|
|
22480
|
+
// is at.
|
|
22150
22481
|
//
|
|
22151
22482
|
// The document, because the document is the scrollport in the common case. An
|
|
22152
22483
|
// app that scrolls an element of its own scrolls it itself.
|
|
@@ -22227,7 +22558,9 @@ const applyRouting = (
|
|
|
22227
22558
|
return { ...updateActionsResult, activeRouteSet };
|
|
22228
22559
|
};
|
|
22229
22560
|
|
|
22230
|
-
const
|
|
22561
|
+
const setupBrowserIntegration =
|
|
22562
|
+
setupBrowserIntegrationViaHistory;
|
|
22563
|
+
const browserIntegration = setupBrowserIntegration({
|
|
22231
22564
|
applyActions,
|
|
22232
22565
|
applyRouting,
|
|
22233
22566
|
// Routes are declared by the consumer and registered through
|
|
@@ -36224,24 +36557,6 @@ const TYPE_CONVERTERS = {
|
|
|
36224
36557
|
},
|
|
36225
36558
|
};
|
|
36226
36559
|
|
|
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
36560
|
const promiseStateWeakMap = new WeakMap();
|
|
36246
36561
|
const usePromiseAsyncData = (
|
|
36247
36562
|
promise,
|
|
@@ -38316,89 +38631,6 @@ const RouteUI = ({
|
|
|
38316
38631
|
return element;
|
|
38317
38632
|
};
|
|
38318
38633
|
|
|
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
38634
|
/**
|
|
38403
38635
|
* The window two pages are seen through while one replaces the other, measured
|
|
38404
38636
|
* once and published for the length of the movement.
|
|
@@ -38516,6 +38748,18 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
38516
38748
|
* own CSS (see the JSDoc below). Said without one, the relation plays the
|
|
38517
38749
|
* browser's cross-fade.
|
|
38518
38750
|
*
|
|
38751
|
+
* A relation holds for every way of reaching a page, and one navigation may
|
|
38752
|
+
* know better: the rare way round a pair — a badge that jumps back OUT to the
|
|
38753
|
+
* game it belongs to, a card that leads to the player it describes — is walked
|
|
38754
|
+
* against the map, and there is no telling it from the common way by the
|
|
38755
|
+
* routes alone. So the navigation itself may ask for something: a `<Link
|
|
38756
|
+
* routeTransition>`, or navTo(url, { routeTransition }). What it asks holds
|
|
38757
|
+
* for THAT
|
|
38758
|
+
* navigation and no other, and only for the fields it names — `{ direction:
|
|
38759
|
+
* "back" }` keeps the pair's movement and turns it round (see
|
|
38760
|
+
* readNavigationRequest). A pair no relation was ever written for animates the
|
|
38761
|
+
* same way, for the one press that asks.
|
|
38762
|
+
*
|
|
38519
38763
|
* There is no box in the tree: by default what animates is the document itself
|
|
38520
38764
|
* (its `root` view transition group), which is right for pages that ARE the
|
|
38521
38765
|
* whole viewport. An application whose pages live between fixed bars marks the
|
|
@@ -38549,6 +38793,10 @@ const TRANSITION_DURATION_PROPERTY = "--navi-route-transition-duration";
|
|
|
38549
38793
|
// right for a page that IS the whole viewport.
|
|
38550
38794
|
const TRANSITION_AREA_ATTRIBUTE = "data-navi-route-transition-area";
|
|
38551
38795
|
const TRANSITION_TARGET_ATTRIBUTE = "data-navi-route-transition-target";
|
|
38796
|
+
// What ONE navigation asks for, over whatever the relations say: worn by the
|
|
38797
|
+
// link being pressed (see <Link routeTransition>), or handed to navTo(). It answers
|
|
38798
|
+
// for that navigation and for no other — the next one is back to the relations.
|
|
38799
|
+
const TRANSITION_REQUEST_ATTRIBUTE = "data-navi-route-transition-request";
|
|
38552
38800
|
const AREA_NAME = "navi-route-transition";
|
|
38553
38801
|
// route_travel.jsx wears this on the root for the length of one of its
|
|
38554
38802
|
// travels (its TRAVEL_ATTRIBUTE — a comment there mirrors this one). Read by
|
|
@@ -38694,10 +38942,13 @@ const css$T = /* css */`
|
|
|
38694
38942
|
The movements. One of \`root\` and \`navi-route-transition\` exists at a
|
|
38695
38943
|
time (see the opt-out above), so each is written for both.
|
|
38696
38944
|
------------------------------------------------------------------ */
|
|
38697
|
-
|
|
38698
|
-
|
|
38699
|
-
|
|
38700
|
-
|
|
38945
|
+
/* What a NAMED movement is made of, whatever the movement is — the types
|
|
38946
|
+
navi ships and the ones an application writes alike. The attribute is
|
|
38947
|
+
present for a type and only for a type ("cross-fade" normalizes to no
|
|
38948
|
+
type at all, "none" starts nothing), so the browser's own cross-fade
|
|
38949
|
+
keeps every default below: scaling one picture into the other and seeing
|
|
38950
|
+
through both IS the movement there. */
|
|
38951
|
+
&[data-navi-route-transition-type] {
|
|
38701
38952
|
&::view-transition-old(root),
|
|
38702
38953
|
&::view-transition-new(root),
|
|
38703
38954
|
&::view-transition-old(navi-route-transition),
|
|
@@ -38707,20 +38958,34 @@ const css$T = /* css */`
|
|
|
38707
38958
|
the page it crosses. The picture is as wide as the box the browser
|
|
38708
38959
|
gives it — the arriving one's — so a page leaving a narrower box (a
|
|
38709
38960
|
scrollbar appeared, a side panel closed) would be seen zooming over
|
|
38710
|
-
the length of the movement
|
|
38711
|
-
|
|
38961
|
+
the length of the movement, and one leaving a shorter box would be
|
|
38962
|
+
seen inflating. */
|
|
38712
38963
|
height: auto;
|
|
38713
38964
|
object-fit: none;
|
|
38714
38965
|
object-position: top left;
|
|
38715
|
-
/*
|
|
38716
|
-
|
|
38717
|
-
|
|
38966
|
+
/* Two pages crossing are two solid things, and seeing through one to
|
|
38967
|
+
the other says they are the same page changing its mind. A movement
|
|
38968
|
+
that keeps the browser's fade on one of its two sides wants the
|
|
38969
|
+
opposite, and says so — see zoom below. */
|
|
38718
38970
|
mix-blend-mode: normal;
|
|
38719
|
-
animation-timing-function: ease;
|
|
38720
38971
|
animation-fill-mode: both;
|
|
38721
38972
|
}
|
|
38722
38973
|
}
|
|
38723
38974
|
|
|
38975
|
+
/* Eased, which is a taste about THESE four: a custom type says its own
|
|
38976
|
+
curve. */
|
|
38977
|
+
&[data-navi-route-transition-type="slide-x"],
|
|
38978
|
+
&[data-navi-route-transition-type="slide-y"],
|
|
38979
|
+
&[data-navi-route-transition-type="cover-x"],
|
|
38980
|
+
&[data-navi-route-transition-type="cover-y"] {
|
|
38981
|
+
&::view-transition-old(root),
|
|
38982
|
+
&::view-transition-new(root),
|
|
38983
|
+
&::view-transition-old(navi-route-transition),
|
|
38984
|
+
&::view-transition-new(navi-route-transition) {
|
|
38985
|
+
animation-timing-function: ease;
|
|
38986
|
+
}
|
|
38987
|
+
}
|
|
38988
|
+
|
|
38724
38989
|
&[data-navi-route-transition-type="slide-x"] {
|
|
38725
38990
|
&[data-navi-route-transition="forward"] {
|
|
38726
38991
|
&::view-transition-old(root),
|
|
@@ -38833,7 +39098,11 @@ const css$T = /* css */`
|
|
|
38833
39098
|
&::view-transition-new(root),
|
|
38834
39099
|
&::view-transition-old(navi-route-transition),
|
|
38835
39100
|
&::view-transition-new(navi-route-transition) {
|
|
38836
|
-
|
|
39101
|
+
/* One side of this one is the browser's fade, and a fade is two
|
|
39102
|
+
half-transparent pictures: they must ADD up rather than cover each
|
|
39103
|
+
other, or the page behind shows through the middle of the
|
|
39104
|
+
movement. */
|
|
39105
|
+
mix-blend-mode: plus-lighter;
|
|
38837
39106
|
}
|
|
38838
39107
|
&[data-navi-route-transition="forward"] {
|
|
38839
39108
|
&::view-transition-new(root),
|
|
@@ -38986,6 +39255,12 @@ const RouteTransitionArea = ({
|
|
|
38986
39255
|
* animation-name: my-spin-in;
|
|
38987
39256
|
* }
|
|
38988
39257
|
* }
|
|
39258
|
+
*
|
|
39259
|
+
* Whatever is written here is what EVERY crossing of the pair plays. One
|
|
39260
|
+
* crossing can ask for something else — `<Link routeTransition>`, or
|
|
39261
|
+
* navTo(url, { routeTransition }) — which overrides this field by field, for
|
|
39262
|
+
* that
|
|
39263
|
+
* navigation alone.
|
|
38989
39264
|
* @returns {() => void} remove this relation.
|
|
38990
39265
|
*/
|
|
38991
39266
|
const defineRouteTransition = (from, to, transition) => {
|
|
@@ -39002,13 +39277,11 @@ const defineRouteTransition = (from, to, transition) => {
|
|
|
39002
39277
|
};
|
|
39003
39278
|
relations.push(relation);
|
|
39004
39279
|
rebuildWatcher();
|
|
39005
|
-
updateRoutingObservers();
|
|
39006
39280
|
return () => {
|
|
39007
39281
|
const index = relations.indexOf(relation);
|
|
39008
39282
|
if (index > -1) {
|
|
39009
39283
|
relations.splice(index, 1);
|
|
39010
39284
|
rebuildWatcher();
|
|
39011
|
-
updateRoutingObservers();
|
|
39012
39285
|
}
|
|
39013
39286
|
};
|
|
39014
39287
|
};
|
|
@@ -39030,11 +39303,9 @@ const defineRouteDefaultTransition = transition => {
|
|
|
39030
39303
|
import.meta.css = [css$T, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
39031
39304
|
const value = normalizeTransition(transition);
|
|
39032
39305
|
defaultTransition = value;
|
|
39033
|
-
updateRoutingObservers();
|
|
39034
39306
|
return () => {
|
|
39035
39307
|
if (defaultTransition === value) {
|
|
39036
39308
|
defaultTransition = null;
|
|
39037
|
-
updateRoutingObservers();
|
|
39038
39309
|
}
|
|
39039
39310
|
};
|
|
39040
39311
|
};
|
|
@@ -39055,6 +39326,98 @@ const normalizeTransition = transition => {
|
|
|
39055
39326
|
};
|
|
39056
39327
|
};
|
|
39057
39328
|
|
|
39329
|
+
/**
|
|
39330
|
+
* What THIS navigation asked for, whatever the relations say.
|
|
39331
|
+
*
|
|
39332
|
+
* A relation is about the map of the app and holds for every way of reaching a
|
|
39333
|
+
* page; a request is about one crossing of it. The rare way round a pair — a
|
|
39334
|
+
* badge that jumps back out to the game it belongs to, a card that leads to
|
|
39335
|
+
* the player it describes — is a navigation that knows something the pair does
|
|
39336
|
+
* not, and this is where it says it.
|
|
39337
|
+
*
|
|
39338
|
+
* Two mouths, one meaning: the element being pressed wears it (a `<Link
|
|
39339
|
+
* routeTransition>`, or the attribute by hand on any anchor), or navTo() is
|
|
39340
|
+
* handed
|
|
39341
|
+
* it. Both arrive here through the announcement the navigation makes before it
|
|
39342
|
+
* writes anything (see before_routing.js).
|
|
39343
|
+
*
|
|
39344
|
+
* A request answers FIELD BY FIELD: what it does not say, the relation — or
|
|
39345
|
+
* the default — still answers for. So `{ direction: "back" }` keeps the pair's
|
|
39346
|
+
* movement and only turns it round, and `"none"` cuts where something would
|
|
39347
|
+
* have played.
|
|
39348
|
+
*/
|
|
39349
|
+
const readNavigationRequest = ({
|
|
39350
|
+
routeTransition,
|
|
39351
|
+
element
|
|
39352
|
+
}) => {
|
|
39353
|
+
if (routeTransition !== undefined && routeTransition !== null) {
|
|
39354
|
+
return normalizeRequest(routeTransition);
|
|
39355
|
+
}
|
|
39356
|
+
if (element && element.getAttribute) {
|
|
39357
|
+
const asked = element.getAttribute(TRANSITION_REQUEST_ATTRIBUTE);
|
|
39358
|
+
if (asked === null) {
|
|
39359
|
+
return null;
|
|
39360
|
+
}
|
|
39361
|
+
const value = asked.trim();
|
|
39362
|
+
if (value === "") {
|
|
39363
|
+
return null;
|
|
39364
|
+
}
|
|
39365
|
+
// A type is a name, and a name is all most links have to say. Anything
|
|
39366
|
+
// more — a way round, a pace — is the same object the API takes
|
|
39367
|
+
// everywhere else, written as JSON so that it travels on an attribute
|
|
39368
|
+
// (and so that a plain <a> can say it too).
|
|
39369
|
+
if (value[0] === "{") {
|
|
39370
|
+
let parsed;
|
|
39371
|
+
try {
|
|
39372
|
+
parsed = JSON.parse(value);
|
|
39373
|
+
} catch {
|
|
39374
|
+
console.warn(`${TRANSITION_REQUEST_ATTRIBUTE} is neither a type name nor JSON: ${value}`);
|
|
39375
|
+
return null;
|
|
39376
|
+
}
|
|
39377
|
+
return normalizeRequest(parsed);
|
|
39378
|
+
}
|
|
39379
|
+
return normalizeRequest(value);
|
|
39380
|
+
}
|
|
39381
|
+
return null;
|
|
39382
|
+
};
|
|
39383
|
+
const normalizeRequest = transition => {
|
|
39384
|
+
const {
|
|
39385
|
+
type,
|
|
39386
|
+
duration,
|
|
39387
|
+
direction
|
|
39388
|
+
} = typeof transition === "string" ? {
|
|
39389
|
+
type: transition
|
|
39390
|
+
} : transition;
|
|
39391
|
+
return {
|
|
39392
|
+
type: type === "cross-fade" ? undefined : type,
|
|
39393
|
+
// Whether a type was SAID, which is not the same as having one: asking for
|
|
39394
|
+
// "cross-fade" is asking for the browser's own animation, and a request
|
|
39395
|
+
// that names no type at all keeps the relation's.
|
|
39396
|
+
typeSaid: type !== undefined,
|
|
39397
|
+
duration,
|
|
39398
|
+
direction
|
|
39399
|
+
};
|
|
39400
|
+
};
|
|
39401
|
+
|
|
39402
|
+
// The request first, field by field, then what was defined for this pair (or
|
|
39403
|
+
// for everything). Written as one function because both ends of the file
|
|
39404
|
+
// resolve the same way: the one that knows the pair, and the one that only
|
|
39405
|
+
// knows a navigation landed.
|
|
39406
|
+
const resolveTransition = (request, base) => {
|
|
39407
|
+
const baseType = base ? base.type : undefined;
|
|
39408
|
+
const baseDuration = base ? base.duration : undefined;
|
|
39409
|
+
if (!request) {
|
|
39410
|
+
return {
|
|
39411
|
+
type: baseType,
|
|
39412
|
+
duration: baseDuration
|
|
39413
|
+
};
|
|
39414
|
+
}
|
|
39415
|
+
return {
|
|
39416
|
+
type: request.typeSaid ? request.type : baseType,
|
|
39417
|
+
duration: request.duration === undefined ? baseDuration : request.duration
|
|
39418
|
+
};
|
|
39419
|
+
};
|
|
39420
|
+
|
|
39058
39421
|
// Every relation defined, and the single watcher standing over all of them.
|
|
39059
39422
|
const relations = [];
|
|
39060
39423
|
let watcher = null;
|
|
@@ -39098,27 +39461,32 @@ const rebuildWatcher = () => {
|
|
|
39098
39461
|
return;
|
|
39099
39462
|
}
|
|
39100
39463
|
const found = findRelation(pages[fromIndex], pages[index]);
|
|
39101
|
-
if (!found) {
|
|
39102
|
-
// No relation says anything about these two
|
|
39103
|
-
// silence is the fact — not a
|
|
39464
|
+
if (!found && !navigationRequest) {
|
|
39465
|
+
// No relation says anything about these two and this navigation asked
|
|
39466
|
+
// for nothing: they are side by side, and silence is the fact — not a
|
|
39467
|
+
// missing case.
|
|
39104
39468
|
return;
|
|
39105
39469
|
}
|
|
39106
39470
|
const {
|
|
39107
|
-
|
|
39108
|
-
|
|
39109
|
-
} = found;
|
|
39110
|
-
if (
|
|
39471
|
+
type,
|
|
39472
|
+
duration
|
|
39473
|
+
} = resolveTransition(navigationRequest, found ? found.relation : null);
|
|
39474
|
+
if (type === "none") {
|
|
39111
39475
|
// Silence said out loud: this way of the pair was written to play
|
|
39112
|
-
// nothing
|
|
39113
|
-
// have played.
|
|
39476
|
+
// nothing — or this one navigation asked for nothing — where the reverse
|
|
39477
|
+
// of the other way, or the default, would have played.
|
|
39114
39478
|
navigationAnimated = true;
|
|
39115
39479
|
return;
|
|
39116
39480
|
}
|
|
39117
39481
|
beginTransition({
|
|
39118
39482
|
page: pages[index],
|
|
39119
|
-
|
|
39120
|
-
|
|
39121
|
-
|
|
39483
|
+
// Which way it plays: what the navigation itself said first — the link
|
|
39484
|
+
// being pressed is where the way the app is being walked is known — then
|
|
39485
|
+
// the relation, and forward for a navigation that asked for a movement
|
|
39486
|
+
// between two pages no relation orders.
|
|
39487
|
+
direction: navigationRequest && navigationRequest.direction || found && found.direction || "forward",
|
|
39488
|
+
type,
|
|
39489
|
+
duration
|
|
39122
39490
|
});
|
|
39123
39491
|
};
|
|
39124
39492
|
// `subscribe` rather than `effect`: it hands the value to a callback that is
|
|
@@ -39130,52 +39498,57 @@ const rebuildWatcher = () => {
|
|
|
39130
39498
|
};
|
|
39131
39499
|
};
|
|
39132
39500
|
|
|
39133
|
-
// What plays when no relation matched (see defineRouteDefaultTransition),
|
|
39134
|
-
//
|
|
39135
|
-
//
|
|
39136
|
-
//
|
|
39501
|
+
// What plays when no relation matched (see defineRouteDefaultTransition), what
|
|
39502
|
+
// the navigation now landing asked for on its own (see readNavigationRequest),
|
|
39503
|
+
// and whether it found an answer already — a relation's transition, a "none",
|
|
39504
|
+
// a RouteTravel travel. The last two are read at the start of every
|
|
39505
|
+
// navigation, so they are always about the latest one.
|
|
39137
39506
|
let defaultTransition = null;
|
|
39507
|
+
let navigationRequest = null;
|
|
39138
39508
|
let navigationAnimated = false;
|
|
39139
39509
|
|
|
39140
|
-
// The two ends of
|
|
39141
|
-
//
|
|
39142
|
-
//
|
|
39143
|
-
//
|
|
39144
|
-
//
|
|
39145
|
-
// its say by then.
|
|
39146
|
-
|
|
39147
|
-
|
|
39148
|
-
|
|
39149
|
-
if (
|
|
39150
|
-
const stopWatchingStart = observeBeforeRouting(() => {
|
|
39151
|
-
navigationAnimated = false;
|
|
39152
|
-
holdRenderingForRouting();
|
|
39153
|
-
});
|
|
39154
|
-
const stopWatchingEnd = observeAfterRouting(() => {
|
|
39155
|
-
if (defaultTransition && defaultTransition.type !== "none" && !navigationAnimated) {
|
|
39156
|
-
beginTransition({
|
|
39157
|
-
page: null,
|
|
39158
|
-
// A default has no direction: nothing says which of two arbitrary
|
|
39159
|
-
// pages is before the other. The attribute is worn empty — present
|
|
39160
|
-
// for whoever keys on "one of ours is playing", silent on the way.
|
|
39161
|
-
direction: "",
|
|
39162
|
-
type: defaultTransition.type,
|
|
39163
|
-
duration: defaultTransition.duration
|
|
39164
|
-
});
|
|
39165
|
-
}
|
|
39166
|
-
releaseRoutingRenderingHold();
|
|
39167
|
-
});
|
|
39168
|
-
stopRoutingObservers = () => {
|
|
39169
|
-
stopWatchingStart();
|
|
39170
|
-
stopWatchingEnd();
|
|
39171
|
-
};
|
|
39510
|
+
// The two ends of every navigation, watched from here on. The picture of the
|
|
39511
|
+
// page being left has to be honest, so rendering is held from before the
|
|
39512
|
+
// navigation's first write (see rendering_hold.js) — but only when something
|
|
39513
|
+
// could be photographed: a document where nothing is defined and nothing is
|
|
39514
|
+
// asked for holds nothing. It is given back at the far end, which is also the
|
|
39515
|
+
// one moment the DEFAULT can decide: every relation has had its say by then.
|
|
39516
|
+
observeBeforeRouting(details => {
|
|
39517
|
+
navigationAnimated = false;
|
|
39518
|
+
navigationRequest = readNavigationRequest(details);
|
|
39519
|
+
if (relations.length === 0 && !defaultTransition && !navigationRequest) {
|
|
39172
39520
|
return;
|
|
39173
39521
|
}
|
|
39174
|
-
|
|
39175
|
-
|
|
39176
|
-
|
|
39522
|
+
holdRenderingForRouting();
|
|
39523
|
+
});
|
|
39524
|
+
observeAfterRouting(() => {
|
|
39525
|
+
const request = navigationRequest;
|
|
39526
|
+
// Read here and dropped here: a request answers for the navigation it was
|
|
39527
|
+
// made on, and the next one is back to the relations.
|
|
39528
|
+
navigationRequest = null;
|
|
39529
|
+
if (!navigationAnimated && (request || defaultTransition)) {
|
|
39530
|
+
const {
|
|
39531
|
+
type,
|
|
39532
|
+
duration
|
|
39533
|
+
} = resolveTransition(request, defaultTransition);
|
|
39534
|
+
if (type !== "none") {
|
|
39535
|
+
beginTransition({
|
|
39536
|
+
page: null,
|
|
39537
|
+
// A default has no direction: nothing says which of two arbitrary
|
|
39538
|
+
// pages is before the other, and the attribute is then worn empty —
|
|
39539
|
+
// present for whoever keys on "one of ours is playing", silent on the
|
|
39540
|
+
// way. A request is the other case: a navigation IS a way round, so a
|
|
39541
|
+
// press that names the movement means forward unless it says
|
|
39542
|
+
// otherwise — and a movement of navi's is written on the direction,
|
|
39543
|
+
// so left empty it would play nothing at all.
|
|
39544
|
+
direction: request && request.direction || (request && request.typeSaid ? "forward" : ""),
|
|
39545
|
+
type,
|
|
39546
|
+
duration
|
|
39547
|
+
});
|
|
39548
|
+
}
|
|
39177
39549
|
}
|
|
39178
|
-
|
|
39550
|
+
releaseRoutingRenderingHold();
|
|
39551
|
+
});
|
|
39179
39552
|
|
|
39180
39553
|
// The exact way travelled first, over the whole registry, and only then the
|
|
39181
39554
|
// reverses: a relation written B → A owns that way, and being the reverse of
|
|
@@ -43351,6 +43724,14 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
43351
43724
|
* out of flow — the "#" anchor-on-hover pattern (e.g. inside a `Title`).
|
|
43352
43725
|
* @param {boolean} [props.hrefFallback] - Use `href` as the visible text when
|
|
43353
43726
|
* no children are given; defaults to `true` unless `anchor`.
|
|
43727
|
+
* @param {string|{type?: string, duration?: number|string, direction?: "forward"|"back"}} [props.routeTransition] -
|
|
43728
|
+
* What pressing THIS link asks of a route transition, for that one
|
|
43729
|
+
* navigation: a type name (`"slide-x"`, `"none"`, …), or an object to also
|
|
43730
|
+
* say the pace or which way it plays. It overrides field by field what
|
|
43731
|
+
* `defineRouteTransition` wrote for the pair — `{ direction: "back" }` keeps
|
|
43732
|
+
* the pair's movement and only turns it round, which is what the rare way
|
|
43733
|
+
* round a pair usually needs. Said nowhere else, the relations answer as
|
|
43734
|
+
* they always do.
|
|
43354
43735
|
* @param {boolean} [props.preventDefault] - Call `event.preventDefault()` on
|
|
43355
43736
|
* click (navigation suppressed; `onClick` still runs).
|
|
43356
43737
|
* @param {(event: MouseEvent) => void} [props.onClick]
|
|
@@ -43414,6 +43795,7 @@ const LinkPlain = props => {
|
|
|
43414
43795
|
endIcon,
|
|
43415
43796
|
revealOnInteraction = false,
|
|
43416
43797
|
hrefFallback = !anchor,
|
|
43798
|
+
routeTransition,
|
|
43417
43799
|
children
|
|
43418
43800
|
} = props;
|
|
43419
43801
|
if (anchor && !props.id) {
|
|
@@ -43520,6 +43902,13 @@ const LinkPlain = props => {
|
|
|
43520
43902
|
} else {
|
|
43521
43903
|
innerEndIcon = endIcon;
|
|
43522
43904
|
}
|
|
43905
|
+
|
|
43906
|
+
// What this link asks of a route transition, worn as an attribute so that
|
|
43907
|
+
// the navigation reads it off the element being pressed (see
|
|
43908
|
+
// route_transition.jsx, which owns the name and does the reading). A type is
|
|
43909
|
+
// a name; anything more travels as JSON, which is also how a plain <a>
|
|
43910
|
+
// writes it by hand.
|
|
43911
|
+
const routeTransitionRequest = routeTransition === undefined || routeTransition === null ? undefined : typeof routeTransition === "string" ? routeTransition : JSON.stringify(routeTransition);
|
|
43523
43912
|
const innerChildren = children || (hrefFallback ? href : children);
|
|
43524
43913
|
const startIconEl = startIcon;
|
|
43525
43914
|
const endIconEl = innerEndIcon;
|
|
@@ -43570,6 +43959,8 @@ const LinkPlain = props => {
|
|
|
43570
43959
|
startIcon: undefined,
|
|
43571
43960
|
endIcon: undefined,
|
|
43572
43961
|
hrefFallback: undefined,
|
|
43962
|
+
routeTransition: undefined,
|
|
43963
|
+
"data-navi-route-transition-request": routeTransitionRequest,
|
|
43573
43964
|
onClick: e => {
|
|
43574
43965
|
onClick?.(e);
|
|
43575
43966
|
if (slide) {
|
|
@@ -60181,6 +60572,18 @@ const useItemStore = ({
|
|
|
60181
60572
|
memoryBudget,
|
|
60182
60573
|
onRequestStateChange
|
|
60183
60574
|
}) => {
|
|
60575
|
+
// The run's asking, on the same channel as the window it asks for — they are
|
|
60576
|
+
// one subject: what the list is about to draw is what it goes to fetch (see
|
|
60577
|
+
// `useRequestMissing`, and `updateRenderWindow` which logs the other half).
|
|
60578
|
+
//
|
|
60579
|
+
// A run that decides NOT to ask is the case this exists for. It sends
|
|
60580
|
+
// nothing and changes no state, so nothing outside can see it: the network
|
|
60581
|
+
// is silent, and `onRequestStateChange` — which reports what a request is
|
|
60582
|
+
// doing — has no request to report. A run that declined and a run that was
|
|
60583
|
+
// never mounted look identical from the application's side, which makes
|
|
60584
|
+
// "this list stopped refreshing" a question with no observable answer.
|
|
60585
|
+
// Here it has one, and every pass says which.
|
|
60586
|
+
const debugScroll = useDebugScroll();
|
|
60184
60587
|
// What the source kept of the collection when the screen it was on went away
|
|
60185
60588
|
// (a range reader keeps the composition: see resource_range_reader.js). The
|
|
60186
60589
|
// rows are drawn from it right away and the window is asked for again — the
|
|
@@ -60407,17 +60810,32 @@ const useItemStore = ({
|
|
|
60407
60810
|
}
|
|
60408
60811
|
}
|
|
60409
60812
|
const ask = () => {
|
|
60813
|
+
// One line per pass, whatever the outcome — an absence in the trace
|
|
60814
|
+
// then means the run did not render, which is a different fact from
|
|
60815
|
+
// the run choosing not to ask. The state that decides is on the line
|
|
60816
|
+
// rather than left to be inferred: `revalidating` says the run knows
|
|
60817
|
+
// what it holds is from before, `holdPending` that the list is on its
|
|
60818
|
+
// way somewhere the window does not frame yet, `count` that it knows
|
|
60819
|
+
// how many rows it stands for.
|
|
60820
|
+
const debugAsk = outcome => {
|
|
60821
|
+
debugScroll(`ask ${start}-${end}: ${outcome}`, `(revalidating=${revalidating} holdPending=${virtual.holdPending} count=${pages.count})`);
|
|
60822
|
+
};
|
|
60410
60823
|
if (start === -1) {
|
|
60824
|
+
// Nothing missing and nothing to revalidate: the run has what it
|
|
60825
|
+
// draws.
|
|
60826
|
+
debugAsk("nothing missing");
|
|
60411
60827
|
return;
|
|
60412
60828
|
}
|
|
60413
60829
|
if (virtual.holdPending && pages.count !== undefined && !askingAroundWantedRow) {
|
|
60414
60830
|
// The one ask a hold lets through: the row the list is held on is
|
|
60415
60831
|
// what would lift the hold, and nothing else is going to bring it.
|
|
60832
|
+
debugAsk("held on a row not reached yet");
|
|
60416
60833
|
return;
|
|
60417
60834
|
}
|
|
60418
60835
|
const request = requestRef.current;
|
|
60419
60836
|
if (revalidating && request.busy) {
|
|
60420
60837
|
if (request.revalidating) {
|
|
60838
|
+
debugAsk("already revalidating");
|
|
60421
60839
|
return;
|
|
60422
60840
|
}
|
|
60423
60841
|
// A page for a window that is about to be replaced wholesale.
|
|
@@ -60435,6 +60853,7 @@ const useItemStore = ({
|
|
|
60435
60853
|
// waiting for to exist at all.
|
|
60436
60854
|
const stillWanted = pages.count === undefined || request.start <= windowTo && request.end >= windowFrom;
|
|
60437
60855
|
if (stillWanted) {
|
|
60856
|
+
debugAsk("a request still covers this window");
|
|
60438
60857
|
return;
|
|
60439
60858
|
}
|
|
60440
60859
|
request.controller?.abort();
|
|
@@ -60445,6 +60864,7 @@ const useItemStore = ({
|
|
|
60445
60864
|
// nothing since, can only produce the same answer. A revalidation is
|
|
60446
60865
|
// exactly the case where it produces another one.
|
|
60447
60866
|
if (!revalidating && request.start === start && request.end === end && request.held === held) {
|
|
60867
|
+
debugAsk("this range was asked for already");
|
|
60448
60868
|
return;
|
|
60449
60869
|
}
|
|
60450
60870
|
request.start = start;
|
|
@@ -60468,6 +60888,7 @@ const useItemStore = ({
|
|
|
60468
60888
|
};
|
|
60469
60889
|
request.busy = true;
|
|
60470
60890
|
request.revalidating = revalidating;
|
|
60891
|
+
debugAsk("sent");
|
|
60471
60892
|
if (revalidating) {
|
|
60472
60893
|
setRefreshing(true);
|
|
60473
60894
|
}
|