@jsenv/navi 0.29.32 → 0.29.33
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/README.md +2 -0
- package/dist/jsenv_navi.js +1090 -281
- package/dist/jsenv_navi.js.map +78 -36
- package/dist/jsenv_navi_side_effects.js +1 -0
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +41 -6
- package/docs/control_group.md +4 -1
- package/docs/interactions.md +131 -16
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -1459,6 +1459,31 @@ naviI18n.addAll({
|
|
|
1459
1459
|
},
|
|
1460
1460
|
});
|
|
1461
1461
|
|
|
1462
|
+
// Time spin messages — what a clock writes between an hour and its minutes,
|
|
1463
|
+
// and how the two ends of a span are named.
|
|
1464
|
+
naviI18n.addAll({
|
|
1465
|
+
"time.hour_separator": {
|
|
1466
|
+
en: ":",
|
|
1467
|
+
fr: "h",
|
|
1468
|
+
},
|
|
1469
|
+
"time.hour_label": {
|
|
1470
|
+
en: "Hours",
|
|
1471
|
+
fr: "Heures",
|
|
1472
|
+
},
|
|
1473
|
+
"time.minute_label": {
|
|
1474
|
+
en: "Minutes",
|
|
1475
|
+
fr: "Minutes",
|
|
1476
|
+
},
|
|
1477
|
+
"time_range.from": {
|
|
1478
|
+
en: "From",
|
|
1479
|
+
fr: "De",
|
|
1480
|
+
},
|
|
1481
|
+
"time_range.to": {
|
|
1482
|
+
en: "to",
|
|
1483
|
+
fr: "à",
|
|
1484
|
+
},
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1462
1487
|
// List messages — override any key to customize list messages
|
|
1463
1488
|
naviI18n.addAll({
|
|
1464
1489
|
"list.empty": {
|
|
@@ -1605,6 +1630,14 @@ naviI18n.addAll({
|
|
|
1605
1630
|
fr: "Ce champ doit être identique au précédent.",
|
|
1606
1631
|
en: "This field must match the previous one.",
|
|
1607
1632
|
},
|
|
1633
|
+
"constraint.time_after.default": {
|
|
1634
|
+
fr: "L'heure de fin ne peut pas être avant l'heure de début.",
|
|
1635
|
+
en: "The end time cannot be before the start time.",
|
|
1636
|
+
},
|
|
1637
|
+
"constraint.time_after.min_duration": {
|
|
1638
|
+
fr: "La plage doit durer au moins <strong>[duration]</strong> minutes.",
|
|
1639
|
+
en: "The span must last at least <strong>[duration]</strong> minutes.",
|
|
1640
|
+
},
|
|
1608
1641
|
"constraint.required.checkbox": {
|
|
1609
1642
|
fr: "Veuillez cocher cette case.",
|
|
1610
1643
|
en: "Please check this box.",
|
|
@@ -2258,6 +2291,9 @@ const generateSignalId = () => {
|
|
|
2258
2291
|
* @param {any} [options.default] - Static fallback value used when defaultValue is a signal and that signal's value is undefined
|
|
2259
2292
|
* @param {boolean} [options.persists=false] - Whether to persist the signal value in localStorage using the signal ID as key
|
|
2260
2293
|
* @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
|
|
2294
|
+
* @param {"string" | "number" | "boolean"} [options.itemType] - For array type: type of the array items.
|
|
2295
|
+
* Used when reading the value back from a url search param, where everything is a string:
|
|
2296
|
+
* `?level=3,4` becomes `[3, 4]` instead of `["3", "4"]`. Without it items stay strings.
|
|
2261
2297
|
* @param {number} [options.step] - For number type: step size for precision. Values will be rounded to nearest multiple of step.
|
|
2262
2298
|
* @param {Array} [options.oneOf] - Array of valid values for validation. Signal will be marked invalid if value is not in this array
|
|
2263
2299
|
* @param {boolean} [options.weak=false] - The param qualifies one visit, not the screen: it is written into a
|
|
@@ -5321,7 +5357,13 @@ const buildQueryString = (params) => {
|
|
|
5321
5357
|
|
|
5322
5358
|
// Handle array values - join with commas
|
|
5323
5359
|
if (Array.isArray(value)) {
|
|
5324
|
-
if (value.length === 0)
|
|
5360
|
+
if (value.length === 0) {
|
|
5361
|
+
// Empty array - written as "key=", the form extractSearchParams reads
|
|
5362
|
+
// back as []. Omitting the param entirely would mean "absent" which
|
|
5363
|
+
// resolves to the default value, making "nothing selected"
|
|
5364
|
+
// inexpressible for a signal whose default is non-empty.
|
|
5365
|
+
searchParamPairs.push(`${encodedKey}=`);
|
|
5366
|
+
} else {
|
|
5325
5367
|
const encodedValue = value
|
|
5326
5368
|
.map((item) => encodeURIComponent(String(item)))
|
|
5327
5369
|
.join(",");
|
|
@@ -5348,6 +5390,26 @@ const buildQueryString = (params) => {
|
|
|
5348
5390
|
return searchParamPairs.join("&");
|
|
5349
5391
|
};
|
|
5350
5392
|
|
|
5393
|
+
/**
|
|
5394
|
+
* Cast an array item read from the URL into the item type declared on the
|
|
5395
|
+
* signal (`stateSignal([], { type: "array", itemType: "number" })`).
|
|
5396
|
+
*
|
|
5397
|
+
* Without it every item comes back as a string and the value no longer equals
|
|
5398
|
+
* what was assigned, so the URL→signal sync overwrites the signal with strings.
|
|
5399
|
+
* Declared explicitly rather than guessed, so a "42" string item stays a string
|
|
5400
|
+
* unless the signal says otherwise.
|
|
5401
|
+
*/
|
|
5402
|
+
const castStringToItemType = (item, itemType) => {
|
|
5403
|
+
if (itemType === "number" || itemType === "float") {
|
|
5404
|
+
const numberValue = Number(item);
|
|
5405
|
+
return isNaN(numberValue) ? item : numberValue;
|
|
5406
|
+
}
|
|
5407
|
+
if (itemType === "boolean") {
|
|
5408
|
+
return item === "true" || item === "1" || item === "";
|
|
5409
|
+
}
|
|
5410
|
+
return item;
|
|
5411
|
+
};
|
|
5412
|
+
|
|
5351
5413
|
/**
|
|
5352
5414
|
* Extract search parameters from URL
|
|
5353
5415
|
*/
|
|
@@ -5385,6 +5447,7 @@ const extractSearchParams = (urlObj, queryConnectionMap) => {
|
|
|
5385
5447
|
|
|
5386
5448
|
const connection = queryConnectionMap.get(key);
|
|
5387
5449
|
const signalType = connection ? connection.type : null;
|
|
5450
|
+
const itemType = connection ? connection.itemType : null;
|
|
5388
5451
|
|
|
5389
5452
|
// Cast value based on signal type
|
|
5390
5453
|
if (signalType === "array") {
|
|
@@ -5399,7 +5462,8 @@ const extractSearchParams = (urlObj, queryConnectionMap) => {
|
|
|
5399
5462
|
params[key] = rawValue
|
|
5400
5463
|
.split(",")
|
|
5401
5464
|
.map((item) => decodeURIComponent(item))
|
|
5402
|
-
.filter((item) => item.trim() !== "")
|
|
5465
|
+
.filter((item) => item.trim() !== "")
|
|
5466
|
+
.map((item) => castStringToItemType(item, itemType));
|
|
5403
5467
|
}
|
|
5404
5468
|
} else if (signalType === "number" || signalType === "float") {
|
|
5405
5469
|
const decodedValue = decodeURIComponent(rawValue);
|
|
@@ -7010,7 +7074,7 @@ const getUIStateFromElement = (el, { own } = {}) => {
|
|
|
7010
7074
|
*/
|
|
7011
7075
|
const asControlHostValue = (
|
|
7012
7076
|
jsValue,
|
|
7013
|
-
{ controlType, type, inputMode },
|
|
7077
|
+
{ controlType, type, inputMode, pad },
|
|
7014
7078
|
) => {
|
|
7015
7079
|
if (controlType === "select") {
|
|
7016
7080
|
// A select holds one of its options, always a string; holding nothing is
|
|
@@ -7027,7 +7091,7 @@ const asControlHostValue = (
|
|
|
7027
7091
|
inputMode === "numeric" ||
|
|
7028
7092
|
inputMode === "decimal"
|
|
7029
7093
|
) {
|
|
7030
|
-
return asNumberString(jsValue);
|
|
7094
|
+
return asNumberString(jsValue, pad);
|
|
7031
7095
|
}
|
|
7032
7096
|
if (type === "color") {
|
|
7033
7097
|
return asColorString(jsValue);
|
|
@@ -7051,11 +7115,24 @@ const asDatetimeLocalString = (dateTimeString) => {
|
|
|
7051
7115
|
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
7052
7116
|
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
|
7053
7117
|
};
|
|
7054
|
-
|
|
7118
|
+
// `pad` is how many digits the number is WRITTEN on — an hour is held as 7 and
|
|
7119
|
+
// shown as "07". Held and shown are two things here, the way they are for a
|
|
7120
|
+
// datetime-local above: what the field says is derived from what the control
|
|
7121
|
+
// holds, and reading it back (readNumberFromInput) gives the number again.
|
|
7122
|
+
const asNumberString = (jsValue, pad) => {
|
|
7055
7123
|
if (jsValue === undefined) {
|
|
7056
7124
|
return "";
|
|
7057
7125
|
}
|
|
7058
|
-
|
|
7126
|
+
if (!pad || jsValue === "" || jsValue === null) {
|
|
7127
|
+
return jsValue;
|
|
7128
|
+
}
|
|
7129
|
+
const number = Number(jsValue);
|
|
7130
|
+
if (Number.isNaN(number)) {
|
|
7131
|
+
return jsValue;
|
|
7132
|
+
}
|
|
7133
|
+
const negative = number < 0;
|
|
7134
|
+
const digits = String(negative ? -number : number).padStart(Number(pad), "0");
|
|
7135
|
+
return negative ? `-${digits}` : digits;
|
|
7059
7136
|
};
|
|
7060
7137
|
// Browser requires a non-empty value for <input type="color">.
|
|
7061
7138
|
// When our logical value is empty we give it #000000 so it doesn't choke.
|
|
@@ -7207,11 +7284,15 @@ const getRadioSiblings = (radioUIStateController) => {
|
|
|
7207
7284
|
return siblings;
|
|
7208
7285
|
};
|
|
7209
7286
|
|
|
7210
|
-
const toDomValue = (
|
|
7287
|
+
const toDomValue = (
|
|
7288
|
+
jsValue,
|
|
7289
|
+
{ controlType, id, type, inputMode, pad },
|
|
7290
|
+
) => {
|
|
7211
7291
|
const domValue = asControlHostValue(jsValue, {
|
|
7212
7292
|
controlType,
|
|
7213
7293
|
type,
|
|
7214
7294
|
inputMode,
|
|
7295
|
+
pad,
|
|
7215
7296
|
});
|
|
7216
7297
|
if (isSerializableAsDomValue(domValue)) {
|
|
7217
7298
|
return domValue;
|
|
@@ -12243,33 +12324,67 @@ const swipeTypeOf = (axis, pulled) => {
|
|
|
12243
12324
|
};
|
|
12244
12325
|
|
|
12245
12326
|
/**
|
|
12246
|
-
* `move`, `reorder`, `toss` — one grab, and what letting go of it means.
|
|
12327
|
+
* `move`, `reorder`, `land`, `toss` — one grab, and what letting go of it means.
|
|
12247
12328
|
*
|
|
12248
|
-
* All
|
|
12329
|
+
* All four are the same gesture: the element is picked up and carried. What
|
|
12249
12330
|
* differs is the answer at the release, so one detector reads them all — it is one
|
|
12250
12331
|
* press, and something has to arbitrate it.
|
|
12251
12332
|
*
|
|
12252
12333
|
* interactions={{ reorder: moveBefore, toss: remove }}
|
|
12334
|
+
* interactions={{ land: swapPlaces }}
|
|
12253
12335
|
* interactions={{ move: remember }}
|
|
12254
12336
|
*
|
|
12255
|
-
* `reorder` and `
|
|
12256
|
-
* same task thrown far and fast is gotten rid of.
|
|
12257
|
-
*
|
|
12258
|
-
*
|
|
12337
|
+
* `toss` combines with `reorder` and with `land`: a task dragged onto another
|
|
12338
|
+
* changes places, the same task thrown far and fast is gotten rid of. The three
|
|
12339
|
+
* others do not combine with each other — an element either goes where it is put,
|
|
12340
|
+
* takes a place in a list, or comes down on a place, and no two of those answers
|
|
12341
|
+
* can both be true of one release.
|
|
12259
12342
|
*
|
|
12260
|
-
* `move` carries the element ITSELF and leaves it where it was put; the
|
|
12343
|
+
* `move` carries the element ITSELF and leaves it where it was put; the others
|
|
12261
12344
|
* carry a copy and put the original back. That is the same difference said in
|
|
12262
12345
|
* layout terms: something moved has a new place of its own, something reordered
|
|
12263
12346
|
* had its place taken by the list.
|
|
12264
12347
|
*
|
|
12348
|
+
* `reorder` VS `land`: both come down on an item, and what separates them is what
|
|
12349
|
+
* a place IS. A row of a list is a place BETWEEN two others — free by construction,
|
|
12350
|
+
* so the answer is an insertion, and putting a row back where it already was is a
|
|
12351
|
+
* no-op. A place of a board is a place of its own, which may already be taken — so
|
|
12352
|
+
* nothing is inserted, nothing is a no-op, and the answer is simply "this one came
|
|
12353
|
+
* down on that one". What that means is the application's: take the place, swap the
|
|
12354
|
+
* two, refuse.
|
|
12355
|
+
*
|
|
12356
|
+
* interactions={{ land: (event) => {
|
|
12357
|
+
* const { fromId, toId, syncCloneWithDropTarget } = event.detail;
|
|
12358
|
+
* …
|
|
12359
|
+
* }}}
|
|
12360
|
+
*
|
|
12361
|
+
* `toId` is an element and never null: a copy over nothing is a release that meant
|
|
12362
|
+
* nothing, and the interaction does not happen at all.
|
|
12363
|
+
*
|
|
12364
|
+
* `syncCloneWithDropTarget` takes an element here, which `reorder` has no use for:
|
|
12365
|
+
* a place of a board can be larger than what stands on it (a quarter of a court, a
|
|
12366
|
+
* square holding a smaller piece), and the copy has to come down where the piece
|
|
12367
|
+
* will be rather than filling the place. Left alone, it lands on the place itself.
|
|
12368
|
+
*
|
|
12369
|
+
* WHICH ELEMENTS ARE PLACES: those marked `data-droppable`, and only those.
|
|
12370
|
+
* Declaring `land` says an element can be CARRIED, which on a board is a different
|
|
12371
|
+
* thing from being somewhere one can be put: a zone receives without ever being
|
|
12372
|
+
* carried, a piece is carried without ever receiving, and both at once is a third
|
|
12373
|
+
* case (dropped on a piece, the two swap). A list has no such distinction — every
|
|
12374
|
+
* row is both, which is why `reorder` needs no marker in the markup.
|
|
12375
|
+
*
|
|
12376
|
+
* The set of places is looked for inside the carried element's PARENT, so a place
|
|
12377
|
+
* and a piece are siblings: a piece nested inside its place would see only that
|
|
12378
|
+
* one, and have nowhere else to go.
|
|
12379
|
+
*
|
|
12265
12380
|
* Nothing of the gesture is decided here. `startDragTo` owns all of it — the
|
|
12266
12381
|
* copy carried above the page while the original keeps its place in the layout, the
|
|
12267
12382
|
* drop hint, the drop targets found by intersection, the no-op drops filtered out,
|
|
12268
12383
|
* the flight of a thrown copy and its return when the answer refuses. This says
|
|
12269
12384
|
* which elements are the items, how they are named, and what a given release means.
|
|
12270
12385
|
*
|
|
12271
|
-
* WHICH ELEMENTS
|
|
12272
|
-
* items IS the set of elements that declared it — no selector to pass, nothing to
|
|
12386
|
+
* WHICH ELEMENTS, for `reorder`. Every element declaring it marks itself, so the
|
|
12387
|
+
* set of items IS the set of elements that declared it — no selector to pass, nothing to
|
|
12273
12388
|
* keep in sync with the markup, and an item that must not move simply does not
|
|
12274
12389
|
* declare it. An element that only declares `toss` marks nothing: it is not a place
|
|
12275
12390
|
* anything lands.
|
|
@@ -12308,7 +12423,7 @@ const swipeTypeOf = (axis, pulled) => {
|
|
|
12308
12423
|
*
|
|
12309
12424
|
* interactions={{ toss: remove, grab: () => navigator.vibrate?.(10) }}
|
|
12310
12425
|
*
|
|
12311
|
-
* The
|
|
12426
|
+
* The four above all answer the RELEASE, and between the press and the release
|
|
12312
12427
|
* there is one instant that counts for the hand making the gesture: the one where
|
|
12313
12428
|
* the object stops being pressed and starts being held. `grab` is that instant,
|
|
12314
12429
|
* and it is the same one whichever way the drag was entered — a finger held still,
|
|
@@ -12322,7 +12437,7 @@ const swipeTypeOf = (axis, pulled) => {
|
|
|
12322
12437
|
*
|
|
12323
12438
|
* It is told, not asked: `grab` reports, so what it returns is not waited on and
|
|
12324
12439
|
* preventing its event does not call the gesture off. And it is not an interaction
|
|
12325
|
-
* on its own — declared without one of the
|
|
12440
|
+
* on its own — declared without one of the four above there is no gesture for it
|
|
12326
12441
|
* to be the beginning of.
|
|
12327
12442
|
*
|
|
12328
12443
|
* A `longpress` needs nothing of this: it already happens at the moment the hold
|
|
@@ -12337,12 +12452,19 @@ const swipeTypeOf = (axis, pulled) => {
|
|
|
12337
12452
|
|
|
12338
12453
|
const MOVE = "move";
|
|
12339
12454
|
const REORDER = "reorder";
|
|
12455
|
+
// "drop" is taken: it is the name of the platform's own drag-and-drop event, and
|
|
12456
|
+
// an interaction is dispatched as an event of its own name — so anything listening
|
|
12457
|
+
// for a file being dropped on the page would get this one and read it as such.
|
|
12458
|
+
const LAND = "land";
|
|
12340
12459
|
const TOSS = "toss";
|
|
12341
12460
|
// The moment the press stops being a press and becomes a hold on the object.
|
|
12342
12461
|
const GRAB = "grab";
|
|
12343
12462
|
|
|
12344
12463
|
// What makes an element a place something can land, written by the detector itself.
|
|
12345
12464
|
const REORDERABLE_ATTRIBUTE = "data-reorderable";
|
|
12465
|
+
// The same for `land`, except the markup is what writes it: a place of a board is
|
|
12466
|
+
// not the same thing as a piece of it (see the top of this file).
|
|
12467
|
+
const DROPPABLE_ATTRIBUTE = "data-droppable";
|
|
12346
12468
|
// Which axes the drag walks: "x", "y" or "xy". Its default is not the same for
|
|
12347
12469
|
// every outcome — a list runs one way, and something being put somewhere goes
|
|
12348
12470
|
// wherever it is put.
|
|
@@ -12356,12 +12478,17 @@ const TOSS_SPEED_ATTRIBUTE = "data-toss-speed";
|
|
|
12356
12478
|
defineInteractionDetector({
|
|
12357
12479
|
name: "drag",
|
|
12358
12480
|
claims: (type) =>
|
|
12359
|
-
type === MOVE ||
|
|
12481
|
+
type === MOVE ||
|
|
12482
|
+
type === REORDER ||
|
|
12483
|
+
type === LAND ||
|
|
12484
|
+
type === TOSS ||
|
|
12485
|
+
type === GRAB,
|
|
12360
12486
|
setup: (element, trigger, { types, readConfig }) => {
|
|
12361
12487
|
const canMove = types.includes(MOVE);
|
|
12362
12488
|
const canReorder = types.includes(REORDER);
|
|
12489
|
+
const canLand = types.includes(LAND);
|
|
12363
12490
|
const canToss = types.includes(TOSS);
|
|
12364
|
-
if (!canMove && !canReorder && !canToss) {
|
|
12491
|
+
if (!canMove && !canReorder && !canLand && !canToss) {
|
|
12365
12492
|
return undefined;
|
|
12366
12493
|
}
|
|
12367
12494
|
const tellsWhenGrabbed = types.includes(GRAB);
|
|
@@ -12371,9 +12498,9 @@ defineInteractionDetector({
|
|
|
12371
12498
|
const axes =
|
|
12372
12499
|
axisHolder?.getAttribute(AXIS_ATTRIBUTE) ||
|
|
12373
12500
|
// A list runs one way, and reordering walks it. Anything else goes wherever
|
|
12374
|
-
// the hand takes it: a
|
|
12375
|
-
// a throw goes where it was thrown.
|
|
12376
|
-
(canReorder && !canToss ? "y" : "xy");
|
|
12501
|
+
// the hand takes it: a board has places all around, a thing put somewhere has
|
|
12502
|
+
// two axes to be put along, and a throw goes where it was thrown.
|
|
12503
|
+
(canReorder && !canLand && !canToss ? "y" : "xy");
|
|
12377
12504
|
|
|
12378
12505
|
if (canReorder) {
|
|
12379
12506
|
element.setAttribute(REORDERABLE_ATTRIBUTE, "");
|
|
@@ -12394,7 +12521,11 @@ defineInteractionDetector({
|
|
|
12394
12521
|
startDragTo(pointerDownEvent, effects, {
|
|
12395
12522
|
draggedElement: element,
|
|
12396
12523
|
// Nothing to land on when nothing reorders.
|
|
12397
|
-
itemSelector:
|
|
12524
|
+
itemSelector: canLand
|
|
12525
|
+
? `[${DROPPABLE_ATTRIBUTE}]`
|
|
12526
|
+
: canReorder
|
|
12527
|
+
? `[${REORDERABLE_ATTRIBUTE}]`
|
|
12528
|
+
: undefined,
|
|
12398
12529
|
getItemId: (itemElement) => itemElement.id,
|
|
12399
12530
|
direction: { x: axes.includes("x"), y: axes.includes("y") },
|
|
12400
12531
|
// Where it may go, said in the DOM. A thing that is put somewhere stays
|
|
@@ -12434,6 +12565,12 @@ defineInteractionDetector({
|
|
|
12434
12565
|
toId,
|
|
12435
12566
|
syncCloneWithDropTarget,
|
|
12436
12567
|
}),
|
|
12568
|
+
onLand: (fromId, toId, syncCloneWithDropTarget) =>
|
|
12569
|
+
trigger(LAND, pointerDownEvent, {
|
|
12570
|
+
fromId,
|
|
12571
|
+
toId,
|
|
12572
|
+
syncCloneWithDropTarget,
|
|
12573
|
+
}),
|
|
12437
12574
|
onToss: ({ gestureInfo }) =>
|
|
12438
12575
|
trigger(TOSS, pointerDownEvent, {
|
|
12439
12576
|
id: element.id,
|
|
@@ -14716,6 +14853,62 @@ const SINGLE_SPACE_CONSTRAINT = {
|
|
|
14716
14853
|
};
|
|
14717
14854
|
CONSTRAINT_ATTRIBUTE_SET.add("data-single-space");
|
|
14718
14855
|
|
|
14856
|
+
// A time that must not land before another one: the field says which one it
|
|
14857
|
+
// comes after (data-time-after, the id of the control holding it) and how much
|
|
14858
|
+
// room there must be between the two at least (data-time-min-duration, in
|
|
14859
|
+
// minutes — zero by default, so a span of no length is a span all the same).
|
|
14860
|
+
// Carried by the LATER of the two: it is the one that would have to move, so it
|
|
14861
|
+
// is the one the answer is about.
|
|
14862
|
+
const TIME_RANGE_CONSTRAINT = {
|
|
14863
|
+
name: "time_after",
|
|
14864
|
+
messageAttribute: "data-time-after-message",
|
|
14865
|
+
check: (field) => {
|
|
14866
|
+
const after = field.controlHostProps["data-time-after"];
|
|
14867
|
+
if (after === undefined) {
|
|
14868
|
+
return null;
|
|
14869
|
+
}
|
|
14870
|
+
const otherController = getUIStateControllerById(after);
|
|
14871
|
+
if (!otherController) {
|
|
14872
|
+
console.warn(`Time after constraint: no control with id "${after}"`);
|
|
14873
|
+
return null;
|
|
14874
|
+
}
|
|
14875
|
+
const timeBefore = minutesFromTime(otherController.uiState);
|
|
14876
|
+
const timeAfter = minutesFromTime(field.uiState);
|
|
14877
|
+
if (timeBefore === null || timeAfter === null) {
|
|
14878
|
+
return null;
|
|
14879
|
+
}
|
|
14880
|
+
const minDuration = Number(
|
|
14881
|
+
field.controlHostProps["data-time-min-duration"] ?? 0,
|
|
14882
|
+
);
|
|
14883
|
+
const duration = timeAfter - timeBefore;
|
|
14884
|
+
if (duration >= minDuration) {
|
|
14885
|
+
return null;
|
|
14886
|
+
}
|
|
14887
|
+
if (minDuration > 0) {
|
|
14888
|
+
return naviI18n("constraint.time_after.min_duration").replace(
|
|
14889
|
+
"[duration]",
|
|
14890
|
+
String(minDuration),
|
|
14891
|
+
);
|
|
14892
|
+
}
|
|
14893
|
+
return naviI18n("constraint.time_after.default");
|
|
14894
|
+
},
|
|
14895
|
+
};
|
|
14896
|
+
CONSTRAINT_ATTRIBUTE_SET.add("data-time-after");
|
|
14897
|
+
CONSTRAINT_ATTRIBUTE_SET.add("data-time-min-duration");
|
|
14898
|
+
|
|
14899
|
+
// "HH:MM" as a number of minutes, which is what two times are compared and
|
|
14900
|
+
// subtracted as. Anything else is a time nobody has finished writing.
|
|
14901
|
+
const minutesFromTime = (time) => {
|
|
14902
|
+
if (typeof time !== "string") {
|
|
14903
|
+
return null;
|
|
14904
|
+
}
|
|
14905
|
+
const match = /^(\d{1,2}):(\d{1,2})$/.exec(time);
|
|
14906
|
+
if (!match) {
|
|
14907
|
+
return null;
|
|
14908
|
+
}
|
|
14909
|
+
return Number(match[1]) * 60 + Number(match[2]);
|
|
14910
|
+
};
|
|
14911
|
+
|
|
14719
14912
|
/**
|
|
14720
14913
|
* Custom form validation implementation
|
|
14721
14914
|
*
|
|
@@ -14785,6 +14978,7 @@ const NAVI_CONSTRAINT_SET = new Set([
|
|
|
14785
14978
|
MIN_LOWER_LETTER_CONSTRAINT,
|
|
14786
14979
|
SAME_AS_CONSTRAINT,
|
|
14787
14980
|
ONE_OF_CONSTRAINT,
|
|
14981
|
+
TIME_RANGE_CONSTRAINT,
|
|
14788
14982
|
]);
|
|
14789
14983
|
const DEFAULT_CONSTRAINT_SET = new Set([
|
|
14790
14984
|
...STANDARD_CONSTRAINT_SET,
|
|
@@ -17048,6 +17242,7 @@ const resolveSpacingSize = (size, element, property = "padding") => {
|
|
|
17048
17242
|
};
|
|
17049
17243
|
|
|
17050
17244
|
const COLOR_KEYWORD_MAP = {
|
|
17245
|
+
primary: "var(--navi-color-primary)",
|
|
17051
17246
|
secondary: "var(--navi-color-secondary)",
|
|
17052
17247
|
emphasis: "var(--navi-color-emphasis)",
|
|
17053
17248
|
discrete: "var(--navi-color-discrete)",
|
|
@@ -22144,6 +22339,7 @@ const CONTROL_ATTRIBUTE_SET = new Set([
|
|
|
22144
22339
|
|
|
22145
22340
|
// "ui-action-target",
|
|
22146
22341
|
"navi-input-type",
|
|
22342
|
+
"navi-value-pad",
|
|
22147
22343
|
"navi-control-proxy-for",
|
|
22148
22344
|
"navi-command-proxy-for",
|
|
22149
22345
|
"navi-command-target",
|
|
@@ -23408,6 +23604,12 @@ const useUIGroupStateController = (
|
|
|
23408
23604
|
const debugUIGroup = useDebugUIState();
|
|
23409
23605
|
const debugFocus = useDebugFocus();
|
|
23410
23606
|
|
|
23607
|
+
// What the group is worth is one key per child (or one item per child) only
|
|
23608
|
+
// as long as nobody said otherwise: a group with its own aggregate is worth
|
|
23609
|
+
// whatever IT says — a "HH:MM", an ISO duration — and the shape checks in
|
|
23610
|
+
// setUIState below are about the default shape, not about that one.
|
|
23611
|
+
const stateShapeIsTheDefaultOne =
|
|
23612
|
+
!aggregateChildStates && !distributeChildUIState;
|
|
23411
23613
|
const defaults = GROUP_DEFAULTS[controlType] ?? GROUP_DEFAULTS[stateType];
|
|
23412
23614
|
const resolvedChildControlFilter =
|
|
23413
23615
|
childControlFilter ?? defaults?.childControlFilter ?? null;
|
|
@@ -23581,6 +23783,7 @@ const useUIGroupStateController = (
|
|
|
23581
23783
|
setUIState: (newUIState, e) => {
|
|
23582
23784
|
if (
|
|
23583
23785
|
stateType === "object" &&
|
|
23786
|
+
stateShapeIsTheDefaultOne &&
|
|
23584
23787
|
(newUIState === null || typeof newUIState !== "object")
|
|
23585
23788
|
) {
|
|
23586
23789
|
console.warn(
|
|
@@ -23589,7 +23792,11 @@ const useUIGroupStateController = (
|
|
|
23589
23792
|
);
|
|
23590
23793
|
return;
|
|
23591
23794
|
}
|
|
23592
|
-
if (
|
|
23795
|
+
if (
|
|
23796
|
+
stateType === "array" &&
|
|
23797
|
+
stateShapeIsTheDefaultOne &&
|
|
23798
|
+
!Array.isArray(newUIState)
|
|
23799
|
+
) {
|
|
23593
23800
|
console.warn(
|
|
23594
23801
|
`[${controlType}] setUIState received a non-array value: ${JSON.stringify(newUIState)} (expected an array). Ignoring.`,
|
|
23595
23802
|
newUIState,
|
|
@@ -24321,7 +24528,10 @@ const useControlProps = (props, {
|
|
|
24321
24528
|
controlType,
|
|
24322
24529
|
id: props.id,
|
|
24323
24530
|
type: props.type,
|
|
24324
|
-
inputMode: props.inputMode
|
|
24531
|
+
inputMode: props.inputMode,
|
|
24532
|
+
// How the value is WRITTEN where it is held one way and shown another —
|
|
24533
|
+
// a number on two digits ("07" for 7). See asControlHostValue.
|
|
24534
|
+
pad: props["navi-value-pad"]
|
|
24325
24535
|
});
|
|
24326
24536
|
return {
|
|
24327
24537
|
value: domValue
|
|
@@ -24332,6 +24542,13 @@ const useControlProps = (props, {
|
|
|
24332
24542
|
if (!el) {
|
|
24333
24543
|
return;
|
|
24334
24544
|
}
|
|
24545
|
+
// The field one is typing in is where the value comes FROM, and what is in
|
|
24546
|
+
// it already says this: writing it back in the form it is shown in ("07"
|
|
24547
|
+
// for a 7 just typed) would move the caret and stop the person mid-number.
|
|
24548
|
+
// What is shown is derived again when the field is left (see below).
|
|
24549
|
+
if (document.activeElement === el && readControlValue(el) === newUIState) {
|
|
24550
|
+
return;
|
|
24551
|
+
}
|
|
24335
24552
|
const domProps = toDomProps(newUIState);
|
|
24336
24553
|
Object.assign(el, domProps);
|
|
24337
24554
|
debugUIState(e, `syncDomState: updated to ${getElementSignature(el)}`, domProps);
|
|
@@ -24919,9 +25136,32 @@ const useControlProps = (props, {
|
|
|
24919
25136
|
onPaste,
|
|
24920
25137
|
onInput
|
|
24921
25138
|
});
|
|
25139
|
+
// A value written in a form of its own ("07" for the number 7) is derived
|
|
25140
|
+
// again when the field is left: while it is being typed into, what is in
|
|
25141
|
+
// the field is what the person is writing and nothing rewrites it (see
|
|
25142
|
+
// syncDomState). Only for such a control — for every other one the field
|
|
25143
|
+
// already shows exactly what is held, and there is nothing to derive.
|
|
25144
|
+
if (props["navi-value-pad"]) {
|
|
25145
|
+
const onBlurFromProps = controlHostProps.onBlur;
|
|
25146
|
+
controlHostProps.onBlur = e => {
|
|
25147
|
+
onBlurFromProps?.(e);
|
|
25148
|
+
// Read from the field: what was just typed is in there, whatever the
|
|
25149
|
+
// control has had time to settle on.
|
|
25150
|
+
const el = e.currentTarget;
|
|
25151
|
+
syncDomState(readControlValue(el), e);
|
|
25152
|
+
};
|
|
25153
|
+
}
|
|
24922
25154
|
}
|
|
24923
25155
|
const uiState = uiStateController.uiStateSignal.peek();
|
|
24924
25156
|
const domProps = toDomProps(uiState);
|
|
25157
|
+
{
|
|
25158
|
+
// Same as syncDomState: a field being typed into keeps its own text, so a
|
|
25159
|
+
// render happening mid-number does not put the caret back at the end.
|
|
25160
|
+
const el = props.ref.current;
|
|
25161
|
+
if (el && document.activeElement === el && readControlValue(el) === uiState) {
|
|
25162
|
+
domProps.value = el.value;
|
|
25163
|
+
}
|
|
25164
|
+
}
|
|
24925
25165
|
Object.assign(controlHostProps, domProps);
|
|
24926
25166
|
return [controlRootProps, controlHostProps, {
|
|
24927
25167
|
uiStateController
|
|
@@ -25061,17 +25301,26 @@ const createControlInfo = (props, {
|
|
|
25061
25301
|
};
|
|
25062
25302
|
// color, radio, image, file etc do not support readonly
|
|
25063
25303
|
const INPUT_TYPE_SUPPORTING_READONLY_SET = new Set(["text", "date", "datetime-local", "email", "month", "number", "password", "search", "tel", "time", "url", "week"]);
|
|
25064
|
-
|
|
25065
|
-
|
|
25066
|
-
|
|
25067
|
-
|
|
25304
|
+
// Who, if anyone, is listening to what this control is worth: a handler of its
|
|
25305
|
+
// own, a bound signal, a command — or the form/group around it, which is the
|
|
25306
|
+
// one that will send the value and hand a new one back. Held apart from the
|
|
25307
|
+
// warning below so a group can ask the same question a single control does: a
|
|
25308
|
+
// control with a `value` and nobody listening cannot be changed by hand, and
|
|
25309
|
+
// that is true whatever the control is.
|
|
25310
|
+
const useIsControlListenedTo = props => {
|
|
25068
25311
|
const isProxy = Boolean(props["navi-control-proxy-for"]);
|
|
25069
25312
|
const formContext = useContext(FormContext);
|
|
25070
25313
|
const parentUIStateController = useContext(ParentUIStateControllerContext);
|
|
25071
|
-
|
|
25314
|
+
return Boolean(props.signal ||
|
|
25072
25315
|
// a bound signal is written back on uiAction → interactive
|
|
25073
|
-
props.uiAction || props.action || formContext || parentUIStateController || isProxy || props.command;
|
|
25074
|
-
|
|
25316
|
+
props.uiAction || props.action || formContext || parentUIStateController || isProxy || props.command);
|
|
25317
|
+
};
|
|
25318
|
+
const useReadOnlyUncontrolled = (props, controlInfo) => {
|
|
25319
|
+
const listenedTo = useIsControlListenedTo(props);
|
|
25320
|
+
if (!controlInfo.hasStateProp) {
|
|
25321
|
+
return false;
|
|
25322
|
+
}
|
|
25323
|
+
if (listenedTo) {
|
|
25075
25324
|
return false;
|
|
25076
25325
|
}
|
|
25077
25326
|
if (
|
|
@@ -25117,9 +25366,12 @@ const useControlgroupProps = (props, {
|
|
|
25117
25366
|
cascadeValidationToChildren
|
|
25118
25367
|
});
|
|
25119
25368
|
const [boundAction] = useActionBoundToOneParam(action, uiGroupStateController.uiStateSignal);
|
|
25120
|
-
// Mirror single-input behaviour: a controlled value with
|
|
25121
|
-
// group read-only so children don't appear interactive when they
|
|
25122
|
-
|
|
25369
|
+
// Mirror single-input behaviour: a controlled value with nobody listening
|
|
25370
|
+
// makes the group read-only so children don't appear interactive when they
|
|
25371
|
+
// can't change. A form or a group around it IS someone listening — that is
|
|
25372
|
+
// what will send the value and hand a new one back.
|
|
25373
|
+
const listenedTo = useIsControlListenedTo(props);
|
|
25374
|
+
const implicitReadOnly = uiGroupStateController.hasValueProp && !listenedTo;
|
|
25123
25375
|
if (implicitReadOnly && !props.readOnly) {
|
|
25124
25376
|
props.readOnly = true;
|
|
25125
25377
|
}
|
|
@@ -44930,20 +45182,16 @@ const InputModeNumericOrDecimal = props => {
|
|
|
44930
45182
|
return;
|
|
44931
45183
|
}
|
|
44932
45184
|
const input = e.currentTarget;
|
|
44933
|
-
let maxLength;
|
|
44934
|
-
|
|
44935
|
-
if (maxLengthProp === -1) {
|
|
45185
|
+
let maxLength = input.maxLength;
|
|
45186
|
+
if (maxLength === -1) {
|
|
44936
45187
|
const naviMaxLengthAttr = input.getAttribute("navi-max-length");
|
|
44937
|
-
|
|
44938
|
-
// no max length
|
|
44939
|
-
return;
|
|
44940
|
-
}
|
|
44941
|
-
maxLength = Number(naviMaxLengthAttr);
|
|
45188
|
+
maxLength = naviMaxLengthAttr === null ? undefined : Number(naviMaxLengthAttr);
|
|
44942
45189
|
}
|
|
44943
|
-
|
|
45190
|
+
const caretAtEnd = input.selectionStart === input.value.length;
|
|
45191
|
+
if (!caretAtEnd) {
|
|
44944
45192
|
return;
|
|
44945
45193
|
}
|
|
44946
|
-
if (input
|
|
45194
|
+
if (!isFull(input, maxLength)) {
|
|
44947
45195
|
return;
|
|
44948
45196
|
}
|
|
44949
45197
|
// Field is full and caret is at the end: notify listeners then
|
|
@@ -44952,7 +45200,11 @@ const InputModeNumericOrDecimal = props => {
|
|
|
44952
45200
|
const allowed = dispatchPublicCustomEvent(input, "navi_input_full", {
|
|
44953
45201
|
event: e
|
|
44954
45202
|
});
|
|
44955
|
-
|
|
45203
|
+
// Only for a value being typed: selecting focuses the field, and a
|
|
45204
|
+
// value set from elsewhere (a chevron, a form) has nobody typing — on a
|
|
45205
|
+
// phone that focus raises the on-screen keyboard over the page. Same
|
|
45206
|
+
// question useInputGroup asks before moving along to the next field.
|
|
45207
|
+
if (allowed && e.isTrusted) {
|
|
44956
45208
|
input.select();
|
|
44957
45209
|
}
|
|
44958
45210
|
},
|
|
@@ -44969,6 +45221,32 @@ const InputModeNumericOrDecimal = props => {
|
|
|
44969
45221
|
});
|
|
44970
45222
|
};
|
|
44971
45223
|
|
|
45224
|
+
// A field is full when there is no room for another digit — and room is not
|
|
45225
|
+
// only a number of characters: an hour of two digits at most is also full at
|
|
45226
|
+
// "7", because 7 followed by anything is past the 23 it accepts. Both are the
|
|
45227
|
+
// same question ("can one more digit still land here?"), so both move the
|
|
45228
|
+
// person filling it in along to the next field.
|
|
45229
|
+
const isFull = (input, maxLength) => {
|
|
45230
|
+
const value = input.value;
|
|
45231
|
+
if (value === "") {
|
|
45232
|
+
return false;
|
|
45233
|
+
}
|
|
45234
|
+
if (maxLength !== undefined && value.length >= maxLength) {
|
|
45235
|
+
return true;
|
|
45236
|
+
}
|
|
45237
|
+
const max = input.max === "" ? undefined : Number(input.max);
|
|
45238
|
+
if (max === undefined || Number.isNaN(max)) {
|
|
45239
|
+
return false;
|
|
45240
|
+
}
|
|
45241
|
+
// The smallest number one more digit could make: a zero appended to what is
|
|
45242
|
+
// already there.
|
|
45243
|
+
const withOneMoreDigit = Number(`${value}0`);
|
|
45244
|
+
if (Number.isNaN(withOneMoreDigit)) {
|
|
45245
|
+
return false;
|
|
45246
|
+
}
|
|
45247
|
+
return withOneMoreDigit > max;
|
|
45248
|
+
};
|
|
45249
|
+
|
|
44972
45250
|
// hum il manque le faire de request interaction ici
|
|
44973
45251
|
const performArrowUpDown = e => {
|
|
44974
45252
|
const input = e.currentTarget;
|
|
@@ -50366,6 +50644,228 @@ const toDate = (value, parseString) => {
|
|
|
50366
50644
|
return null;
|
|
50367
50645
|
};
|
|
50368
50646
|
|
|
50647
|
+
/**
|
|
50648
|
+
* Wraps multiple inputs together and handles keyboard navigation and paste
|
|
50649
|
+
* distribution between them.
|
|
50650
|
+
*
|
|
50651
|
+
* Keyboard navigation:
|
|
50652
|
+
* ArrowRight at the end of an input moves focus to the next input.
|
|
50653
|
+
* ArrowLeft at the start of an input moves focus to the previous input.
|
|
50654
|
+
* navi_input_full (emitted when an input reaches maxLength) also moves forward.
|
|
50655
|
+
*
|
|
50656
|
+
* Paste distribution:
|
|
50657
|
+
* When an input has a data-separator attribute, pasting a string that
|
|
50658
|
+
* contains that separator (e.g. "27/04/1990" into a day input with
|
|
50659
|
+
* data-separator="/") splits the text on each separator and fills the
|
|
50660
|
+
* corresponding sub-inputs in order.
|
|
50661
|
+
*/
|
|
50662
|
+
const useInputGroup = (ref) => {
|
|
50663
|
+
const debugFocus = useDebugFocus();
|
|
50664
|
+
|
|
50665
|
+
useEffect(() => {
|
|
50666
|
+
const el = ref.current;
|
|
50667
|
+
if (!el) {
|
|
50668
|
+
return () => {};
|
|
50669
|
+
}
|
|
50670
|
+
|
|
50671
|
+
const getInputs = () =>
|
|
50672
|
+
Array.from(el.querySelectorAll(".navi_control_input"));
|
|
50673
|
+
|
|
50674
|
+
const focusInput = (input) => {
|
|
50675
|
+
input.focus();
|
|
50676
|
+
input.select();
|
|
50677
|
+
};
|
|
50678
|
+
|
|
50679
|
+
const handleKeyDown = (e) => {
|
|
50680
|
+
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") {
|
|
50681
|
+
return;
|
|
50682
|
+
}
|
|
50683
|
+
const active = document.activeElement;
|
|
50684
|
+
if (!isTextInputElement(active) || !el.contains(active)) {
|
|
50685
|
+
return;
|
|
50686
|
+
}
|
|
50687
|
+
if (e.key === "ArrowRight") {
|
|
50688
|
+
const allSelected =
|
|
50689
|
+
active.selectionStart === 0 &&
|
|
50690
|
+
active.selectionEnd === active.value.length;
|
|
50691
|
+
const atEnd =
|
|
50692
|
+
allSelected ||
|
|
50693
|
+
(active.selectionStart === active.value.length &&
|
|
50694
|
+
active.selectionEnd === active.value.length);
|
|
50695
|
+
if (!atEnd) {
|
|
50696
|
+
return;
|
|
50697
|
+
}
|
|
50698
|
+
const inputs = getInputs();
|
|
50699
|
+
const idx = inputs.indexOf(active);
|
|
50700
|
+
if (idx === -1) {
|
|
50701
|
+
debugFocus(
|
|
50702
|
+
e,
|
|
50703
|
+
"InputGroup ArrowRight on non group input → do nothing",
|
|
50704
|
+
);
|
|
50705
|
+
return;
|
|
50706
|
+
}
|
|
50707
|
+
if (idx === inputs.length - 1) {
|
|
50708
|
+
debugFocus(
|
|
50709
|
+
e,
|
|
50710
|
+
"InputGroup ArrowRight at end of last input → do nothing",
|
|
50711
|
+
);
|
|
50712
|
+
return;
|
|
50713
|
+
}
|
|
50714
|
+
|
|
50715
|
+
debugFocus(
|
|
50716
|
+
e,
|
|
50717
|
+
"InputGroup ArrowRight at end of input[%d] → focus input[%d]",
|
|
50718
|
+
idx,
|
|
50719
|
+
idx + 1,
|
|
50720
|
+
);
|
|
50721
|
+
e.preventDefault();
|
|
50722
|
+
focusInput(inputs[idx + 1]);
|
|
50723
|
+
return;
|
|
50724
|
+
}
|
|
50725
|
+
const allSelected =
|
|
50726
|
+
active.selectionStart === 0 &&
|
|
50727
|
+
active.selectionEnd === active.value.length;
|
|
50728
|
+
const atStart =
|
|
50729
|
+
allSelected ||
|
|
50730
|
+
(active.selectionStart === 0 && active.selectionEnd === 0);
|
|
50731
|
+
if (!atStart) {
|
|
50732
|
+
return;
|
|
50733
|
+
}
|
|
50734
|
+
const inputs = getInputs();
|
|
50735
|
+
const idx = inputs.indexOf(active);
|
|
50736
|
+
if (idx === 0) {
|
|
50737
|
+
return;
|
|
50738
|
+
}
|
|
50739
|
+
debugFocus(
|
|
50740
|
+
e,
|
|
50741
|
+
"InputGroup ArrowLeft at start of input[%d] → focus input[%d]",
|
|
50742
|
+
idx,
|
|
50743
|
+
idx - 1,
|
|
50744
|
+
);
|
|
50745
|
+
e.preventDefault();
|
|
50746
|
+
focusInput(inputs[idx - 1]);
|
|
50747
|
+
};
|
|
50748
|
+
|
|
50749
|
+
const handleNaviInputFull = (e) => {
|
|
50750
|
+
if (!e.detail.event?.isTrusted) {
|
|
50751
|
+
// Programmatic value change (e.g. ArrowUp/Down) — don't auto-advance.
|
|
50752
|
+
return;
|
|
50753
|
+
}
|
|
50754
|
+
const input = e.detail.event.currentTarget;
|
|
50755
|
+
if (!el.contains(input)) {
|
|
50756
|
+
return;
|
|
50757
|
+
}
|
|
50758
|
+
const inputs = getInputs();
|
|
50759
|
+
const idx = inputs.indexOf(input);
|
|
50760
|
+
if (idx === -1) {
|
|
50761
|
+
return;
|
|
50762
|
+
}
|
|
50763
|
+
if (idx === inputs.length - 1) {
|
|
50764
|
+
return;
|
|
50765
|
+
}
|
|
50766
|
+
const nextInput = inputs[idx + 1];
|
|
50767
|
+
debugFocus(
|
|
50768
|
+
e,
|
|
50769
|
+
"InputGroup navi_input_full on input -> move to next input",
|
|
50770
|
+
input,
|
|
50771
|
+
nextInput,
|
|
50772
|
+
);
|
|
50773
|
+
e.preventDefault();
|
|
50774
|
+
focusInput(nextInput);
|
|
50775
|
+
};
|
|
50776
|
+
|
|
50777
|
+
// const handlePaste = (e) => {
|
|
50778
|
+
// const active = document.activeElement;
|
|
50779
|
+
// if (!isTextInputElement(active) || !el.contains(active)) {
|
|
50780
|
+
// return;
|
|
50781
|
+
// }
|
|
50782
|
+
// const inputs = getInputs();
|
|
50783
|
+
// const startIdx = inputs.indexOf(active);
|
|
50784
|
+
// if (startIdx === -1) {
|
|
50785
|
+
// return;
|
|
50786
|
+
// }
|
|
50787
|
+
// const pastedText = e.clipboardData?.getData("text") ?? "";
|
|
50788
|
+
// if (!pastedText) {
|
|
50789
|
+
// return;
|
|
50790
|
+
// }
|
|
50791
|
+
// // Only intercept when the pasted text contains at least one separator
|
|
50792
|
+
// // from the inputs starting at the focused position.
|
|
50793
|
+
// const remainingInputs = inputs.slice(startIdx);
|
|
50794
|
+
// const hasSeparatorMatch = remainingInputs.some(
|
|
50795
|
+
// (input) =>
|
|
50796
|
+
// input.dataset.separator &&
|
|
50797
|
+
// pastedText.includes(input.dataset.separator),
|
|
50798
|
+
// );
|
|
50799
|
+
// if (!hasSeparatorMatch) {
|
|
50800
|
+
// return;
|
|
50801
|
+
// }
|
|
50802
|
+
// e.preventDefault();
|
|
50803
|
+
// let remaining = pastedText;
|
|
50804
|
+
// let lastFilledIdx = startIdx;
|
|
50805
|
+
// for (let i = 0; i < remainingInputs.length; i++) {
|
|
50806
|
+
// const input = remainingInputs[i];
|
|
50807
|
+
// const separator = input.dataset.separator;
|
|
50808
|
+
// let part;
|
|
50809
|
+
// if (separator && remaining.includes(separator)) {
|
|
50810
|
+
// const sepIdx = remaining.indexOf(separator);
|
|
50811
|
+
// part = remaining.slice(0, sepIdx);
|
|
50812
|
+
// remaining = remaining.slice(sepIdx + separator.length);
|
|
50813
|
+
// } else {
|
|
50814
|
+
// part = remaining;
|
|
50815
|
+
// remaining = "";
|
|
50816
|
+
// }
|
|
50817
|
+
// requestSubPaste(input, part, e);
|
|
50818
|
+
// lastFilledIdx = startIdx + i;
|
|
50819
|
+
// if (remaining === "") {
|
|
50820
|
+
// break;
|
|
50821
|
+
// }
|
|
50822
|
+
// }
|
|
50823
|
+
// focusInput(inputs[lastFilledIdx]);
|
|
50824
|
+
// };
|
|
50825
|
+
|
|
50826
|
+
el.addEventListener("keydown", handleKeyDown, { capture: true });
|
|
50827
|
+
el.addEventListener("navi_input_full", handleNaviInputFull);
|
|
50828
|
+
// el.addEventListener("paste", handlePaste, { capture: true });
|
|
50829
|
+
return () => {
|
|
50830
|
+
el.removeEventListener("keydown", handleKeyDown, { capture: true });
|
|
50831
|
+
el.removeEventListener("navi_input_full", handleNaviInputFull);
|
|
50832
|
+
// el.removeEventListener("paste", handlePaste, { capture: true });
|
|
50833
|
+
};
|
|
50834
|
+
}, [debugFocus]);
|
|
50835
|
+
};
|
|
50836
|
+
|
|
50837
|
+
// const requestSubPaste = (input, value, event) => {
|
|
50838
|
+
// dispatchRequestInteraction(input, {
|
|
50839
|
+
// event,
|
|
50840
|
+
// name: "subpaste",
|
|
50841
|
+
// allowed: () => {
|
|
50842
|
+
// dispatchRequestSetUIState(input, value, { event });
|
|
50843
|
+
// },
|
|
50844
|
+
// });
|
|
50845
|
+
// };
|
|
50846
|
+
|
|
50847
|
+
const isTextInputElement = (el) => {
|
|
50848
|
+
if (!el) {
|
|
50849
|
+
return false;
|
|
50850
|
+
}
|
|
50851
|
+
if (el.tagName === "TEXTAREA") {
|
|
50852
|
+
return true;
|
|
50853
|
+
}
|
|
50854
|
+
if (el.tagName !== "INPUT") {
|
|
50855
|
+
return false;
|
|
50856
|
+
}
|
|
50857
|
+
const type = el.type || "text";
|
|
50858
|
+
return (
|
|
50859
|
+
type === "text" ||
|
|
50860
|
+
type === "search" ||
|
|
50861
|
+
type === "url" ||
|
|
50862
|
+
type === "tel" ||
|
|
50863
|
+
type === "email" ||
|
|
50864
|
+
type === "password" ||
|
|
50865
|
+
type === "number"
|
|
50866
|
+
);
|
|
50867
|
+
};
|
|
50868
|
+
|
|
50369
50869
|
// When a component render a prop that can be anything (js value of preact element)
|
|
50370
50870
|
// make sure it cannot throw during render by converting it to a string if it's not a valid preact element or a primitive value
|
|
50371
50871
|
const renderSafe = (value) => {
|
|
@@ -53404,6 +53904,14 @@ const css$v = /* css */`
|
|
|
53404
53904
|
text-transform: uppercase;
|
|
53405
53905
|
letter-spacing: 0.05em;
|
|
53406
53906
|
}
|
|
53907
|
+
|
|
53908
|
+
/* A group whose rows all failed the search keeps its height (its rows are
|
|
53909
|
+
still there, invisible) — the label must disappear with them, otherwise
|
|
53910
|
+
the list shows a title standing over nothing. Same aria-hidden + inert
|
|
53911
|
+
pair as the rows themselves. */
|
|
53912
|
+
&[aria-hidden="true"][inert] {
|
|
53913
|
+
opacity: 0;
|
|
53914
|
+
}
|
|
53407
53915
|
}
|
|
53408
53916
|
.navi_list_item_group_list {
|
|
53409
53917
|
display: flex;
|
|
@@ -56523,6 +57031,14 @@ const ListItemGroup = ({
|
|
|
56523
57031
|
}) => {
|
|
56524
57032
|
const groupId = useId();
|
|
56525
57033
|
const groupTracker = useItemTracker();
|
|
57034
|
+
const searchNoMatchMode = useContext(SearchNoMatchModeContext);
|
|
57035
|
+
const groupItemCount = groupTracker.countSignal.value;
|
|
57036
|
+
const groupNoMatchCount = groupTracker.noMatchCountSignal.value;
|
|
57037
|
+
// Every row of this group failed the search: the label has nothing left to
|
|
57038
|
+
// title. "remove" empties the group on its own (and hiddenWhileEmpty takes it
|
|
57039
|
+
// out of the flow), "muted" keeps the rows readable so the label stays useful
|
|
57040
|
+
// — only "invisible_and_inert" would leave a title floating over blank space.
|
|
57041
|
+
const labelHidden = searchNoMatchMode === "invisible_and_inert" && groupNoMatchCount > 0 && groupNoMatchCount === groupItemCount;
|
|
56526
57042
|
const groupRef = useRef(null);
|
|
56527
57043
|
const labelRef = useRef(null);
|
|
56528
57044
|
useDisplayedLayoutEffect(labelRef, labelEl => {
|
|
@@ -56544,7 +57060,9 @@ const ListItemGroup = ({
|
|
|
56544
57060
|
ref: labelRef,
|
|
56545
57061
|
id: groupId,
|
|
56546
57062
|
className: "navi_list_item_group_label",
|
|
56547
|
-
role: "presentation"
|
|
57063
|
+
role: "presentation",
|
|
57064
|
+
"aria-hidden": labelHidden ? "true" : undefined,
|
|
57065
|
+
inert: labelHidden ? true : undefined
|
|
56548
57066
|
// eslint-disable-next-line react/no-unknown-property
|
|
56549
57067
|
,
|
|
56550
57068
|
|
|
@@ -58200,6 +58718,15 @@ const css$q = /* css */`
|
|
|
58200
58718
|
--picker-spin-padding-x-default: var(--navi-picker-padding-x-default);
|
|
58201
58719
|
--picker-spin-padding-y-default: var(--navi-picker-padding-y-default);
|
|
58202
58720
|
}
|
|
58721
|
+
/* Written in what it is written with, for a value one TYPES: a field is as
|
|
58722
|
+
wide as the digits in it and no wider, so the room around them has to
|
|
58723
|
+
come from here — and the two chevrons, which take the same, are then big
|
|
58724
|
+
enough to be aimed at with a finger. Said as a default, so a padding prop
|
|
58725
|
+
still wins. */
|
|
58726
|
+
.navi_picker_spin:has(> .navi_picker_spin_middle > .navi_input) {
|
|
58727
|
+
--picker-spin-padding-x-default: 0.6em;
|
|
58728
|
+
--picker-spin-padding-y-default: 0.25em;
|
|
58729
|
+
}
|
|
58203
58730
|
}
|
|
58204
58731
|
|
|
58205
58732
|
.navi_picker_spin {
|
|
@@ -58331,6 +58858,16 @@ const css$q = /* css */`
|
|
|
58331
58858
|
min-width: 0;
|
|
58332
58859
|
flex: 1 1 auto;
|
|
58333
58860
|
}
|
|
58861
|
+
/* The same room around a value one TYPES as around one one picks (the
|
|
58862
|
+
[data-slide] rule below writes it there): without it the column is as
|
|
58863
|
+
narrow as the two digits in it, and a number pressed against both sides
|
|
58864
|
+
reads as a field too small for what it holds. Handed to the field as its
|
|
58865
|
+
own padding rather than kept by the middle: the field then IS the column —
|
|
58866
|
+
it fills it, and a click anywhere in it lands on the caret. */
|
|
58867
|
+
.navi_picker_spin_middle > .navi_input {
|
|
58868
|
+
--padding-right: var(--x-picker-spin-padding-right);
|
|
58869
|
+
--padding-left: var(--x-picker-spin-padding-left);
|
|
58870
|
+
}
|
|
58334
58871
|
/* Where the padding lands: all four sides on the value, the two vertical
|
|
58335
58872
|
ones on the chevrons below — the same number above and below is what makes
|
|
58336
58873
|
the three one line rather than three boxes, while sideways it is the room
|
|
@@ -58408,8 +58945,19 @@ const css$q = /* css */`
|
|
|
58408
58945
|
aspect-ratio: 1;
|
|
58409
58946
|
justify-content: center;
|
|
58410
58947
|
}
|
|
58948
|
+
/* Standing up, a chevron takes the whole width and whatever height is left
|
|
58949
|
+
over: a box taller than the three pieces in it (a height of its own, room
|
|
58950
|
+
asked for around the value) would otherwise leave a strip of nothing
|
|
58951
|
+
between the chevron and the border, and one pressing what looks like the
|
|
58952
|
+
bottom of the box would hit nothing. */
|
|
58411
58953
|
.navi_picker_spin[data-vertical] > .navi_picker_spin_way_out {
|
|
58412
58954
|
width: 100%;
|
|
58955
|
+
height: auto;
|
|
58956
|
+
min-height: calc(
|
|
58957
|
+
1lh + var(--x-picker-spin-padding-top) +
|
|
58958
|
+
var(--x-picker-spin-padding-bottom)
|
|
58959
|
+
);
|
|
58960
|
+
flex: 1 0 auto;
|
|
58413
58961
|
justify-content: center;
|
|
58414
58962
|
}
|
|
58415
58963
|
/* The corners of the box belong to what sits in them: a chevron in the corner
|
|
@@ -58441,6 +58989,92 @@ const css$q = /* css */`
|
|
|
58441
58989
|
border-end-end-radius: inherit;
|
|
58442
58990
|
border-end-start-radius: inherit;
|
|
58443
58991
|
}
|
|
58992
|
+
|
|
58993
|
+
/* ── SpinGroup ─────────────────────────────────────────────────────────────
|
|
58994
|
+
Several spins read as one value: an hour is "7h30", not a 7 next to a 30.
|
|
58995
|
+
So the frame goes around the group, the spins inside give theirs up, and
|
|
58996
|
+
what sits between them (an "h", a ":") is inside the frame with them. */
|
|
58997
|
+
.navi_spin_group {
|
|
58998
|
+
/* What the loading outline is drawn around. */
|
|
58999
|
+
position: relative;
|
|
59000
|
+
display: inline-flex;
|
|
59001
|
+
align-items: center;
|
|
59002
|
+
font-size: var(--navi-control-font-size);
|
|
59003
|
+
font-family: var(--navi-control-font-family);
|
|
59004
|
+
border: var(--navi-control-border-width) solid
|
|
59005
|
+
var(--navi-control-border-color);
|
|
59006
|
+
border-radius: var(--navi-control-border-radius);
|
|
59007
|
+
outline-width: var(--navi-focus-outline-width);
|
|
59008
|
+
outline-color: var(--navi-focus-outline-color);
|
|
59009
|
+
outline-offset: 0px;
|
|
59010
|
+
-webkit-tap-highlight-color: var(--navi-control-tap-highlight-color);
|
|
59011
|
+
}
|
|
59012
|
+
/* A value one PICKS has nowhere of its own to wear a ring — its middle is a
|
|
59013
|
+
container that hands the ring over (data-focus-outline-delegate) — so the
|
|
59014
|
+
group wears it for that spin. A value one TYPES keeps its own, on the
|
|
59015
|
+
field: the spins in a group are edited one at a time, and the ring is what
|
|
59016
|
+
says which one the keyboard is in. */
|
|
59017
|
+
.navi_spin_group[data-focus-visible],
|
|
59018
|
+
.navi_spin_group:has([data-focus-outline-delegate][data-focus-visible]) {
|
|
59019
|
+
outline-style: solid;
|
|
59020
|
+
}
|
|
59021
|
+
.navi_spin_group .navi_picker_spin {
|
|
59022
|
+
border: none;
|
|
59023
|
+
border-radius: 0;
|
|
59024
|
+
}
|
|
59025
|
+
/* The corners of the group belong to the spins sitting in them, and through
|
|
59026
|
+
them to their chevrons, which are rounded by whatever their spin is. */
|
|
59027
|
+
.navi_spin_group > .navi_picker_spin:first-child {
|
|
59028
|
+
border-start-start-radius: inherit;
|
|
59029
|
+
border-end-start-radius: inherit;
|
|
59030
|
+
}
|
|
59031
|
+
.navi_spin_group > .navi_picker_spin:last-child {
|
|
59032
|
+
border-start-end-radius: inherit;
|
|
59033
|
+
border-end-end-radius: inherit;
|
|
59034
|
+
}
|
|
59035
|
+
.navi_spin_group .navi_picker_spin[data-focus-visible],
|
|
59036
|
+
.navi_spin_group
|
|
59037
|
+
.navi_picker_spin:has([data-focus-outline-delegate][data-focus-visible]),
|
|
59038
|
+
.navi_spin_group .navi_picker_spin:has(.navi_input[data-focus-visible]) {
|
|
59039
|
+
outline-style: none;
|
|
59040
|
+
}
|
|
59041
|
+
/* Fading the frame is the group's to do, since the frame is the group's. */
|
|
59042
|
+
.navi_spin_group[data-readonly],
|
|
59043
|
+
.navi_spin_group[data-loading] {
|
|
59044
|
+
border-color: color-mix(
|
|
59045
|
+
in srgb,
|
|
59046
|
+
var(--navi-control-border-color) 45%,
|
|
59047
|
+
transparent
|
|
59048
|
+
);
|
|
59049
|
+
}
|
|
59050
|
+
.navi_spin_group[data-disabled] {
|
|
59051
|
+
color: color-mix(in srgb, currentColor 40%, transparent);
|
|
59052
|
+
border-color: color-mix(
|
|
59053
|
+
in srgb,
|
|
59054
|
+
var(--navi-control-border-color) 30%,
|
|
59055
|
+
transparent
|
|
59056
|
+
);
|
|
59057
|
+
}
|
|
59058
|
+
/* As tall as the spins beside it, so what it says lands on their line. */
|
|
59059
|
+
.navi_spin_group_separator {
|
|
59060
|
+
display: flex;
|
|
59061
|
+
align-items: center;
|
|
59062
|
+
align-self: stretch;
|
|
59063
|
+
justify-content: center;
|
|
59064
|
+
color: inherit;
|
|
59065
|
+
white-space: nowrap;
|
|
59066
|
+
user-select: none;
|
|
59067
|
+
}
|
|
59068
|
+
/* The "h" fades with the values it sits between: it is one control, and a
|
|
59069
|
+
word left black beside two grey numbers reads as half a control out of
|
|
59070
|
+
service. Same mixes as the spins' own (see above). */
|
|
59071
|
+
.navi_spin_group[data-readonly] .navi_spin_group_separator,
|
|
59072
|
+
.navi_spin_group[data-loading] .navi_spin_group_separator {
|
|
59073
|
+
color: color-mix(in srgb, currentColor 60%, transparent);
|
|
59074
|
+
}
|
|
59075
|
+
.navi_spin_group[data-disabled] .navi_spin_group_separator {
|
|
59076
|
+
color: color-mix(in srgb, currentColor 40%, transparent);
|
|
59077
|
+
}
|
|
58444
59078
|
`;
|
|
58445
59079
|
|
|
58446
59080
|
/**
|
|
@@ -58533,6 +59167,7 @@ const Spin = ({
|
|
|
58533
59167
|
readOnly,
|
|
58534
59168
|
disabled,
|
|
58535
59169
|
loading,
|
|
59170
|
+
size,
|
|
58536
59171
|
maxLines,
|
|
58537
59172
|
previousLabel,
|
|
58538
59173
|
nextLabel,
|
|
@@ -58540,6 +59175,10 @@ const Spin = ({
|
|
|
58540
59175
|
}) => {
|
|
58541
59176
|
import.meta.css = [css$q, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
|
|
58542
59177
|
const id = useId();
|
|
59178
|
+
// What the group around it says, when there is one: how big the whole thing
|
|
59179
|
+
// is written is said once, on the group, and every spin in it follows.
|
|
59180
|
+
const group = useContext(SpinGroupContext);
|
|
59181
|
+
const sizeResolved = size ?? group?.size;
|
|
58543
59182
|
const containerId = `${id}_values`;
|
|
58544
59183
|
const controlId = `${id}_control`;
|
|
58545
59184
|
// The control holds the value, and it is asked rather than shadowed:
|
|
@@ -58666,7 +59305,19 @@ const Spin = ({
|
|
|
58666
59305
|
// it in this spin, so one can keep going with the keys where one was. Found
|
|
58667
59306
|
// in the middle rather than by id — a field's id lands on the box around it,
|
|
58668
59307
|
// and what takes the keyboard is the input inside.
|
|
59308
|
+
//
|
|
59309
|
+
// Not under a finger: focusing a field on a phone raises the on-screen
|
|
59310
|
+
// keyboard over half the page, and someone pressing a chevron is stepping
|
|
59311
|
+
// through values, not about to type one. With a mouse the opposite is true —
|
|
59312
|
+
// one presses once and then keeps going with the arrow keys — so the keyboard
|
|
59313
|
+
// is put back where it was. Which one it is comes from the pointer that
|
|
59314
|
+
// started the press (same convention as button_ui.jsx and
|
|
59315
|
+
// use_autoselect_read_only.js).
|
|
59316
|
+
const wayOutPointerTypeRef = useRef(null);
|
|
58669
59317
|
const focusMiddle = () => {
|
|
59318
|
+
if (wayOutPointerTypeRef.current === "touch") {
|
|
59319
|
+
return;
|
|
59320
|
+
}
|
|
58670
59321
|
const target = editable ? middleRef.current?.querySelector(".navi_control_input") : document.getElementById(containerId);
|
|
58671
59322
|
target?.focus({
|
|
58672
59323
|
preventScroll: true
|
|
@@ -58686,6 +59337,9 @@ const Spin = ({
|
|
|
58686
59337
|
const isNext = atStart ? startIsNext : !startIsNext;
|
|
58687
59338
|
return jsx(WayOut, {
|
|
58688
59339
|
atStart: atStart,
|
|
59340
|
+
onPointerDown: e => {
|
|
59341
|
+
wayOutPointerTypeRef.current = e.pointerType;
|
|
59342
|
+
},
|
|
58689
59343
|
unavailableMessage: wayOutMessage(atStart ? startAllowed : endAllowed, isNext ? "spin.nothing_after" : "spin.nothing_before"),
|
|
58690
59344
|
label: isNext ? nextLabel ?? naviI18n("spin.next") : previousLabel ?? naviI18n("spin.previous"),
|
|
58691
59345
|
onPress: e => {
|
|
@@ -58713,6 +59367,7 @@ const Spin = ({
|
|
|
58713
59367
|
// asked for by hand (pseudoState) as well as held for real.
|
|
58714
59368
|
,
|
|
58715
59369
|
|
|
59370
|
+
size: sizeResolved,
|
|
58716
59371
|
pseudoClasses: PICKER_SPIN_PSEUDO_CLASSES,
|
|
58717
59372
|
styleCSSVars: PICKER_SPIN_STYLE_CSS_VARS,
|
|
58718
59373
|
"data-vertical": vertical ? "" : undefined,
|
|
@@ -58737,14 +59392,21 @@ const Spin = ({
|
|
|
58737
59392
|
readOnly: readOnly,
|
|
58738
59393
|
disabled: disabled,
|
|
58739
59394
|
loading: loading
|
|
58740
|
-
//
|
|
58741
|
-
//
|
|
58742
|
-
|
|
58743
|
-
|
|
59395
|
+
// The field is written as big as the box around it: a size that
|
|
59396
|
+
// only grew the chevrons would be half a size.
|
|
59397
|
+
,
|
|
59398
|
+
|
|
59399
|
+
size: sizeResolved
|
|
59400
|
+
// No frame of its own inside a frame: the spin draws it (see the
|
|
59401
|
+
// CSS above). The ring is the spin's too — an outline of zero width
|
|
59402
|
+
// is how a field stands down without its focus state being touched
|
|
59403
|
+
// — except inside a group, where the frame belongs to the group and
|
|
59404
|
+
// the ring stays on the field: the spins are typed into one at a
|
|
59405
|
+
// time, and the ring is what says which one holds the keyboard.
|
|
58744
59406
|
,
|
|
58745
59407
|
|
|
58746
59408
|
variant: "discrete",
|
|
58747
|
-
outlineWidth: "0",
|
|
59409
|
+
outlineWidth: group ? undefined : "0",
|
|
58748
59410
|
textAlign: "center",
|
|
58749
59411
|
expandX: true,
|
|
58750
59412
|
uiAction: (valueNext, event) => {
|
|
@@ -58868,6 +59530,7 @@ const WayOut = ({
|
|
|
58868
59530
|
unavailableMessage,
|
|
58869
59531
|
label,
|
|
58870
59532
|
onPress,
|
|
59533
|
+
onPointerDown,
|
|
58871
59534
|
children
|
|
58872
59535
|
}) => jsx(Box, {
|
|
58873
59536
|
as: "span",
|
|
@@ -58914,6 +59577,7 @@ const WayOut = ({
|
|
|
58914
59577
|
onClick: e => {
|
|
58915
59578
|
e.preventDefault();
|
|
58916
59579
|
},
|
|
59580
|
+
onPointerDown: onPointerDown,
|
|
58917
59581
|
onMouseDown: e => {
|
|
58918
59582
|
// No focus, no text selection: the keyboard is put on the middle below.
|
|
58919
59583
|
e.preventDefault();
|
|
@@ -58964,6 +59628,122 @@ const compareValuesDefault = (a, b) => {
|
|
|
58964
59628
|
};
|
|
58965
59629
|
const renderValueDefault = value => String(value ?? "");
|
|
58966
59630
|
|
|
59631
|
+
/**
|
|
59632
|
+
* Several spins read as one value: "7h30" is an hour, not a 7 beside a 30. The
|
|
59633
|
+
* frame goes around the group, the spins inside give theirs up, and what is
|
|
59634
|
+
* written between them — an "h", a ":", a word — sits inside the frame with
|
|
59635
|
+
* them.
|
|
59636
|
+
*
|
|
59637
|
+
* A group IS a control: its named spins aggregate into `{ hour: …, minute: … }`
|
|
59638
|
+
* for the form around it, and `aggregateChildStates`/`distributeChildUIState`
|
|
59639
|
+
* turn that into whatever the group is really worth instead ("07:30", a number
|
|
59640
|
+
* of minutes) — one value in both directions, so the group can be driven by a
|
|
59641
|
+
* single `value`/`signal` like any other control. `TimeSpin` is that, for a
|
|
59642
|
+
* time of day.
|
|
59643
|
+
*
|
|
59644
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
59645
|
+
* name?: string,
|
|
59646
|
+
* value?: any,
|
|
59647
|
+
* defaultValue?: any,
|
|
59648
|
+
* signal?: import("@preact/signals").Signal<any>,
|
|
59649
|
+
* aggregateChildStates?: (childUIStateControllers: any[]) => any,
|
|
59650
|
+
* distributeChildUIState?: (groupState: any, childUIStateController: any) => any,
|
|
59651
|
+
* children?: import("ignore:preact").ComponentChildren,
|
|
59652
|
+
* [key: string]: any,
|
|
59653
|
+
* }>}
|
|
59654
|
+
* Everything a box takes is taken here too — `width`, `borderWidth`,
|
|
59655
|
+
* `borderRadius`, `backgroundColor`: the frame is the group's, and its corners
|
|
59656
|
+
* are passed on to the spins sitting in them.
|
|
59657
|
+
*/
|
|
59658
|
+
const SpinGroup = props => {
|
|
59659
|
+
import.meta.css = [css$q, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
|
|
59660
|
+
const {
|
|
59661
|
+
size
|
|
59662
|
+
} = props;
|
|
59663
|
+
const defaultRef = useRef(null);
|
|
59664
|
+
props.ref = props.ref || defaultRef;
|
|
59665
|
+
const groupRef = props.ref;
|
|
59666
|
+
// Two digit fields side by side are filled the way a date or a code is: the
|
|
59667
|
+
// hour reaching its two digits moves on to the minutes, and Left/Right at
|
|
59668
|
+
// either end of a field walk between them.
|
|
59669
|
+
useInputGroup(groupRef);
|
|
59670
|
+
const [controlgroupRootProps, controlgroupProps, childrenWrapperProps] = useControlgroupProps(props, {
|
|
59671
|
+
allowCapture: true,
|
|
59672
|
+
wantRequesterButtonState: true,
|
|
59673
|
+
controlType: "control_group",
|
|
59674
|
+
stateType: "object",
|
|
59675
|
+
cascadeValidationToChildren: true,
|
|
59676
|
+
aggregateChildStates: props.aggregateChildStates,
|
|
59677
|
+
distributeChildUIState: props.distributeChildUIState
|
|
59678
|
+
});
|
|
59679
|
+
const {
|
|
59680
|
+
children
|
|
59681
|
+
} = controlgroupProps;
|
|
59682
|
+
const {
|
|
59683
|
+
readOnly,
|
|
59684
|
+
disabled,
|
|
59685
|
+
loading
|
|
59686
|
+
} = childrenWrapperProps;
|
|
59687
|
+
return jsxs(Box, {
|
|
59688
|
+
...controlgroupRootProps,
|
|
59689
|
+
...controlgroupProps,
|
|
59690
|
+
// Consumed by the group hook above; blanked after the spreads so they do
|
|
59691
|
+
// not reach the DOM as unknown attributes.
|
|
59692
|
+
aggregateChildStates: undefined,
|
|
59693
|
+
distributeChildUIState: undefined,
|
|
59694
|
+
baseClassName: "navi_spin_group",
|
|
59695
|
+
pseudoClasses: SPIN_GROUP_PSEUDO_CLASSES
|
|
59696
|
+
// What the frame and what sits between the spins are drawn from: the
|
|
59697
|
+
// spins fade themselves, and the group is what holds those two.
|
|
59698
|
+
,
|
|
59699
|
+
|
|
59700
|
+
"data-readonly": readOnly ? "" : undefined,
|
|
59701
|
+
"data-disabled": disabled ? "" : undefined,
|
|
59702
|
+
"data-loading": loading ? "" : undefined,
|
|
59703
|
+
children: [jsx(LoadingOutline, {
|
|
59704
|
+
loading: loading,
|
|
59705
|
+
color: "var(--navi-loader-color)",
|
|
59706
|
+
inset: -2
|
|
59707
|
+
}), jsx(SpinGroupContext.Provider, {
|
|
59708
|
+
value: {
|
|
59709
|
+
size
|
|
59710
|
+
},
|
|
59711
|
+
children: jsx(ControlgroupChildrenWrapper, {
|
|
59712
|
+
...childrenWrapperProps,
|
|
59713
|
+
// The group's name says where its value lands in the form; each spin
|
|
59714
|
+
// inside is named on its own.
|
|
59715
|
+
name: undefined,
|
|
59716
|
+
children: children
|
|
59717
|
+
})
|
|
59718
|
+
})]
|
|
59719
|
+
});
|
|
59720
|
+
};
|
|
59721
|
+
|
|
59722
|
+
// Lets a group hand down what is said once for all the spins in it (how big
|
|
59723
|
+
// they are written), and lets a spin know it is in one at all — which is what
|
|
59724
|
+
// moves the frame and the focus ring off the spin and onto the group.
|
|
59725
|
+
const SpinGroupContext = createContext(null);
|
|
59726
|
+
const SPIN_GROUP_PSEUDO_CLASSES = [":focus-within",
|
|
59727
|
+
// Nothing focuses the group for real — a spin inside it takes the keyboard —
|
|
59728
|
+
// but it is where the ring is drawn, so a demo can hold it there.
|
|
59729
|
+
":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
|
|
59730
|
+
|
|
59731
|
+
/**
|
|
59732
|
+
* SpinGroup.Separator — what is written between two spins ("h", ":", a word).
|
|
59733
|
+
* It stands as tall as they do, so what it says lands on their line.
|
|
59734
|
+
*/
|
|
59735
|
+
const SpinGroupSeparator = ({
|
|
59736
|
+
children,
|
|
59737
|
+
...rest
|
|
59738
|
+
}) => jsx(Box, {
|
|
59739
|
+
as: "span",
|
|
59740
|
+
...rest,
|
|
59741
|
+
className: "navi_spin_group_separator",
|
|
59742
|
+
"aria-hidden": "true",
|
|
59743
|
+
children: children
|
|
59744
|
+
});
|
|
59745
|
+
SpinGroup.Separator = SpinGroupSeparator;
|
|
59746
|
+
|
|
58967
59747
|
/**
|
|
58968
59748
|
* A whole number one steps through and types into: the field IS the middle, so
|
|
58969
59749
|
* the value can be typed as readily as stepped, and the two chevrons stand
|
|
@@ -58976,16 +59756,26 @@ const renderValueDefault = value => String(value ?? "");
|
|
|
58976
59756
|
* min?: number,
|
|
58977
59757
|
* max?: number,
|
|
58978
59758
|
* step?: number,
|
|
59759
|
+
* pad?: number,
|
|
59760
|
+
* loop?: boolean,
|
|
58979
59761
|
* [key: string]: any,
|
|
58980
59762
|
* }>}
|
|
58981
59763
|
* @param {number} [min=0] The lowest number one can reach; `max` is the
|
|
58982
59764
|
* highest. They also bound what typing can produce, and how wide the field
|
|
58983
59765
|
* is asked to be (see `maxLength`).
|
|
59766
|
+
* @param {boolean} [loop] The numbers go round: the step after `max` is `min`,
|
|
59767
|
+
* and the one before `min` is `max`. Both ends have to be known for that.
|
|
59768
|
+
* @param {number} [pad] How many digits the number is written on, zeroes in
|
|
59769
|
+
* front of it: `pad={2}` writes 0 as "00". What an hour, a minute or a second
|
|
59770
|
+
* is read as — a clock says "07:00", never "7:0". Only the way it is written:
|
|
59771
|
+
* what the control holds, and what a form carries, is the number itself.
|
|
58984
59772
|
*/
|
|
58985
59773
|
const NumberSpin = ({
|
|
58986
59774
|
min = 0,
|
|
58987
59775
|
max,
|
|
58988
59776
|
step = 1,
|
|
59777
|
+
pad,
|
|
59778
|
+
loop,
|
|
58989
59779
|
vertical = true,
|
|
58990
59780
|
growsUpward = true,
|
|
58991
59781
|
controlProps,
|
|
@@ -58999,29 +59789,64 @@ const NumberSpin = ({
|
|
|
58999
59789
|
step: step,
|
|
59000
59790
|
vertical: vertical,
|
|
59001
59791
|
fallbackValue: min,
|
|
59002
|
-
valueAtStep: (value, count) => numberAtStep(value, count,
|
|
59792
|
+
valueAtStep: (value, count) => numberAtStep(value, count, {
|
|
59793
|
+
min,
|
|
59794
|
+
max,
|
|
59795
|
+
loop
|
|
59796
|
+
}),
|
|
59003
59797
|
compareValues: (a, b) => Number(a) - Number(b),
|
|
59004
59798
|
controlProps: {
|
|
59005
59799
|
// The numeric keypad on a phone, and — through
|
|
59006
59800
|
// input_resolver_mode — the "this field is full" event a group of
|
|
59007
59801
|
// fields moves along on (see useInputGroup).
|
|
59008
|
-
inputMode: "numeric",
|
|
59009
|
-
maxLength: max
|
|
59802
|
+
"inputMode": "numeric",
|
|
59803
|
+
"maxLength": numberMaxLength(max, pad),
|
|
59804
|
+
// What the number is HELD as stays a number; `pad` is how it is written
|
|
59805
|
+
// in the field (see asControlHostValue).
|
|
59806
|
+
"navi-value-pad": pad,
|
|
59010
59807
|
...controlProps
|
|
59011
59808
|
},
|
|
59012
59809
|
...rest
|
|
59013
59810
|
});
|
|
59014
59811
|
|
|
59812
|
+
// How many characters the field takes: what the biggest number is worth, or
|
|
59813
|
+
// what the padding writes, whichever is longer.
|
|
59814
|
+
const numberMaxLength = (max, pad) => {
|
|
59815
|
+
if (max === undefined) {
|
|
59816
|
+
return pad;
|
|
59817
|
+
}
|
|
59818
|
+
const maxLength = String(max).length;
|
|
59819
|
+
if (pad && pad > maxLength) {
|
|
59820
|
+
return pad;
|
|
59821
|
+
}
|
|
59822
|
+
return maxLength;
|
|
59823
|
+
};
|
|
59824
|
+
|
|
59015
59825
|
// One step away, bounds included: a step past `max` is a real number that
|
|
59016
59826
|
// simply is not allowed, and Spin is the one that reads it as "nothing that
|
|
59017
59827
|
// way" — clamping here would answer the chevron before it had a chance to say
|
|
59018
59828
|
// so. A field mid-edit holding nothing (or nothing numeric) starts from `min`.
|
|
59019
|
-
|
|
59829
|
+
//
|
|
59830
|
+
// Unless the numbers go round: the step after the last one is the first, so
|
|
59831
|
+
// the value handed back is always one Spin can reach and no chevron ever says
|
|
59832
|
+
// there is nothing that way. Going round needs both ends to be known — a
|
|
59833
|
+
// stretch open at one end has no other end to come back from.
|
|
59834
|
+
const numberAtStep = (value, count, {
|
|
59835
|
+
min,
|
|
59836
|
+
max,
|
|
59837
|
+
loop
|
|
59838
|
+
}) => {
|
|
59020
59839
|
const number = Number(value);
|
|
59021
59840
|
if (value === "" || value === undefined || Number.isNaN(number)) {
|
|
59022
59841
|
return min;
|
|
59023
59842
|
}
|
|
59024
|
-
|
|
59843
|
+
const numberNext = number + count;
|
|
59844
|
+
if (!loop || max === undefined) {
|
|
59845
|
+
return numberNext;
|
|
59846
|
+
}
|
|
59847
|
+
const valueCount = max - min + 1;
|
|
59848
|
+
const offset = numberNext - min;
|
|
59849
|
+
return min + (offset % valueCount + valueCount) % valueCount;
|
|
59025
59850
|
};
|
|
59026
59851
|
|
|
59027
59852
|
/**
|
|
@@ -59155,6 +59980,212 @@ const addDays = (day, count) => {
|
|
|
59155
59980
|
return dateToDay(date);
|
|
59156
59981
|
};
|
|
59157
59982
|
|
|
59983
|
+
/**
|
|
59984
|
+
* A time of day, and a span between two of them, written the way a clock
|
|
59985
|
+
* writes them: two digits, an "h" (a ":" in English) between hours and
|
|
59986
|
+
* minutes, and one frame around the whole thing — "07h00" is one value, not a
|
|
59987
|
+
* 7 beside a 0.
|
|
59988
|
+
*
|
|
59989
|
+
* `TimeSpin` is a `SpinGroup` of two `NumberSpin`s: it takes and hands back a
|
|
59990
|
+
* single "HH:MM", so a form carries one field for it. `TimeRangeSpin` is two
|
|
59991
|
+
* of those, and it carries `{ start, end }` — with the one rule such a pair
|
|
59992
|
+
* always has, "the end comes after the start", checked when the form is sent
|
|
59993
|
+
* (see time_range_constraint.js).
|
|
59994
|
+
*/
|
|
59995
|
+
|
|
59996
|
+
const HOUR_MAX = 23;
|
|
59997
|
+
const MINUTE_MAX = 59;
|
|
59998
|
+
|
|
59999
|
+
/**
|
|
60000
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
60001
|
+
* name?: string,
|
|
60002
|
+
* value?: string,
|
|
60003
|
+
* defaultValue?: string,
|
|
60004
|
+
* signal?: import("@preact/signals").Signal<string>,
|
|
60005
|
+
* minuteStep?: number,
|
|
60006
|
+
* pad?: number,
|
|
60007
|
+
* loop?: boolean,
|
|
60008
|
+
* separator?: import("ignore:preact").ComponentChildren,
|
|
60009
|
+
* hourLabel?: string,
|
|
60010
|
+
* minuteLabel?: string,
|
|
60011
|
+
* [key: string]: any,
|
|
60012
|
+
* }>}
|
|
60013
|
+
* @param {string} [value] The time shown, as "HH:MM".
|
|
60014
|
+
* @param {number} [minuteStep=1] How many minutes a press on a minute chevron
|
|
60015
|
+
* covers — 15 for quarters of an hour.
|
|
60016
|
+
* @param {boolean} [loop=true] The hours and the minutes go round: 23h then 0h,
|
|
60017
|
+
* 59 minutes then 0. What a clock does — and there is no first or last hour
|
|
60018
|
+
* of a day to stop at. Say `loop={false}` for two ends one cannot step past.
|
|
60019
|
+
* @param {number} [pad=2] How many digits an hour and a minute are written on:
|
|
60020
|
+
* a clock says "07:00", never "7:0". Say `pad={0}` for the bare numbers.
|
|
60021
|
+
* @param {import("ignore:preact").ComponentChildren} [separator] What is written
|
|
60022
|
+
* between the hours and the minutes. "h" in French, ":" elsewhere.
|
|
60023
|
+
* Everything a box takes is taken here too — `width`, `borderRadius`, `size`.
|
|
60024
|
+
*/
|
|
60025
|
+
const TimeSpin = ({
|
|
60026
|
+
minuteStep = 1,
|
|
60027
|
+
pad = 2,
|
|
60028
|
+
loop = true,
|
|
60029
|
+
separator = naviI18n("time.hour_separator"),
|
|
60030
|
+
hourLabel = naviI18n("time.hour_label"),
|
|
60031
|
+
minuteLabel = naviI18n("time.minute_label"),
|
|
60032
|
+
...rest
|
|
60033
|
+
}) => jsxs(SpinGroup, {
|
|
60034
|
+
aggregateChildStates: aggregateTime,
|
|
60035
|
+
distributeChildUIState: distributeTime,
|
|
60036
|
+
...rest,
|
|
60037
|
+
children: [jsx(NumberSpin, {
|
|
60038
|
+
name: "hour",
|
|
60039
|
+
min: 0,
|
|
60040
|
+
max: HOUR_MAX,
|
|
60041
|
+
pad: pad,
|
|
60042
|
+
loop: loop,
|
|
60043
|
+
controlProps: {
|
|
60044
|
+
"aria-label": hourLabel
|
|
60045
|
+
}
|
|
60046
|
+
}), jsx(SpinGroup.Separator, {
|
|
60047
|
+
children: separator
|
|
60048
|
+
}), jsx(NumberSpin, {
|
|
60049
|
+
name: "minute",
|
|
60050
|
+
min: 0,
|
|
60051
|
+
max: MINUTE_MAX,
|
|
60052
|
+
step: minuteStep,
|
|
60053
|
+
pad: pad,
|
|
60054
|
+
loop: loop,
|
|
60055
|
+
controlProps: {
|
|
60056
|
+
"aria-label": minuteLabel
|
|
60057
|
+
}
|
|
60058
|
+
})]
|
|
60059
|
+
});
|
|
60060
|
+
|
|
60061
|
+
// The two fields as one value, "HH:MM" — and nothing at all while one of them
|
|
60062
|
+
// is empty: half a time is not a time, and a form has nothing to send about it.
|
|
60063
|
+
const aggregateTime = childUIStateControllers => {
|
|
60064
|
+
let hour = "";
|
|
60065
|
+
let minute = "";
|
|
60066
|
+
for (const child of childUIStateControllers) {
|
|
60067
|
+
if (child.name === "hour") {
|
|
60068
|
+
hour = child.uiState ?? "";
|
|
60069
|
+
}
|
|
60070
|
+
if (child.name === "minute") {
|
|
60071
|
+
minute = child.uiState ?? "";
|
|
60072
|
+
}
|
|
60073
|
+
}
|
|
60074
|
+
if (hour === "" || minute === "") {
|
|
60075
|
+
return undefined;
|
|
60076
|
+
}
|
|
60077
|
+
return `${padTwo(hour)}:${padTwo(minute)}`;
|
|
60078
|
+
};
|
|
60079
|
+
|
|
60080
|
+
// The way back: what the group is set to (a picked value, a form being reset)
|
|
60081
|
+
// lands on the field it belongs to.
|
|
60082
|
+
const distributeTime = (groupState, childUIStateController) => {
|
|
60083
|
+
const parts = parseTime(groupState);
|
|
60084
|
+
if (!parts) {
|
|
60085
|
+
return undefined;
|
|
60086
|
+
}
|
|
60087
|
+
return parts[childUIStateController.name];
|
|
60088
|
+
};
|
|
60089
|
+
const parseTime = time => {
|
|
60090
|
+
if (typeof time !== "string") {
|
|
60091
|
+
return null;
|
|
60092
|
+
}
|
|
60093
|
+
const match = /^(\d{1,2}):(\d{1,2})/.exec(time);
|
|
60094
|
+
if (!match) {
|
|
60095
|
+
return null;
|
|
60096
|
+
}
|
|
60097
|
+
// Numbers: what an hour and a minute are held as. How they are written —
|
|
60098
|
+
// "07" — is the field's business (see NumberSpin's `pad`).
|
|
60099
|
+
return {
|
|
60100
|
+
hour: Number(match[1]),
|
|
60101
|
+
minute: Number(match[2])
|
|
60102
|
+
};
|
|
60103
|
+
};
|
|
60104
|
+
const padTwo = value => String(value).padStart(2, "0");
|
|
60105
|
+
|
|
60106
|
+
/**
|
|
60107
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
60108
|
+
* name?: string,
|
|
60109
|
+
* value?: { start?: string, end?: string },
|
|
60110
|
+
* defaultValue?: { start?: string, end?: string },
|
|
60111
|
+
* minuteStep?: number,
|
|
60112
|
+
* minDuration?: number,
|
|
60113
|
+
* pad?: number,
|
|
60114
|
+
* timeProps?: object,
|
|
60115
|
+
* loop?: boolean,
|
|
60116
|
+
* size?: string,
|
|
60117
|
+
* startLabel?: import("ignore:preact").ComponentChildren,
|
|
60118
|
+
* endLabel?: import("ignore:preact").ComponentChildren,
|
|
60119
|
+
* [key: string]: any,
|
|
60120
|
+
* }>}
|
|
60121
|
+
* @param {{ start?: string, end?: string }} [value] The span shown, as two
|
|
60122
|
+
* "HH:MM".
|
|
60123
|
+
* @param {import("ignore:preact").ComponentChildren} [startLabel] What is written
|
|
60124
|
+
* before the first time ("De"), and `endLabel` between the two ("à"). Say
|
|
60125
|
+
* `null` for neither.
|
|
60126
|
+
* @param {number} [minuteStep=1] How many minutes a press on a minute chevron
|
|
60127
|
+
* covers, on both times.
|
|
60128
|
+
* @param {object} [timeProps] Anything a `TimeSpin` takes, said once for both
|
|
60129
|
+
* of them — a radius, a width. `startTimeProps`/`endTimeProps` say it to one
|
|
60130
|
+
* of the two, and win over this one.
|
|
60131
|
+
* @param {number} [minDuration=0] How long the span must last at least, in
|
|
60132
|
+
* minutes. Zero by default: a span of no length is a span all the same, only
|
|
60133
|
+
* one that goes backwards is not. Checked when the form is sent, and the
|
|
60134
|
+
* answer is given on the end time.
|
|
60135
|
+
*/
|
|
60136
|
+
const TimeRangeSpin = ({
|
|
60137
|
+
minuteStep = 1,
|
|
60138
|
+
minDuration = 0,
|
|
60139
|
+
pad = 2,
|
|
60140
|
+
loop = true,
|
|
60141
|
+
size,
|
|
60142
|
+
startLabel = naviI18n("time_range.from"),
|
|
60143
|
+
endLabel = naviI18n("time_range.to"),
|
|
60144
|
+
timeProps,
|
|
60145
|
+
startTimeProps,
|
|
60146
|
+
endTimeProps,
|
|
60147
|
+
...rest
|
|
60148
|
+
}) => {
|
|
60149
|
+
const startId = useId();
|
|
60150
|
+
return jsxs(ControlGroup, {
|
|
60151
|
+
flex: true,
|
|
60152
|
+
alignY: "center",
|
|
60153
|
+
spacing: "s",
|
|
60154
|
+
size: size,
|
|
60155
|
+
...rest,
|
|
60156
|
+
children: [startLabel === null ? null : jsx(Text, {
|
|
60157
|
+
size: size,
|
|
60158
|
+
children: startLabel
|
|
60159
|
+
}), jsx(TimeSpin, {
|
|
60160
|
+
id: startId,
|
|
60161
|
+
name: "start",
|
|
60162
|
+
minuteStep: minuteStep,
|
|
60163
|
+
pad: pad,
|
|
60164
|
+
loop: loop,
|
|
60165
|
+
size: size,
|
|
60166
|
+
...timeProps,
|
|
60167
|
+
...startTimeProps
|
|
60168
|
+
}), endLabel === null ? null : jsx(Text, {
|
|
60169
|
+
size: size,
|
|
60170
|
+
children: endLabel
|
|
60171
|
+
}), jsx(TimeSpin, {
|
|
60172
|
+
name: "end",
|
|
60173
|
+
minuteStep: minuteStep,
|
|
60174
|
+
pad: pad,
|
|
60175
|
+
loop: loop,
|
|
60176
|
+
size: size
|
|
60177
|
+
// Which time it comes after, and how much room there must be between
|
|
60178
|
+
// the two: said on the LATER of the two, so the answer is given where
|
|
60179
|
+
// the time one would have to move is (see time_range_constraint.js).
|
|
60180
|
+
,
|
|
60181
|
+
"data-time-after": startId,
|
|
60182
|
+
"data-time-min-duration": minDuration,
|
|
60183
|
+
...timeProps,
|
|
60184
|
+
...endTimeProps
|
|
60185
|
+
})]
|
|
60186
|
+
});
|
|
60187
|
+
};
|
|
60188
|
+
|
|
59158
60189
|
installImportMetaCssBuild(import.meta);// TOFIX: select in data then reset, it reset to red/blue instead of red/blue/green
|
|
59159
60190
|
const css$p = /* css */`
|
|
59160
60191
|
.navi_checkbox_group {
|
|
@@ -59575,228 +60606,6 @@ const formatIntlUnit = (unit, {
|
|
|
59575
60606
|
}
|
|
59576
60607
|
};
|
|
59577
60608
|
|
|
59578
|
-
/**
|
|
59579
|
-
* Wraps multiple inputs together and handles keyboard navigation and paste
|
|
59580
|
-
* distribution between them.
|
|
59581
|
-
*
|
|
59582
|
-
* Keyboard navigation:
|
|
59583
|
-
* ArrowRight at the end of an input moves focus to the next input.
|
|
59584
|
-
* ArrowLeft at the start of an input moves focus to the previous input.
|
|
59585
|
-
* navi_input_full (emitted when an input reaches maxLength) also moves forward.
|
|
59586
|
-
*
|
|
59587
|
-
* Paste distribution:
|
|
59588
|
-
* When an input has a data-separator attribute, pasting a string that
|
|
59589
|
-
* contains that separator (e.g. "27/04/1990" into a day input with
|
|
59590
|
-
* data-separator="/") splits the text on each separator and fills the
|
|
59591
|
-
* corresponding sub-inputs in order.
|
|
59592
|
-
*/
|
|
59593
|
-
const useInputGroup = (ref) => {
|
|
59594
|
-
const debugFocus = useDebugFocus();
|
|
59595
|
-
|
|
59596
|
-
useEffect(() => {
|
|
59597
|
-
const el = ref.current;
|
|
59598
|
-
if (!el) {
|
|
59599
|
-
return () => {};
|
|
59600
|
-
}
|
|
59601
|
-
|
|
59602
|
-
const getInputs = () =>
|
|
59603
|
-
Array.from(el.querySelectorAll(".navi_control_input"));
|
|
59604
|
-
|
|
59605
|
-
const focusInput = (input) => {
|
|
59606
|
-
input.focus();
|
|
59607
|
-
input.select();
|
|
59608
|
-
};
|
|
59609
|
-
|
|
59610
|
-
const handleKeyDown = (e) => {
|
|
59611
|
-
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") {
|
|
59612
|
-
return;
|
|
59613
|
-
}
|
|
59614
|
-
const active = document.activeElement;
|
|
59615
|
-
if (!isTextInputElement(active) || !el.contains(active)) {
|
|
59616
|
-
return;
|
|
59617
|
-
}
|
|
59618
|
-
if (e.key === "ArrowRight") {
|
|
59619
|
-
const allSelected =
|
|
59620
|
-
active.selectionStart === 0 &&
|
|
59621
|
-
active.selectionEnd === active.value.length;
|
|
59622
|
-
const atEnd =
|
|
59623
|
-
allSelected ||
|
|
59624
|
-
(active.selectionStart === active.value.length &&
|
|
59625
|
-
active.selectionEnd === active.value.length);
|
|
59626
|
-
if (!atEnd) {
|
|
59627
|
-
return;
|
|
59628
|
-
}
|
|
59629
|
-
const inputs = getInputs();
|
|
59630
|
-
const idx = inputs.indexOf(active);
|
|
59631
|
-
if (idx === -1) {
|
|
59632
|
-
debugFocus(
|
|
59633
|
-
e,
|
|
59634
|
-
"InputGroup ArrowRight on non group input → do nothing",
|
|
59635
|
-
);
|
|
59636
|
-
return;
|
|
59637
|
-
}
|
|
59638
|
-
if (idx === inputs.length - 1) {
|
|
59639
|
-
debugFocus(
|
|
59640
|
-
e,
|
|
59641
|
-
"InputGroup ArrowRight at end of last input → do nothing",
|
|
59642
|
-
);
|
|
59643
|
-
return;
|
|
59644
|
-
}
|
|
59645
|
-
|
|
59646
|
-
debugFocus(
|
|
59647
|
-
e,
|
|
59648
|
-
"InputGroup ArrowRight at end of input[%d] → focus input[%d]",
|
|
59649
|
-
idx,
|
|
59650
|
-
idx + 1,
|
|
59651
|
-
);
|
|
59652
|
-
e.preventDefault();
|
|
59653
|
-
focusInput(inputs[idx + 1]);
|
|
59654
|
-
return;
|
|
59655
|
-
}
|
|
59656
|
-
const allSelected =
|
|
59657
|
-
active.selectionStart === 0 &&
|
|
59658
|
-
active.selectionEnd === active.value.length;
|
|
59659
|
-
const atStart =
|
|
59660
|
-
allSelected ||
|
|
59661
|
-
(active.selectionStart === 0 && active.selectionEnd === 0);
|
|
59662
|
-
if (!atStart) {
|
|
59663
|
-
return;
|
|
59664
|
-
}
|
|
59665
|
-
const inputs = getInputs();
|
|
59666
|
-
const idx = inputs.indexOf(active);
|
|
59667
|
-
if (idx === 0) {
|
|
59668
|
-
return;
|
|
59669
|
-
}
|
|
59670
|
-
debugFocus(
|
|
59671
|
-
e,
|
|
59672
|
-
"InputGroup ArrowLeft at start of input[%d] → focus input[%d]",
|
|
59673
|
-
idx,
|
|
59674
|
-
idx - 1,
|
|
59675
|
-
);
|
|
59676
|
-
e.preventDefault();
|
|
59677
|
-
focusInput(inputs[idx - 1]);
|
|
59678
|
-
};
|
|
59679
|
-
|
|
59680
|
-
const handleNaviInputFull = (e) => {
|
|
59681
|
-
if (!e.detail.event?.isTrusted) {
|
|
59682
|
-
// Programmatic value change (e.g. ArrowUp/Down) — don't auto-advance.
|
|
59683
|
-
return;
|
|
59684
|
-
}
|
|
59685
|
-
const input = e.detail.event.currentTarget;
|
|
59686
|
-
if (!el.contains(input)) {
|
|
59687
|
-
return;
|
|
59688
|
-
}
|
|
59689
|
-
const inputs = getInputs();
|
|
59690
|
-
const idx = inputs.indexOf(input);
|
|
59691
|
-
if (idx === -1) {
|
|
59692
|
-
return;
|
|
59693
|
-
}
|
|
59694
|
-
if (idx === inputs.length - 1) {
|
|
59695
|
-
return;
|
|
59696
|
-
}
|
|
59697
|
-
const nextInput = inputs[idx + 1];
|
|
59698
|
-
debugFocus(
|
|
59699
|
-
e,
|
|
59700
|
-
"InputGroup navi_input_full on input -> move to next input",
|
|
59701
|
-
input,
|
|
59702
|
-
nextInput,
|
|
59703
|
-
);
|
|
59704
|
-
e.preventDefault();
|
|
59705
|
-
focusInput(nextInput);
|
|
59706
|
-
};
|
|
59707
|
-
|
|
59708
|
-
// const handlePaste = (e) => {
|
|
59709
|
-
// const active = document.activeElement;
|
|
59710
|
-
// if (!isTextInputElement(active) || !el.contains(active)) {
|
|
59711
|
-
// return;
|
|
59712
|
-
// }
|
|
59713
|
-
// const inputs = getInputs();
|
|
59714
|
-
// const startIdx = inputs.indexOf(active);
|
|
59715
|
-
// if (startIdx === -1) {
|
|
59716
|
-
// return;
|
|
59717
|
-
// }
|
|
59718
|
-
// const pastedText = e.clipboardData?.getData("text") ?? "";
|
|
59719
|
-
// if (!pastedText) {
|
|
59720
|
-
// return;
|
|
59721
|
-
// }
|
|
59722
|
-
// // Only intercept when the pasted text contains at least one separator
|
|
59723
|
-
// // from the inputs starting at the focused position.
|
|
59724
|
-
// const remainingInputs = inputs.slice(startIdx);
|
|
59725
|
-
// const hasSeparatorMatch = remainingInputs.some(
|
|
59726
|
-
// (input) =>
|
|
59727
|
-
// input.dataset.separator &&
|
|
59728
|
-
// pastedText.includes(input.dataset.separator),
|
|
59729
|
-
// );
|
|
59730
|
-
// if (!hasSeparatorMatch) {
|
|
59731
|
-
// return;
|
|
59732
|
-
// }
|
|
59733
|
-
// e.preventDefault();
|
|
59734
|
-
// let remaining = pastedText;
|
|
59735
|
-
// let lastFilledIdx = startIdx;
|
|
59736
|
-
// for (let i = 0; i < remainingInputs.length; i++) {
|
|
59737
|
-
// const input = remainingInputs[i];
|
|
59738
|
-
// const separator = input.dataset.separator;
|
|
59739
|
-
// let part;
|
|
59740
|
-
// if (separator && remaining.includes(separator)) {
|
|
59741
|
-
// const sepIdx = remaining.indexOf(separator);
|
|
59742
|
-
// part = remaining.slice(0, sepIdx);
|
|
59743
|
-
// remaining = remaining.slice(sepIdx + separator.length);
|
|
59744
|
-
// } else {
|
|
59745
|
-
// part = remaining;
|
|
59746
|
-
// remaining = "";
|
|
59747
|
-
// }
|
|
59748
|
-
// requestSubPaste(input, part, e);
|
|
59749
|
-
// lastFilledIdx = startIdx + i;
|
|
59750
|
-
// if (remaining === "") {
|
|
59751
|
-
// break;
|
|
59752
|
-
// }
|
|
59753
|
-
// }
|
|
59754
|
-
// focusInput(inputs[lastFilledIdx]);
|
|
59755
|
-
// };
|
|
59756
|
-
|
|
59757
|
-
el.addEventListener("keydown", handleKeyDown, { capture: true });
|
|
59758
|
-
el.addEventListener("navi_input_full", handleNaviInputFull);
|
|
59759
|
-
// el.addEventListener("paste", handlePaste, { capture: true });
|
|
59760
|
-
return () => {
|
|
59761
|
-
el.removeEventListener("keydown", handleKeyDown, { capture: true });
|
|
59762
|
-
el.removeEventListener("navi_input_full", handleNaviInputFull);
|
|
59763
|
-
// el.removeEventListener("paste", handlePaste, { capture: true });
|
|
59764
|
-
};
|
|
59765
|
-
}, [debugFocus]);
|
|
59766
|
-
};
|
|
59767
|
-
|
|
59768
|
-
// const requestSubPaste = (input, value, event) => {
|
|
59769
|
-
// dispatchRequestInteraction(input, {
|
|
59770
|
-
// event,
|
|
59771
|
-
// name: "subpaste",
|
|
59772
|
-
// allowed: () => {
|
|
59773
|
-
// dispatchRequestSetUIState(input, value, { event });
|
|
59774
|
-
// },
|
|
59775
|
-
// });
|
|
59776
|
-
// };
|
|
59777
|
-
|
|
59778
|
-
const isTextInputElement = (el) => {
|
|
59779
|
-
if (!el) {
|
|
59780
|
-
return false;
|
|
59781
|
-
}
|
|
59782
|
-
if (el.tagName === "TEXTAREA") {
|
|
59783
|
-
return true;
|
|
59784
|
-
}
|
|
59785
|
-
if (el.tagName !== "INPUT") {
|
|
59786
|
-
return false;
|
|
59787
|
-
}
|
|
59788
|
-
const type = el.type || "text";
|
|
59789
|
-
return (
|
|
59790
|
-
type === "text" ||
|
|
59791
|
-
type === "search" ||
|
|
59792
|
-
type === "url" ||
|
|
59793
|
-
type === "tel" ||
|
|
59794
|
-
type === "email" ||
|
|
59795
|
-
type === "password" ||
|
|
59796
|
-
type === "number"
|
|
59797
|
-
);
|
|
59798
|
-
};
|
|
59799
|
-
|
|
59800
60609
|
installImportMetaCssBuild(import.meta);const css$n = /* css */`
|
|
59801
60610
|
.navi_input_duration {
|
|
59802
60611
|
--duration-separator-spacing: 4px;
|
|
@@ -69182,5 +69991,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
69182
69991
|
})
|
|
69183
69992
|
});
|
|
69184
69993
|
|
|
69185
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
69994
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeSpin, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
69186
69995
|
//# sourceMappingURL=jsenv_navi.js.map
|