@jsenv/navi 0.29.142 → 0.29.143
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 +233 -138
- package/dist/jsenv_navi.js.map +9 -16
- package/docs/AI_INSTRUCTIONS.md +17 -3
- package/docs/actions.md +3 -0
- package/docs/control_value.md +7 -1
- package/docs/css_architecture.md +1 -1
- package/docs/navigation.md +117 -35
- package/docs/state_binding.md +142 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -16349,6 +16349,14 @@ const generateSignalId = () => {
|
|
|
16349
16349
|
* value, and it goes back to the default value when the route stops matching. Use it for params like an
|
|
16350
16350
|
* "edit this one" id, where every other link to the screen must lead to the plain screen.
|
|
16351
16351
|
* Incompatible with `persists` (throws).
|
|
16352
|
+
* @param {"replace"|"push"} [options.history="replace"] - what writing this state
|
|
16353
|
+
* is worth in the browser's history, when it is bound to a url (a route's
|
|
16354
|
+
* `searchParams`, a path param). Replacement by default: a state that
|
|
16355
|
+
* qualifies the screen one is on — a step in a form, a view mode, a sort — is
|
|
16356
|
+
* not a place, and one entry per write turns a single back-press into as many
|
|
16357
|
+
* as the user moved. `"push"` is for a state whose values ARE places one came
|
|
16358
|
+
* from (the photo one is looking at in a gallery). Whatever is said here, one
|
|
16359
|
+
* write can say otherwise: `signal.set(value, { history })`.
|
|
16352
16360
|
* @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
|
|
16353
16361
|
* @returns {import("@preact/signals").Signal} A signal that can be synchronized with a source signal and/or persisted in localStorage. The signal includes a `validity` property for validation state.
|
|
16354
16362
|
*
|
|
@@ -16422,8 +16430,14 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
16422
16430
|
ignoreArrayOrder,
|
|
16423
16431
|
autoFix,
|
|
16424
16432
|
weak = false,
|
|
16433
|
+
history = "replace",
|
|
16425
16434
|
} = options;
|
|
16426
16435
|
|
|
16436
|
+
if (history !== "replace" && history !== "push") {
|
|
16437
|
+
throw new TypeError(
|
|
16438
|
+
`stateSignal "${id}": history must be "replace" or "push", got ${history}.`,
|
|
16439
|
+
);
|
|
16440
|
+
}
|
|
16427
16441
|
if (weak && persists) {
|
|
16428
16442
|
throw new TypeError(
|
|
16429
16443
|
`stateSignal "${id}": weak and persists are contradictory — a weak param qualifies one visit, it cannot be restored from a previous session.`,
|
|
@@ -16600,7 +16614,35 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
16600
16614
|
configurable: true,
|
|
16601
16615
|
});
|
|
16602
16616
|
|
|
16617
|
+
// What the write being made right now asks of the history, over what this
|
|
16618
|
+
// state declares. It lives for the length of one assignment: the effect that
|
|
16619
|
+
// writes the url runs synchronously inside it (see route.js), and reads it
|
|
16620
|
+
// there. A write made inside batch() is therefore out of its reach — say the
|
|
16621
|
+
// history on the state itself for those.
|
|
16622
|
+
let historyForCurrentWrite;
|
|
16603
16623
|
const facadeSignal = preactSignal;
|
|
16624
|
+
/**
|
|
16625
|
+
* Write the state, saying what this ONE write is worth in the history.
|
|
16626
|
+
*
|
|
16627
|
+
* The state says which of the two it usually is (`history` above); a writer
|
|
16628
|
+
* that knows THIS move is not one of them says so here — a slide reached by
|
|
16629
|
+
* dragging inside a container whose slides are places one came from.
|
|
16630
|
+
*
|
|
16631
|
+
* @param {any} value
|
|
16632
|
+
* @param {{history?: "replace"|"push"}} [options]
|
|
16633
|
+
*/
|
|
16634
|
+
facadeSignal.set = (value, { history: historyAsked } = {}) => {
|
|
16635
|
+
if (historyAsked === undefined) {
|
|
16636
|
+
facadeSignal.value = value;
|
|
16637
|
+
return;
|
|
16638
|
+
}
|
|
16639
|
+
historyForCurrentWrite = historyAsked;
|
|
16640
|
+
try {
|
|
16641
|
+
facadeSignal.value = value;
|
|
16642
|
+
} finally {
|
|
16643
|
+
historyForCurrentWrite = undefined;
|
|
16644
|
+
}
|
|
16645
|
+
};
|
|
16604
16646
|
facadeSignal.validity = validity;
|
|
16605
16647
|
facadeSignal.validSignal = computed(() => {
|
|
16606
16648
|
// Reading facadeSignal.value establishes the reactive dependency.
|
|
@@ -16745,6 +16787,8 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
16745
16787
|
localStorageKey,
|
|
16746
16788
|
debug,
|
|
16747
16789
|
...options,
|
|
16790
|
+
history,
|
|
16791
|
+
getHistory: () => historyForCurrentWrite || history,
|
|
16748
16792
|
};
|
|
16749
16793
|
globalSignalRegistry.set(signalIdString, {
|
|
16750
16794
|
signal: facadeSignal,
|
|
@@ -18012,9 +18056,20 @@ const POSITION_PROPS = {
|
|
|
18012
18056
|
return { transform: `skew(${value})` };
|
|
18013
18057
|
},
|
|
18014
18058
|
};
|
|
18059
|
+
// Only the inline axis has something to truncate here, so the clip must not
|
|
18060
|
+
// constrain the block one: `hidden` would make the element a scroll container,
|
|
18061
|
+
// whose flex automatic minimum size is 0, and a single line inside a column
|
|
18062
|
+
// flex container shorter than 1lh then shrinks under its own line box and gets
|
|
18063
|
+
// cut through the glyphs. `clip` keeps the content-based minimum, so the line
|
|
18064
|
+
// overflows visibly instead. The margin leaves room for ink drawn outside the
|
|
18065
|
+
// advance width; it starts from the padding box, having no hidden line to hold
|
|
18066
|
+
// back the way lineClampStyles below does.
|
|
18015
18067
|
const singleLineEllipsisStyles = () => {
|
|
18016
18068
|
return {
|
|
18017
|
-
overflow: "
|
|
18069
|
+
overflow: "clip",
|
|
18070
|
+
// The `padding-box` keyword is redundant (it is the default box) but
|
|
18071
|
+
// Chromium drops the declaration when a bare calc() follows the property.
|
|
18072
|
+
overflowClipMargin: "padding-box calc((1lh - 1em) / 2)",
|
|
18018
18073
|
textOverflow: "ellipsis",
|
|
18019
18074
|
overflowWrap: "normal",
|
|
18020
18075
|
};
|
|
@@ -24540,7 +24595,15 @@ let isUpdatingRoutesFromUrl = false;
|
|
|
24540
24595
|
* `:name=${signal}` binds that param to a signal.
|
|
24541
24596
|
* @param {object} [options]
|
|
24542
24597
|
* @param {Object<string, import("@preact/signals").Signal>} [options.searchParams]
|
|
24543
|
-
* Search params this route two-way syncs with, by name.
|
|
24598
|
+
* Search params this route two-way syncs with, by name. The signal and the url
|
|
24599
|
+
* are the same state: writing the signal rewrites the address, an address
|
|
24600
|
+
* arriving from outside writes the signal, and the param disappears from the
|
|
24601
|
+
* url while the signal holds its default.
|
|
24602
|
+
* Writing one AMENDS the history entry one is on — a param qualifies the
|
|
24603
|
+
* screen, it is not a place, and one entry per write turns a single
|
|
24604
|
+
* back-press into as many as the user moved. A state whose values ARE places
|
|
24605
|
+
* says so where it is declared (`stateSignal(v, { history: "push" })`), and
|
|
24606
|
+
* one write can still say otherwise (`signal.set(v, { history })`).
|
|
24544
24607
|
* @param {Object<string, RegExp | Array | ((value: string) => boolean)>} [options.params]
|
|
24545
24608
|
* Which values a path param accepts, by param name: a regexp tested against the
|
|
24546
24609
|
* decoded segment, the list of accepted values (compared as strings, so it can
|
|
@@ -24711,17 +24774,20 @@ const route = (
|
|
|
24711
24774
|
const routeUrl = route.buildUrl(params);
|
|
24712
24775
|
return integration.navTo(routeUrl, options);
|
|
24713
24776
|
};
|
|
24714
|
-
route.redirectTo = (params, { callReason } = {}) => {
|
|
24777
|
+
route.redirectTo = (params, { callReason, history = "replace" } = {}) => {
|
|
24715
24778
|
if (!integration) {
|
|
24716
24779
|
return Promise.resolve();
|
|
24717
24780
|
}
|
|
24718
24781
|
const routeUrl = route.buildUrl(params);
|
|
24719
24782
|
return integration.navTo(routeUrl, {
|
|
24720
|
-
replace:
|
|
24783
|
+
replace: history !== "push",
|
|
24721
24784
|
callReason,
|
|
24722
24785
|
});
|
|
24723
24786
|
};
|
|
24724
|
-
route.replaceParams = (
|
|
24787
|
+
route.replaceParams = (
|
|
24788
|
+
newParams,
|
|
24789
|
+
{ callReason, isSignalChange, history = "replace" } = {},
|
|
24790
|
+
) => {
|
|
24725
24791
|
const matching = route.matchingSignal.peek();
|
|
24726
24792
|
if (!matching) {
|
|
24727
24793
|
console.warn(
|
|
@@ -24774,6 +24840,7 @@ const route = (
|
|
|
24774
24840
|
}
|
|
24775
24841
|
return mostSpecificRoute.redirectTo(newParams, {
|
|
24776
24842
|
callReason: `replaceParams delegation from ${route} to ${mostSpecificRoute} (original reason: ${callReason})`,
|
|
24843
|
+
history,
|
|
24777
24844
|
});
|
|
24778
24845
|
}
|
|
24779
24846
|
|
|
@@ -24789,7 +24856,7 @@ const route = (
|
|
|
24789
24856
|
newParams,
|
|
24790
24857
|
);
|
|
24791
24858
|
return integration.navTo(targetUrl, {
|
|
24792
|
-
replace:
|
|
24859
|
+
replace: history !== "push",
|
|
24793
24860
|
callReason,
|
|
24794
24861
|
});
|
|
24795
24862
|
};
|
|
@@ -24860,6 +24927,13 @@ const route = (
|
|
|
24860
24927
|
const { connections } = routePattern;
|
|
24861
24928
|
for (const connection of connections) {
|
|
24862
24929
|
const { signal: paramSignal, debug, paramName } = connection;
|
|
24930
|
+
// What a write of this state is worth in the history: what the state
|
|
24931
|
+
// declares, and what the write happening right now says instead (see
|
|
24932
|
+
// stateSignal's `history` and its `set`). Read at the moment of the
|
|
24933
|
+
// navigation and never before. A plain signal says nothing and replaces,
|
|
24934
|
+
// which is what a param qualifying a screen is.
|
|
24935
|
+
const historyOfWrite = () =>
|
|
24936
|
+
paramSignal.options?.getHistory?.() || "replace";
|
|
24863
24937
|
if (debug) {
|
|
24864
24938
|
console.debug(
|
|
24865
24939
|
`[route] connecting url param "${paramName}" to signal`,
|
|
@@ -24902,6 +24976,7 @@ const route = (
|
|
|
24902
24976
|
{
|
|
24903
24977
|
callReason: `${paramName} signal change on ${route}`,
|
|
24904
24978
|
isSignalChange: true,
|
|
24979
|
+
history: historyOfWrite(),
|
|
24905
24980
|
},
|
|
24906
24981
|
);
|
|
24907
24982
|
return;
|
|
@@ -24919,6 +24994,7 @@ const route = (
|
|
|
24919
24994
|
{
|
|
24920
24995
|
callReason: `${paramName} signal reset to default on ${route}`,
|
|
24921
24996
|
isSignalChange: true,
|
|
24997
|
+
history: historyOfWrite(),
|
|
24922
24998
|
},
|
|
24923
24999
|
);
|
|
24924
25000
|
return;
|
|
@@ -24938,6 +25014,7 @@ const route = (
|
|
|
24938
25014
|
{
|
|
24939
25015
|
callReason: `${paramName} signal change on ${route}`,
|
|
24940
25016
|
isSignalChange: true,
|
|
25017
|
+
history: historyOfWrite(),
|
|
24941
25018
|
},
|
|
24942
25019
|
);
|
|
24943
25020
|
});
|
|
@@ -39015,8 +39092,17 @@ const css$X = /* css */`
|
|
|
39015
39092
|
display: block;
|
|
39016
39093
|
min-width: 0;
|
|
39017
39094
|
text-overflow: ellipsis;
|
|
39018
|
-
|
|
39095
|
+
/* Only the inline axis has something to truncate, so the clip must not
|
|
39096
|
+
constrain the block one: overflow hidden would make the element a
|
|
39097
|
+
scroll container, whose flex automatic minimum size is 0, and inside a
|
|
39098
|
+
column flex container shorter than 1lh the text then shrinks under its
|
|
39099
|
+
own line box and gets cut through the glyphs. The margin leaves room
|
|
39100
|
+
for ink drawn outside the advance width. */
|
|
39101
|
+
overflow: clip;
|
|
39019
39102
|
overflow-wrap: normal;
|
|
39103
|
+
/* The padding-box keyword is redundant (it is the default box) but
|
|
39104
|
+
Chromium drops the declaration when a bare calc() follows. */
|
|
39105
|
+
overflow-clip-margin: padding-box calc((1lh - 1em) / 2);
|
|
39020
39106
|
}
|
|
39021
39107
|
|
|
39022
39108
|
&[data-skeleton] {
|
|
@@ -51130,6 +51216,10 @@ const causeOfEvent = event => {
|
|
|
51130
51216
|
}
|
|
51131
51217
|
return "command";
|
|
51132
51218
|
};
|
|
51219
|
+
|
|
51220
|
+
// The causes that are not places one went, whatever the state they are written
|
|
51221
|
+
// into says (see writeAreaAsked).
|
|
51222
|
+
const CAUSES_WRITTEN_BY_REPLACEMENT = ["drag", "refusal", "state"];
|
|
51133
51223
|
const readArea = slideElement => slideElement.getAttribute("data-slide-area") || slideElement.id || "";
|
|
51134
51224
|
|
|
51135
51225
|
/**
|
|
@@ -51166,33 +51256,65 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
51166
51256
|
* repeated spans), except that a row needs no trailing hole: what is not
|
|
51167
51257
|
* written is simply not there.
|
|
51168
51258
|
* @param {string} [props.current] - area (or id) of the slide being shown; omit
|
|
51169
|
-
* to keep it here and drive it by command.
|
|
51170
|
-
*
|
|
51171
|
-
*
|
|
51172
|
-
*
|
|
51173
|
-
*
|
|
51174
|
-
*
|
|
51175
|
-
*
|
|
51259
|
+
* to keep it here and drive it by command. An area named from outside is a
|
|
51260
|
+
* REQUEST rather than a placement — see `signal` for what that means.
|
|
51261
|
+
* @param {import("@preact/signals").Signal<string>} [props.signal] - where the
|
|
51262
|
+
* area lives, said the way every navi control says it: the container shows
|
|
51263
|
+
* the area the signal holds, and writes into it the area it travels to. One
|
|
51264
|
+
* binding instead of `current` + `onCurrentChange`, and the state stays where
|
|
51265
|
+
* the app put it — which is what lets something else read where the slides
|
|
51266
|
+
* are (a field carrying the current tab into a form, see
|
|
51176
51267
|
* docs/control_object.md#a-settings-sheet) or move them by writing it.
|
|
51177
51268
|
* Excludes `current`; `onCurrentChange` still fires, for the `cause` and for
|
|
51178
51269
|
* the right to refuse.
|
|
51179
|
-
*
|
|
51180
|
-
*
|
|
51270
|
+
* An area written from outside is WALKED to, not jumped to: every slide
|
|
51271
|
+
* between here and there is asked to let go the way a key going that way
|
|
51272
|
+
* would ask it, and the first one that holds (`preventNav`, a `required` step
|
|
51273
|
+
* still unanswered) is where one stops — after which the signal is written
|
|
51274
|
+
* with the area actually shown, so it says where one IS and never where one
|
|
51275
|
+
* asked to be.
|
|
51276
|
+
* PUTTING THE AREA IN THE URL is that binding and nothing more: hand it a
|
|
51277
|
+
* `stateSignal` a route declares as a search param, and `?step=<area>` is
|
|
51278
|
+
* written on every travel, read on a load, a bookmark, a link, a traversal —
|
|
51279
|
+
* through the walk above, so an address cannot open a slide the walk may not
|
|
51280
|
+
* reach.
|
|
51281
|
+
* ```js
|
|
51282
|
+
* const stepSignal = stateSignal("when", { id: "step", weak: true });
|
|
51283
|
+
* route("/alerts/:id/edit", { searchParams: { step: stepSignal } });
|
|
51284
|
+
* ```
|
|
51285
|
+
* ```jsx
|
|
51286
|
+
* <SlideContainer signal={stepSignal}>
|
|
51287
|
+
* ```
|
|
51288
|
+
* Where it opens is the state's own default (`stateSignal`'s first argument —
|
|
51289
|
+
* a signal there, for a start that depends on where one is), and the param
|
|
51290
|
+
* stays out of the address while the area IS that default. `weak` keeps the
|
|
51291
|
+
* step from being inherited by links built to that route. Written by
|
|
51292
|
+
* replacement unless the state says `history: "push"`, and even then a slide
|
|
51293
|
+
* reached by DRAGGING replaces — see docs/navigation.md.
|
|
51294
|
+
* @param {string} [props.defaultCurrent] - which slide to open on, for a
|
|
51295
|
+
* container that owns its position. Mount-only, like every other `default*`:
|
|
51181
51296
|
* it says where one starts, not where one is — say `current` for that.
|
|
51182
51297
|
* Without it the first slide is the one shown, the way a stack of pages opens
|
|
51183
51298
|
* on its first page.
|
|
51184
|
-
*
|
|
51299
|
+
* A container bound to a `signal` does not own its position, and where it
|
|
51300
|
+
* opens is that state's own default (`stateSignal`'s first argument) rather
|
|
51301
|
+
* than this: one place says where the area starts, and it is the place a
|
|
51302
|
+
* reset goes back to. This still answers for a signal holding nothing at
|
|
51303
|
+
* all.
|
|
51304
|
+
* @param {(area: string, detail: {cause: "drag"|"keyboard"|"command"|"code"|"state", event: Event}) => void|false|Promise<void|false>} [props.onCurrentChange]
|
|
51185
51305
|
* - the slide being shown has changed. `cause` says what asked for it, which
|
|
51186
|
-
* is what tells a place browsed past from a place aimed at
|
|
51187
|
-
*
|
|
51188
|
-
*
|
|
51189
|
-
*
|
|
51306
|
+
* is what tells a place browsed past from a place aimed at — a slide dragged
|
|
51307
|
+
* to rather than pressed for. `cause: "state"` is the bound state asking (a
|
|
51308
|
+
* write from the application, a load on a link, a traversal) rather than
|
|
51309
|
+
* anything done inside the box, and its `event` is null.
|
|
51190
51310
|
* Answer `false` to REFUSE the change and the slide goes back where it came
|
|
51191
51311
|
* from — a guard that says no, a session that is gone. A promise refuses it
|
|
51192
51312
|
* late, once whatever it had to ask has answered; the travel plays meanwhile
|
|
51193
51313
|
* and is undone if the answer is no.
|
|
51194
|
-
*
|
|
51195
|
-
*
|
|
51314
|
+
* It is not where the area is REMEMBERED: `signal` does that in both
|
|
51315
|
+
* directions and is written by every way of travelling, while a callback
|
|
51316
|
+
* assigning it catches only the ones it was written for — see
|
|
51317
|
+
* docs/state_binding.md. What is left here is the `cause` and the refusal.
|
|
51196
51318
|
* @param {"now"|"rest"} [props.commit="now"] - when the change is told.
|
|
51197
51319
|
* "rest" waits for the travel to be over, and lets the container hold the
|
|
51198
51320
|
* slide it is going to meanwhile: the picture moves with the finger and the
|
|
@@ -51210,33 +51332,6 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
51210
51332
|
* one that just travelled there. Called once the travel is over, and in the
|
|
51211
51333
|
* same render as the return to rest — anything later shows the old content
|
|
51212
51334
|
* for a frame.
|
|
51213
|
-
* @param {string|{name: string, history?: "replace"|"push"}} [props.urlParam]
|
|
51214
|
-
* - the search param this container owns, and where it stands is written
|
|
51215
|
-
* into: `urlParam="step"` puts `?step=<area>` in the address on every travel,
|
|
51216
|
-
* and opens on the area it names on a load, a bookmark, a traversal, a link
|
|
51217
|
-
* from anywhere else in the application.
|
|
51218
|
-
* It is the answer to "the step should be readable and should survive a
|
|
51219
|
-
* reload" WITHOUT one route per slide: no route to declare, no route guard to
|
|
51220
|
-
* re-express the walk's own rules in, and no page transition — what travels
|
|
51221
|
-
* is the box, and the address is a label on where the box stands. Say it on a
|
|
51222
|
-
* container whose slides are places one may be sent to; a walk nobody links
|
|
51223
|
-
* into keeps its position to itself.
|
|
51224
|
-
* Written by REPLACEMENT: four steps that each stacked an entry would turn
|
|
51225
|
-
* one back-press into four, and the back arrow of a page means "leave this
|
|
51226
|
-
* page", not "one question back". `history: "push"` says the opposite for the
|
|
51227
|
-
* containers that mean it — slides that ARE places one came from, a gallery
|
|
51228
|
-
* one browses — and even there a slide reached by dragging replaces, so
|
|
51229
|
-
* swiping back and forth does not bury the way out.
|
|
51230
|
-
* The address is READ through the walk, not jumped to: it comes from outside
|
|
51231
|
-
* the box, so every slide between here and there is asked to let go the way a
|
|
51232
|
-
* key going that way would ask it, and the first one that holds
|
|
51233
|
-
* (`preventNav`, or a `required` step still unanswered) is where one stops —
|
|
51234
|
-
* `?step=done` cannot open a confirmation screen for something nobody sent.
|
|
51235
|
-
* Whatever comes of it, the address is then rewritten with the area actually
|
|
51236
|
-
* shown: it says where one IS, never where one asked to be. A step the app
|
|
51237
|
-
* knows is already answered on a reload is told so by its own `required`
|
|
51238
|
-
* (`required={!alreadyFilled}`) — the container remembers nothing across a
|
|
51239
|
-
* load, and cannot.
|
|
51240
51335
|
* Each way of travelling can be shut off, or narrowed to one axis: `true`
|
|
51241
51336
|
* (every axis the map has), `false`, `"x"`, `"y"`, `"xy"`.
|
|
51242
51337
|
*
|
|
@@ -51289,7 +51384,6 @@ const SlideContainer = ({
|
|
|
51289
51384
|
commit = "now",
|
|
51290
51385
|
loop,
|
|
51291
51386
|
onLoop,
|
|
51292
|
-
urlParam,
|
|
51293
51387
|
travelByKeyboard = true,
|
|
51294
51388
|
travelByDrag = true,
|
|
51295
51389
|
travelByScroll = "x",
|
|
@@ -51306,7 +51400,11 @@ const SlideContainer = ({
|
|
|
51306
51400
|
const containerRef = useRef();
|
|
51307
51401
|
// The AREA of the slide being shown, not its rank: a rank would be wrong the
|
|
51308
51402
|
// moment a slide appears before it, and there is nothing to renumber here.
|
|
51309
|
-
|
|
51403
|
+
// This is where the container's own answer lives, and it is the only one the
|
|
51404
|
+
// picture is drawn from — an area coming from outside is a REQUEST that has
|
|
51405
|
+
// to be walked to first (see the effect reading currentFromCaller). At mount
|
|
51406
|
+
// there is no road to walk yet, so what the caller holds is taken as it is.
|
|
51407
|
+
const [currentAreaState, setCurrentAreaState] = useState(() => (currentSignal ? currentSignal.peek() : currentProp) ?? defaultCurrent);
|
|
51310
51408
|
// The slide this container is travelling to while its controller has not been
|
|
51311
51409
|
// told yet (commit="rest"): for the length of that travel the container is
|
|
51312
51410
|
// ahead of whoever holds `current`, and this is where it keeps its own answer
|
|
@@ -51384,51 +51482,42 @@ const SlideContainer = ({
|
|
|
51384
51482
|
// nothing measured per frame.
|
|
51385
51483
|
const followerElementsRef = useRef([]);
|
|
51386
51484
|
const currentFromCaller = currentSignal ? currentSignal.value : currentProp;
|
|
51387
|
-
const current = rollingArea ?? provisionalArea ??
|
|
51388
|
-
// The
|
|
51389
|
-
//
|
|
51390
|
-
|
|
51391
|
-
|
|
51392
|
-
// What the address says right now. Read from the signal rather than from
|
|
51393
|
-
// window.location so that a traversal — the browser's own back and forward —
|
|
51394
|
-
// is a render like any other: the address moved, and the slides follow it.
|
|
51395
|
-
// Read only when there is a param to read, so a container that owns none is
|
|
51396
|
-
// not re-rendered by every navigation in the application.
|
|
51397
|
-
const areaInUrl = urlParamName ? new URL(documentUrlSignal.value).searchParams.get(urlParamName) : null;
|
|
51398
|
-
// The last thing this container knows the address said, whether it wrote it
|
|
51399
|
-
// or read it. What makes a two-way binding stop turning: the address is only
|
|
51400
|
-
// an INSTRUCTION when it changed by itself (a load, a traversal, a link), and
|
|
51485
|
+
const current = rollingArea ?? provisionalArea ?? currentAreaState;
|
|
51486
|
+
// The last area this container knows the caller holds, whether it wrote it
|
|
51487
|
+
// there or read it from there. What makes a two-way binding stop turning: an
|
|
51488
|
+
// area held outside is an INSTRUCTION only when it changed by itself (a load,
|
|
51489
|
+
// a traversal, a link, something in the application writing the signal), and
|
|
51401
51490
|
// what this container put there is not news. Without it, a container ahead of
|
|
51402
|
-
// its own
|
|
51403
|
-
//
|
|
51404
|
-
const
|
|
51405
|
-
// Where the container stands, said
|
|
51406
|
-
//
|
|
51407
|
-
//
|
|
51408
|
-
//
|
|
51409
|
-
//
|
|
51410
|
-
//
|
|
51411
|
-
|
|
51412
|
-
|
|
51491
|
+
// its own state (commit="rest", where the picture arrives before the change
|
|
51492
|
+
// is told) would read its own lateness as an order to go back.
|
|
51493
|
+
const areaAskedSeenRef = useRef(currentSignal ? currentSignal.peek() : currentProp);
|
|
51494
|
+
// Where the container stands, said where the caller holds it.
|
|
51495
|
+
//
|
|
51496
|
+
// Whether a slide is a place one came from is the STATE's own business (see
|
|
51497
|
+
// stateSignal's `history`), and this container has no opinion on it. What it
|
|
51498
|
+
// does know is which of its own writes are not places at all, and those are
|
|
51499
|
+
// written by REPLACEMENT whatever the state declares:
|
|
51500
|
+
// - a drag, because browsing back and forth with a thumb is not a trail one
|
|
51501
|
+
// wants to walk home along;
|
|
51502
|
+
// - a correction — a refused travel put back, a walk that stopped before
|
|
51503
|
+
// where it was asked to go — because it is the entry that asked for the
|
|
51504
|
+
// wrong thing being amended, not a place one was;
|
|
51505
|
+
// - the state itself asking (`cause: "state"`), because reading a value back
|
|
51506
|
+
// and answering it is not somewhere one went.
|
|
51507
|
+
const writeAreaAsked = (area, {
|
|
51508
|
+
cause
|
|
51413
51509
|
} = {}) => {
|
|
51414
|
-
|
|
51510
|
+
areaAskedSeenRef.current = area;
|
|
51511
|
+
if (!currentSignal) {
|
|
51415
51512
|
return;
|
|
51416
51513
|
}
|
|
51417
|
-
|
|
51418
|
-
|
|
51419
|
-
|
|
51514
|
+
if (CAUSES_WRITTEN_BY_REPLACEMENT.includes(cause) && currentSignal.set) {
|
|
51515
|
+
currentSignal.set(area, {
|
|
51516
|
+
history: "replace"
|
|
51517
|
+
});
|
|
51420
51518
|
return;
|
|
51421
51519
|
}
|
|
51422
|
-
|
|
51423
|
-
navTo(urlObject.href, {
|
|
51424
|
-
replace: replace || urlParamHistory !== "push" || cause === "drag" ||
|
|
51425
|
-
// The address already says it: reading it back is not a place one went.
|
|
51426
|
-
cause === "url",
|
|
51427
|
-
// What travels is the box, not the page: the address is a label on where
|
|
51428
|
-
// the box stands, and a route transition would move the whole document
|
|
51429
|
-
// for a slide that has already travelled by itself.
|
|
51430
|
-
routeTransition: "none"
|
|
51431
|
-
});
|
|
51520
|
+
currentSignal.value = area;
|
|
51432
51521
|
};
|
|
51433
51522
|
const vertical = layout === "column";
|
|
51434
51523
|
// What the map has, and what each way of asking is allowed to use of it.
|
|
@@ -52059,7 +52148,7 @@ const SlideContainer = ({
|
|
|
52059
52148
|
}
|
|
52060
52149
|
const leftArea = readArea(currentElement);
|
|
52061
52150
|
setCurrentAreaState(area);
|
|
52062
|
-
if (!onCurrentChange && !currentSignal
|
|
52151
|
+
if (!onCurrentChange && !currentSignal) {
|
|
52063
52152
|
return true;
|
|
52064
52153
|
}
|
|
52065
52154
|
// What asked for this, read off the interaction rather than carried down
|
|
@@ -52090,21 +52179,16 @@ const SlideContainer = ({
|
|
|
52090
52179
|
return true;
|
|
52091
52180
|
};
|
|
52092
52181
|
|
|
52093
|
-
// The caller learns where the container went: the bound
|
|
52094
|
-
//
|
|
52095
|
-
//
|
|
52096
|
-
//
|
|
52097
|
-
// the
|
|
52098
|
-
//
|
|
52182
|
+
// The caller learns where the container went: the bound state is written
|
|
52183
|
+
// first, then `onCurrentChange` is called, so a caller reading the state from
|
|
52184
|
+
// inside its own handler reads where it now is. Written here rather than by
|
|
52185
|
+
// the caller so that it is told about the travels that HAPPENED and about no
|
|
52186
|
+
// others: the ones a lock refused never reach this point, and one refused
|
|
52187
|
+
// late is written back below (see goBackToRefusedArea).
|
|
52099
52188
|
const tellCurrentChange = (area, detail, leftArea) => {
|
|
52100
|
-
|
|
52101
|
-
|
|
52102
|
-
}
|
|
52103
|
-
// The address is told here rather than by the caller so that it is told
|
|
52104
|
-
// about the travels that HAPPENED and about no others: the ones a lock
|
|
52105
|
-
// refused never reach this point, and one refused late is written back
|
|
52106
|
-
// below (see goBackToRefusedArea).
|
|
52107
|
-
writeUrlParam(area, detail.cause);
|
|
52189
|
+
writeAreaAsked(area, {
|
|
52190
|
+
cause: detail.cause
|
|
52191
|
+
});
|
|
52108
52192
|
if (!onCurrentChange) {
|
|
52109
52193
|
return;
|
|
52110
52194
|
}
|
|
@@ -52132,47 +52216,44 @@ const SlideContainer = ({
|
|
|
52132
52216
|
const goBackToRefusedArea = leftArea => {
|
|
52133
52217
|
setProvisionalArea(null);
|
|
52134
52218
|
setCurrentAreaState(leftArea);
|
|
52135
|
-
|
|
52136
|
-
|
|
52137
|
-
|
|
52138
|
-
|
|
52139
|
-
|
|
52140
|
-
// being corrected, and a refusal is not a place one was.
|
|
52141
|
-
writeUrlParam(leftArea, "code", {
|
|
52142
|
-
replace: true
|
|
52219
|
+
// Written over rather than stacked on, whatever the state says otherwise:
|
|
52220
|
+
// the entry a refused travel wrote is the one being corrected, and a
|
|
52221
|
+
// refusal is not a place one was.
|
|
52222
|
+
writeAreaAsked(leftArea, {
|
|
52223
|
+
cause: "refusal"
|
|
52143
52224
|
});
|
|
52144
52225
|
};
|
|
52145
52226
|
|
|
52146
|
-
//
|
|
52147
|
-
//
|
|
52148
|
-
//
|
|
52149
|
-
//
|
|
52150
|
-
//
|
|
52151
|
-
//
|
|
52227
|
+
// Something outside the box asks for a slide: the application writing the
|
|
52228
|
+
// bound signal, and — when that signal is a route's own (see
|
|
52229
|
+
// docs/navigation.md) — a load on a link, a traversal, an address typed by
|
|
52230
|
+
// hand. All the same event here, and the answer to all of them is the same:
|
|
52231
|
+
// it is a REQUEST, walked to rather than jumped to.
|
|
52232
|
+
//
|
|
52233
|
+
// Walked, because the request comes from outside and the road may be closed:
|
|
52234
|
+
// every slide between here and there is asked to let go the way a key going
|
|
52235
|
+
// that way would ask it, and the first one that holds is where one stops.
|
|
52236
|
+
// `?step=done` cannot open a confirmation screen for something nobody sent.
|
|
52237
|
+
// Whatever comes of it, the state is then written with the area actually
|
|
52238
|
+
// shown: it says where one IS, never where one asked to be.
|
|
52152
52239
|
useLayoutEffect(() => {
|
|
52153
|
-
if (
|
|
52240
|
+
if (currentFromCaller === undefined) {
|
|
52154
52241
|
return;
|
|
52155
52242
|
}
|
|
52156
|
-
if (
|
|
52243
|
+
if (currentFromCaller === areaAskedSeenRef.current) {
|
|
52157
52244
|
// Not news: either nothing moved, or this container is reading back what
|
|
52158
52245
|
// it wrote itself.
|
|
52159
52246
|
return;
|
|
52160
52247
|
}
|
|
52161
|
-
|
|
52162
|
-
if (areaInUrl === null) {
|
|
52163
|
-
// An address that says nothing is not an address saying "the first
|
|
52164
|
-
// slide": a container opened without the param opens where it would have
|
|
52165
|
-
// opened anyway, and the param appears the first time one travels.
|
|
52166
|
-
return;
|
|
52167
|
-
}
|
|
52248
|
+
areaAskedSeenRef.current = currentFromCaller;
|
|
52168
52249
|
// Where the box IS, read off the DOM: `current` is undefined until someone
|
|
52169
52250
|
// names a slide, and the container standing on its first one is a fact only
|
|
52170
52251
|
// the map knows (see the layout effect that paints it).
|
|
52171
|
-
const areaOnScreen = containerRef.current?.getAttribute(
|
|
52172
|
-
if (!areaOnScreen ||
|
|
52252
|
+
const areaOnScreen = containerRef.current?.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
|
|
52253
|
+
if (!areaOnScreen || currentFromCaller === areaOnScreen) {
|
|
52173
52254
|
return;
|
|
52174
52255
|
}
|
|
52175
|
-
const reached = reachableTowards(areaOnScreen,
|
|
52256
|
+
const reached = reachableTowards(areaOnScreen, currentFromCaller);
|
|
52176
52257
|
if (reached && reached !== areaOnScreen) {
|
|
52177
52258
|
// Told rather than asked: the walk above has already put every lock on
|
|
52178
52259
|
// the way the question goToArea would have put the first one, and this
|
|
@@ -52181,15 +52262,25 @@ const SlideContainer = ({
|
|
|
52181
52262
|
// any travel nobody pressed anything for.
|
|
52182
52263
|
setCurrentAreaState(reached);
|
|
52183
52264
|
tellCurrentChange(reached, {
|
|
52184
|
-
cause: "
|
|
52265
|
+
cause: "state"
|
|
52185
52266
|
}, areaOnScreen);
|
|
52186
52267
|
return;
|
|
52187
52268
|
}
|
|
52188
|
-
//
|
|
52189
|
-
//
|
|
52190
|
-
//
|
|
52191
|
-
|
|
52192
|
-
|
|
52269
|
+
// Nowhere this map knows, or somewhere the walk is not allowed to reach:
|
|
52270
|
+
// the state is put back on the slide one is actually looking at, rather
|
|
52271
|
+
// than left saying one is somewhere one is not. A caller holding `current`
|
|
52272
|
+
// themselves is told instead — it is their value to correct.
|
|
52273
|
+
if (currentSignal) {
|
|
52274
|
+
writeAreaAsked(areaOnScreen, {
|
|
52275
|
+
cause: "refusal"
|
|
52276
|
+
});
|
|
52277
|
+
return;
|
|
52278
|
+
}
|
|
52279
|
+
onCurrentChange?.(areaOnScreen, {
|
|
52280
|
+
cause: "state",
|
|
52281
|
+
event: null
|
|
52282
|
+
});
|
|
52283
|
+
}, [currentFromCaller]);
|
|
52193
52284
|
|
|
52194
52285
|
// The press kept during a roll, taken once the window rests and the travel is
|
|
52195
52286
|
// given back (noTravel off): by direction when there was one, so it is read
|
|
@@ -53287,6 +53378,10 @@ const SlideContainer = ({
|
|
|
53287
53378
|
* ahead to a screen that has nothing to show yet. Answering a step
|
|
53288
53379
|
* un-answers the ones after it, since what they were answered about has just
|
|
53289
53380
|
* changed.
|
|
53381
|
+
* A container remembers nothing across a load, and cannot: a step the app
|
|
53382
|
+
* knows is already answered when the page opens says so itself
|
|
53383
|
+
* (`required={!alreadyFilled}`), which is what lets an address naming a later
|
|
53384
|
+
* step be walked to.
|
|
53290
53385
|
* @param {boolean} [props.preventNav] - hold the user here, whichever way they
|
|
53291
53386
|
* try to leave.
|
|
53292
53387
|
* @param {boolean} [props.preventNavNext] - hold them from going right or down.
|