@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/jsenv_navi.js
CHANGED
|
@@ -636,6 +636,11 @@ naviI18n.addAll({
|
|
|
636
636
|
en: ":",
|
|
637
637
|
fr: "h",
|
|
638
638
|
},
|
|
639
|
+
// A time of whole hours has nothing to stand between: what follows the hour.
|
|
640
|
+
"time.hour_suffix": {
|
|
641
|
+
en: "",
|
|
642
|
+
fr: "h",
|
|
643
|
+
},
|
|
639
644
|
"time.hour_label": {
|
|
640
645
|
en: "Hours",
|
|
641
646
|
fr: "Heures",
|
|
@@ -4708,14 +4713,12 @@ const minutesFromTime$1 = (time) => {
|
|
|
4708
4713
|
return parts.hour * 60 + parts.minute;
|
|
4709
4714
|
};
|
|
4710
4715
|
|
|
4716
|
+
// Not folded back into the day: 1440 is "24:00", the end of the day a span can
|
|
4717
|
+
// run into.
|
|
4711
4718
|
const timeFromMinutes = (minutes) => {
|
|
4712
|
-
|
|
4713
|
-
((minutes % MINUTES_PER_DAY) + MINUTES_PER_DAY) % MINUTES_PER_DAY;
|
|
4714
|
-
return `${padTwo$1(Math.floor(inDay / 60))}:${padTwo$1(inDay % 60)}`;
|
|
4719
|
+
return `${padTwo$1(Math.floor(minutes / 60))}:${padTwo$1(minutes % 60)}`;
|
|
4715
4720
|
};
|
|
4716
4721
|
|
|
4717
|
-
const MINUTES_PER_DAY = 24 * 60;
|
|
4718
|
-
|
|
4719
4722
|
const padTwo$1 = (value) => String(value).padStart(2, "0");
|
|
4720
4723
|
|
|
4721
4724
|
// Maps validity type names → navi input type names.
|
|
@@ -5551,6 +5554,14 @@ const constraintAttributeFromProp = (key) => {
|
|
|
5551
5554
|
const isConstraintAttributeOn = (value) =>
|
|
5552
5555
|
value !== undefined && value !== null && value !== false;
|
|
5553
5556
|
|
|
5557
|
+
/**
|
|
5558
|
+
* Whether a constraint attribute asks for the value to be CORRECTED rather
|
|
5559
|
+
* than refused: `singleSpace="autoFix"`. Only constraints whose rule knows a
|
|
5560
|
+
* correction offer it, and only the fields that write it get one — a
|
|
5561
|
+
* `maxLength` silently truncating what someone wrote would be a bad default.
|
|
5562
|
+
*/
|
|
5563
|
+
const isConstraintAttributeAutoFix = (value) => value === "autoFix";
|
|
5564
|
+
|
|
5554
5565
|
const CONSTRAINT_NAME_TO_PROP = {
|
|
5555
5566
|
disabled: "disabledMessage",
|
|
5556
5567
|
required: "requiredMessage",
|
|
@@ -10298,14 +10309,13 @@ const tryActionAfterInteractionAllowed = (
|
|
|
10298
10309
|
|
|
10299
10310
|
// Resolve proxy so navi_action_* fires on the real control element.
|
|
10300
10311
|
let elementForAction = controlHost;
|
|
10301
|
-
let
|
|
10312
|
+
let activeController = controller;
|
|
10302
10313
|
if (controller) {
|
|
10303
10314
|
const proxyTargetController = findControlProxyTargetController(controller);
|
|
10304
10315
|
if (proxyTargetController) {
|
|
10305
10316
|
elementForAction = proxyTargetController.ref.current;
|
|
10317
|
+
activeController = proxyTargetController;
|
|
10306
10318
|
}
|
|
10307
|
-
const activeController = proxyTargetController ?? controller;
|
|
10308
|
-
uiState = activeController?.uiState;
|
|
10309
10319
|
}
|
|
10310
10320
|
|
|
10311
10321
|
// Validity gate: re-check (handles autoResetOnAction side effects), then read
|
|
@@ -10332,6 +10342,11 @@ const tryActionAfterInteractionAllowed = (
|
|
|
10332
10342
|
}
|
|
10333
10343
|
}
|
|
10334
10344
|
|
|
10345
|
+
// Read after the gate, never before: a constraint allowed to correct the
|
|
10346
|
+
// value rewrites it in there (see applyAutoFix), and what goes out has to be
|
|
10347
|
+
// what the control ends up holding.
|
|
10348
|
+
const uiState = activeController?.uiState;
|
|
10349
|
+
|
|
10335
10350
|
if (action === "auto" || action?.isAction) {
|
|
10336
10351
|
// A control that commits gets the last word on whether this particular
|
|
10337
10352
|
// value is worth acting on — see Form's own `shouldRequestAction`, which
|
|
@@ -12642,9 +12657,22 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-same-as");
|
|
|
12642
12657
|
* `data-single-space` — no leading or trailing space, never two in a row.
|
|
12643
12658
|
* The rule itself is @jsenv/validity's SINGLE_SPACE_RULE, so a server checking
|
|
12644
12659
|
* the value again refuses it for the same reason and in the same words.
|
|
12660
|
+
*
|
|
12661
|
+
* `data-single-space="autoFix"` corrects the value instead of refusing it. A
|
|
12662
|
+
* text field ends with a space more often than anyone means it to — a word
|
|
12663
|
+
* then a pause, a mobile keyboard after a suggestion, a paste — and the person
|
|
12664
|
+
* is then asked to find and delete a character they cannot see. The correction
|
|
12665
|
+
* is the rule's own, so the value a server re-checks with `singleSpace: true`
|
|
12666
|
+
* is one it accepts.
|
|
12645
12667
|
*/
|
|
12646
12668
|
|
|
12647
12669
|
|
|
12670
|
+
const applyRule = (field) => {
|
|
12671
|
+
const valueAsString =
|
|
12672
|
+
field.uiState === undefined ? "" : String(field.uiState);
|
|
12673
|
+
return SINGLE_SPACE_RULE.applyOn(true, valueAsString);
|
|
12674
|
+
};
|
|
12675
|
+
|
|
12648
12676
|
const SINGLE_SPACE_CONSTRAINT = {
|
|
12649
12677
|
name: "single_space",
|
|
12650
12678
|
messageAttribute: "data-single-space-message",
|
|
@@ -12653,14 +12681,29 @@ const SINGLE_SPACE_CONSTRAINT = {
|
|
|
12653
12681
|
if (!isConstraintAttributeOn(singleSpace)) {
|
|
12654
12682
|
return null;
|
|
12655
12683
|
}
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
|
|
12684
|
+
if (isConstraintAttributeAutoFix(singleSpace)) {
|
|
12685
|
+
// The correction the rule knows always lands on a value the rule
|
|
12686
|
+
// accepts, and it runs on every commit — so there is nothing left for
|
|
12687
|
+
// the person to do about this, and nothing to say to them.
|
|
12688
|
+
return null;
|
|
12689
|
+
}
|
|
12690
|
+
const result = applyRule(field);
|
|
12659
12691
|
if (!result) {
|
|
12660
12692
|
return null;
|
|
12661
12693
|
}
|
|
12662
12694
|
return naviI18nFromValidityMessage(result);
|
|
12663
12695
|
},
|
|
12696
|
+
autoFix: (field) => {
|
|
12697
|
+
const singleSpace = field.controlHostProps["data-single-space"];
|
|
12698
|
+
if (!isConstraintAttributeAutoFix(singleSpace)) {
|
|
12699
|
+
return null;
|
|
12700
|
+
}
|
|
12701
|
+
const result = applyRule(field);
|
|
12702
|
+
if (!result) {
|
|
12703
|
+
return null;
|
|
12704
|
+
}
|
|
12705
|
+
return result.autoFix();
|
|
12706
|
+
},
|
|
12664
12707
|
};
|
|
12665
12708
|
CONSTRAINT_ATTRIBUTE_SET.add("data-single-space");
|
|
12666
12709
|
|
|
@@ -12852,6 +12895,74 @@ const createControlValidation = (
|
|
|
12852
12895
|
const getConstraintValidityState = () => constraintValidityState;
|
|
12853
12896
|
controlValidity.getConstraintValidityState = getConstraintValidityState;
|
|
12854
12897
|
|
|
12898
|
+
const getConstraintSet = () => {
|
|
12899
|
+
const constraintSet = new Set([
|
|
12900
|
+
...DEFAULT_CONSTRAINT_SET,
|
|
12901
|
+
...dynamicConstraintSet,
|
|
12902
|
+
]);
|
|
12903
|
+
// An app constraint declared at the call site: `constraints={[MY_CONSTRAINT]}`.
|
|
12904
|
+
// Read from the raw props on every check so a constraint whose parameters
|
|
12905
|
+
// are closed over is re-created freely, and last so the constraints navi
|
|
12906
|
+
// ships are the ones reported first (see pickConstraintFailureInfo).
|
|
12907
|
+
const constraintsFromProps = controller.props.constraints;
|
|
12908
|
+
if (constraintsFromProps) {
|
|
12909
|
+
for (const constraintFromProps of constraintsFromProps) {
|
|
12910
|
+
constraintSet.add(normalizeConstraint(constraintFromProps));
|
|
12911
|
+
}
|
|
12912
|
+
}
|
|
12913
|
+
return constraintSet;
|
|
12914
|
+
};
|
|
12915
|
+
|
|
12916
|
+
/**
|
|
12917
|
+
* Lets the constraints allowed to correct the value do so, and puts what
|
|
12918
|
+
* they return in the control — ui state, bound signal, the field itself.
|
|
12919
|
+
*
|
|
12920
|
+
* Called at the moments a value is COMMITTED: the field is left, an action
|
|
12921
|
+
* is about to read it. Never while it is being typed into, where removing
|
|
12922
|
+
* the trailing space would eat the one the person is about to follow with a
|
|
12923
|
+
* word.
|
|
12924
|
+
*
|
|
12925
|
+
* Returns whether the value moved, so a caller outside the validity pass can
|
|
12926
|
+
* re-read what the control is now worth.
|
|
12927
|
+
*/
|
|
12928
|
+
const applyAutoFix = (event) => {
|
|
12929
|
+
const proxyTargetController = findControlProxyTargetController(controller);
|
|
12930
|
+
if (proxyTargetController) {
|
|
12931
|
+
return proxyTargetController.rules.validation.applyAutoFix(event);
|
|
12932
|
+
}
|
|
12933
|
+
// A value nobody can edit is not navi's to rewrite: it is the app's, and
|
|
12934
|
+
// correcting it would change what gets sent behind the app's back.
|
|
12935
|
+
const controlHostProps = controller.controlHostProps;
|
|
12936
|
+
if (
|
|
12937
|
+
controlHostProps.disabled ||
|
|
12938
|
+
controlHostProps.readOnly ||
|
|
12939
|
+
controlHostProps["aria-readonly"] === "true"
|
|
12940
|
+
) {
|
|
12941
|
+
return false;
|
|
12942
|
+
}
|
|
12943
|
+
let fixed = false;
|
|
12944
|
+
for (const constraint of getConstraintSet()) {
|
|
12945
|
+
if (!constraint.autoFix) {
|
|
12946
|
+
continue;
|
|
12947
|
+
}
|
|
12948
|
+
const fixedValue = constraint.autoFix(controller);
|
|
12949
|
+
if (fixedValue === null || fixedValue === undefined) {
|
|
12950
|
+
continue;
|
|
12951
|
+
}
|
|
12952
|
+
if (compareTwoJsValues(fixedValue, controller.uiState)) {
|
|
12953
|
+
continue;
|
|
12954
|
+
}
|
|
12955
|
+
const autoFixEvent = new CustomEvent("auto_fix", {
|
|
12956
|
+
detail: { constraint: constraint.name },
|
|
12957
|
+
});
|
|
12958
|
+
chainEvent(autoFixEvent, event);
|
|
12959
|
+
controller.setUIState(fixedValue, autoFixEvent);
|
|
12960
|
+
fixed = true;
|
|
12961
|
+
}
|
|
12962
|
+
return fixed;
|
|
12963
|
+
};
|
|
12964
|
+
controlValidity.applyAutoFix = applyAutoFix;
|
|
12965
|
+
|
|
12855
12966
|
const checkValidity = ({
|
|
12856
12967
|
event,
|
|
12857
12968
|
requester = controller.ref.current,
|
|
@@ -12892,21 +13003,17 @@ const createControlValidation = (
|
|
|
12892
13003
|
}
|
|
12893
13004
|
}
|
|
12894
13005
|
|
|
12895
|
-
|
|
12896
|
-
|
|
12897
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
|
|
12902
|
-
// are closed over is re-created freely, and last so the constraints navi
|
|
12903
|
-
// ships are the ones reported first (see pickConstraintFailureInfo).
|
|
12904
|
-
const constraintsFromProps = controller.props.constraints;
|
|
12905
|
-
if (constraintsFromProps) {
|
|
12906
|
-
for (const constraintFromProps of constraintsFromProps) {
|
|
12907
|
-
constraintSet.add(normalizeConstraint(constraintFromProps));
|
|
12908
|
-
}
|
|
13006
|
+
// The value is about to be read and sent: whoever may correct it gets the
|
|
13007
|
+
// last word before the constraints judge it, so a field is never refused
|
|
13008
|
+
// for something navi knows how to put right. Unless the request comes from
|
|
13009
|
+
// typing — an action run as you type (a search, debounced or not) is not
|
|
13010
|
+
// a commit, and correcting there would eat the space before the next word.
|
|
13011
|
+
if (fromRequestAction && !findEvent(event, "input")) {
|
|
13012
|
+
applyAutoFix(event);
|
|
12909
13013
|
}
|
|
13014
|
+
|
|
13015
|
+
let newConstraintValidityState = { valid: true };
|
|
13016
|
+
const constraintSet = getConstraintSet();
|
|
12910
13017
|
const elementSig = getElementSignature(controller.ref.current);
|
|
12911
13018
|
// Not logged: every control checks its constraints on every interaction and
|
|
12912
13019
|
// almost always passes, so this line alone was most of the debug output —
|
|
@@ -25554,10 +25661,27 @@ let isUpdatingRoutesFromUrl = false;
|
|
|
25554
25661
|
* route("/legacy/:id", { redirectRoute: GAME_PAGE, redirectRouteParams: ({ id }) => ({ gameId: id }) });
|
|
25555
25662
|
* route("/:gameId/invite", { redirectRoute: HOME_PAGE, redirectRouteParams: null });
|
|
25556
25663
|
* ```
|
|
25664
|
+
* @param {string[]} [options.dropSearchParams]
|
|
25665
|
+
* Search params this route's address never keeps: read by whoever fetched
|
|
25666
|
+
* the address (a link preview crawler, a cache), never by the app. When this
|
|
25667
|
+
* route's own address arrives carrying one, the navigation goes to the same
|
|
25668
|
+
* address without it, at the door, like a redirection: no history entry, no
|
|
25669
|
+
* route action, no signal written. Every other search param stays as written.
|
|
25670
|
+
*
|
|
25671
|
+
* ```js
|
|
25672
|
+
* // ?v= makes WhatsApp fetch a new preview, the address bar never shows it
|
|
25673
|
+
* route(`/games/:gameId=${gamePageIdSignal}`, { dropSearchParams: ["v"] });
|
|
25674
|
+
* ```
|
|
25557
25675
|
*/
|
|
25558
25676
|
const route = (
|
|
25559
25677
|
pattern,
|
|
25560
|
-
{
|
|
25678
|
+
{
|
|
25679
|
+
searchParams,
|
|
25680
|
+
params,
|
|
25681
|
+
redirectRoute,
|
|
25682
|
+
redirectRouteParams,
|
|
25683
|
+
dropSearchParams,
|
|
25684
|
+
} = {},
|
|
25561
25685
|
) => {
|
|
25562
25686
|
const routePattern = createRoutePattern(pattern, { searchParams, params });
|
|
25563
25687
|
const { cleanPattern } = routePattern;
|
|
@@ -25617,6 +25741,7 @@ const route = (
|
|
|
25617
25741
|
routePattern,
|
|
25618
25742
|
redirectRoute,
|
|
25619
25743
|
redirectRouteParams,
|
|
25744
|
+
dropSearchParams,
|
|
25620
25745
|
setup: null,
|
|
25621
25746
|
updateStatus: null,
|
|
25622
25747
|
cleanup: null,
|
|
@@ -25976,6 +26101,7 @@ const route = (
|
|
|
25976
26101
|
const [publishRouteMutations, observeRouteMutations] = createPubSub();
|
|
25977
26102
|
|
|
25978
26103
|
let redirectingRouteSet = null;
|
|
26104
|
+
let droppingRouteSet = null;
|
|
25979
26105
|
/**
|
|
25980
26106
|
* Where does this url really lead?
|
|
25981
26107
|
*
|
|
@@ -25992,14 +26118,21 @@ let redirectingRouteSet = null;
|
|
|
25992
26118
|
* @returns {string|null} The url to go to instead, or null.
|
|
25993
26119
|
*/
|
|
25994
26120
|
const resolveRouteRedirection = (url) => {
|
|
25995
|
-
if (
|
|
26121
|
+
if (
|
|
26122
|
+
!redirectingRouteSet ||
|
|
26123
|
+
(redirectingRouteSet.size === 0 && droppingRouteSet.size === 0)
|
|
26124
|
+
) {
|
|
25996
26125
|
return null;
|
|
25997
26126
|
}
|
|
25998
26127
|
let urlToResolve = url;
|
|
25999
26128
|
let redirectionUrl = null;
|
|
26000
26129
|
const urlChain = [url];
|
|
26001
26130
|
while (true) {
|
|
26002
|
-
|
|
26131
|
+
// A redirection first: the address it lands on may carry a param its own
|
|
26132
|
+
// route drops, and the next turn removes it there.
|
|
26133
|
+
const nextUrl =
|
|
26134
|
+
resolveRedirectionOnce(urlToResolve) ||
|
|
26135
|
+
dropSearchParamsOnce(urlToResolve);
|
|
26003
26136
|
if (!nextUrl || nextUrl === urlToResolve) {
|
|
26004
26137
|
break;
|
|
26005
26138
|
}
|
|
@@ -26040,6 +26173,35 @@ const resolveRedirectionOnce = (url) => {
|
|
|
26040
26173
|
}
|
|
26041
26174
|
return buildRedirectionUrl(redirectingRoute, redirectingRouteParams);
|
|
26042
26175
|
};
|
|
26176
|
+
// The query is edited as written rather than rebuilt through URLSearchParams,
|
|
26177
|
+
// which would rewrite every param kept ("?weather" into "?weather=", commas
|
|
26178
|
+
// encoded — see extractSearchParams).
|
|
26179
|
+
const dropSearchParamsOnce = (url) => {
|
|
26180
|
+
const urlObject = new URL(url);
|
|
26181
|
+
if (!urlObject.search) {
|
|
26182
|
+
return null;
|
|
26183
|
+
}
|
|
26184
|
+
for (const route of droppingRouteSet) {
|
|
26185
|
+
const { routePattern, dropSearchParams } = getRoutePrivateProperties(route);
|
|
26186
|
+
// exact, for the same reason as a redirection: the addresses below a
|
|
26187
|
+
// trailing slash belong to the routes declared for them
|
|
26188
|
+
if (!routePattern.applyOn(url, { exact: true })) {
|
|
26189
|
+
continue;
|
|
26190
|
+
}
|
|
26191
|
+
const pairs = urlObject.search.slice(1).split("&");
|
|
26192
|
+
const pairsKept = pairs.filter((pair) => {
|
|
26193
|
+
const eqIndex = pair.indexOf("=");
|
|
26194
|
+
const key = eqIndex > -1 ? pair.slice(0, eqIndex) : pair;
|
|
26195
|
+
return !dropSearchParams.includes(decodeURIComponent(key));
|
|
26196
|
+
});
|
|
26197
|
+
if (pairsKept.length === pairs.length) {
|
|
26198
|
+
continue;
|
|
26199
|
+
}
|
|
26200
|
+
urlObject.search = pairsKept.length ? `?${pairsKept.join("&")}` : "";
|
|
26201
|
+
return urlObject.href;
|
|
26202
|
+
}
|
|
26203
|
+
return null;
|
|
26204
|
+
};
|
|
26043
26205
|
const buildRedirectionUrl = (route, urlParams) => {
|
|
26044
26206
|
const { redirectRoute, redirectRouteParams } =
|
|
26045
26207
|
getRoutePrivateProperties(route);
|
|
@@ -26143,8 +26305,13 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
26143
26305
|
// Checked here rather than at declaration: a route may redirect to one
|
|
26144
26306
|
// declared after it, and reading the target then would forbid that order.
|
|
26145
26307
|
redirectingRouteSet = new Set();
|
|
26308
|
+
droppingRouteSet = new Set();
|
|
26146
26309
|
for (const route of routeSet) {
|
|
26147
|
-
const { redirectRoute } =
|
|
26310
|
+
const { redirectRoute, dropSearchParams } =
|
|
26311
|
+
getRoutePrivateProperties(route);
|
|
26312
|
+
if (dropSearchParams && dropSearchParams.length) {
|
|
26313
|
+
droppingRouteSet.add(route);
|
|
26314
|
+
}
|
|
26148
26315
|
if (!redirectRoute) {
|
|
26149
26316
|
continue;
|
|
26150
26317
|
}
|
|
@@ -26430,6 +26597,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
26430
26597
|
}
|
|
26431
26598
|
routeSet.clear();
|
|
26432
26599
|
redirectingRouteSet = null;
|
|
26600
|
+
droppingRouteSet = null;
|
|
26433
26601
|
setupRoutesCalled = false;
|
|
26434
26602
|
activeRouteSet = null;
|
|
26435
26603
|
};
|
|
@@ -36907,7 +37075,8 @@ const useUIStateController = (
|
|
|
36907
37075
|
}
|
|
36908
37076
|
if (
|
|
36909
37077
|
e.type === "facade_propagate_up" ||
|
|
36910
|
-
e.type === "cancel_rollback"
|
|
37078
|
+
e.type === "cancel_rollback" ||
|
|
37079
|
+
e.type === "auto_fix"
|
|
36911
37080
|
) {
|
|
36912
37081
|
// Exception: when the facade propagates a child state change up to the
|
|
36913
37082
|
// real picker input, also notify the parent group (e.g. Form) so it
|
|
@@ -36917,6 +37086,9 @@ const useUIStateController = (
|
|
|
36917
37086
|
// A cancel takes the same road back: the Form was told what the
|
|
36918
37087
|
// popup was picking, so it has to be told the picker went back to
|
|
36919
37088
|
// where it opened, or it sends a value the user said no to.
|
|
37089
|
+
// A correction is the same story once more: the Form sends what
|
|
37090
|
+
// its fields add up to, and a field that just put its own value
|
|
37091
|
+
// right has to be counted for the corrected one.
|
|
36920
37092
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
36921
37093
|
stateChanged: true,
|
|
36922
37094
|
});
|
|
@@ -38758,6 +38930,11 @@ const INTERNAL_EVENT_SET = new Set([
|
|
|
38758
38930
|
// notification below still happen, exactly as they did on the way in (see
|
|
38759
38931
|
// picker_custom.jsx's onClose).
|
|
38760
38932
|
"cancel_rollback",
|
|
38933
|
+
// A constraint allowed to correct the value put it right as the value was
|
|
38934
|
+
// committed (see applyAutoFix). Nobody pressed anything, so no command and
|
|
38935
|
+
// no action of the control's own — but what it holds really did move, so
|
|
38936
|
+
// uiAction, the bound signal and the parent notification below all happen.
|
|
38937
|
+
"auto_fix",
|
|
38761
38938
|
]);
|
|
38762
38939
|
const isInternalEvent = (e) => {
|
|
38763
38940
|
return INTERNAL_EVENT_SET.has(e.type);
|
|
@@ -39634,7 +39811,9 @@ const useControlProps = (props, {
|
|
|
39634
39811
|
requester: control
|
|
39635
39812
|
});
|
|
39636
39813
|
if (dispatched) {
|
|
39637
|
-
|
|
39814
|
+
// Read again: the gate may have corrected the value (see applyAutoFix),
|
|
39815
|
+
// and what the next request is compared with is what went out.
|
|
39816
|
+
lastActionValueRef.current = readControlValue(control);
|
|
39638
39817
|
}
|
|
39639
39818
|
return dispatched;
|
|
39640
39819
|
};
|
|
@@ -39781,6 +39960,24 @@ const useControlProps = (props, {
|
|
|
39781
39960
|
syncDomState(readControlValue(el), e);
|
|
39782
39961
|
};
|
|
39783
39962
|
}
|
|
39963
|
+
// Leaving the field is one of the two moments a value is committed — the
|
|
39964
|
+
// other being an action about to read it (see applyAutoFix). A constraint
|
|
39965
|
+
// allowed to correct the value puts it right here, so what the field
|
|
39966
|
+
// shows, what the counter counts and what a submit would send are one
|
|
39967
|
+
// thing well before the submit.
|
|
39968
|
+
if (controlType === "input") {
|
|
39969
|
+
const onBlurFromProps = controlHostProps.onBlur;
|
|
39970
|
+
controlHostProps.onBlur = e => {
|
|
39971
|
+
onBlurFromProps?.(e);
|
|
39972
|
+
const validation = uiStateController.rules.validation;
|
|
39973
|
+
if (validation.applyAutoFix(e)) {
|
|
39974
|
+
// The value moved without anyone typing: what the constraints had to
|
|
39975
|
+
// say about the old one is out of date, and so is what the controls
|
|
39976
|
+
// above read from this one.
|
|
39977
|
+
validation.syncValidity(e);
|
|
39978
|
+
}
|
|
39979
|
+
};
|
|
39980
|
+
}
|
|
39784
39981
|
}
|
|
39785
39982
|
const uiState = uiStateController.uiStateSignal.peek();
|
|
39786
39983
|
const domProps = toDomProps(uiState);
|
|
@@ -79071,8 +79268,10 @@ const css$l = /* css */`.navi_time_range_label {
|
|
|
79071
79268
|
}
|
|
79072
79269
|
`;
|
|
79073
79270
|
const HOUR_COUNT = 24;
|
|
79271
|
+
const END_OF_DAY_HOUR = 24;
|
|
79074
79272
|
const MINUTES_PER_HOUR = 60;
|
|
79075
79273
|
const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
|
|
79274
|
+
const END_OF_DAY = END_OF_DAY_HOUR * MINUTES_PER_HOUR;
|
|
79076
79275
|
|
|
79077
79276
|
/**
|
|
79078
79277
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
@@ -79092,16 +79291,21 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
|
|
|
79092
79291
|
* }>}
|
|
79093
79292
|
* @param {string} [value] The time shown, as "HH:MM".
|
|
79094
79293
|
* @param {number} [minuteStep=1] How many minutes apart the values on the
|
|
79095
|
-
* minute wheel are — 15 for quarters of an hour.
|
|
79294
|
+
* minute wheel are — 15 for quarters of an hour. At 60 the time is a whole
|
|
79295
|
+
* hour: there is no minute wheel at all, the value stays "HH:MM" ("08:00"),
|
|
79296
|
+
* and a value arriving with minutes is shown on its hour.
|
|
79096
79297
|
* @param {{min?: number, max?: number}|number[]} [hours] Which hours the wheel
|
|
79097
79298
|
* offers: `{ min: 7, max: 21 }` for a day that starts and ends somewhere, or
|
|
79098
|
-
* the list itself.
|
|
79099
|
-
* in the way.
|
|
79299
|
+
* the list itself. 0 to 23 by default. Rows nobody will ever land on are rows
|
|
79300
|
+
* in the way. 24 is the end of the day ("24:00", midnight at the end of a
|
|
79301
|
+
* span) and has no minutes: turned onto it, the minute wheel goes back to 0.
|
|
79100
79302
|
* @param {boolean} [loop=true] The wheels go round: 23h then 0h, 59 minutes
|
|
79101
79303
|
* then 0. What a clock does. Say `loop={false}` for two ends one cannot turn
|
|
79102
79304
|
* past.
|
|
79103
79305
|
* @param {import("ignore:preact").ComponentChildren} [separator] What is written
|
|
79104
|
-
* between the hours and the minutes. "h" in French, ":" elsewhere.
|
|
79306
|
+
* between the hours and the minutes. "h" in French, ":" elsewhere. In a time
|
|
79307
|
+
* of whole hours it follows each hour on its row instead ("8h"), and nothing
|
|
79308
|
+
* is written by default outside French.
|
|
79105
79309
|
* @param {string} [placeholder] What the wheels show while the time holds
|
|
79106
79310
|
* nothing, as "HH:MM". Wheels have no blank row to land on, so their
|
|
79107
79311
|
* placeholder is a position rather than a grey word — shown, but not an
|
|
@@ -79116,13 +79320,20 @@ const TimeWheel = ({
|
|
|
79116
79320
|
hours,
|
|
79117
79321
|
loop = true,
|
|
79118
79322
|
placeholder,
|
|
79119
|
-
separator
|
|
79323
|
+
separator,
|
|
79120
79324
|
hourLabel = naviI18n("time.hour_label"),
|
|
79121
79325
|
minuteLabel = naviI18n("time.minute_label"),
|
|
79122
79326
|
size,
|
|
79123
79327
|
wheelProps,
|
|
79328
|
+
onnavi_wheel_settle,
|
|
79124
79329
|
...rest
|
|
79125
79330
|
}) => {
|
|
79331
|
+
const wholeHours = minuteStep >= MINUTES_PER_HOUR;
|
|
79332
|
+
if (separator === undefined) {
|
|
79333
|
+
separator = naviI18n(wholeHours ? "time.hour_suffix" : "time.hour_separator");
|
|
79334
|
+
}
|
|
79335
|
+
const hourWheelRef = useRef(null);
|
|
79336
|
+
const minuteWheelRef = useRef(null);
|
|
79126
79337
|
const minutes = useMemo(() => {
|
|
79127
79338
|
const minuteList = [];
|
|
79128
79339
|
let minute = 0;
|
|
@@ -79136,13 +79347,42 @@ const TimeWheel = ({
|
|
|
79136
79347
|
const {
|
|
79137
79348
|
aggregateChildStates,
|
|
79138
79349
|
distributeChildUIState
|
|
79139
|
-
} = useAnswered(placeholder, rest, aggregateTime, distributeTime);
|
|
79350
|
+
} = useAnswered(placeholder, rest, aggregateTime, (groupState, child) => distributeTime(groupState, child, minuteStep));
|
|
79140
79351
|
const placeholderParts = parseTimeParts(placeholder);
|
|
79352
|
+
|
|
79353
|
+
// The end of the day has no minutes. The time already reads "24:00" whatever
|
|
79354
|
+
// the minute wheel shows (see aggregateTime); on settle the wheel is brought
|
|
79355
|
+
// back to 0 so what is drawn is what is held.
|
|
79356
|
+
const endOfDayHasNoMinutes = e => {
|
|
79357
|
+
const hourEl = hourWheelRef.current;
|
|
79358
|
+
const minuteEl = minuteWheelRef.current;
|
|
79359
|
+
if (!hourEl || !minuteEl) {
|
|
79360
|
+
return;
|
|
79361
|
+
}
|
|
79362
|
+
if (getUIStateFromElement(hourEl) !== END_OF_DAY_HOUR) {
|
|
79363
|
+
return;
|
|
79364
|
+
}
|
|
79365
|
+
if (getUIStateFromElement(minuteEl) === 0) {
|
|
79366
|
+
return;
|
|
79367
|
+
}
|
|
79368
|
+
dispatchRequestSetUIState(minuteEl, 0, {
|
|
79369
|
+
event: e
|
|
79370
|
+
});
|
|
79371
|
+
};
|
|
79141
79372
|
return jsxs(WheelGroup, {
|
|
79142
79373
|
aggregateChildStates: aggregateChildStates,
|
|
79143
79374
|
distributeChildUIState: distributeChildUIState,
|
|
79375
|
+
onnavi_wheel_settle: e => {
|
|
79376
|
+
if (!wholeHours) {
|
|
79377
|
+
endOfDayHasNoMinutes(e);
|
|
79378
|
+
}
|
|
79379
|
+
if (onnavi_wheel_settle) {
|
|
79380
|
+
onnavi_wheel_settle(e);
|
|
79381
|
+
}
|
|
79382
|
+
},
|
|
79144
79383
|
...rest,
|
|
79145
79384
|
children: [jsx(Wheel, {
|
|
79385
|
+
ref: hourWheelRef,
|
|
79146
79386
|
name: "hour",
|
|
79147
79387
|
type: "integer",
|
|
79148
79388
|
bounded: !loop,
|
|
@@ -79153,24 +79393,29 @@ const TimeWheel = ({
|
|
|
79153
79393
|
children: hourList.map(hour => jsx(Wheel.Item, {
|
|
79154
79394
|
value: hour,
|
|
79155
79395
|
paddingX: "s",
|
|
79156
|
-
children:
|
|
79396
|
+
children: wholeHours ? jsxs(Fragment, {
|
|
79397
|
+
children: [hour, separator]
|
|
79398
|
+
}) : padTwo(hour)
|
|
79157
79399
|
}, hour))
|
|
79158
|
-
}),
|
|
79159
|
-
|
|
79160
|
-
|
|
79161
|
-
|
|
79162
|
-
|
|
79163
|
-
|
|
79164
|
-
|
|
79165
|
-
|
|
79166
|
-
|
|
79167
|
-
|
|
79168
|
-
|
|
79169
|
-
|
|
79170
|
-
|
|
79171
|
-
|
|
79172
|
-
|
|
79173
|
-
|
|
79400
|
+
}), wholeHours ? null : jsxs(Fragment, {
|
|
79401
|
+
children: [jsx(WheelGroup.Separator, {
|
|
79402
|
+
size: size,
|
|
79403
|
+
children: separator
|
|
79404
|
+
}), jsx(Wheel, {
|
|
79405
|
+
ref: minuteWheelRef,
|
|
79406
|
+
name: "minute",
|
|
79407
|
+
type: "integer",
|
|
79408
|
+
bounded: !loop,
|
|
79409
|
+
size: size,
|
|
79410
|
+
"aria-label": minuteLabel,
|
|
79411
|
+
defaultValue: placeholderParts ? floorToStep(placeholderParts.minute, minuteStep) : undefined,
|
|
79412
|
+
...wheelProps,
|
|
79413
|
+
children: minutes.map(minute => jsx(Wheel.Item, {
|
|
79414
|
+
value: minute,
|
|
79415
|
+
paddingX: "s",
|
|
79416
|
+
children: padTwo(minute)
|
|
79417
|
+
}, minute))
|
|
79418
|
+
})]
|
|
79174
79419
|
})]
|
|
79175
79420
|
});
|
|
79176
79421
|
};
|
|
@@ -79204,11 +79449,12 @@ const TimeWheel = ({
|
|
|
79204
79449
|
* prepositions belong there; the group is still a row, and takes `flexWrap`
|
|
79205
79450
|
* for screens too narrow to hold both columns.
|
|
79206
79451
|
* @param {number} [minuteStep=1] How many minutes apart the values on both
|
|
79207
|
-
* minute wheels are.
|
|
79452
|
+
* minute wheels are. 60 for a span of whole hours ("de 8h à 12h").
|
|
79208
79453
|
* @param {{min?: number, max?: number}|number[]} [hours] Which hours both
|
|
79209
|
-
* wheels offer — see `TimeWheel`.
|
|
79454
|
+
* wheels offer — see `TimeWheel`. A 24 is offered to the end alone: a span
|
|
79455
|
+
* can run until midnight ("24:00"), it cannot start there.
|
|
79210
79456
|
* @param {number} [minDuration=0] How long the span must last at least, in
|
|
79211
|
-
* minutes. Zero by default: a span of no length is a span all the same, only
|
|
79457
|
+
* 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
|
|
79212
79458
|
* one that goes backwards is not. It is what the bounds keep between them as
|
|
79213
79459
|
* they turn — turn the start into the end and the end moves along, keeping
|
|
79214
79460
|
* that much room.
|
|
@@ -79241,6 +79487,12 @@ const TimeRangeWheel = ({
|
|
|
79241
79487
|
const startId = useId();
|
|
79242
79488
|
const startRef = useRef(null);
|
|
79243
79489
|
const endRef = useRef(null);
|
|
79490
|
+
const endHourList = useMemo(() => resolveHourList(hours), [hours ? hours.min : undefined, hours ? hours.max : undefined, hours]);
|
|
79491
|
+
const startHourList = useMemo(() => endHourList.filter(hour => hour !== END_OF_DAY_HOUR), [endHourList]);
|
|
79492
|
+
const step = minuteStep >= MINUTES_PER_HOUR ? MINUTES_PER_HOUR : minuteStep;
|
|
79493
|
+
const minGap = ceilToStep(minDuration, step);
|
|
79494
|
+
// The latest the end can be pushed to: the last time its wheels can show.
|
|
79495
|
+
const lastEnd = endHourList.includes(END_OF_DAY_HOUR) ? END_OF_DAY : floorToStep(LAST_MINUTE_OF_DAY, step);
|
|
79244
79496
|
// One turn settles the whole span: a start somebody chose makes the end an
|
|
79245
79497
|
// answer too, left where the placeholder put it.
|
|
79246
79498
|
const {
|
|
@@ -79272,18 +79524,18 @@ const TimeRangeWheel = ({
|
|
|
79272
79524
|
return;
|
|
79273
79525
|
}
|
|
79274
79526
|
const duration = movedSide === "start" ? otherMinutes - movedMinutes : movedMinutes - otherMinutes;
|
|
79275
|
-
if (duration >=
|
|
79527
|
+
if (duration >= minGap) {
|
|
79276
79528
|
return;
|
|
79277
79529
|
}
|
|
79278
|
-
let pushedMinutes = movedSide === "start" ? movedMinutes +
|
|
79530
|
+
let pushedMinutes = movedSide === "start" ? movedMinutes + minGap : movedMinutes - minGap;
|
|
79279
79531
|
// The day has ends the wheels do not: pushed past midnight, the other bound
|
|
79280
79532
|
// would come back round on the wrong side of the one that pushed it. It
|
|
79281
79533
|
// stops at the edge instead, and the span that no longer fits is what the
|
|
79282
79534
|
// send-time constraint is there to say (see time_range_constraint.js).
|
|
79283
79535
|
if (pushedMinutes < 0) {
|
|
79284
79536
|
pushedMinutes = 0;
|
|
79285
|
-
} else if (pushedMinutes >
|
|
79286
|
-
pushedMinutes =
|
|
79537
|
+
} else if (pushedMinutes > lastEnd) {
|
|
79538
|
+
pushedMinutes = lastEnd;
|
|
79287
79539
|
}
|
|
79288
79540
|
dispatchRequestSetUIState(otherEl, timeFromMinutes(pushedMinutes), {
|
|
79289
79541
|
event: e
|
|
@@ -79311,7 +79563,7 @@ const TimeRangeWheel = ({
|
|
|
79311
79563
|
ref: startRef,
|
|
79312
79564
|
name: "start",
|
|
79313
79565
|
minuteStep: minuteStep,
|
|
79314
|
-
hours:
|
|
79566
|
+
hours: startHourList,
|
|
79315
79567
|
loop: loop,
|
|
79316
79568
|
size: size,
|
|
79317
79569
|
placeholder: placeholder ? placeholder.start : undefined
|
|
@@ -79336,7 +79588,7 @@ const TimeRangeWheel = ({
|
|
|
79336
79588
|
ref: endRef,
|
|
79337
79589
|
name: "end",
|
|
79338
79590
|
minuteStep: minuteStep,
|
|
79339
|
-
hours:
|
|
79591
|
+
hours: endHourList,
|
|
79340
79592
|
loop: loop,
|
|
79341
79593
|
size: size,
|
|
79342
79594
|
placeholder: placeholder ? placeholder.end : undefined,
|
|
@@ -79348,7 +79600,7 @@ const TimeRangeWheel = ({
|
|
|
79348
79600
|
// the time one would have to move is (see time_range_constraint.js).
|
|
79349
79601
|
,
|
|
79350
79602
|
"data-time-after": startId,
|
|
79351
|
-
"data-time-min-duration":
|
|
79603
|
+
"data-time-min-duration": minGap,
|
|
79352
79604
|
...timeProps,
|
|
79353
79605
|
...endTimeProps
|
|
79354
79606
|
})
|
|
@@ -79510,10 +79762,11 @@ const distributeSpan = (groupState, childUIStateController) => {
|
|
|
79510
79762
|
return groupState[childUIStateController.name];
|
|
79511
79763
|
};
|
|
79512
79764
|
|
|
79513
|
-
// The
|
|
79765
|
+
// The wheels as one value, "HH:MM". A time of whole hours has no minute wheel
|
|
79766
|
+
// and is on the hour; so is the end of the day, whatever the minute wheel says.
|
|
79514
79767
|
const aggregateTime = childUIStateControllers => {
|
|
79515
79768
|
let hour = "";
|
|
79516
|
-
let minute =
|
|
79769
|
+
let minute = 0;
|
|
79517
79770
|
for (const child of childUIStateControllers) {
|
|
79518
79771
|
if (child.name === "hour") {
|
|
79519
79772
|
hour = child.uiState ?? "";
|
|
@@ -79522,18 +79775,27 @@ const aggregateTime = childUIStateControllers => {
|
|
|
79522
79775
|
minute = child.uiState ?? "";
|
|
79523
79776
|
}
|
|
79524
79777
|
}
|
|
79778
|
+
if (hour === END_OF_DAY_HOUR) {
|
|
79779
|
+
return formatTimeParts(hour, 0);
|
|
79780
|
+
}
|
|
79525
79781
|
return formatTimeParts(hour, minute);
|
|
79526
79782
|
};
|
|
79527
79783
|
|
|
79528
79784
|
// The way back: what the group is set to (a value given to it, a form being
|
|
79529
|
-
// reset, the other bound pushing it) lands on the wheel it belongs to.
|
|
79530
|
-
|
|
79785
|
+
// reset, the other bound pushing it) lands on the wheel it belongs to. Minutes
|
|
79786
|
+
// the wheel does not offer land on the one before.
|
|
79787
|
+
const distributeTime = (groupState, childUIStateController, minuteStep) => {
|
|
79531
79788
|
const parts = parseTimeParts(groupState);
|
|
79532
79789
|
if (!parts) {
|
|
79533
79790
|
return undefined;
|
|
79534
79791
|
}
|
|
79792
|
+
if (childUIStateController.name === "minute") {
|
|
79793
|
+
return floorToStep(parts.minute, minuteStep);
|
|
79794
|
+
}
|
|
79535
79795
|
return parts[childUIStateController.name];
|
|
79536
79796
|
};
|
|
79797
|
+
const floorToStep = (minutes, step) => Math.floor(minutes / step) * step;
|
|
79798
|
+
const ceilToStep = (minutes, step) => Math.ceil(minutes / step) * step;
|
|
79537
79799
|
|
|
79538
79800
|
const TableSelectionContext = createContext();
|
|
79539
79801
|
const useTableSelectionContextValue = (
|