@jsenv/navi 0.29.361 → 0.29.363
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev/jsenv_navi.js +328 -144
- package/dist/dev/jsenv_navi.js.map +15 -13
- package/dist/jsenv_navi.js +328 -144
- package/dist/jsenv_navi.js.map +15 -13
- package/docs/popup_open.md +2 -1
- package/docs/scroll.md +6 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -8914,6 +8914,17 @@ const naviI18nFromValidityMessage = ({ key, params }) => {
|
|
|
8914
8914
|
return naviI18n(`constraint.${key}`, params);
|
|
8915
8915
|
};
|
|
8916
8916
|
|
|
8917
|
+
// The characters a ui state holds, for the rules that read a value as text
|
|
8918
|
+
// (length, pattern, number, email…). `undefined` and `null` both hold none:
|
|
8919
|
+
// `String(null)` would hand those rules the four letters "null" — a number
|
|
8920
|
+
// field cleared with `value={null}` then reported "must be a number".
|
|
8921
|
+
const uiStateAsText = (uiState) => {
|
|
8922
|
+
if (uiState === undefined || uiState === null) {
|
|
8923
|
+
return "";
|
|
8924
|
+
}
|
|
8925
|
+
return String(uiState);
|
|
8926
|
+
};
|
|
8927
|
+
|
|
8917
8928
|
/**
|
|
8918
8929
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Constraint_validation
|
|
8919
8930
|
*/
|
|
@@ -8980,8 +8991,7 @@ const REQUIRED_CONSTRAINT = {
|
|
|
8980
8991
|
return naviI18n("constraint.required.checkbox");
|
|
8981
8992
|
}
|
|
8982
8993
|
|
|
8983
|
-
const valueAsString =
|
|
8984
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
8994
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
8985
8995
|
if (valueAsString) {
|
|
8986
8996
|
return null;
|
|
8987
8997
|
}
|
|
@@ -9032,8 +9042,7 @@ const PATTERN_CONSTRAINT = {
|
|
|
9032
9042
|
if (!pattern) {
|
|
9033
9043
|
return null;
|
|
9034
9044
|
}
|
|
9035
|
-
const valueAsString =
|
|
9036
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9045
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9037
9046
|
if (!valueAsString) {
|
|
9038
9047
|
return null;
|
|
9039
9048
|
}
|
|
@@ -9068,8 +9077,7 @@ const TYPE_EMAIL_CONSTRAINT = {
|
|
|
9068
9077
|
if (type !== "email") {
|
|
9069
9078
|
return null;
|
|
9070
9079
|
}
|
|
9071
|
-
const valueAsString =
|
|
9072
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9080
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9073
9081
|
if (!valueAsString) {
|
|
9074
9082
|
return null;
|
|
9075
9083
|
}
|
|
@@ -9102,8 +9110,7 @@ const MIN_LENGTH_CONSTRAINT = {
|
|
|
9102
9110
|
if (minLength === undefined) {
|
|
9103
9111
|
return null;
|
|
9104
9112
|
}
|
|
9105
|
-
const valueAsString =
|
|
9106
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9113
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9107
9114
|
if (!valueAsString && !field.controlHostProps.required) {
|
|
9108
9115
|
return null;
|
|
9109
9116
|
}
|
|
@@ -9198,8 +9205,7 @@ const MAX_LENGTH_CONSTRAINT = {
|
|
|
9198
9205
|
if (maxLength === undefined) {
|
|
9199
9206
|
return null;
|
|
9200
9207
|
}
|
|
9201
|
-
const valueAsString =
|
|
9202
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9208
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9203
9209
|
if (!valueAsString) {
|
|
9204
9210
|
return null;
|
|
9205
9211
|
}
|
|
@@ -9240,8 +9246,7 @@ const TYPE_NUMBER_CONSTRAINT = {
|
|
|
9240
9246
|
if (!isNumberInput(type, naviType)) {
|
|
9241
9247
|
return null;
|
|
9242
9248
|
}
|
|
9243
|
-
const valueAsString =
|
|
9244
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9249
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9245
9250
|
if (!valueAsString) {
|
|
9246
9251
|
return null;
|
|
9247
9252
|
}
|
|
@@ -9306,8 +9311,7 @@ const MIN_CONSTRAINT = {
|
|
|
9306
9311
|
}
|
|
9307
9312
|
const type = field.controlHostProps.type;
|
|
9308
9313
|
const naviInputType = field.controlHostProps["navi-input-type"];
|
|
9309
|
-
const valueAsString =
|
|
9310
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9314
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9311
9315
|
if (!valueAsString) {
|
|
9312
9316
|
return null;
|
|
9313
9317
|
}
|
|
@@ -9406,8 +9410,7 @@ const MAX_CONSTRAINT = {
|
|
|
9406
9410
|
}
|
|
9407
9411
|
const type = field.controlHostProps.type;
|
|
9408
9412
|
const naviInputType = field.controlHostProps["navi-input-type"];
|
|
9409
|
-
const valueAsString =
|
|
9410
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9413
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9411
9414
|
if (!valueAsString) {
|
|
9412
9415
|
return null;
|
|
9413
9416
|
}
|
|
@@ -9540,8 +9543,7 @@ const STEP_CONSTRAINT = {
|
|
|
9540
9543
|
return null;
|
|
9541
9544
|
}
|
|
9542
9545
|
const stepString = String(stepRaw);
|
|
9543
|
-
const valueAsString =
|
|
9544
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9546
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9545
9547
|
if (!valueAsString) {
|
|
9546
9548
|
return null;
|
|
9547
9549
|
}
|
|
@@ -12341,8 +12343,7 @@ const DISPLAYABLE_CONSTRAINT = {
|
|
|
12341
12343
|
if (!isConstraintAttributeOn(displayable)) {
|
|
12342
12344
|
return null;
|
|
12343
12345
|
}
|
|
12344
|
-
const valueAsString =
|
|
12345
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12346
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12346
12347
|
const maxStackedMarksAttribute =
|
|
12347
12348
|
field.controlHostProps["data-max-stacked-marks"];
|
|
12348
12349
|
const result = DISPLAYABLE_RULE.applyOn(true, valueAsString, {
|
|
@@ -12383,8 +12384,7 @@ const MAX_LINE_BREAKS_CONSTRAINT = {
|
|
|
12383
12384
|
if (isNaN(maxLineBreaks)) {
|
|
12384
12385
|
return null;
|
|
12385
12386
|
}
|
|
12386
|
-
const valueAsString =
|
|
12387
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12387
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12388
12388
|
const result = MAX_LINE_BREAKS_RULE.applyOn(maxLineBreaks, valueAsString);
|
|
12389
12389
|
if (!result) {
|
|
12390
12390
|
return null;
|
|
@@ -12413,8 +12413,7 @@ const NO_EMOJI_CONSTRAINT = {
|
|
|
12413
12413
|
if (!isConstraintAttributeOn(noEmoji)) {
|
|
12414
12414
|
return null;
|
|
12415
12415
|
}
|
|
12416
|
-
const valueAsString =
|
|
12417
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12416
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12418
12417
|
const result = NO_EMOJI_RULE.applyOn(true, valueAsString);
|
|
12419
12418
|
if (!result) {
|
|
12420
12419
|
return null;
|
|
@@ -12428,8 +12427,7 @@ const MIN_LOWER_LETTER_CONSTRAINT = {
|
|
|
12428
12427
|
name: "min_lower_letter",
|
|
12429
12428
|
messageAttribute: "data-min-lower-letter-message",
|
|
12430
12429
|
check: (field) => {
|
|
12431
|
-
const valueAsString =
|
|
12432
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12430
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12433
12431
|
const required = field.controlHostProps.required;
|
|
12434
12432
|
if (!valueAsString && !required) {
|
|
12435
12433
|
return "";
|
|
@@ -12474,8 +12472,7 @@ const MIN_UPPER_LETTER_CONSTRAINT = {
|
|
|
12474
12472
|
name: "min_upper_letter",
|
|
12475
12473
|
messageAttribute: "data-min-upper-letter-message",
|
|
12476
12474
|
check: (field) => {
|
|
12477
|
-
const valueAsString =
|
|
12478
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12475
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12479
12476
|
const required = field.controlHostProps.required;
|
|
12480
12477
|
if (!valueAsString && !required) {
|
|
12481
12478
|
return null;
|
|
@@ -12511,8 +12508,7 @@ const MIN_DIGIT_CONSTRAINT = {
|
|
|
12511
12508
|
name: "min_digit",
|
|
12512
12509
|
messageAttribute: "data-min-digit-message",
|
|
12513
12510
|
check: (field) => {
|
|
12514
|
-
const valueAsString =
|
|
12515
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12511
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12516
12512
|
const required = field.controlHostProps.required;
|
|
12517
12513
|
if (!valueAsString && !required) {
|
|
12518
12514
|
return null;
|
|
@@ -12548,8 +12544,7 @@ const MIN_SPECIAL_CHAR_CONSTRAINT = {
|
|
|
12548
12544
|
name: "min_special_char",
|
|
12549
12545
|
messageAttribute: "data-min-special-char-message",
|
|
12550
12546
|
check: (field) => {
|
|
12551
|
-
const valueAsString =
|
|
12552
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12547
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12553
12548
|
const required = field.controlHostProps.required;
|
|
12554
12549
|
if (!valueAsString && !required) {
|
|
12555
12550
|
return null;
|
|
@@ -12666,8 +12661,7 @@ const SAME_AS_CONSTRAINT = {
|
|
|
12666
12661
|
// Reference field is empty — nothing to compare against yet.
|
|
12667
12662
|
return null;
|
|
12668
12663
|
}
|
|
12669
|
-
const valueAsString =
|
|
12670
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12664
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12671
12665
|
if (valueAsString === otherFieldValue) {
|
|
12672
12666
|
return null;
|
|
12673
12667
|
}
|
|
@@ -12708,8 +12702,7 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-same-as");
|
|
|
12708
12702
|
|
|
12709
12703
|
|
|
12710
12704
|
const applyRule = (field) => {
|
|
12711
|
-
const valueAsString =
|
|
12712
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12705
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12713
12706
|
return SINGLE_SPACE_RULE.applyOn(true, valueAsString);
|
|
12714
12707
|
};
|
|
12715
12708
|
|
|
@@ -26978,6 +26971,18 @@ const takeoverRoutingRenderingHold = () => {
|
|
|
26978
26971
|
* to a reader. Kept in the session too, so a reload lands where the browser
|
|
26979
26972
|
* would have landed — the flag above is a promise to do the whole job.
|
|
26980
26973
|
*
|
|
26974
|
+
* The document is not the only scrollport of a page. A list scrolling itself
|
|
26975
|
+
* (a `<List expandY>` under a search field that stays put) is left and come
|
|
26976
|
+
* back to the same way, and the browser never knew it was a scrollport at
|
|
26977
|
+
* all: those say where they are by name (rememberScrollerPosition), under the
|
|
26978
|
+
* same URL and for the same session, and ask it back when they mount again
|
|
26979
|
+
* (recallScrollerPosition). What a push means for them is what it means for
|
|
26980
|
+
* the document — an arrival opens at the top (see startAtTop): the page
|
|
26981
|
+
* arrived at has its named positions dropped before its lists render, so a
|
|
26982
|
+
* list recalls only on the way back. A scroller that goes while its page
|
|
26983
|
+
* stays — a popup closing over the same address — has nothing to come back
|
|
26984
|
+
* to, and says so as it leaves (forgetScrollerUnlessPageLeft).
|
|
26985
|
+
*
|
|
26981
26986
|
* What is NOT covered, and cannot be from here: a page whose height depends on
|
|
26982
26987
|
* something still loading. Its content is not there at the moment it is put
|
|
26983
26988
|
* back, so a position beyond what has arrived is clamped as before. Only the
|
|
@@ -26995,7 +27000,23 @@ const takeoverRoutingRenderingHold = () => {
|
|
|
26995
27000
|
const STORAGE_KEY = "navi_scroll_positions";
|
|
26996
27001
|
|
|
26997
27002
|
const positionByUrl = new Map();
|
|
26998
|
-
|
|
27003
|
+
// url -> (scroller name -> position). The position is whatever the scroller
|
|
27004
|
+
// handed in: what it can put itself back on, in its own terms.
|
|
27005
|
+
const scrollerPositionsByUrl = new Map();
|
|
27006
|
+
// The url each named scroller last spoke under: what tells a scroller leaving
|
|
27007
|
+
// a page that stays from one leaving with its page.
|
|
27008
|
+
const urlByScrollerName = new Map();
|
|
27009
|
+
|
|
27010
|
+
// Read once, the first time anyone needs the positions: the document's
|
|
27011
|
+
// restoration is installed by the routing, and a list remembering itself may
|
|
27012
|
+
// mount in an app that never routes.
|
|
27013
|
+
let storeLoaded = false;
|
|
27014
|
+
const loadStore = () => {
|
|
27015
|
+
if (storeLoaded) {
|
|
27016
|
+
return;
|
|
27017
|
+
}
|
|
27018
|
+
storeLoaded = true;
|
|
27019
|
+
window.addEventListener("pagehide", storePositions);
|
|
26999
27020
|
let stored;
|
|
27000
27021
|
try {
|
|
27001
27022
|
stored = window.sessionStorage.getItem(STORAGE_KEY);
|
|
@@ -27008,18 +27029,31 @@ const readStoredPositions = () => {
|
|
|
27008
27029
|
return;
|
|
27009
27030
|
}
|
|
27010
27031
|
try {
|
|
27011
|
-
|
|
27032
|
+
const { document: documentPositions, scrollers } = JSON.parse(stored);
|
|
27033
|
+
for (const [url, position] of Object.entries(documentPositions || {})) {
|
|
27012
27034
|
positionByUrl.set(url, position);
|
|
27013
27035
|
}
|
|
27036
|
+
for (const [url, positionByName] of Object.entries(scrollers || {})) {
|
|
27037
|
+
scrollerPositionsByUrl.set(url, new Map(Object.entries(positionByName)));
|
|
27038
|
+
}
|
|
27014
27039
|
} catch {
|
|
27015
27040
|
// Something else wrote there, or it was truncated.
|
|
27016
27041
|
}
|
|
27017
27042
|
};
|
|
27018
27043
|
const storePositions = () => {
|
|
27044
|
+
const scrollers = {};
|
|
27045
|
+
for (const [url, positionByName] of scrollerPositionsByUrl) {
|
|
27046
|
+
if (positionByName.size > 0) {
|
|
27047
|
+
scrollers[url] = Object.fromEntries(positionByName);
|
|
27048
|
+
}
|
|
27049
|
+
}
|
|
27019
27050
|
try {
|
|
27020
27051
|
window.sessionStorage.setItem(
|
|
27021
27052
|
STORAGE_KEY,
|
|
27022
|
-
JSON.stringify(
|
|
27053
|
+
JSON.stringify({
|
|
27054
|
+
document: Object.fromEntries(positionByUrl),
|
|
27055
|
+
scrollers,
|
|
27056
|
+
}),
|
|
27023
27057
|
);
|
|
27024
27058
|
} catch {
|
|
27025
27059
|
// Full, or refused: the session is the only thing lost.
|
|
@@ -27059,7 +27093,7 @@ const installScrollRestoration = () => {
|
|
|
27059
27093
|
return;
|
|
27060
27094
|
}
|
|
27061
27095
|
window.history.scrollRestoration = "manual";
|
|
27062
|
-
|
|
27096
|
+
loadStore();
|
|
27063
27097
|
// Read as it happens rather than when leaving: a traverse changes the url
|
|
27064
27098
|
// before anything here is told, so a position read then would be read for
|
|
27065
27099
|
// the wrong page.
|
|
@@ -27076,7 +27110,6 @@ const installScrollRestoration = () => {
|
|
|
27076
27110
|
},
|
|
27077
27111
|
{ passive: true },
|
|
27078
27112
|
);
|
|
27079
|
-
window.addEventListener("pagehide", storePositions);
|
|
27080
27113
|
// What a reload asks for, now that the browser has been told not to do it.
|
|
27081
27114
|
// Once, and at the first render of a route: the position is only meaningful
|
|
27082
27115
|
// once there is a page under it.
|
|
@@ -27119,18 +27152,35 @@ const restoreScrollPosition = (url) => {
|
|
|
27119
27152
|
// The document, because the document is the scrollport in the common case. An
|
|
27120
27153
|
// app that scrolls an element of its own scrolls it itself.
|
|
27121
27154
|
const startAtTop = (url, { from } = {}) => {
|
|
27155
|
+
if (!isArrival(url, { from })) {
|
|
27156
|
+
return;
|
|
27157
|
+
}
|
|
27158
|
+
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
27159
|
+
};
|
|
27160
|
+
const isArrival = (url, { from }) => {
|
|
27122
27161
|
const urlObject = new URL(url, window.location.href);
|
|
27123
27162
|
// A fragment names where to land, and the browser is the one that finds it.
|
|
27124
27163
|
if (urlObject.hash) {
|
|
27125
|
-
return;
|
|
27164
|
+
return false;
|
|
27126
27165
|
}
|
|
27127
27166
|
if (
|
|
27128
27167
|
from !== undefined &&
|
|
27129
27168
|
new URL(from, window.location.href).pathname === urlObject.pathname
|
|
27130
27169
|
) {
|
|
27170
|
+
return false;
|
|
27171
|
+
}
|
|
27172
|
+
return true;
|
|
27173
|
+
};
|
|
27174
|
+
|
|
27175
|
+
// The same arrival, for the page's own scrollers. The document is scrolled to
|
|
27176
|
+
// its top once the page is there; a list opens where it decides to in its
|
|
27177
|
+
// first render, so what it must not find is dropped before the routing
|
|
27178
|
+
// renders anything.
|
|
27179
|
+
const forgetScrollersOnArrival = (url, { from } = {}) => {
|
|
27180
|
+
if (!isArrival(url, { from })) {
|
|
27131
27181
|
return;
|
|
27132
27182
|
}
|
|
27133
|
-
|
|
27183
|
+
scrollerPositionsByUrl.delete(new URL(url, window.location.href).href);
|
|
27134
27184
|
};
|
|
27135
27185
|
|
|
27136
27186
|
// An arrival at a page whose scrollport is already showing another one: the
|
|
@@ -27149,6 +27199,49 @@ const scrollTo = ({ x, y }) => {
|
|
|
27149
27199
|
window.scrollTo({ top: y, left: x, behavior: "instant" });
|
|
27150
27200
|
};
|
|
27151
27201
|
|
|
27202
|
+
const rememberScrollerPosition = (name, position) => {
|
|
27203
|
+
loadStore();
|
|
27204
|
+
const url = window.location.href;
|
|
27205
|
+
let positionByName = scrollerPositionsByUrl.get(url);
|
|
27206
|
+
if (!positionByName) {
|
|
27207
|
+
positionByName = new Map();
|
|
27208
|
+
scrollerPositionsByUrl.set(url, positionByName);
|
|
27209
|
+
}
|
|
27210
|
+
positionByName.set(name, position);
|
|
27211
|
+
urlByScrollerName.set(name, url);
|
|
27212
|
+
};
|
|
27213
|
+
|
|
27214
|
+
const recallScrollerPosition = (name) => {
|
|
27215
|
+
loadStore();
|
|
27216
|
+
const positionByName = scrollerPositionsByUrl.get(window.location.href);
|
|
27217
|
+
if (!positionByName) {
|
|
27218
|
+
return undefined;
|
|
27219
|
+
}
|
|
27220
|
+
return positionByName.get(name);
|
|
27221
|
+
};
|
|
27222
|
+
|
|
27223
|
+
// Said by a scroller as it unmounts. Its page is being left when the url is
|
|
27224
|
+
// already another one — the history is written before the page it names is
|
|
27225
|
+
// taken down — and then its position is kept for the way back. The url still
|
|
27226
|
+
// being the one it spoke under means the page stays and the scroller alone
|
|
27227
|
+
// goes (a popup closing, a section folding): there is no coming back to a
|
|
27228
|
+
// place that was not left, and a position kept would greet the next mount
|
|
27229
|
+
// under this address as a return.
|
|
27230
|
+
const forgetScrollerUnlessPageLeft = (name) => {
|
|
27231
|
+
const url = urlByScrollerName.get(name);
|
|
27232
|
+
if (url === undefined) {
|
|
27233
|
+
return;
|
|
27234
|
+
}
|
|
27235
|
+
urlByScrollerName.delete(name);
|
|
27236
|
+
if (url !== window.location.href) {
|
|
27237
|
+
return;
|
|
27238
|
+
}
|
|
27239
|
+
const positionByName = scrollerPositionsByUrl.get(url);
|
|
27240
|
+
if (positionByName) {
|
|
27241
|
+
positionByName.delete(name);
|
|
27242
|
+
}
|
|
27243
|
+
};
|
|
27244
|
+
|
|
27152
27245
|
/**
|
|
27153
27246
|
* A navigation is ABOUT to be applied — said before its very first write.
|
|
27154
27247
|
*
|
|
@@ -27511,6 +27604,13 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
27511
27604
|
return undefined;
|
|
27512
27605
|
}
|
|
27513
27606
|
|
|
27607
|
+
// The page's own scrollers are told of an arrival before the routing
|
|
27608
|
+
// renders anything: a list arriving decides where it opens in its first
|
|
27609
|
+
// render (see scroll_restoration.js). The document itself is moved once
|
|
27610
|
+
// the page is there, below.
|
|
27611
|
+
if (navigationType === "push") {
|
|
27612
|
+
forgetScrollersOnArrival(url, { from: urlLeft });
|
|
27613
|
+
}
|
|
27514
27614
|
if (abortController) {
|
|
27515
27615
|
abortController.abort(`navigating to ${url}`);
|
|
27516
27616
|
}
|
|
@@ -36428,9 +36528,9 @@ const getInvalidCharsMessage = (
|
|
|
36428
36528
|
uiState,
|
|
36429
36529
|
{ charClass, messageKey, uiStateNow },
|
|
36430
36530
|
) => {
|
|
36431
|
-
const str =
|
|
36531
|
+
const str = uiStateAsText(uiState);
|
|
36432
36532
|
if (compileCharClassAnchored(charClass).test(str)) return null;
|
|
36433
|
-
const strNow =
|
|
36533
|
+
const strNow = uiStateAsText(uiStateNow);
|
|
36434
36534
|
if (
|
|
36435
36535
|
countCharsOutsideClass(str, charClass) <=
|
|
36436
36536
|
countCharsOutsideClass(strNow, charClass)
|
|
@@ -36443,9 +36543,9 @@ const getInvalidCharsMessage = (
|
|
|
36443
36543
|
// Paste / set: truncate what the gesture adds beyond the limit.
|
|
36444
36544
|
const getLengthOverflowResult = (uiState, { maxLength, uiStateNow }) => {
|
|
36445
36545
|
if (maxLength === undefined) return null;
|
|
36446
|
-
const str =
|
|
36546
|
+
const str = uiStateAsText(uiState);
|
|
36447
36547
|
if (str.length <= maxLength) return null;
|
|
36448
|
-
const strNow =
|
|
36548
|
+
const strNow = uiStateAsText(uiStateNow);
|
|
36449
36549
|
if (str.length <= strNow.length) return null;
|
|
36450
36550
|
return {
|
|
36451
36551
|
fixedValue: str.slice(0, maxLength),
|
|
@@ -63836,6 +63936,12 @@ const PickerCustom = props => {
|
|
|
63836
63936
|
// PickerNative takes.
|
|
63837
63937
|
pickerProps.resetOnError = true;
|
|
63838
63938
|
}
|
|
63939
|
+
if (pickerProps.resetOnAbort === undefined) {
|
|
63940
|
+
// Same reasoning for an action the app abandons (an AbortError, e.g. a
|
|
63941
|
+
// verification step the user dismissed): the popup is already closed, so
|
|
63942
|
+
// the abandoned value rolls back instead of staying selected in the list.
|
|
63943
|
+
pickerProps.resetOnAbort = true;
|
|
63944
|
+
}
|
|
63839
63945
|
// ref
|
|
63840
63946
|
const popupRef = useRef(null);
|
|
63841
63947
|
popupProps.ref = popupRef;
|
|
@@ -67205,10 +67311,11 @@ const ListUI = props => {
|
|
|
67205
67311
|
onListVisibleItemsChange,
|
|
67206
67312
|
virtualItemSize,
|
|
67207
67313
|
scrolled,
|
|
67208
|
-
defaultScrolled = "start",
|
|
67314
|
+
defaultScrolled: defaultScrolledProp = "start",
|
|
67209
67315
|
onScrolledChange,
|
|
67210
67316
|
scroller = "self",
|
|
67211
67317
|
hoverWhileScrolling = false,
|
|
67318
|
+
scrollResetOnNavigation = false,
|
|
67212
67319
|
lockSize,
|
|
67213
67320
|
columns,
|
|
67214
67321
|
itemColumns,
|
|
@@ -67227,6 +67334,20 @@ const ListUI = props => {
|
|
|
67227
67334
|
listRows,
|
|
67228
67335
|
...rest
|
|
67229
67336
|
} = props;
|
|
67337
|
+
// Remembered by name, and a name made up at render (see ListFirstResolver)
|
|
67338
|
+
// names no list a later mount would recognize.
|
|
67339
|
+
const rememberScroll = !scrollResetOnNavigation && !isLikelyPreactGeneratedId(rest.id);
|
|
67340
|
+
// Where the list was when its screen was left, when this is the way back
|
|
67341
|
+
// (see scroll_restoration.js). Read once: `defaultScrolled` is held by
|
|
67342
|
+
// reference, and a place read again each render would be a list moved each
|
|
67343
|
+
// render. A list held by its caller (`scrolled`) is where the caller says.
|
|
67344
|
+
const [scrolledRemembered] = useState(() => {
|
|
67345
|
+
if (!rememberScroll || scrolled !== undefined && scrolled !== null) {
|
|
67346
|
+
return undefined;
|
|
67347
|
+
}
|
|
67348
|
+
return recallScrollerPosition(rest.id);
|
|
67349
|
+
});
|
|
67350
|
+
const defaultScrolled = scrolledRemembered || defaultScrolledProp;
|
|
67230
67351
|
const scrollBoxPaddingProps = {};
|
|
67231
67352
|
for (const name of LIST_PADDING_PROP_SET) {
|
|
67232
67353
|
if (name in rest) {
|
|
@@ -67321,6 +67442,8 @@ const ListUI = props => {
|
|
|
67321
67442
|
scrolled,
|
|
67322
67443
|
defaultScrolled,
|
|
67323
67444
|
onScrolledChange,
|
|
67445
|
+
rememberScroll,
|
|
67446
|
+
listId: rest.id,
|
|
67324
67447
|
scroller,
|
|
67325
67448
|
searchText,
|
|
67326
67449
|
horizontal
|
|
@@ -67353,7 +67476,7 @@ const ListUI = props => {
|
|
|
67353
67476
|
// locator). Both answer here, so a row is reachable whether or not the
|
|
67354
67477
|
// window happens to frame it.
|
|
67355
67478
|
const getItemById = itemId => {
|
|
67356
|
-
const itemDrawn = listRows.itemsSignal.peek().find(item => item.
|
|
67479
|
+
const itemDrawn = listRows.itemsSignal.peek().find(item => item.itemId === itemId);
|
|
67357
67480
|
if (itemDrawn) {
|
|
67358
67481
|
return itemDrawn;
|
|
67359
67482
|
}
|
|
@@ -67363,6 +67486,7 @@ const ListUI = props => {
|
|
|
67363
67486
|
}
|
|
67364
67487
|
return {
|
|
67365
67488
|
id: itemId,
|
|
67489
|
+
itemId,
|
|
67366
67490
|
index: rowIndex
|
|
67367
67491
|
};
|
|
67368
67492
|
};
|
|
@@ -67645,6 +67769,8 @@ const useListScrollSync = ({
|
|
|
67645
67769
|
scrolled,
|
|
67646
67770
|
defaultScrolled,
|
|
67647
67771
|
onScrolledChange,
|
|
67772
|
+
rememberScroll,
|
|
67773
|
+
listId,
|
|
67648
67774
|
scroller,
|
|
67649
67775
|
searchText,
|
|
67650
67776
|
horizontal
|
|
@@ -67857,15 +67983,18 @@ const useListScrollSync = ({
|
|
|
67857
67983
|
`"${event.type}" on ${getElementSignature(event.target)} (${reason})`;
|
|
67858
67984
|
// When we display the list we prefer to have selected item at the center
|
|
67859
67985
|
// otherwise, usually when focused by arrow nav, we want to keep it into view close to the nearest edge
|
|
67860
|
-
const
|
|
67861
|
-
`${getElementSignature(itemEl)}.scrollIntoView({ block: "${
|
|
67986
|
+
const align = blockRequested || (event.type === "navi_displayed" ? "center" : "nearest");
|
|
67987
|
+
`${getElementSignature(itemEl)}.scrollIntoView({ block: "${align}", inline: "${align}", container: "nearest" })`;
|
|
67862
67988
|
// The list is going somewhere on purpose, so there is no view to hold
|
|
67863
67989
|
// still any more: an anchor captured before this drop it, or it would
|
|
67864
67990
|
// put the list back where it was the moment the rows move under it.
|
|
67865
67991
|
anchorRef.current = null;
|
|
67992
|
+
// One alignment, said on both axes: the axis the list scrolls on is the
|
|
67993
|
+
// one that reads it, and the other has nothing to move.
|
|
67866
67994
|
scrollIntoViewScoped(itemEl, {
|
|
67867
67995
|
container: getScroller(),
|
|
67868
|
-
block
|
|
67996
|
+
block: align,
|
|
67997
|
+
inline: align
|
|
67869
67998
|
});
|
|
67870
67999
|
const listEl = getListEl();
|
|
67871
68000
|
dispatchPublicCustomEvent(listEl, "navi_scroll", {
|
|
@@ -67877,7 +68006,7 @@ const useListScrollSync = ({
|
|
|
67877
68006
|
// Whether the row is drawn is asked of the dom, not of the render window:
|
|
67878
68007
|
// the window says what a run draws, and a list whose rows are declared one
|
|
67879
68008
|
// by one has them all in the dom whatever the window says.
|
|
67880
|
-
const itemEl = findRowElement(getListEl(), item.
|
|
68009
|
+
const itemEl = findRowElement(getListEl(), item.itemId);
|
|
67881
68010
|
if (itemEl) {
|
|
67882
68011
|
scrollItemIntoView(itemEl);
|
|
67883
68012
|
return;
|
|
@@ -67885,7 +68014,7 @@ const useListScrollSync = ({
|
|
|
67885
68014
|
// Not in DOM — shift the render window. The item will read
|
|
67886
68015
|
// pendingScrollRef on mount and scroll into view.
|
|
67887
68016
|
pendingScrollRef.current = {
|
|
67888
|
-
id: item.
|
|
68017
|
+
id: item.itemId,
|
|
67889
68018
|
resolve: itemEl => {
|
|
67890
68019
|
pendingScrollRef.current = null;
|
|
67891
68020
|
scrollItemIntoView(itemEl);
|
|
@@ -68236,6 +68365,8 @@ const useListScrollSync = ({
|
|
|
68236
68365
|
// one was looking at is then somewhere else.
|
|
68237
68366
|
const onScrolledChangeRef = useRef(null);
|
|
68238
68367
|
onScrolledChangeRef.current = onScrolledChange;
|
|
68368
|
+
const rememberScrollRef = useRef(false);
|
|
68369
|
+
rememberScrollRef.current = rememberScroll;
|
|
68239
68370
|
// Where the list was at the last thing that moved it. Kept whether anyone
|
|
68240
68371
|
// asked for it or not: it is what a resize needs to put things back.
|
|
68241
68372
|
const positionRef = useRef(null);
|
|
@@ -68253,16 +68384,32 @@ const useListScrollSync = ({
|
|
|
68253
68384
|
return;
|
|
68254
68385
|
}
|
|
68255
68386
|
positionRef.current = position;
|
|
68256
|
-
|
|
68387
|
+
const remember = rememberScrollRef.current;
|
|
68388
|
+
const onScrolledChange = onScrolledChangeRef.current;
|
|
68389
|
+
if (!remember && !onScrolledChange) {
|
|
68257
68390
|
return;
|
|
68258
68391
|
}
|
|
68259
68392
|
const rowEl = findRowElement(getListEl(), position.id);
|
|
68260
|
-
|
|
68393
|
+
const scrolledNow = {
|
|
68261
68394
|
id: position.id,
|
|
68262
68395
|
index: position.index,
|
|
68263
68396
|
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal)
|
|
68264
|
-
}
|
|
68397
|
+
};
|
|
68398
|
+
if (remember) {
|
|
68399
|
+
rememberScrollerPosition(listId, scrolledNow);
|
|
68400
|
+
}
|
|
68401
|
+
if (onScrolledChange) {
|
|
68402
|
+
onScrolledChange(scrolledNow);
|
|
68403
|
+
}
|
|
68265
68404
|
};
|
|
68405
|
+
// Leaving: with its page, or alone (see forgetScrollerUnlessPageLeft).
|
|
68406
|
+
useLayoutEffect(() => {
|
|
68407
|
+
return () => {
|
|
68408
|
+
if (rememberScrollRef.current) {
|
|
68409
|
+
forgetScrollerUnlessPageLeft(listId);
|
|
68410
|
+
}
|
|
68411
|
+
};
|
|
68412
|
+
}, []);
|
|
68266
68413
|
|
|
68267
68414
|
// A list that gets narrower rewraps every row it holds, so everything below
|
|
68268
68415
|
// moves and the reader loses their place — the very thing scrolling a long
|
|
@@ -68335,7 +68482,7 @@ const useListScrollSync = ({
|
|
|
68335
68482
|
return;
|
|
68336
68483
|
}
|
|
68337
68484
|
const items = listRows.visibleItemsSignal.peek();
|
|
68338
|
-
const itemNow = items.find(i => i.
|
|
68485
|
+
const itemNow = items.find(i => i.itemId === anchor.id);
|
|
68339
68486
|
if (!itemNow) {
|
|
68340
68487
|
anchorRef.current = null;
|
|
68341
68488
|
return;
|
|
@@ -68938,17 +69085,22 @@ const resolveScrollInset = (value, viewportSize) => {
|
|
|
68938
69085
|
return number;
|
|
68939
69086
|
};
|
|
68940
69087
|
|
|
68941
|
-
// The row
|
|
68942
|
-
//
|
|
68943
|
-
//
|
|
68944
|
-
//
|
|
68945
|
-
//
|
|
69088
|
+
// The row of that name, IN THIS LIST — by the name the list knows it under
|
|
69089
|
+
// (see ListItemUI), not the element's id. Not document.getElementById either:
|
|
69090
|
+
// a name is only ever unique within a list — two lists on the same page can be
|
|
69091
|
+
// showing the same collection — and a list acting on a row that belongs to
|
|
69092
|
+
// another one is a spectacular kind of wrong (it scrolls to hold still
|
|
69093
|
+
// something it is not even showing).
|
|
68946
69094
|
const findRowElement = (listEl, id) => {
|
|
68947
|
-
return listEl.querySelector(`[
|
|
69095
|
+
return listEl.querySelector(`[navi-list-item-real="${CSS.escape(id)}"]`);
|
|
68948
69096
|
};
|
|
69097
|
+
const getRowName = rowEl => rowEl.getAttribute("navi-list-item-real");
|
|
68949
69098
|
|
|
68950
69099
|
// The row the user is looking at, and where it sits: what must not move when
|
|
68951
|
-
// the list is rebuilt around it.
|
|
69100
|
+
// the list is rebuilt around it. Read off the rows' own boxes, not by
|
|
69101
|
+
// hit-testing the screen: a scrolling list takes its rows out of hit-testing
|
|
69102
|
+
// (see the navi-scrolling rule in the css above), and the scroll event is
|
|
69103
|
+
// precisely when this is asked.
|
|
68952
69104
|
const captureScrollAnchor = ({
|
|
68953
69105
|
scrollerEl,
|
|
68954
69106
|
listEl,
|
|
@@ -68960,30 +69112,31 @@ const captureScrollAnchor = ({
|
|
|
68960
69112
|
}
|
|
68961
69113
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
68962
69114
|
const listRect = listEl.getBoundingClientRect();
|
|
68963
|
-
const
|
|
68964
|
-
if (!
|
|
69115
|
+
const range = getListVisibleRange(viewportRect, listRect, horizontal);
|
|
69116
|
+
if (!range) {
|
|
68965
69117
|
return null;
|
|
68966
69118
|
}
|
|
69119
|
+
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
|
|
69120
|
+
const {
|
|
69121
|
+
rowEls,
|
|
69122
|
+
index
|
|
69123
|
+
} = findRowsFrom(listEl, range.from, horizontal);
|
|
68967
69124
|
let fallbackAnchor = null;
|
|
68968
|
-
for (let
|
|
68969
|
-
const
|
|
68970
|
-
const
|
|
68971
|
-
const
|
|
68972
|
-
if (
|
|
68973
|
-
|
|
68974
|
-
}
|
|
68975
|
-
const itemEl = el.closest(REAL_LIST_ITEM_SELECTOR);
|
|
68976
|
-
if (!itemEl) {
|
|
68977
|
-
continue;
|
|
69125
|
+
for (let i = index; i < rowEls.length; i++) {
|
|
69126
|
+
const rowEl = rowEls[i];
|
|
69127
|
+
const rowRect = rowEl.getBoundingClientRect();
|
|
69128
|
+
const rowStart = horizontal ? rowRect.left : rowRect.top;
|
|
69129
|
+
if (rowStart >= range.to) {
|
|
69130
|
+
break;
|
|
68978
69131
|
}
|
|
68979
|
-
const
|
|
69132
|
+
const rowName = getRowName(rowEl);
|
|
69133
|
+
const item = items.find(i => i.itemId === rowName);
|
|
68980
69134
|
if (!item) {
|
|
68981
69135
|
continue;
|
|
68982
69136
|
}
|
|
68983
|
-
const
|
|
68984
|
-
const offset = horizontal ? itemRect.left - viewportRect.left : itemRect.top - viewportRect.top;
|
|
69137
|
+
const offset = rowStart - viewportFrom;
|
|
68985
69138
|
const anchor = {
|
|
68986
|
-
id: item.
|
|
69139
|
+
id: item.itemId,
|
|
68987
69140
|
index: item.index,
|
|
68988
69141
|
offset
|
|
68989
69142
|
};
|
|
@@ -69003,43 +69156,61 @@ const captureScrollAnchor = ({
|
|
|
69003
69156
|
// The part of the list that is on screen, along the scrolling axis. Both edges
|
|
69004
69157
|
// matter: the scroller may be larger than the list (scroller="parent") as well
|
|
69005
69158
|
// as smaller (the list scrolls inside its own box).
|
|
69006
|
-
const
|
|
69007
|
-
// The screen has a say too: what is asked here is answered by
|
|
69008
|
-
// elementFromPoint, which only knows about points that are actually on it. A
|
|
69009
|
-
// list whose scroll box hangs below the fold of the page would otherwise be
|
|
69010
|
-
// probed where nothing can be hit — and would silently stop keeping its rows
|
|
69011
|
-
// still, which is exactly when it matters.
|
|
69012
|
-
const screenTo = horizontal ? document.documentElement.clientWidth : document.documentElement.clientHeight;
|
|
69159
|
+
const getListVisibleRange = (viewportRect, listRect, horizontal) => {
|
|
69013
69160
|
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
|
|
69014
69161
|
const viewportTo = horizontal ? viewportRect.right : viewportRect.bottom;
|
|
69015
69162
|
const listFrom = horizontal ? listRect.left : listRect.top;
|
|
69016
69163
|
const listTo = horizontal ? listRect.right : listRect.bottom;
|
|
69017
|
-
|
|
69018
|
-
|
|
69019
|
-
if (from < 0) {
|
|
69020
|
-
from = 0;
|
|
69021
|
-
}
|
|
69022
|
-
if (to > screenTo) {
|
|
69023
|
-
to = screenTo;
|
|
69024
|
-
}
|
|
69164
|
+
const from = listFrom > viewportFrom ? listFrom : viewportFrom;
|
|
69165
|
+
const to = listTo < viewportTo ? listTo : viewportTo;
|
|
69025
69166
|
if (to - from < 2) {
|
|
69026
69167
|
return null;
|
|
69027
69168
|
}
|
|
69028
|
-
// Where to put the probe on the other axis: inside the list, inside the
|
|
69029
|
-
// viewport.
|
|
69030
|
-
const crossFrom = horizontal ? listRect.top : listRect.left;
|
|
69031
|
-
const crossViewportFrom = horizontal ? viewportRect.top : viewportRect.left;
|
|
69032
|
-
const crossPos = (crossFrom > crossViewportFrom ? crossFrom : crossViewportFrom) + 1;
|
|
69033
69169
|
return {
|
|
69034
69170
|
from,
|
|
69035
|
-
to
|
|
69036
|
-
crossPos
|
|
69171
|
+
to
|
|
69037
69172
|
};
|
|
69038
69173
|
};
|
|
69174
|
+
// The real rows of the list, and the first of them reaching past `from` along
|
|
69175
|
+
// the scroll axis. A binary search over their boxes: the rows stand in
|
|
69176
|
+
// document order along that axis, so their far edges only grow.
|
|
69177
|
+
const findRowsFrom = (listEl, from, horizontal) => {
|
|
69178
|
+
const rowEls = listEl.querySelectorAll(REAL_LIST_ITEM_SELECTOR);
|
|
69179
|
+
let low = 0;
|
|
69180
|
+
let high = rowEls.length;
|
|
69181
|
+
while (low < high) {
|
|
69182
|
+
const mid = low + high >> 1;
|
|
69183
|
+
const rect = rowEls[mid].getBoundingClientRect();
|
|
69184
|
+
const end = horizontal ? rect.right : rect.bottom;
|
|
69185
|
+
if (end > from) {
|
|
69186
|
+
high = mid;
|
|
69187
|
+
} else {
|
|
69188
|
+
low = mid + 1;
|
|
69189
|
+
}
|
|
69190
|
+
}
|
|
69191
|
+
return {
|
|
69192
|
+
rowEls,
|
|
69193
|
+
index: low
|
|
69194
|
+
};
|
|
69195
|
+
};
|
|
69196
|
+
// Whether a filler (the room held for rows outside the window) is what stands
|
|
69197
|
+
// at that position along the scroll axis.
|
|
69198
|
+
const isFillerAt = (listEl, position, horizontal) => {
|
|
69199
|
+
for (const fillerEl of listEl.querySelectorAll("[navi-virtual-filler]")) {
|
|
69200
|
+
const rect = fillerEl.getBoundingClientRect();
|
|
69201
|
+
const from = horizontal ? rect.left : rect.top;
|
|
69202
|
+
const to = horizontal ? rect.right : rect.bottom;
|
|
69203
|
+
if (position >= from && position < to) {
|
|
69204
|
+
return true;
|
|
69205
|
+
}
|
|
69206
|
+
}
|
|
69207
|
+
return false;
|
|
69208
|
+
};
|
|
69039
69209
|
|
|
69040
|
-
// Which row of the collection sits at the current scroll position.
|
|
69041
|
-
//
|
|
69042
|
-
// on screen is only
|
|
69210
|
+
// Which row of the collection sits at the current scroll position. Read off
|
|
69211
|
+
// the rows' boxes when a real row is there (see captureScrollAnchor for why
|
|
69212
|
+
// not hit-testing), and from the row size when what is on screen is only
|
|
69213
|
+
// reserved room.
|
|
69043
69214
|
// Returns { index, item, reason } or null if nothing can be determined.
|
|
69044
69215
|
const getScrollInfo = ({
|
|
69045
69216
|
scrollValues,
|
|
@@ -69053,34 +69224,31 @@ const getScrollInfo = ({
|
|
|
69053
69224
|
const items = listRows.itemsSignal.peek();
|
|
69054
69225
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
69055
69226
|
const listRect = listEl.getBoundingClientRect();
|
|
69056
|
-
|
|
69057
|
-
|
|
69058
|
-
const scanRange = getListVisibleScanRange(viewportRect, listRect, horizontal);
|
|
69059
|
-
if (!scanRange) {
|
|
69227
|
+
const range = getListVisibleRange(viewportRect, listRect, horizontal);
|
|
69228
|
+
if (!range) {
|
|
69060
69229
|
return null;
|
|
69061
69230
|
}
|
|
69062
|
-
//
|
|
69063
|
-
//
|
|
69064
|
-
//
|
|
69065
|
-
//
|
|
69066
|
-
const scanStart = (
|
|
69067
|
-
|
|
69068
|
-
|
|
69069
|
-
|
|
69070
|
-
|
|
69071
|
-
|
|
69072
|
-
|
|
69073
|
-
|
|
69074
|
-
|
|
69075
|
-
|
|
69076
|
-
|
|
69077
|
-
|
|
69078
|
-
|
|
69079
|
-
|
|
69080
|
-
|
|
69081
|
-
|
|
69082
|
-
|
|
69083
|
-
break;
|
|
69231
|
+
// Read from the center of the visible part of the list along the main axis.
|
|
69232
|
+
// The render window places half its budget before and half after the hit
|
|
69233
|
+
// index. Anchoring to the center maximises how many rendered items fall
|
|
69234
|
+
// within the visible area.
|
|
69235
|
+
const scanStart = (range.from + range.to) / 2;
|
|
69236
|
+
let hitEl = null;
|
|
69237
|
+
const hitFiller = isFillerAt(listEl, scanStart, horizontal);
|
|
69238
|
+
if (!hitFiller) {
|
|
69239
|
+
// The first real row from the center down, the way a probe walking down
|
|
69240
|
+
// from it would meet one — past a separator or a group label in between.
|
|
69241
|
+
const {
|
|
69242
|
+
rowEls,
|
|
69243
|
+
index
|
|
69244
|
+
} = findRowsFrom(listEl, scanStart, horizontal);
|
|
69245
|
+
const rowEl = rowEls[index];
|
|
69246
|
+
if (rowEl) {
|
|
69247
|
+
const rowRect = rowEl.getBoundingClientRect();
|
|
69248
|
+
const rowStart = horizontal ? rowRect.left : rowRect.top;
|
|
69249
|
+
if (rowStart < range.to) {
|
|
69250
|
+
hitEl = rowEl;
|
|
69251
|
+
}
|
|
69084
69252
|
}
|
|
69085
69253
|
}
|
|
69086
69254
|
// Shared by the "hit a filler" and "hit nothing at all" cases below: both
|
|
@@ -69111,8 +69279,8 @@ const getScrollInfo = ({
|
|
|
69111
69279
|
return estimateFromScrollPos("hit filler");
|
|
69112
69280
|
}
|
|
69113
69281
|
if (hitEl) {
|
|
69114
|
-
const
|
|
69115
|
-
const item = items.find(i => i.
|
|
69282
|
+
const hitName = getRowName(hitEl);
|
|
69283
|
+
const item = items.find(i => i.itemId === hitName);
|
|
69116
69284
|
if (!item) {
|
|
69117
69285
|
return null;
|
|
69118
69286
|
}
|
|
@@ -69122,13 +69290,11 @@ const getScrollInfo = ({
|
|
|
69122
69290
|
reason: `hit item at ${item.index} (${item.value})`
|
|
69123
69291
|
};
|
|
69124
69292
|
}
|
|
69125
|
-
//
|
|
69126
|
-
// the
|
|
69127
|
-
//
|
|
69128
|
-
//
|
|
69129
|
-
//
|
|
69130
|
-
// estimate as the hitFiller case is a safe fallback: it only needs the
|
|
69131
|
-
// scroll position, not a successful hit-test.
|
|
69293
|
+
// No real row stands between the center and the end of what is visible.
|
|
69294
|
+
// Keeping the stale renderWindow here means the DOM never gets asked to
|
|
69295
|
+
// catch up with a scrollTop that may have jumped far away — the user ends
|
|
69296
|
+
// up staring at blank space. Same estimate as the hitFiller case is a safe
|
|
69297
|
+
// fallback: it only needs the scroll position.
|
|
69132
69298
|
const estimated = estimateFromScrollPos("no hit");
|
|
69133
69299
|
if (estimated) {
|
|
69134
69300
|
return estimated;
|
|
@@ -69563,6 +69729,12 @@ const ListItemUI = props => {
|
|
|
69563
69729
|
// gave the row its place and decided it is inside the render window.
|
|
69564
69730
|
const row = useContext(ListRowContext);
|
|
69565
69731
|
const slotId = useContext(ListSlotContext);
|
|
69732
|
+
// What the row is called in the list — the run's name for a row it draws,
|
|
69733
|
+
// whatever `id` the caller put on the element (a run row may need a DOM id of
|
|
69734
|
+
// its own, to keep clear of another element's). A position names a row by
|
|
69735
|
+
// this (see captureScrollAnchor), and a row asked for by name is found by
|
|
69736
|
+
// this (locateRow, findRowElement); the DOM id is the caller's.
|
|
69737
|
+
props.itemId = row ? row.id : props.id;
|
|
69566
69738
|
// There is no standalone match/matchScore/highlight prop — participation
|
|
69567
69739
|
// in a matching system (search, filter…) only goes through `matchInfo`
|
|
69568
69740
|
// (e.g. useSearchText's getItemMatchInfo(item): { match, matchScore,
|
|
@@ -69643,6 +69815,7 @@ const ListItemReal = props => {
|
|
|
69643
69815
|
const {
|
|
69644
69816
|
ref,
|
|
69645
69817
|
id,
|
|
69818
|
+
itemId,
|
|
69646
69819
|
hidden,
|
|
69647
69820
|
muted,
|
|
69648
69821
|
loading,
|
|
@@ -69662,7 +69835,7 @@ const ListItemReal = props => {
|
|
|
69662
69835
|
// see the state change, which only the caller can arrange).
|
|
69663
69836
|
const pendingScrollRef = useContext(PendingScrollRefContext);
|
|
69664
69837
|
const pendingScroll = pendingScrollRef.current;
|
|
69665
|
-
const needScrollOnMount = pendingScroll && pendingScroll.id ===
|
|
69838
|
+
const needScrollOnMount = pendingScroll && pendingScroll.id === itemId;
|
|
69666
69839
|
useLayoutEffect(() => {
|
|
69667
69840
|
if (!needScrollOnMount) {
|
|
69668
69841
|
return;
|
|
@@ -69761,7 +69934,7 @@ const ListItemReal = props => {
|
|
|
69761
69934
|
baseClassName: "navi_list_item",
|
|
69762
69935
|
styleCSSVars: LIST_ITEM_STYLE_CSS_VARS,
|
|
69763
69936
|
id: id,
|
|
69764
|
-
"navi-list-item-real":
|
|
69937
|
+
"navi-list-item-real": itemId,
|
|
69765
69938
|
...rest,
|
|
69766
69939
|
...itemColumnsOverrideProps,
|
|
69767
69940
|
index: undefined,
|
|
@@ -71133,6 +71306,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
71133
71306
|
* onScrolledChange?: (scrolled: {id: string, index: number, offset: number}) => void,
|
|
71134
71307
|
* scroller?: "self" | "parent" | "document" | Element | {current: Element},
|
|
71135
71308
|
* hoverWhileScrolling?: boolean,
|
|
71309
|
+
* scrollResetOnNavigation?: boolean,
|
|
71136
71310
|
* fallback?: import("ignore:preact").ComponentChildren,
|
|
71137
71311
|
* searchFallback?: import("ignore:preact").ComponentChildren,
|
|
71138
71312
|
* searchText?: string,
|
|
@@ -71224,7 +71398,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
71224
71398
|
* list is already known to be empty: the empty `fallback` shows right away
|
|
71225
71399
|
* rather than an empty frame, so nothing moves when the response arrives.
|
|
71226
71400
|
* @param {"start"|"end"|number|{id: string, offset?: number}} [props.defaultScrolled="start"]
|
|
71227
|
-
* Where the list opens, after which the user owns the scroll
|
|
71401
|
+
* Where the list opens, after which the user owns the scroll — unless it is
|
|
71402
|
+
* being come back to (see `scrollResetOnNavigation`). `"end"` is a
|
|
71228
71403
|
* thread read backwards — the last rows are the ones to show, and the ones
|
|
71229
71404
|
* asked for first. A number opens on that row of the collection. `{id,
|
|
71230
71405
|
* offset}` — what `onScrolledChange` hands out — opens on a NAMED row,
|
|
@@ -71307,6 +71482,15 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
71307
71482
|
* Pass `true` for a list whose rows must stay live under the pointer while
|
|
71308
71483
|
* it scrolls. The trade of the default is the mirror one: right after a
|
|
71309
71484
|
* scroll, the row under the pointer lights up only once the pointer moves.
|
|
71485
|
+
* @param {boolean} [props.scrollResetOnNavigation=false]
|
|
71486
|
+
* A list that opens the same way every time. Without it the list comes back
|
|
71487
|
+
* where it was when its screen is left and come back to — the way the page
|
|
71488
|
+
* does, and for a list that scrolls itself the page's own restoration cannot
|
|
71489
|
+
* see. The position is kept under the list's `id` and the page's url, for
|
|
71490
|
+
* the session (a reload comes back too); a list without an `id` of its own
|
|
71491
|
+
* has nothing to be remembered by. A fresh arrival at the page opens at
|
|
71492
|
+
* `defaultScrolled` either way, and so does a list the caller holds through
|
|
71493
|
+
* `scrolled`.
|
|
71310
71494
|
* @param {boolean} [props.deselectable]
|
|
71311
71495
|
* A single-select list allowed to hold nothing: the selected row, pressed
|
|
71312
71496
|
* again, lets go. Without it the list is a radio group — a choice, once
|