@jsenv/navi 0.29.68 → 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 +347 -76
- package/dist/jsenv_navi.js.map +32 -23
- package/docs/AI_INSTRUCTIONS.md +5 -3
- package/docs/list_refresh.md +57 -4
- package/docs/navigation.md +15 -5
- package/docs/route_transitions.md +100 -7
- 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.
|
|
@@ -22120,8 +22130,67 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
22120
22130
|
visitedUrlsSignal.value++;
|
|
22121
22131
|
};
|
|
22122
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
|
+
|
|
22123
22173
|
let abortController = null;
|
|
22124
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
|
+
}
|
|
22125
22194
|
// Before anything is written: the visited set, the URL and every route are
|
|
22126
22195
|
// about to change, and this is the last moment the page still stands as it
|
|
22127
22196
|
// was. And after, whichever way the change went out — so that whoever took
|
|
@@ -22181,6 +22250,7 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
22181
22250
|
} else {
|
|
22182
22251
|
window.history.replaceState(effectiveState, null, url);
|
|
22183
22252
|
}
|
|
22253
|
+
rememberEntryIsOfThisDocument();
|
|
22184
22254
|
updateDocumentUrl(url);
|
|
22185
22255
|
updateDocumentState(effectiveState);
|
|
22186
22256
|
} else {
|
|
@@ -22285,6 +22355,12 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
22285
22355
|
handleRoutingTask(href, {
|
|
22286
22356
|
reason: `"click" on a[href="${href}"]`,
|
|
22287
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,
|
|
22288
22364
|
});
|
|
22289
22365
|
},
|
|
22290
22366
|
{ capture: true },
|
|
@@ -22322,11 +22398,15 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
22322
22398
|
updateDocumentUrl(window.location.href);
|
|
22323
22399
|
});
|
|
22324
22400
|
|
|
22325
|
-
const navTo = async (url, { replace, state } = {}) => {
|
|
22401
|
+
const navTo = async (url, { replace, state, routeTransition } = {}) => {
|
|
22326
22402
|
handleRoutingTask(url, {
|
|
22327
22403
|
reason: `navTo called with "${url}"`,
|
|
22328
22404
|
navigationType: replace ? "replace" : "push",
|
|
22329
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,
|
|
22330
22410
|
});
|
|
22331
22411
|
};
|
|
22332
22412
|
|
|
@@ -22478,7 +22558,9 @@ const applyRouting = (
|
|
|
22478
22558
|
return { ...updateActionsResult, activeRouteSet };
|
|
22479
22559
|
};
|
|
22480
22560
|
|
|
22481
|
-
const
|
|
22561
|
+
const setupBrowserIntegration =
|
|
22562
|
+
setupBrowserIntegrationViaHistory;
|
|
22563
|
+
const browserIntegration = setupBrowserIntegration({
|
|
22482
22564
|
applyActions,
|
|
22483
22565
|
applyRouting,
|
|
22484
22566
|
// Routes are declared by the consumer and registered through
|
|
@@ -38666,6 +38748,18 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
38666
38748
|
* own CSS (see the JSDoc below). Said without one, the relation plays the
|
|
38667
38749
|
* browser's cross-fade.
|
|
38668
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
|
+
*
|
|
38669
38763
|
* There is no box in the tree: by default what animates is the document itself
|
|
38670
38764
|
* (its `root` view transition group), which is right for pages that ARE the
|
|
38671
38765
|
* whole viewport. An application whose pages live between fixed bars marks the
|
|
@@ -38699,6 +38793,10 @@ const TRANSITION_DURATION_PROPERTY = "--navi-route-transition-duration";
|
|
|
38699
38793
|
// right for a page that IS the whole viewport.
|
|
38700
38794
|
const TRANSITION_AREA_ATTRIBUTE = "data-navi-route-transition-area";
|
|
38701
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";
|
|
38702
38800
|
const AREA_NAME = "navi-route-transition";
|
|
38703
38801
|
// route_travel.jsx wears this on the root for the length of one of its
|
|
38704
38802
|
// travels (its TRAVEL_ATTRIBUTE — a comment there mirrors this one). Read by
|
|
@@ -38844,10 +38942,13 @@ const css$T = /* css */`
|
|
|
38844
38942
|
The movements. One of \`root\` and \`navi-route-transition\` exists at a
|
|
38845
38943
|
time (see the opt-out above), so each is written for both.
|
|
38846
38944
|
------------------------------------------------------------------ */
|
|
38847
|
-
|
|
38848
|
-
|
|
38849
|
-
|
|
38850
|
-
|
|
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] {
|
|
38851
38952
|
&::view-transition-old(root),
|
|
38852
38953
|
&::view-transition-new(root),
|
|
38853
38954
|
&::view-transition-old(navi-route-transition),
|
|
@@ -38857,20 +38958,34 @@ const css$T = /* css */`
|
|
|
38857
38958
|
the page it crosses. The picture is as wide as the box the browser
|
|
38858
38959
|
gives it — the arriving one's — so a page leaving a narrower box (a
|
|
38859
38960
|
scrollbar appeared, a side panel closed) would be seen zooming over
|
|
38860
|
-
the length of the movement
|
|
38861
|
-
|
|
38961
|
+
the length of the movement, and one leaving a shorter box would be
|
|
38962
|
+
seen inflating. */
|
|
38862
38963
|
height: auto;
|
|
38863
38964
|
object-fit: none;
|
|
38864
38965
|
object-position: top left;
|
|
38865
|
-
/*
|
|
38866
|
-
|
|
38867
|
-
|
|
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. */
|
|
38868
38970
|
mix-blend-mode: normal;
|
|
38869
|
-
animation-timing-function: ease;
|
|
38870
38971
|
animation-fill-mode: both;
|
|
38871
38972
|
}
|
|
38872
38973
|
}
|
|
38873
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
|
+
|
|
38874
38989
|
&[data-navi-route-transition-type="slide-x"] {
|
|
38875
38990
|
&[data-navi-route-transition="forward"] {
|
|
38876
38991
|
&::view-transition-old(root),
|
|
@@ -38983,7 +39098,11 @@ const css$T = /* css */`
|
|
|
38983
39098
|
&::view-transition-new(root),
|
|
38984
39099
|
&::view-transition-old(navi-route-transition),
|
|
38985
39100
|
&::view-transition-new(navi-route-transition) {
|
|
38986
|
-
|
|
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;
|
|
38987
39106
|
}
|
|
38988
39107
|
&[data-navi-route-transition="forward"] {
|
|
38989
39108
|
&::view-transition-new(root),
|
|
@@ -39136,6 +39255,12 @@ const RouteTransitionArea = ({
|
|
|
39136
39255
|
* animation-name: my-spin-in;
|
|
39137
39256
|
* }
|
|
39138
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.
|
|
39139
39264
|
* @returns {() => void} remove this relation.
|
|
39140
39265
|
*/
|
|
39141
39266
|
const defineRouteTransition = (from, to, transition) => {
|
|
@@ -39152,13 +39277,11 @@ const defineRouteTransition = (from, to, transition) => {
|
|
|
39152
39277
|
};
|
|
39153
39278
|
relations.push(relation);
|
|
39154
39279
|
rebuildWatcher();
|
|
39155
|
-
updateRoutingObservers();
|
|
39156
39280
|
return () => {
|
|
39157
39281
|
const index = relations.indexOf(relation);
|
|
39158
39282
|
if (index > -1) {
|
|
39159
39283
|
relations.splice(index, 1);
|
|
39160
39284
|
rebuildWatcher();
|
|
39161
|
-
updateRoutingObservers();
|
|
39162
39285
|
}
|
|
39163
39286
|
};
|
|
39164
39287
|
};
|
|
@@ -39180,11 +39303,9 @@ const defineRouteDefaultTransition = transition => {
|
|
|
39180
39303
|
import.meta.css = [css$T, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
39181
39304
|
const value = normalizeTransition(transition);
|
|
39182
39305
|
defaultTransition = value;
|
|
39183
|
-
updateRoutingObservers();
|
|
39184
39306
|
return () => {
|
|
39185
39307
|
if (defaultTransition === value) {
|
|
39186
39308
|
defaultTransition = null;
|
|
39187
|
-
updateRoutingObservers();
|
|
39188
39309
|
}
|
|
39189
39310
|
};
|
|
39190
39311
|
};
|
|
@@ -39205,6 +39326,98 @@ const normalizeTransition = transition => {
|
|
|
39205
39326
|
};
|
|
39206
39327
|
};
|
|
39207
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
|
+
|
|
39208
39421
|
// Every relation defined, and the single watcher standing over all of them.
|
|
39209
39422
|
const relations = [];
|
|
39210
39423
|
let watcher = null;
|
|
@@ -39248,27 +39461,32 @@ const rebuildWatcher = () => {
|
|
|
39248
39461
|
return;
|
|
39249
39462
|
}
|
|
39250
39463
|
const found = findRelation(pages[fromIndex], pages[index]);
|
|
39251
|
-
if (!found) {
|
|
39252
|
-
// No relation says anything about these two
|
|
39253
|
-
// 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.
|
|
39254
39468
|
return;
|
|
39255
39469
|
}
|
|
39256
39470
|
const {
|
|
39257
|
-
|
|
39258
|
-
|
|
39259
|
-
} = found;
|
|
39260
|
-
if (
|
|
39471
|
+
type,
|
|
39472
|
+
duration
|
|
39473
|
+
} = resolveTransition(navigationRequest, found ? found.relation : null);
|
|
39474
|
+
if (type === "none") {
|
|
39261
39475
|
// Silence said out loud: this way of the pair was written to play
|
|
39262
|
-
// nothing
|
|
39263
|
-
// 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.
|
|
39264
39478
|
navigationAnimated = true;
|
|
39265
39479
|
return;
|
|
39266
39480
|
}
|
|
39267
39481
|
beginTransition({
|
|
39268
39482
|
page: pages[index],
|
|
39269
|
-
|
|
39270
|
-
|
|
39271
|
-
|
|
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
|
|
39272
39490
|
});
|
|
39273
39491
|
};
|
|
39274
39492
|
// `subscribe` rather than `effect`: it hands the value to a callback that is
|
|
@@ -39280,52 +39498,57 @@ const rebuildWatcher = () => {
|
|
|
39280
39498
|
};
|
|
39281
39499
|
};
|
|
39282
39500
|
|
|
39283
|
-
// What plays when no relation matched (see defineRouteDefaultTransition),
|
|
39284
|
-
//
|
|
39285
|
-
//
|
|
39286
|
-
//
|
|
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.
|
|
39287
39506
|
let defaultTransition = null;
|
|
39507
|
+
let navigationRequest = null;
|
|
39288
39508
|
let navigationAnimated = false;
|
|
39289
39509
|
|
|
39290
|
-
// The two ends of
|
|
39291
|
-
//
|
|
39292
|
-
//
|
|
39293
|
-
//
|
|
39294
|
-
//
|
|
39295
|
-
// its say by then.
|
|
39296
|
-
|
|
39297
|
-
|
|
39298
|
-
|
|
39299
|
-
if (
|
|
39300
|
-
const stopWatchingStart = observeBeforeRouting(() => {
|
|
39301
|
-
navigationAnimated = false;
|
|
39302
|
-
holdRenderingForRouting();
|
|
39303
|
-
});
|
|
39304
|
-
const stopWatchingEnd = observeAfterRouting(() => {
|
|
39305
|
-
if (defaultTransition && defaultTransition.type !== "none" && !navigationAnimated) {
|
|
39306
|
-
beginTransition({
|
|
39307
|
-
page: null,
|
|
39308
|
-
// A default has no direction: nothing says which of two arbitrary
|
|
39309
|
-
// pages is before the other. The attribute is worn empty — present
|
|
39310
|
-
// for whoever keys on "one of ours is playing", silent on the way.
|
|
39311
|
-
direction: "",
|
|
39312
|
-
type: defaultTransition.type,
|
|
39313
|
-
duration: defaultTransition.duration
|
|
39314
|
-
});
|
|
39315
|
-
}
|
|
39316
|
-
releaseRoutingRenderingHold();
|
|
39317
|
-
});
|
|
39318
|
-
stopRoutingObservers = () => {
|
|
39319
|
-
stopWatchingStart();
|
|
39320
|
-
stopWatchingEnd();
|
|
39321
|
-
};
|
|
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) {
|
|
39322
39520
|
return;
|
|
39323
39521
|
}
|
|
39324
|
-
|
|
39325
|
-
|
|
39326
|
-
|
|
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
|
+
}
|
|
39327
39549
|
}
|
|
39328
|
-
|
|
39550
|
+
releaseRoutingRenderingHold();
|
|
39551
|
+
});
|
|
39329
39552
|
|
|
39330
39553
|
// The exact way travelled first, over the whole registry, and only then the
|
|
39331
39554
|
// reverses: a relation written B → A owns that way, and being the reverse of
|
|
@@ -43501,6 +43724,14 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
43501
43724
|
* out of flow — the "#" anchor-on-hover pattern (e.g. inside a `Title`).
|
|
43502
43725
|
* @param {boolean} [props.hrefFallback] - Use `href` as the visible text when
|
|
43503
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.
|
|
43504
43735
|
* @param {boolean} [props.preventDefault] - Call `event.preventDefault()` on
|
|
43505
43736
|
* click (navigation suppressed; `onClick` still runs).
|
|
43506
43737
|
* @param {(event: MouseEvent) => void} [props.onClick]
|
|
@@ -43564,6 +43795,7 @@ const LinkPlain = props => {
|
|
|
43564
43795
|
endIcon,
|
|
43565
43796
|
revealOnInteraction = false,
|
|
43566
43797
|
hrefFallback = !anchor,
|
|
43798
|
+
routeTransition,
|
|
43567
43799
|
children
|
|
43568
43800
|
} = props;
|
|
43569
43801
|
if (anchor && !props.id) {
|
|
@@ -43670,6 +43902,13 @@ const LinkPlain = props => {
|
|
|
43670
43902
|
} else {
|
|
43671
43903
|
innerEndIcon = endIcon;
|
|
43672
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);
|
|
43673
43912
|
const innerChildren = children || (hrefFallback ? href : children);
|
|
43674
43913
|
const startIconEl = startIcon;
|
|
43675
43914
|
const endIconEl = innerEndIcon;
|
|
@@ -43720,6 +43959,8 @@ const LinkPlain = props => {
|
|
|
43720
43959
|
startIcon: undefined,
|
|
43721
43960
|
endIcon: undefined,
|
|
43722
43961
|
hrefFallback: undefined,
|
|
43962
|
+
routeTransition: undefined,
|
|
43963
|
+
"data-navi-route-transition-request": routeTransitionRequest,
|
|
43723
43964
|
onClick: e => {
|
|
43724
43965
|
onClick?.(e);
|
|
43725
43966
|
if (slide) {
|
|
@@ -60331,6 +60572,18 @@ const useItemStore = ({
|
|
|
60331
60572
|
memoryBudget,
|
|
60332
60573
|
onRequestStateChange
|
|
60333
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();
|
|
60334
60587
|
// What the source kept of the collection when the screen it was on went away
|
|
60335
60588
|
// (a range reader keeps the composition: see resource_range_reader.js). The
|
|
60336
60589
|
// rows are drawn from it right away and the window is asked for again — the
|
|
@@ -60557,17 +60810,32 @@ const useItemStore = ({
|
|
|
60557
60810
|
}
|
|
60558
60811
|
}
|
|
60559
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
|
+
};
|
|
60560
60823
|
if (start === -1) {
|
|
60824
|
+
// Nothing missing and nothing to revalidate: the run has what it
|
|
60825
|
+
// draws.
|
|
60826
|
+
debugAsk("nothing missing");
|
|
60561
60827
|
return;
|
|
60562
60828
|
}
|
|
60563
60829
|
if (virtual.holdPending && pages.count !== undefined && !askingAroundWantedRow) {
|
|
60564
60830
|
// The one ask a hold lets through: the row the list is held on is
|
|
60565
60831
|
// what would lift the hold, and nothing else is going to bring it.
|
|
60832
|
+
debugAsk("held on a row not reached yet");
|
|
60566
60833
|
return;
|
|
60567
60834
|
}
|
|
60568
60835
|
const request = requestRef.current;
|
|
60569
60836
|
if (revalidating && request.busy) {
|
|
60570
60837
|
if (request.revalidating) {
|
|
60838
|
+
debugAsk("already revalidating");
|
|
60571
60839
|
return;
|
|
60572
60840
|
}
|
|
60573
60841
|
// A page for a window that is about to be replaced wholesale.
|
|
@@ -60585,6 +60853,7 @@ const useItemStore = ({
|
|
|
60585
60853
|
// waiting for to exist at all.
|
|
60586
60854
|
const stillWanted = pages.count === undefined || request.start <= windowTo && request.end >= windowFrom;
|
|
60587
60855
|
if (stillWanted) {
|
|
60856
|
+
debugAsk("a request still covers this window");
|
|
60588
60857
|
return;
|
|
60589
60858
|
}
|
|
60590
60859
|
request.controller?.abort();
|
|
@@ -60595,6 +60864,7 @@ const useItemStore = ({
|
|
|
60595
60864
|
// nothing since, can only produce the same answer. A revalidation is
|
|
60596
60865
|
// exactly the case where it produces another one.
|
|
60597
60866
|
if (!revalidating && request.start === start && request.end === end && request.held === held) {
|
|
60867
|
+
debugAsk("this range was asked for already");
|
|
60598
60868
|
return;
|
|
60599
60869
|
}
|
|
60600
60870
|
request.start = start;
|
|
@@ -60618,6 +60888,7 @@ const useItemStore = ({
|
|
|
60618
60888
|
};
|
|
60619
60889
|
request.busy = true;
|
|
60620
60890
|
request.revalidating = revalidating;
|
|
60891
|
+
debugAsk("sent");
|
|
60621
60892
|
if (revalidating) {
|
|
60622
60893
|
setRefreshing(true);
|
|
60623
60894
|
}
|