@jsenv/navi 0.29.353 → 0.29.355
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/dev/jsenv_navi.js +330 -68
- package/dist/dev/jsenv_navi.js.map +11 -11
- package/dist/jsenv_navi.js +330 -68
- package/dist/jsenv_navi.js.map +11 -11
- package/docs/navigation.md +25 -0
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -663,6 +663,11 @@ naviI18n.addAll({
|
|
|
663
663
|
en: ":",
|
|
664
664
|
fr: "h",
|
|
665
665
|
},
|
|
666
|
+
// A time of whole hours has nothing to stand between: what follows the hour.
|
|
667
|
+
"time.hour_suffix": {
|
|
668
|
+
en: "",
|
|
669
|
+
fr: "h",
|
|
670
|
+
},
|
|
666
671
|
"time.hour_label": {
|
|
667
672
|
en: "Hours",
|
|
668
673
|
fr: "Heures",
|
|
@@ -4869,14 +4874,12 @@ const minutesFromTime$1 = (time) => {
|
|
|
4869
4874
|
return parts.hour * 60 + parts.minute;
|
|
4870
4875
|
};
|
|
4871
4876
|
|
|
4877
|
+
// Not folded back into the day: 1440 is "24:00", the end of the day a span can
|
|
4878
|
+
// run into.
|
|
4872
4879
|
const timeFromMinutes = (minutes) => {
|
|
4873
|
-
|
|
4874
|
-
((minutes % MINUTES_PER_DAY) + MINUTES_PER_DAY) % MINUTES_PER_DAY;
|
|
4875
|
-
return `${padTwo$1(Math.floor(inDay / 60))}:${padTwo$1(inDay % 60)}`;
|
|
4880
|
+
return `${padTwo$1(Math.floor(minutes / 60))}:${padTwo$1(minutes % 60)}`;
|
|
4876
4881
|
};
|
|
4877
4882
|
|
|
4878
|
-
const MINUTES_PER_DAY = 24 * 60;
|
|
4879
|
-
|
|
4880
4883
|
const padTwo$1 = (value) => String(value).padStart(2, "0");
|
|
4881
4884
|
|
|
4882
4885
|
// Maps validity type names → navi input type names.
|
|
@@ -5772,6 +5775,14 @@ const constraintAttributeFromProp = (key) => {
|
|
|
5772
5775
|
const isConstraintAttributeOn = (value) =>
|
|
5773
5776
|
value !== undefined && value !== null && value !== false;
|
|
5774
5777
|
|
|
5778
|
+
/**
|
|
5779
|
+
* Whether a constraint attribute asks for the value to be CORRECTED rather
|
|
5780
|
+
* than refused: `singleSpace="autoFix"`. Only constraints whose rule knows a
|
|
5781
|
+
* correction offer it, and only the fields that write it get one — a
|
|
5782
|
+
* `maxLength` silently truncating what someone wrote would be a bad default.
|
|
5783
|
+
*/
|
|
5784
|
+
const isConstraintAttributeAutoFix = (value) => value === "autoFix";
|
|
5785
|
+
|
|
5775
5786
|
const CONSTRAINT_NAME_TO_PROP = {
|
|
5776
5787
|
disabled: "disabledMessage",
|
|
5777
5788
|
required: "requiredMessage",
|
|
@@ -10639,14 +10650,13 @@ const tryActionAfterInteractionAllowed = (
|
|
|
10639
10650
|
|
|
10640
10651
|
// Resolve proxy so navi_action_* fires on the real control element.
|
|
10641
10652
|
let elementForAction = controlHost;
|
|
10642
|
-
let
|
|
10653
|
+
let activeController = controller;
|
|
10643
10654
|
if (controller) {
|
|
10644
10655
|
const proxyTargetController = findControlProxyTargetController(controller);
|
|
10645
10656
|
if (proxyTargetController) {
|
|
10646
10657
|
elementForAction = proxyTargetController.ref.current;
|
|
10658
|
+
activeController = proxyTargetController;
|
|
10647
10659
|
}
|
|
10648
|
-
const activeController = proxyTargetController ?? controller;
|
|
10649
|
-
uiState = activeController?.uiState;
|
|
10650
10660
|
}
|
|
10651
10661
|
|
|
10652
10662
|
// Validity gate: re-check (handles autoResetOnAction side effects), then read
|
|
@@ -10673,6 +10683,11 @@ const tryActionAfterInteractionAllowed = (
|
|
|
10673
10683
|
}
|
|
10674
10684
|
}
|
|
10675
10685
|
|
|
10686
|
+
// Read after the gate, never before: a constraint allowed to correct the
|
|
10687
|
+
// value rewrites it in there (see applyAutoFix), and what goes out has to be
|
|
10688
|
+
// what the control ends up holding.
|
|
10689
|
+
const uiState = activeController?.uiState;
|
|
10690
|
+
|
|
10676
10691
|
if (action === "auto" || action?.isAction) {
|
|
10677
10692
|
// A control that commits gets the last word on whether this particular
|
|
10678
10693
|
// value is worth acting on — see Form's own `shouldRequestAction`, which
|
|
@@ -13125,9 +13140,22 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-same-as");
|
|
|
13125
13140
|
* `data-single-space` — no leading or trailing space, never two in a row.
|
|
13126
13141
|
* The rule itself is @jsenv/validity's SINGLE_SPACE_RULE, so a server checking
|
|
13127
13142
|
* the value again refuses it for the same reason and in the same words.
|
|
13143
|
+
*
|
|
13144
|
+
* `data-single-space="autoFix"` corrects the value instead of refusing it. A
|
|
13145
|
+
* text field ends with a space more often than anyone means it to — a word
|
|
13146
|
+
* then a pause, a mobile keyboard after a suggestion, a paste — and the person
|
|
13147
|
+
* is then asked to find and delete a character they cannot see. The correction
|
|
13148
|
+
* is the rule's own, so the value a server re-checks with `singleSpace: true`
|
|
13149
|
+
* is one it accepts.
|
|
13128
13150
|
*/
|
|
13129
13151
|
|
|
13130
13152
|
|
|
13153
|
+
const applyRule = (field) => {
|
|
13154
|
+
const valueAsString =
|
|
13155
|
+
field.uiState === undefined ? "" : String(field.uiState);
|
|
13156
|
+
return SINGLE_SPACE_RULE.applyOn(true, valueAsString);
|
|
13157
|
+
};
|
|
13158
|
+
|
|
13131
13159
|
const SINGLE_SPACE_CONSTRAINT = {
|
|
13132
13160
|
name: "single_space",
|
|
13133
13161
|
messageAttribute: "data-single-space-message",
|
|
@@ -13136,14 +13164,29 @@ const SINGLE_SPACE_CONSTRAINT = {
|
|
|
13136
13164
|
if (!isConstraintAttributeOn(singleSpace)) {
|
|
13137
13165
|
return null;
|
|
13138
13166
|
}
|
|
13139
|
-
|
|
13140
|
-
|
|
13141
|
-
|
|
13167
|
+
if (isConstraintAttributeAutoFix(singleSpace)) {
|
|
13168
|
+
// The correction the rule knows always lands on a value the rule
|
|
13169
|
+
// accepts, and it runs on every commit — so there is nothing left for
|
|
13170
|
+
// the person to do about this, and nothing to say to them.
|
|
13171
|
+
return null;
|
|
13172
|
+
}
|
|
13173
|
+
const result = applyRule(field);
|
|
13142
13174
|
if (!result) {
|
|
13143
13175
|
return null;
|
|
13144
13176
|
}
|
|
13145
13177
|
return naviI18nFromValidityMessage(result);
|
|
13146
13178
|
},
|
|
13179
|
+
autoFix: (field) => {
|
|
13180
|
+
const singleSpace = field.controlHostProps["data-single-space"];
|
|
13181
|
+
if (!isConstraintAttributeAutoFix(singleSpace)) {
|
|
13182
|
+
return null;
|
|
13183
|
+
}
|
|
13184
|
+
const result = applyRule(field);
|
|
13185
|
+
if (!result) {
|
|
13186
|
+
return null;
|
|
13187
|
+
}
|
|
13188
|
+
return result.autoFix();
|
|
13189
|
+
},
|
|
13147
13190
|
};
|
|
13148
13191
|
CONSTRAINT_ATTRIBUTE_SET.add("data-single-space");
|
|
13149
13192
|
|
|
@@ -13335,6 +13378,74 @@ const createControlValidation = (
|
|
|
13335
13378
|
const getConstraintValidityState = () => constraintValidityState;
|
|
13336
13379
|
controlValidity.getConstraintValidityState = getConstraintValidityState;
|
|
13337
13380
|
|
|
13381
|
+
const getConstraintSet = () => {
|
|
13382
|
+
const constraintSet = new Set([
|
|
13383
|
+
...DEFAULT_CONSTRAINT_SET,
|
|
13384
|
+
...dynamicConstraintSet,
|
|
13385
|
+
]);
|
|
13386
|
+
// An app constraint declared at the call site: `constraints={[MY_CONSTRAINT]}`.
|
|
13387
|
+
// Read from the raw props on every check so a constraint whose parameters
|
|
13388
|
+
// are closed over is re-created freely, and last so the constraints navi
|
|
13389
|
+
// ships are the ones reported first (see pickConstraintFailureInfo).
|
|
13390
|
+
const constraintsFromProps = controller.props.constraints;
|
|
13391
|
+
if (constraintsFromProps) {
|
|
13392
|
+
for (const constraintFromProps of constraintsFromProps) {
|
|
13393
|
+
constraintSet.add(normalizeConstraint(constraintFromProps));
|
|
13394
|
+
}
|
|
13395
|
+
}
|
|
13396
|
+
return constraintSet;
|
|
13397
|
+
};
|
|
13398
|
+
|
|
13399
|
+
/**
|
|
13400
|
+
* Lets the constraints allowed to correct the value do so, and puts what
|
|
13401
|
+
* they return in the control — ui state, bound signal, the field itself.
|
|
13402
|
+
*
|
|
13403
|
+
* Called at the moments a value is COMMITTED: the field is left, an action
|
|
13404
|
+
* is about to read it. Never while it is being typed into, where removing
|
|
13405
|
+
* the trailing space would eat the one the person is about to follow with a
|
|
13406
|
+
* word.
|
|
13407
|
+
*
|
|
13408
|
+
* Returns whether the value moved, so a caller outside the validity pass can
|
|
13409
|
+
* re-read what the control is now worth.
|
|
13410
|
+
*/
|
|
13411
|
+
const applyAutoFix = (event) => {
|
|
13412
|
+
const proxyTargetController = findControlProxyTargetController(controller);
|
|
13413
|
+
if (proxyTargetController) {
|
|
13414
|
+
return proxyTargetController.rules.validation.applyAutoFix(event);
|
|
13415
|
+
}
|
|
13416
|
+
// A value nobody can edit is not navi's to rewrite: it is the app's, and
|
|
13417
|
+
// correcting it would change what gets sent behind the app's back.
|
|
13418
|
+
const controlHostProps = controller.controlHostProps;
|
|
13419
|
+
if (
|
|
13420
|
+
controlHostProps.disabled ||
|
|
13421
|
+
controlHostProps.readOnly ||
|
|
13422
|
+
controlHostProps["aria-readonly"] === "true"
|
|
13423
|
+
) {
|
|
13424
|
+
return false;
|
|
13425
|
+
}
|
|
13426
|
+
let fixed = false;
|
|
13427
|
+
for (const constraint of getConstraintSet()) {
|
|
13428
|
+
if (!constraint.autoFix) {
|
|
13429
|
+
continue;
|
|
13430
|
+
}
|
|
13431
|
+
const fixedValue = constraint.autoFix(controller);
|
|
13432
|
+
if (fixedValue === null || fixedValue === undefined) {
|
|
13433
|
+
continue;
|
|
13434
|
+
}
|
|
13435
|
+
if (compareTwoJsValues(fixedValue, controller.uiState)) {
|
|
13436
|
+
continue;
|
|
13437
|
+
}
|
|
13438
|
+
const autoFixEvent = new CustomEvent("auto_fix", {
|
|
13439
|
+
detail: { constraint: constraint.name },
|
|
13440
|
+
});
|
|
13441
|
+
chainEvent(autoFixEvent, event);
|
|
13442
|
+
controller.setUIState(fixedValue, autoFixEvent);
|
|
13443
|
+
fixed = true;
|
|
13444
|
+
}
|
|
13445
|
+
return fixed;
|
|
13446
|
+
};
|
|
13447
|
+
controlValidity.applyAutoFix = applyAutoFix;
|
|
13448
|
+
|
|
13338
13449
|
const checkValidity = ({
|
|
13339
13450
|
event,
|
|
13340
13451
|
requester = controller.ref.current,
|
|
@@ -13375,21 +13486,17 @@ const createControlValidation = (
|
|
|
13375
13486
|
}
|
|
13376
13487
|
}
|
|
13377
13488
|
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
|
|
13384
|
-
|
|
13385
|
-
// are closed over is re-created freely, and last so the constraints navi
|
|
13386
|
-
// ships are the ones reported first (see pickConstraintFailureInfo).
|
|
13387
|
-
const constraintsFromProps = controller.props.constraints;
|
|
13388
|
-
if (constraintsFromProps) {
|
|
13389
|
-
for (const constraintFromProps of constraintsFromProps) {
|
|
13390
|
-
constraintSet.add(normalizeConstraint(constraintFromProps));
|
|
13391
|
-
}
|
|
13489
|
+
// The value is about to be read and sent: whoever may correct it gets the
|
|
13490
|
+
// last word before the constraints judge it, so a field is never refused
|
|
13491
|
+
// for something navi knows how to put right. Unless the request comes from
|
|
13492
|
+
// typing — an action run as you type (a search, debounced or not) is not
|
|
13493
|
+
// a commit, and correcting there would eat the space before the next word.
|
|
13494
|
+
if (fromRequestAction && !findEvent(event, "input")) {
|
|
13495
|
+
applyAutoFix(event);
|
|
13392
13496
|
}
|
|
13497
|
+
|
|
13498
|
+
let newConstraintValidityState = { valid: true };
|
|
13499
|
+
const constraintSet = getConstraintSet();
|
|
13393
13500
|
const elementSig = getElementSignature(controller.ref.current);
|
|
13394
13501
|
// Not logged: every control checks its constraints on every interaction and
|
|
13395
13502
|
// almost always passes, so this line alone was most of the debug output —
|
|
@@ -26277,10 +26384,27 @@ let isUpdatingRoutesFromUrl = false;
|
|
|
26277
26384
|
* route("/legacy/:id", { redirectRoute: GAME_PAGE, redirectRouteParams: ({ id }) => ({ gameId: id }) });
|
|
26278
26385
|
* route("/:gameId/invite", { redirectRoute: HOME_PAGE, redirectRouteParams: null });
|
|
26279
26386
|
* ```
|
|
26387
|
+
* @param {string[]} [options.dropSearchParams]
|
|
26388
|
+
* Search params this route's address never keeps: read by whoever fetched
|
|
26389
|
+
* the address (a link preview crawler, a cache), never by the app. When this
|
|
26390
|
+
* route's own address arrives carrying one, the navigation goes to the same
|
|
26391
|
+
* address without it, at the door, like a redirection: no history entry, no
|
|
26392
|
+
* route action, no signal written. Every other search param stays as written.
|
|
26393
|
+
*
|
|
26394
|
+
* ```js
|
|
26395
|
+
* // ?v= makes WhatsApp fetch a new preview, the address bar never shows it
|
|
26396
|
+
* route(`/games/:gameId=${gamePageIdSignal}`, { dropSearchParams: ["v"] });
|
|
26397
|
+
* ```
|
|
26280
26398
|
*/
|
|
26281
26399
|
const route = (
|
|
26282
26400
|
pattern,
|
|
26283
|
-
{
|
|
26401
|
+
{
|
|
26402
|
+
searchParams,
|
|
26403
|
+
params,
|
|
26404
|
+
redirectRoute,
|
|
26405
|
+
redirectRouteParams,
|
|
26406
|
+
dropSearchParams,
|
|
26407
|
+
} = {},
|
|
26284
26408
|
) => {
|
|
26285
26409
|
const routePattern = createRoutePattern(pattern, { searchParams, params });
|
|
26286
26410
|
const { cleanPattern } = routePattern;
|
|
@@ -26340,6 +26464,7 @@ const route = (
|
|
|
26340
26464
|
routePattern,
|
|
26341
26465
|
redirectRoute,
|
|
26342
26466
|
redirectRouteParams,
|
|
26467
|
+
dropSearchParams,
|
|
26343
26468
|
setup: null,
|
|
26344
26469
|
updateStatus: null,
|
|
26345
26470
|
cleanup: null,
|
|
@@ -26712,6 +26837,7 @@ const route = (
|
|
|
26712
26837
|
const [publishRouteMutations, observeRouteMutations] = createPubSub();
|
|
26713
26838
|
|
|
26714
26839
|
let redirectingRouteSet = null;
|
|
26840
|
+
let droppingRouteSet = null;
|
|
26715
26841
|
/**
|
|
26716
26842
|
* Where does this url really lead?
|
|
26717
26843
|
*
|
|
@@ -26728,14 +26854,21 @@ let redirectingRouteSet = null;
|
|
|
26728
26854
|
* @returns {string|null} The url to go to instead, or null.
|
|
26729
26855
|
*/
|
|
26730
26856
|
const resolveRouteRedirection = (url) => {
|
|
26731
|
-
if (
|
|
26857
|
+
if (
|
|
26858
|
+
!redirectingRouteSet ||
|
|
26859
|
+
(redirectingRouteSet.size === 0 && droppingRouteSet.size === 0)
|
|
26860
|
+
) {
|
|
26732
26861
|
return null;
|
|
26733
26862
|
}
|
|
26734
26863
|
let urlToResolve = url;
|
|
26735
26864
|
let redirectionUrl = null;
|
|
26736
26865
|
const urlChain = [url];
|
|
26737
26866
|
while (true) {
|
|
26738
|
-
|
|
26867
|
+
// A redirection first: the address it lands on may carry a param its own
|
|
26868
|
+
// route drops, and the next turn removes it there.
|
|
26869
|
+
const nextUrl =
|
|
26870
|
+
resolveRedirectionOnce(urlToResolve) ||
|
|
26871
|
+
dropSearchParamsOnce(urlToResolve);
|
|
26739
26872
|
if (!nextUrl || nextUrl === urlToResolve) {
|
|
26740
26873
|
break;
|
|
26741
26874
|
}
|
|
@@ -26776,6 +26909,35 @@ const resolveRedirectionOnce = (url) => {
|
|
|
26776
26909
|
}
|
|
26777
26910
|
return buildRedirectionUrl(redirectingRoute, redirectingRouteParams);
|
|
26778
26911
|
};
|
|
26912
|
+
// The query is edited as written rather than rebuilt through URLSearchParams,
|
|
26913
|
+
// which would rewrite every param kept ("?weather" into "?weather=", commas
|
|
26914
|
+
// encoded — see extractSearchParams).
|
|
26915
|
+
const dropSearchParamsOnce = (url) => {
|
|
26916
|
+
const urlObject = new URL(url);
|
|
26917
|
+
if (!urlObject.search) {
|
|
26918
|
+
return null;
|
|
26919
|
+
}
|
|
26920
|
+
for (const route of droppingRouteSet) {
|
|
26921
|
+
const { routePattern, dropSearchParams } = getRoutePrivateProperties(route);
|
|
26922
|
+
// exact, for the same reason as a redirection: the addresses below a
|
|
26923
|
+
// trailing slash belong to the routes declared for them
|
|
26924
|
+
if (!routePattern.applyOn(url, { exact: true })) {
|
|
26925
|
+
continue;
|
|
26926
|
+
}
|
|
26927
|
+
const pairs = urlObject.search.slice(1).split("&");
|
|
26928
|
+
const pairsKept = pairs.filter((pair) => {
|
|
26929
|
+
const eqIndex = pair.indexOf("=");
|
|
26930
|
+
const key = eqIndex > -1 ? pair.slice(0, eqIndex) : pair;
|
|
26931
|
+
return !dropSearchParams.includes(decodeURIComponent(key));
|
|
26932
|
+
});
|
|
26933
|
+
if (pairsKept.length === pairs.length) {
|
|
26934
|
+
continue;
|
|
26935
|
+
}
|
|
26936
|
+
urlObject.search = pairsKept.length ? `?${pairsKept.join("&")}` : "";
|
|
26937
|
+
return urlObject.href;
|
|
26938
|
+
}
|
|
26939
|
+
return null;
|
|
26940
|
+
};
|
|
26779
26941
|
const buildRedirectionUrl = (route, urlParams) => {
|
|
26780
26942
|
const { redirectRoute, redirectRouteParams } =
|
|
26781
26943
|
getRoutePrivateProperties(route);
|
|
@@ -26879,8 +27041,13 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
26879
27041
|
// Checked here rather than at declaration: a route may redirect to one
|
|
26880
27042
|
// declared after it, and reading the target then would forbid that order.
|
|
26881
27043
|
redirectingRouteSet = new Set();
|
|
27044
|
+
droppingRouteSet = new Set();
|
|
26882
27045
|
for (const route of routeSet) {
|
|
26883
|
-
const { redirectRoute } =
|
|
27046
|
+
const { redirectRoute, dropSearchParams } =
|
|
27047
|
+
getRoutePrivateProperties(route);
|
|
27048
|
+
if (dropSearchParams && dropSearchParams.length) {
|
|
27049
|
+
droppingRouteSet.add(route);
|
|
27050
|
+
}
|
|
26884
27051
|
if (!redirectRoute) {
|
|
26885
27052
|
continue;
|
|
26886
27053
|
}
|
|
@@ -27166,6 +27333,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
27166
27333
|
}
|
|
27167
27334
|
routeSet.clear();
|
|
27168
27335
|
redirectingRouteSet = null;
|
|
27336
|
+
droppingRouteSet = null;
|
|
27169
27337
|
setupRoutesCalled = false;
|
|
27170
27338
|
activeRouteSet = null;
|
|
27171
27339
|
};
|
|
@@ -37859,7 +38027,8 @@ const useUIStateController = (
|
|
|
37859
38027
|
}
|
|
37860
38028
|
if (
|
|
37861
38029
|
e.type === "facade_propagate_up" ||
|
|
37862
|
-
e.type === "cancel_rollback"
|
|
38030
|
+
e.type === "cancel_rollback" ||
|
|
38031
|
+
e.type === "auto_fix"
|
|
37863
38032
|
) {
|
|
37864
38033
|
// Exception: when the facade propagates a child state change up to the
|
|
37865
38034
|
// real picker input, also notify the parent group (e.g. Form) so it
|
|
@@ -37869,6 +38038,9 @@ const useUIStateController = (
|
|
|
37869
38038
|
// A cancel takes the same road back: the Form was told what the
|
|
37870
38039
|
// popup was picking, so it has to be told the picker went back to
|
|
37871
38040
|
// where it opened, or it sends a value the user said no to.
|
|
38041
|
+
// A correction is the same story once more: the Form sends what
|
|
38042
|
+
// its fields add up to, and a field that just put its own value
|
|
38043
|
+
// right has to be counted for the corrected one.
|
|
37872
38044
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
37873
38045
|
stateChanged: true,
|
|
37874
38046
|
});
|
|
@@ -39763,6 +39935,11 @@ const INTERNAL_EVENT_SET = new Set([
|
|
|
39763
39935
|
// notification below still happen, exactly as they did on the way in (see
|
|
39764
39936
|
// picker_custom.jsx's onClose).
|
|
39765
39937
|
"cancel_rollback",
|
|
39938
|
+
// A constraint allowed to correct the value put it right as the value was
|
|
39939
|
+
// committed (see applyAutoFix). Nobody pressed anything, so no command and
|
|
39940
|
+
// no action of the control's own — but what it holds really did move, so
|
|
39941
|
+
// uiAction, the bound signal and the parent notification below all happen.
|
|
39942
|
+
"auto_fix",
|
|
39766
39943
|
]);
|
|
39767
39944
|
const isInternalEvent = (e) => {
|
|
39768
39945
|
return INTERNAL_EVENT_SET.has(e.type);
|
|
@@ -40695,7 +40872,9 @@ const useControlProps = (props, {
|
|
|
40695
40872
|
requester: control
|
|
40696
40873
|
});
|
|
40697
40874
|
if (dispatched) {
|
|
40698
|
-
|
|
40875
|
+
// Read again: the gate may have corrected the value (see applyAutoFix),
|
|
40876
|
+
// and what the next request is compared with is what went out.
|
|
40877
|
+
lastActionValueRef.current = readControlValue(control);
|
|
40699
40878
|
}
|
|
40700
40879
|
return dispatched;
|
|
40701
40880
|
};
|
|
@@ -40843,6 +41022,24 @@ const useControlProps = (props, {
|
|
|
40843
41022
|
syncDomState(readControlValue(el), e);
|
|
40844
41023
|
};
|
|
40845
41024
|
}
|
|
41025
|
+
// Leaving the field is one of the two moments a value is committed — the
|
|
41026
|
+
// other being an action about to read it (see applyAutoFix). A constraint
|
|
41027
|
+
// allowed to correct the value puts it right here, so what the field
|
|
41028
|
+
// shows, what the counter counts and what a submit would send are one
|
|
41029
|
+
// thing well before the submit.
|
|
41030
|
+
if (controlType === "input") {
|
|
41031
|
+
const onBlurFromProps = controlHostProps.onBlur;
|
|
41032
|
+
controlHostProps.onBlur = e => {
|
|
41033
|
+
onBlurFromProps?.(e);
|
|
41034
|
+
const validation = uiStateController.rules.validation;
|
|
41035
|
+
if (validation.applyAutoFix(e)) {
|
|
41036
|
+
// The value moved without anyone typing: what the constraints had to
|
|
41037
|
+
// say about the old one is out of date, and so is what the controls
|
|
41038
|
+
// above read from this one.
|
|
41039
|
+
validation.syncValidity(e);
|
|
41040
|
+
}
|
|
41041
|
+
};
|
|
41042
|
+
}
|
|
40846
41043
|
}
|
|
40847
41044
|
const uiState = uiStateController.uiStateSignal.peek();
|
|
40848
41045
|
const domProps = toDomProps(uiState);
|
|
@@ -81054,8 +81251,10 @@ const css$l = /* css */`.navi_time_range_label {
|
|
|
81054
81251
|
}
|
|
81055
81252
|
`;
|
|
81056
81253
|
const HOUR_COUNT = 24;
|
|
81254
|
+
const END_OF_DAY_HOUR = 24;
|
|
81057
81255
|
const MINUTES_PER_HOUR = 60;
|
|
81058
81256
|
const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
|
|
81257
|
+
const END_OF_DAY = END_OF_DAY_HOUR * MINUTES_PER_HOUR;
|
|
81059
81258
|
|
|
81060
81259
|
/**
|
|
81061
81260
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
@@ -81075,16 +81274,21 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
|
|
|
81075
81274
|
* }>}
|
|
81076
81275
|
* @param {string} [value] The time shown, as "HH:MM".
|
|
81077
81276
|
* @param {number} [minuteStep=1] How many minutes apart the values on the
|
|
81078
|
-
* minute wheel are — 15 for quarters of an hour.
|
|
81277
|
+
* minute wheel are — 15 for quarters of an hour. At 60 the time is a whole
|
|
81278
|
+
* hour: there is no minute wheel at all, the value stays "HH:MM" ("08:00"),
|
|
81279
|
+
* and a value arriving with minutes is shown on its hour.
|
|
81079
81280
|
* @param {{min?: number, max?: number}|number[]} [hours] Which hours the wheel
|
|
81080
81281
|
* offers: `{ min: 7, max: 21 }` for a day that starts and ends somewhere, or
|
|
81081
|
-
* the list itself.
|
|
81082
|
-
* in the way.
|
|
81282
|
+
* the list itself. 0 to 23 by default. Rows nobody will ever land on are rows
|
|
81283
|
+
* in the way. 24 is the end of the day ("24:00", midnight at the end of a
|
|
81284
|
+
* span) and has no minutes: turned onto it, the minute wheel goes back to 0.
|
|
81083
81285
|
* @param {boolean} [loop=true] The wheels go round: 23h then 0h, 59 minutes
|
|
81084
81286
|
* then 0. What a clock does. Say `loop={false}` for two ends one cannot turn
|
|
81085
81287
|
* past.
|
|
81086
81288
|
* @param {import("ignore:preact").ComponentChildren} [separator] What is written
|
|
81087
|
-
* between the hours and the minutes. "h" in French, ":" elsewhere.
|
|
81289
|
+
* between the hours and the minutes. "h" in French, ":" elsewhere. In a time
|
|
81290
|
+
* of whole hours it follows each hour on its row instead ("8h"), and nothing
|
|
81291
|
+
* is written by default outside French.
|
|
81088
81292
|
* @param {string} [placeholder] What the wheels show while the time holds
|
|
81089
81293
|
* nothing, as "HH:MM". Wheels have no blank row to land on, so their
|
|
81090
81294
|
* placeholder is a position rather than a grey word — shown, but not an
|
|
@@ -81099,13 +81303,20 @@ const TimeWheel = ({
|
|
|
81099
81303
|
hours,
|
|
81100
81304
|
loop = true,
|
|
81101
81305
|
placeholder,
|
|
81102
|
-
separator
|
|
81306
|
+
separator,
|
|
81103
81307
|
hourLabel = naviI18n("time.hour_label"),
|
|
81104
81308
|
minuteLabel = naviI18n("time.minute_label"),
|
|
81105
81309
|
size,
|
|
81106
81310
|
wheelProps,
|
|
81311
|
+
onnavi_wheel_settle,
|
|
81107
81312
|
...rest
|
|
81108
81313
|
}) => {
|
|
81314
|
+
const wholeHours = minuteStep >= MINUTES_PER_HOUR;
|
|
81315
|
+
if (separator === undefined) {
|
|
81316
|
+
separator = naviI18n(wholeHours ? "time.hour_suffix" : "time.hour_separator");
|
|
81317
|
+
}
|
|
81318
|
+
const hourWheelRef = useRef(null);
|
|
81319
|
+
const minuteWheelRef = useRef(null);
|
|
81109
81320
|
const minutes = useMemo(() => {
|
|
81110
81321
|
const minuteList = [];
|
|
81111
81322
|
let minute = 0;
|
|
@@ -81119,13 +81330,42 @@ const TimeWheel = ({
|
|
|
81119
81330
|
const {
|
|
81120
81331
|
aggregateChildStates,
|
|
81121
81332
|
distributeChildUIState
|
|
81122
|
-
} = useAnswered(placeholder, rest, aggregateTime, distributeTime);
|
|
81333
|
+
} = useAnswered(placeholder, rest, aggregateTime, (groupState, child) => distributeTime(groupState, child, minuteStep));
|
|
81123
81334
|
const placeholderParts = parseTimeParts(placeholder);
|
|
81335
|
+
|
|
81336
|
+
// The end of the day has no minutes. The time already reads "24:00" whatever
|
|
81337
|
+
// the minute wheel shows (see aggregateTime); on settle the wheel is brought
|
|
81338
|
+
// back to 0 so what is drawn is what is held.
|
|
81339
|
+
const endOfDayHasNoMinutes = e => {
|
|
81340
|
+
const hourEl = hourWheelRef.current;
|
|
81341
|
+
const minuteEl = minuteWheelRef.current;
|
|
81342
|
+
if (!hourEl || !minuteEl) {
|
|
81343
|
+
return;
|
|
81344
|
+
}
|
|
81345
|
+
if (getUIStateFromElement(hourEl) !== END_OF_DAY_HOUR) {
|
|
81346
|
+
return;
|
|
81347
|
+
}
|
|
81348
|
+
if (getUIStateFromElement(minuteEl) === 0) {
|
|
81349
|
+
return;
|
|
81350
|
+
}
|
|
81351
|
+
dispatchRequestSetUIState(minuteEl, 0, {
|
|
81352
|
+
event: e
|
|
81353
|
+
});
|
|
81354
|
+
};
|
|
81124
81355
|
return jsxs(WheelGroup, {
|
|
81125
81356
|
aggregateChildStates: aggregateChildStates,
|
|
81126
81357
|
distributeChildUIState: distributeChildUIState,
|
|
81358
|
+
onnavi_wheel_settle: e => {
|
|
81359
|
+
if (!wholeHours) {
|
|
81360
|
+
endOfDayHasNoMinutes(e);
|
|
81361
|
+
}
|
|
81362
|
+
if (onnavi_wheel_settle) {
|
|
81363
|
+
onnavi_wheel_settle(e);
|
|
81364
|
+
}
|
|
81365
|
+
},
|
|
81127
81366
|
...rest,
|
|
81128
81367
|
children: [jsx(Wheel, {
|
|
81368
|
+
ref: hourWheelRef,
|
|
81129
81369
|
name: "hour",
|
|
81130
81370
|
type: "integer",
|
|
81131
81371
|
bounded: !loop,
|
|
@@ -81136,24 +81376,29 @@ const TimeWheel = ({
|
|
|
81136
81376
|
children: hourList.map(hour => jsx(Wheel.Item, {
|
|
81137
81377
|
value: hour,
|
|
81138
81378
|
paddingX: "s",
|
|
81139
|
-
children:
|
|
81379
|
+
children: wholeHours ? jsxs(Fragment, {
|
|
81380
|
+
children: [hour, separator]
|
|
81381
|
+
}) : padTwo(hour)
|
|
81140
81382
|
}, hour))
|
|
81141
|
-
}),
|
|
81142
|
-
|
|
81143
|
-
|
|
81144
|
-
|
|
81145
|
-
|
|
81146
|
-
|
|
81147
|
-
|
|
81148
|
-
|
|
81149
|
-
|
|
81150
|
-
|
|
81151
|
-
|
|
81152
|
-
|
|
81153
|
-
|
|
81154
|
-
|
|
81155
|
-
|
|
81156
|
-
|
|
81383
|
+
}), wholeHours ? null : jsxs(Fragment, {
|
|
81384
|
+
children: [jsx(WheelGroup.Separator, {
|
|
81385
|
+
size: size,
|
|
81386
|
+
children: separator
|
|
81387
|
+
}), jsx(Wheel, {
|
|
81388
|
+
ref: minuteWheelRef,
|
|
81389
|
+
name: "minute",
|
|
81390
|
+
type: "integer",
|
|
81391
|
+
bounded: !loop,
|
|
81392
|
+
size: size,
|
|
81393
|
+
"aria-label": minuteLabel,
|
|
81394
|
+
defaultValue: placeholderParts ? floorToStep(placeholderParts.minute, minuteStep) : undefined,
|
|
81395
|
+
...wheelProps,
|
|
81396
|
+
children: minutes.map(minute => jsx(Wheel.Item, {
|
|
81397
|
+
value: minute,
|
|
81398
|
+
paddingX: "s",
|
|
81399
|
+
children: padTwo(minute)
|
|
81400
|
+
}, minute))
|
|
81401
|
+
})]
|
|
81157
81402
|
})]
|
|
81158
81403
|
});
|
|
81159
81404
|
};
|
|
@@ -81187,11 +81432,12 @@ const TimeWheel = ({
|
|
|
81187
81432
|
* prepositions belong there; the group is still a row, and takes `flexWrap`
|
|
81188
81433
|
* for screens too narrow to hold both columns.
|
|
81189
81434
|
* @param {number} [minuteStep=1] How many minutes apart the values on both
|
|
81190
|
-
* minute wheels are.
|
|
81435
|
+
* minute wheels are. 60 for a span of whole hours ("de 8h à 12h").
|
|
81191
81436
|
* @param {{min?: number, max?: number}|number[]} [hours] Which hours both
|
|
81192
|
-
* wheels offer — see `TimeWheel`.
|
|
81437
|
+
* wheels offer — see `TimeWheel`. A 24 is offered to the end alone: a span
|
|
81438
|
+
* can run until midnight ("24:00"), it cannot start there.
|
|
81193
81439
|
* @param {number} [minDuration=0] How long the span must last at least, in
|
|
81194
|
-
* minutes. Zero by default: a span of no length is a span all the same, only
|
|
81440
|
+
* minutes, rounded up to the step — the wheels have nothing in between. Zero by default: a span of no length is a span all the same, only
|
|
81195
81441
|
* one that goes backwards is not. It is what the bounds keep between them as
|
|
81196
81442
|
* they turn — turn the start into the end and the end moves along, keeping
|
|
81197
81443
|
* that much room.
|
|
@@ -81224,6 +81470,12 @@ const TimeRangeWheel = ({
|
|
|
81224
81470
|
const startId = useId();
|
|
81225
81471
|
const startRef = useRef(null);
|
|
81226
81472
|
const endRef = useRef(null);
|
|
81473
|
+
const endHourList = useMemo(() => resolveHourList(hours), [hours ? hours.min : undefined, hours ? hours.max : undefined, hours]);
|
|
81474
|
+
const startHourList = useMemo(() => endHourList.filter(hour => hour !== END_OF_DAY_HOUR), [endHourList]);
|
|
81475
|
+
const step = minuteStep >= MINUTES_PER_HOUR ? MINUTES_PER_HOUR : minuteStep;
|
|
81476
|
+
const minGap = ceilToStep(minDuration, step);
|
|
81477
|
+
// The latest the end can be pushed to: the last time its wheels can show.
|
|
81478
|
+
const lastEnd = endHourList.includes(END_OF_DAY_HOUR) ? END_OF_DAY : floorToStep(LAST_MINUTE_OF_DAY, step);
|
|
81227
81479
|
// One turn settles the whole span: a start somebody chose makes the end an
|
|
81228
81480
|
// answer too, left where the placeholder put it.
|
|
81229
81481
|
const {
|
|
@@ -81255,18 +81507,18 @@ const TimeRangeWheel = ({
|
|
|
81255
81507
|
return;
|
|
81256
81508
|
}
|
|
81257
81509
|
const duration = movedSide === "start" ? otherMinutes - movedMinutes : movedMinutes - otherMinutes;
|
|
81258
|
-
if (duration >=
|
|
81510
|
+
if (duration >= minGap) {
|
|
81259
81511
|
return;
|
|
81260
81512
|
}
|
|
81261
|
-
let pushedMinutes = movedSide === "start" ? movedMinutes +
|
|
81513
|
+
let pushedMinutes = movedSide === "start" ? movedMinutes + minGap : movedMinutes - minGap;
|
|
81262
81514
|
// The day has ends the wheels do not: pushed past midnight, the other bound
|
|
81263
81515
|
// would come back round on the wrong side of the one that pushed it. It
|
|
81264
81516
|
// stops at the edge instead, and the span that no longer fits is what the
|
|
81265
81517
|
// send-time constraint is there to say (see time_range_constraint.js).
|
|
81266
81518
|
if (pushedMinutes < 0) {
|
|
81267
81519
|
pushedMinutes = 0;
|
|
81268
|
-
} else if (pushedMinutes >
|
|
81269
|
-
pushedMinutes =
|
|
81520
|
+
} else if (pushedMinutes > lastEnd) {
|
|
81521
|
+
pushedMinutes = lastEnd;
|
|
81270
81522
|
}
|
|
81271
81523
|
dispatchRequestSetUIState(otherEl, timeFromMinutes(pushedMinutes), {
|
|
81272
81524
|
event: e
|
|
@@ -81294,7 +81546,7 @@ const TimeRangeWheel = ({
|
|
|
81294
81546
|
ref: startRef,
|
|
81295
81547
|
name: "start",
|
|
81296
81548
|
minuteStep: minuteStep,
|
|
81297
|
-
hours:
|
|
81549
|
+
hours: startHourList,
|
|
81298
81550
|
loop: loop,
|
|
81299
81551
|
size: size,
|
|
81300
81552
|
placeholder: placeholder ? placeholder.start : undefined
|
|
@@ -81319,7 +81571,7 @@ const TimeRangeWheel = ({
|
|
|
81319
81571
|
ref: endRef,
|
|
81320
81572
|
name: "end",
|
|
81321
81573
|
minuteStep: minuteStep,
|
|
81322
|
-
hours:
|
|
81574
|
+
hours: endHourList,
|
|
81323
81575
|
loop: loop,
|
|
81324
81576
|
size: size,
|
|
81325
81577
|
placeholder: placeholder ? placeholder.end : undefined,
|
|
@@ -81331,7 +81583,7 @@ const TimeRangeWheel = ({
|
|
|
81331
81583
|
// the time one would have to move is (see time_range_constraint.js).
|
|
81332
81584
|
,
|
|
81333
81585
|
"data-time-after": startId,
|
|
81334
|
-
"data-time-min-duration":
|
|
81586
|
+
"data-time-min-duration": minGap,
|
|
81335
81587
|
...timeProps,
|
|
81336
81588
|
...endTimeProps
|
|
81337
81589
|
})
|
|
@@ -81493,10 +81745,11 @@ const distributeSpan = (groupState, childUIStateController) => {
|
|
|
81493
81745
|
return groupState[childUIStateController.name];
|
|
81494
81746
|
};
|
|
81495
81747
|
|
|
81496
|
-
// The
|
|
81748
|
+
// The wheels as one value, "HH:MM". A time of whole hours has no minute wheel
|
|
81749
|
+
// and is on the hour; so is the end of the day, whatever the minute wheel says.
|
|
81497
81750
|
const aggregateTime = childUIStateControllers => {
|
|
81498
81751
|
let hour = "";
|
|
81499
|
-
let minute =
|
|
81752
|
+
let minute = 0;
|
|
81500
81753
|
for (const child of childUIStateControllers) {
|
|
81501
81754
|
if (child.name === "hour") {
|
|
81502
81755
|
hour = child.uiState ?? "";
|
|
@@ -81505,18 +81758,27 @@ const aggregateTime = childUIStateControllers => {
|
|
|
81505
81758
|
minute = child.uiState ?? "";
|
|
81506
81759
|
}
|
|
81507
81760
|
}
|
|
81761
|
+
if (hour === END_OF_DAY_HOUR) {
|
|
81762
|
+
return formatTimeParts(hour, 0);
|
|
81763
|
+
}
|
|
81508
81764
|
return formatTimeParts(hour, minute);
|
|
81509
81765
|
};
|
|
81510
81766
|
|
|
81511
81767
|
// The way back: what the group is set to (a value given to it, a form being
|
|
81512
|
-
// reset, the other bound pushing it) lands on the wheel it belongs to.
|
|
81513
|
-
|
|
81768
|
+
// reset, the other bound pushing it) lands on the wheel it belongs to. Minutes
|
|
81769
|
+
// the wheel does not offer land on the one before.
|
|
81770
|
+
const distributeTime = (groupState, childUIStateController, minuteStep) => {
|
|
81514
81771
|
const parts = parseTimeParts(groupState);
|
|
81515
81772
|
if (!parts) {
|
|
81516
81773
|
return undefined;
|
|
81517
81774
|
}
|
|
81775
|
+
if (childUIStateController.name === "minute") {
|
|
81776
|
+
return floorToStep(parts.minute, minuteStep);
|
|
81777
|
+
}
|
|
81518
81778
|
return parts[childUIStateController.name];
|
|
81519
81779
|
};
|
|
81780
|
+
const floorToStep = (minutes, step) => Math.floor(minutes / step) * step;
|
|
81781
|
+
const ceilToStep = (minutes, step) => Math.ceil(minutes / step) * step;
|
|
81520
81782
|
|
|
81521
81783
|
const TableSelectionContext = createContext();
|
|
81522
81784
|
const useTableSelectionContextValue = (
|