@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/dev/jsenv_navi.js
CHANGED
|
@@ -9248,6 +9248,17 @@ const naviI18nFromValidityMessage = ({ key, params }) => {
|
|
|
9248
9248
|
return naviI18n(`constraint.${key}`, params);
|
|
9249
9249
|
};
|
|
9250
9250
|
|
|
9251
|
+
// The characters a ui state holds, for the rules that read a value as text
|
|
9252
|
+
// (length, pattern, number, email…). `undefined` and `null` both hold none:
|
|
9253
|
+
// `String(null)` would hand those rules the four letters "null" — a number
|
|
9254
|
+
// field cleared with `value={null}` then reported "must be a number".
|
|
9255
|
+
const uiStateAsText = (uiState) => {
|
|
9256
|
+
if (uiState === undefined || uiState === null) {
|
|
9257
|
+
return "";
|
|
9258
|
+
}
|
|
9259
|
+
return String(uiState);
|
|
9260
|
+
};
|
|
9261
|
+
|
|
9251
9262
|
/**
|
|
9252
9263
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Constraint_validation
|
|
9253
9264
|
*/
|
|
@@ -9314,8 +9325,7 @@ const REQUIRED_CONSTRAINT = {
|
|
|
9314
9325
|
return naviI18n("constraint.required.checkbox");
|
|
9315
9326
|
}
|
|
9316
9327
|
|
|
9317
|
-
const valueAsString =
|
|
9318
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9328
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9319
9329
|
if (valueAsString) {
|
|
9320
9330
|
return null;
|
|
9321
9331
|
}
|
|
@@ -9366,8 +9376,7 @@ const PATTERN_CONSTRAINT = {
|
|
|
9366
9376
|
if (!pattern) {
|
|
9367
9377
|
return null;
|
|
9368
9378
|
}
|
|
9369
|
-
const valueAsString =
|
|
9370
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9379
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9371
9380
|
if (!valueAsString) {
|
|
9372
9381
|
return null;
|
|
9373
9382
|
}
|
|
@@ -9402,8 +9411,7 @@ const TYPE_EMAIL_CONSTRAINT = {
|
|
|
9402
9411
|
if (type !== "email") {
|
|
9403
9412
|
return null;
|
|
9404
9413
|
}
|
|
9405
|
-
const valueAsString =
|
|
9406
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9414
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9407
9415
|
if (!valueAsString) {
|
|
9408
9416
|
return null;
|
|
9409
9417
|
}
|
|
@@ -9436,8 +9444,7 @@ const MIN_LENGTH_CONSTRAINT = {
|
|
|
9436
9444
|
if (minLength === undefined) {
|
|
9437
9445
|
return null;
|
|
9438
9446
|
}
|
|
9439
|
-
const valueAsString =
|
|
9440
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9447
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9441
9448
|
if (!valueAsString && !field.controlHostProps.required) {
|
|
9442
9449
|
return null;
|
|
9443
9450
|
}
|
|
@@ -9532,8 +9539,7 @@ const MAX_LENGTH_CONSTRAINT = {
|
|
|
9532
9539
|
if (maxLength === undefined) {
|
|
9533
9540
|
return null;
|
|
9534
9541
|
}
|
|
9535
|
-
const valueAsString =
|
|
9536
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9542
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9537
9543
|
if (!valueAsString) {
|
|
9538
9544
|
return null;
|
|
9539
9545
|
}
|
|
@@ -9574,8 +9580,7 @@ const TYPE_NUMBER_CONSTRAINT = {
|
|
|
9574
9580
|
if (!isNumberInput(type, naviType)) {
|
|
9575
9581
|
return null;
|
|
9576
9582
|
}
|
|
9577
|
-
const valueAsString =
|
|
9578
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9583
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9579
9584
|
if (!valueAsString) {
|
|
9580
9585
|
return null;
|
|
9581
9586
|
}
|
|
@@ -9640,8 +9645,7 @@ const MIN_CONSTRAINT = {
|
|
|
9640
9645
|
}
|
|
9641
9646
|
const type = field.controlHostProps.type;
|
|
9642
9647
|
const naviInputType = field.controlHostProps["navi-input-type"];
|
|
9643
|
-
const valueAsString =
|
|
9644
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9648
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9645
9649
|
if (!valueAsString) {
|
|
9646
9650
|
return null;
|
|
9647
9651
|
}
|
|
@@ -9740,8 +9744,7 @@ const MAX_CONSTRAINT = {
|
|
|
9740
9744
|
}
|
|
9741
9745
|
const type = field.controlHostProps.type;
|
|
9742
9746
|
const naviInputType = field.controlHostProps["navi-input-type"];
|
|
9743
|
-
const valueAsString =
|
|
9744
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9747
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9745
9748
|
if (!valueAsString) {
|
|
9746
9749
|
return null;
|
|
9747
9750
|
}
|
|
@@ -9874,8 +9877,7 @@ const STEP_CONSTRAINT = {
|
|
|
9874
9877
|
return null;
|
|
9875
9878
|
}
|
|
9876
9879
|
const stepString = String(stepRaw);
|
|
9877
|
-
const valueAsString =
|
|
9878
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
9880
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
9879
9881
|
if (!valueAsString) {
|
|
9880
9882
|
return null;
|
|
9881
9883
|
}
|
|
@@ -12824,8 +12826,7 @@ const DISPLAYABLE_CONSTRAINT = {
|
|
|
12824
12826
|
if (!isConstraintAttributeOn(displayable)) {
|
|
12825
12827
|
return null;
|
|
12826
12828
|
}
|
|
12827
|
-
const valueAsString =
|
|
12828
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12829
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12829
12830
|
const maxStackedMarksAttribute =
|
|
12830
12831
|
field.controlHostProps["data-max-stacked-marks"];
|
|
12831
12832
|
const result = DISPLAYABLE_RULE.applyOn(true, valueAsString, {
|
|
@@ -12866,8 +12867,7 @@ const MAX_LINE_BREAKS_CONSTRAINT = {
|
|
|
12866
12867
|
if (isNaN(maxLineBreaks)) {
|
|
12867
12868
|
return null;
|
|
12868
12869
|
}
|
|
12869
|
-
const valueAsString =
|
|
12870
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12870
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12871
12871
|
const result = MAX_LINE_BREAKS_RULE.applyOn(maxLineBreaks, valueAsString);
|
|
12872
12872
|
if (!result) {
|
|
12873
12873
|
return null;
|
|
@@ -12896,8 +12896,7 @@ const NO_EMOJI_CONSTRAINT = {
|
|
|
12896
12896
|
if (!isConstraintAttributeOn(noEmoji)) {
|
|
12897
12897
|
return null;
|
|
12898
12898
|
}
|
|
12899
|
-
const valueAsString =
|
|
12900
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12899
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12901
12900
|
const result = NO_EMOJI_RULE.applyOn(true, valueAsString);
|
|
12902
12901
|
if (!result) {
|
|
12903
12902
|
return null;
|
|
@@ -12911,8 +12910,7 @@ const MIN_LOWER_LETTER_CONSTRAINT = {
|
|
|
12911
12910
|
name: "min_lower_letter",
|
|
12912
12911
|
messageAttribute: "data-min-lower-letter-message",
|
|
12913
12912
|
check: (field) => {
|
|
12914
|
-
const valueAsString =
|
|
12915
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12913
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12916
12914
|
const required = field.controlHostProps.required;
|
|
12917
12915
|
if (!valueAsString && !required) {
|
|
12918
12916
|
return "";
|
|
@@ -12957,8 +12955,7 @@ const MIN_UPPER_LETTER_CONSTRAINT = {
|
|
|
12957
12955
|
name: "min_upper_letter",
|
|
12958
12956
|
messageAttribute: "data-min-upper-letter-message",
|
|
12959
12957
|
check: (field) => {
|
|
12960
|
-
const valueAsString =
|
|
12961
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12958
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12962
12959
|
const required = field.controlHostProps.required;
|
|
12963
12960
|
if (!valueAsString && !required) {
|
|
12964
12961
|
return null;
|
|
@@ -12994,8 +12991,7 @@ const MIN_DIGIT_CONSTRAINT = {
|
|
|
12994
12991
|
name: "min_digit",
|
|
12995
12992
|
messageAttribute: "data-min-digit-message",
|
|
12996
12993
|
check: (field) => {
|
|
12997
|
-
const valueAsString =
|
|
12998
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
12994
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
12999
12995
|
const required = field.controlHostProps.required;
|
|
13000
12996
|
if (!valueAsString && !required) {
|
|
13001
12997
|
return null;
|
|
@@ -13031,8 +13027,7 @@ const MIN_SPECIAL_CHAR_CONSTRAINT = {
|
|
|
13031
13027
|
name: "min_special_char",
|
|
13032
13028
|
messageAttribute: "data-min-special-char-message",
|
|
13033
13029
|
check: (field) => {
|
|
13034
|
-
const valueAsString =
|
|
13035
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
13030
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
13036
13031
|
const required = field.controlHostProps.required;
|
|
13037
13032
|
if (!valueAsString && !required) {
|
|
13038
13033
|
return null;
|
|
@@ -13149,8 +13144,7 @@ const SAME_AS_CONSTRAINT = {
|
|
|
13149
13144
|
// Reference field is empty — nothing to compare against yet.
|
|
13150
13145
|
return null;
|
|
13151
13146
|
}
|
|
13152
|
-
const valueAsString =
|
|
13153
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
13147
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
13154
13148
|
if (valueAsString === otherFieldValue) {
|
|
13155
13149
|
return null;
|
|
13156
13150
|
}
|
|
@@ -13191,8 +13185,7 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-same-as");
|
|
|
13191
13185
|
|
|
13192
13186
|
|
|
13193
13187
|
const applyRule = (field) => {
|
|
13194
|
-
const valueAsString =
|
|
13195
|
-
field.uiState === undefined ? "" : String(field.uiState);
|
|
13188
|
+
const valueAsString = uiStateAsText(field.uiState);
|
|
13196
13189
|
return SINGLE_SPACE_RULE.applyOn(true, valueAsString);
|
|
13197
13190
|
};
|
|
13198
13191
|
|
|
@@ -27733,6 +27726,18 @@ const takeoverRoutingRenderingHold = () => {
|
|
|
27733
27726
|
* to a reader. Kept in the session too, so a reload lands where the browser
|
|
27734
27727
|
* would have landed — the flag above is a promise to do the whole job.
|
|
27735
27728
|
*
|
|
27729
|
+
* The document is not the only scrollport of a page. A list scrolling itself
|
|
27730
|
+
* (a `<List expandY>` under a search field that stays put) is left and come
|
|
27731
|
+
* back to the same way, and the browser never knew it was a scrollport at
|
|
27732
|
+
* all: those say where they are by name (rememberScrollerPosition), under the
|
|
27733
|
+
* same URL and for the same session, and ask it back when they mount again
|
|
27734
|
+
* (recallScrollerPosition). What a push means for them is what it means for
|
|
27735
|
+
* the document — an arrival opens at the top (see startAtTop): the page
|
|
27736
|
+
* arrived at has its named positions dropped before its lists render, so a
|
|
27737
|
+
* list recalls only on the way back. A scroller that goes while its page
|
|
27738
|
+
* stays — a popup closing over the same address — has nothing to come back
|
|
27739
|
+
* to, and says so as it leaves (forgetScrollerUnlessPageLeft).
|
|
27740
|
+
*
|
|
27736
27741
|
* What is NOT covered, and cannot be from here: a page whose height depends on
|
|
27737
27742
|
* something still loading. Its content is not there at the moment it is put
|
|
27738
27743
|
* back, so a position beyond what has arrived is clamped as before. Only the
|
|
@@ -27750,7 +27755,23 @@ const takeoverRoutingRenderingHold = () => {
|
|
|
27750
27755
|
const STORAGE_KEY = "navi_scroll_positions";
|
|
27751
27756
|
|
|
27752
27757
|
const positionByUrl = new Map();
|
|
27753
|
-
|
|
27758
|
+
// url -> (scroller name -> position). The position is whatever the scroller
|
|
27759
|
+
// handed in: what it can put itself back on, in its own terms.
|
|
27760
|
+
const scrollerPositionsByUrl = new Map();
|
|
27761
|
+
// The url each named scroller last spoke under: what tells a scroller leaving
|
|
27762
|
+
// a page that stays from one leaving with its page.
|
|
27763
|
+
const urlByScrollerName = new Map();
|
|
27764
|
+
|
|
27765
|
+
// Read once, the first time anyone needs the positions: the document's
|
|
27766
|
+
// restoration is installed by the routing, and a list remembering itself may
|
|
27767
|
+
// mount in an app that never routes.
|
|
27768
|
+
let storeLoaded = false;
|
|
27769
|
+
const loadStore = () => {
|
|
27770
|
+
if (storeLoaded) {
|
|
27771
|
+
return;
|
|
27772
|
+
}
|
|
27773
|
+
storeLoaded = true;
|
|
27774
|
+
window.addEventListener("pagehide", storePositions);
|
|
27754
27775
|
let stored;
|
|
27755
27776
|
try {
|
|
27756
27777
|
stored = window.sessionStorage.getItem(STORAGE_KEY);
|
|
@@ -27763,18 +27784,31 @@ const readStoredPositions = () => {
|
|
|
27763
27784
|
return;
|
|
27764
27785
|
}
|
|
27765
27786
|
try {
|
|
27766
|
-
|
|
27787
|
+
const { document: documentPositions, scrollers } = JSON.parse(stored);
|
|
27788
|
+
for (const [url, position] of Object.entries(documentPositions || {})) {
|
|
27767
27789
|
positionByUrl.set(url, position);
|
|
27768
27790
|
}
|
|
27791
|
+
for (const [url, positionByName] of Object.entries(scrollers || {})) {
|
|
27792
|
+
scrollerPositionsByUrl.set(url, new Map(Object.entries(positionByName)));
|
|
27793
|
+
}
|
|
27769
27794
|
} catch {
|
|
27770
27795
|
// Something else wrote there, or it was truncated.
|
|
27771
27796
|
}
|
|
27772
27797
|
};
|
|
27773
27798
|
const storePositions = () => {
|
|
27799
|
+
const scrollers = {};
|
|
27800
|
+
for (const [url, positionByName] of scrollerPositionsByUrl) {
|
|
27801
|
+
if (positionByName.size > 0) {
|
|
27802
|
+
scrollers[url] = Object.fromEntries(positionByName);
|
|
27803
|
+
}
|
|
27804
|
+
}
|
|
27774
27805
|
try {
|
|
27775
27806
|
window.sessionStorage.setItem(
|
|
27776
27807
|
STORAGE_KEY,
|
|
27777
|
-
JSON.stringify(
|
|
27808
|
+
JSON.stringify({
|
|
27809
|
+
document: Object.fromEntries(positionByUrl),
|
|
27810
|
+
scrollers,
|
|
27811
|
+
}),
|
|
27778
27812
|
);
|
|
27779
27813
|
} catch {
|
|
27780
27814
|
// Full, or refused: the session is the only thing lost.
|
|
@@ -27814,7 +27848,7 @@ const installScrollRestoration = () => {
|
|
|
27814
27848
|
return;
|
|
27815
27849
|
}
|
|
27816
27850
|
window.history.scrollRestoration = "manual";
|
|
27817
|
-
|
|
27851
|
+
loadStore();
|
|
27818
27852
|
// Read as it happens rather than when leaving: a traverse changes the url
|
|
27819
27853
|
// before anything here is told, so a position read then would be read for
|
|
27820
27854
|
// the wrong page.
|
|
@@ -27831,7 +27865,6 @@ const installScrollRestoration = () => {
|
|
|
27831
27865
|
},
|
|
27832
27866
|
{ passive: true },
|
|
27833
27867
|
);
|
|
27834
|
-
window.addEventListener("pagehide", storePositions);
|
|
27835
27868
|
// What a reload asks for, now that the browser has been told not to do it.
|
|
27836
27869
|
// Once, and at the first render of a route: the position is only meaningful
|
|
27837
27870
|
// once there is a page under it.
|
|
@@ -27874,18 +27907,35 @@ const restoreScrollPosition = (url) => {
|
|
|
27874
27907
|
// The document, because the document is the scrollport in the common case. An
|
|
27875
27908
|
// app that scrolls an element of its own scrolls it itself.
|
|
27876
27909
|
const startAtTop = (url, { from } = {}) => {
|
|
27910
|
+
if (!isArrival(url, { from })) {
|
|
27911
|
+
return;
|
|
27912
|
+
}
|
|
27913
|
+
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
27914
|
+
};
|
|
27915
|
+
const isArrival = (url, { from }) => {
|
|
27877
27916
|
const urlObject = new URL(url, window.location.href);
|
|
27878
27917
|
// A fragment names where to land, and the browser is the one that finds it.
|
|
27879
27918
|
if (urlObject.hash) {
|
|
27880
|
-
return;
|
|
27919
|
+
return false;
|
|
27881
27920
|
}
|
|
27882
27921
|
if (
|
|
27883
27922
|
from !== undefined &&
|
|
27884
27923
|
new URL(from, window.location.href).pathname === urlObject.pathname
|
|
27885
27924
|
) {
|
|
27925
|
+
return false;
|
|
27926
|
+
}
|
|
27927
|
+
return true;
|
|
27928
|
+
};
|
|
27929
|
+
|
|
27930
|
+
// The same arrival, for the page's own scrollers. The document is scrolled to
|
|
27931
|
+
// its top once the page is there; a list opens where it decides to in its
|
|
27932
|
+
// first render, so what it must not find is dropped before the routing
|
|
27933
|
+
// renders anything.
|
|
27934
|
+
const forgetScrollersOnArrival = (url, { from } = {}) => {
|
|
27935
|
+
if (!isArrival(url, { from })) {
|
|
27886
27936
|
return;
|
|
27887
27937
|
}
|
|
27888
|
-
|
|
27938
|
+
scrollerPositionsByUrl.delete(new URL(url, window.location.href).href);
|
|
27889
27939
|
};
|
|
27890
27940
|
|
|
27891
27941
|
// An arrival at a page whose scrollport is already showing another one: the
|
|
@@ -27904,6 +27954,49 @@ const scrollTo = ({ x, y }) => {
|
|
|
27904
27954
|
window.scrollTo({ top: y, left: x, behavior: "instant" });
|
|
27905
27955
|
};
|
|
27906
27956
|
|
|
27957
|
+
const rememberScrollerPosition = (name, position) => {
|
|
27958
|
+
loadStore();
|
|
27959
|
+
const url = window.location.href;
|
|
27960
|
+
let positionByName = scrollerPositionsByUrl.get(url);
|
|
27961
|
+
if (!positionByName) {
|
|
27962
|
+
positionByName = new Map();
|
|
27963
|
+
scrollerPositionsByUrl.set(url, positionByName);
|
|
27964
|
+
}
|
|
27965
|
+
positionByName.set(name, position);
|
|
27966
|
+
urlByScrollerName.set(name, url);
|
|
27967
|
+
};
|
|
27968
|
+
|
|
27969
|
+
const recallScrollerPosition = (name) => {
|
|
27970
|
+
loadStore();
|
|
27971
|
+
const positionByName = scrollerPositionsByUrl.get(window.location.href);
|
|
27972
|
+
if (!positionByName) {
|
|
27973
|
+
return undefined;
|
|
27974
|
+
}
|
|
27975
|
+
return positionByName.get(name);
|
|
27976
|
+
};
|
|
27977
|
+
|
|
27978
|
+
// Said by a scroller as it unmounts. Its page is being left when the url is
|
|
27979
|
+
// already another one — the history is written before the page it names is
|
|
27980
|
+
// taken down — and then its position is kept for the way back. The url still
|
|
27981
|
+
// being the one it spoke under means the page stays and the scroller alone
|
|
27982
|
+
// goes (a popup closing, a section folding): there is no coming back to a
|
|
27983
|
+
// place that was not left, and a position kept would greet the next mount
|
|
27984
|
+
// under this address as a return.
|
|
27985
|
+
const forgetScrollerUnlessPageLeft = (name) => {
|
|
27986
|
+
const url = urlByScrollerName.get(name);
|
|
27987
|
+
if (url === undefined) {
|
|
27988
|
+
return;
|
|
27989
|
+
}
|
|
27990
|
+
urlByScrollerName.delete(name);
|
|
27991
|
+
if (url !== window.location.href) {
|
|
27992
|
+
return;
|
|
27993
|
+
}
|
|
27994
|
+
const positionByName = scrollerPositionsByUrl.get(url);
|
|
27995
|
+
if (positionByName) {
|
|
27996
|
+
positionByName.delete(name);
|
|
27997
|
+
}
|
|
27998
|
+
};
|
|
27999
|
+
|
|
27907
28000
|
/**
|
|
27908
28001
|
* A navigation is ABOUT to be applied — said before its very first write.
|
|
27909
28002
|
*
|
|
@@ -28266,6 +28359,13 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28266
28359
|
return undefined;
|
|
28267
28360
|
}
|
|
28268
28361
|
|
|
28362
|
+
// The page's own scrollers are told of an arrival before the routing
|
|
28363
|
+
// renders anything: a list arriving decides where it opens in its first
|
|
28364
|
+
// render (see scroll_restoration.js). The document itself is moved once
|
|
28365
|
+
// the page is there, below.
|
|
28366
|
+
if (navigationType === "push") {
|
|
28367
|
+
forgetScrollersOnArrival(url, { from: urlLeft });
|
|
28368
|
+
}
|
|
28269
28369
|
if (abortController) {
|
|
28270
28370
|
abortController.abort(`navigating to ${url}`);
|
|
28271
28371
|
}
|
|
@@ -37372,9 +37472,9 @@ const getInvalidCharsMessage = (
|
|
|
37372
37472
|
uiState,
|
|
37373
37473
|
{ charClass, messageKey, uiStateNow },
|
|
37374
37474
|
) => {
|
|
37375
|
-
const str =
|
|
37475
|
+
const str = uiStateAsText(uiState);
|
|
37376
37476
|
if (compileCharClassAnchored(charClass).test(str)) return null;
|
|
37377
|
-
const strNow =
|
|
37477
|
+
const strNow = uiStateAsText(uiStateNow);
|
|
37378
37478
|
if (
|
|
37379
37479
|
countCharsOutsideClass(str, charClass) <=
|
|
37380
37480
|
countCharsOutsideClass(strNow, charClass)
|
|
@@ -37387,9 +37487,9 @@ const getInvalidCharsMessage = (
|
|
|
37387
37487
|
// Paste / set: truncate what the gesture adds beyond the limit.
|
|
37388
37488
|
const getLengthOverflowResult = (uiState, { maxLength, uiStateNow }) => {
|
|
37389
37489
|
if (maxLength === undefined) return null;
|
|
37390
|
-
const str =
|
|
37490
|
+
const str = uiStateAsText(uiState);
|
|
37391
37491
|
if (str.length <= maxLength) return null;
|
|
37392
|
-
const strNow =
|
|
37492
|
+
const strNow = uiStateAsText(uiStateNow);
|
|
37393
37493
|
if (str.length <= strNow.length) return null;
|
|
37394
37494
|
return {
|
|
37395
37495
|
fixedValue: str.slice(0, maxLength),
|
|
@@ -65130,6 +65230,12 @@ const PickerCustom = props => {
|
|
|
65130
65230
|
// PickerNative takes.
|
|
65131
65231
|
pickerProps.resetOnError = true;
|
|
65132
65232
|
}
|
|
65233
|
+
if (pickerProps.resetOnAbort === undefined) {
|
|
65234
|
+
// Same reasoning for an action the app abandons (an AbortError, e.g. a
|
|
65235
|
+
// verification step the user dismissed): the popup is already closed, so
|
|
65236
|
+
// the abandoned value rolls back instead of staying selected in the list.
|
|
65237
|
+
pickerProps.resetOnAbort = true;
|
|
65238
|
+
}
|
|
65133
65239
|
// ref
|
|
65134
65240
|
const popupRef = useRef(null);
|
|
65135
65241
|
popupProps.ref = popupRef;
|
|
@@ -68513,10 +68619,11 @@ const ListUI = props => {
|
|
|
68513
68619
|
onListVisibleItemsChange,
|
|
68514
68620
|
virtualItemSize,
|
|
68515
68621
|
scrolled,
|
|
68516
|
-
defaultScrolled = "start",
|
|
68622
|
+
defaultScrolled: defaultScrolledProp = "start",
|
|
68517
68623
|
onScrolledChange,
|
|
68518
68624
|
scroller = "self",
|
|
68519
68625
|
hoverWhileScrolling = false,
|
|
68626
|
+
scrollResetOnNavigation = false,
|
|
68520
68627
|
lockSize,
|
|
68521
68628
|
columns,
|
|
68522
68629
|
itemColumns,
|
|
@@ -68535,6 +68642,20 @@ const ListUI = props => {
|
|
|
68535
68642
|
listRows,
|
|
68536
68643
|
...rest
|
|
68537
68644
|
} = props;
|
|
68645
|
+
// Remembered by name, and a name made up at render (see ListFirstResolver)
|
|
68646
|
+
// names no list a later mount would recognize.
|
|
68647
|
+
const rememberScroll = !scrollResetOnNavigation && !isLikelyPreactGeneratedId(rest.id);
|
|
68648
|
+
// Where the list was when its screen was left, when this is the way back
|
|
68649
|
+
// (see scroll_restoration.js). Read once: `defaultScrolled` is held by
|
|
68650
|
+
// reference, and a place read again each render would be a list moved each
|
|
68651
|
+
// render. A list held by its caller (`scrolled`) is where the caller says.
|
|
68652
|
+
const [scrolledRemembered] = useState(() => {
|
|
68653
|
+
if (!rememberScroll || scrolled !== undefined && scrolled !== null) {
|
|
68654
|
+
return undefined;
|
|
68655
|
+
}
|
|
68656
|
+
return recallScrollerPosition(rest.id);
|
|
68657
|
+
});
|
|
68658
|
+
const defaultScrolled = scrolledRemembered || defaultScrolledProp;
|
|
68538
68659
|
const scrollBoxPaddingProps = {};
|
|
68539
68660
|
for (const name of LIST_PADDING_PROP_SET) {
|
|
68540
68661
|
if (name in rest) {
|
|
@@ -68629,6 +68750,8 @@ const ListUI = props => {
|
|
|
68629
68750
|
scrolled,
|
|
68630
68751
|
defaultScrolled,
|
|
68631
68752
|
onScrolledChange,
|
|
68753
|
+
rememberScroll,
|
|
68754
|
+
listId: rest.id,
|
|
68632
68755
|
scroller,
|
|
68633
68756
|
searchText,
|
|
68634
68757
|
horizontal
|
|
@@ -68661,7 +68784,7 @@ const ListUI = props => {
|
|
|
68661
68784
|
// locator). Both answer here, so a row is reachable whether or not the
|
|
68662
68785
|
// window happens to frame it.
|
|
68663
68786
|
const getItemById = itemId => {
|
|
68664
|
-
const itemDrawn = listRows.itemsSignal.peek().find(item => item.
|
|
68787
|
+
const itemDrawn = listRows.itemsSignal.peek().find(item => item.itemId === itemId);
|
|
68665
68788
|
if (itemDrawn) {
|
|
68666
68789
|
return itemDrawn;
|
|
68667
68790
|
}
|
|
@@ -68671,6 +68794,7 @@ const ListUI = props => {
|
|
|
68671
68794
|
}
|
|
68672
68795
|
return {
|
|
68673
68796
|
id: itemId,
|
|
68797
|
+
itemId,
|
|
68674
68798
|
index: rowIndex
|
|
68675
68799
|
};
|
|
68676
68800
|
};
|
|
@@ -68953,6 +69077,8 @@ const useListScrollSync = ({
|
|
|
68953
69077
|
scrolled,
|
|
68954
69078
|
defaultScrolled,
|
|
68955
69079
|
onScrolledChange,
|
|
69080
|
+
rememberScroll,
|
|
69081
|
+
listId,
|
|
68956
69082
|
scroller,
|
|
68957
69083
|
searchText,
|
|
68958
69084
|
horizontal
|
|
@@ -69166,16 +69292,19 @@ const useListScrollSync = ({
|
|
|
69166
69292
|
const trigger = `"${event.type}" on ${getElementSignature(event.target)} (${reason})`;
|
|
69167
69293
|
// When we display the list we prefer to have selected item at the center
|
|
69168
69294
|
// otherwise, usually when focused by arrow nav, we want to keep it into view close to the nearest edge
|
|
69169
|
-
const
|
|
69170
|
-
const scrollToItemCall = `${getElementSignature(itemEl)}.scrollIntoView({ block: "${
|
|
69295
|
+
const align = blockRequested || (event.type === "navi_displayed" ? "center" : "nearest");
|
|
69296
|
+
const scrollToItemCall = `${getElementSignature(itemEl)}.scrollIntoView({ block: "${align}", inline: "${align}", container: "nearest" })`;
|
|
69171
69297
|
debugScroll(`${trigger} -> ${scrollToItemCall}`);
|
|
69172
69298
|
// The list is going somewhere on purpose, so there is no view to hold
|
|
69173
69299
|
// still any more: an anchor captured before this drop it, or it would
|
|
69174
69300
|
// put the list back where it was the moment the rows move under it.
|
|
69175
69301
|
anchorRef.current = null;
|
|
69302
|
+
// One alignment, said on both axes: the axis the list scrolls on is the
|
|
69303
|
+
// one that reads it, and the other has nothing to move.
|
|
69176
69304
|
scrollIntoViewScoped(itemEl, {
|
|
69177
69305
|
container: getScroller(),
|
|
69178
|
-
block
|
|
69306
|
+
block: align,
|
|
69307
|
+
inline: align
|
|
69179
69308
|
});
|
|
69180
69309
|
const listEl = getListEl();
|
|
69181
69310
|
dispatchPublicCustomEvent(listEl, "navi_scroll", {
|
|
@@ -69187,7 +69316,7 @@ const useListScrollSync = ({
|
|
|
69187
69316
|
// Whether the row is drawn is asked of the dom, not of the render window:
|
|
69188
69317
|
// the window says what a run draws, and a list whose rows are declared one
|
|
69189
69318
|
// by one has them all in the dom whatever the window says.
|
|
69190
|
-
const itemEl = findRowElement(getListEl(), item.
|
|
69319
|
+
const itemEl = findRowElement(getListEl(), item.itemId);
|
|
69191
69320
|
if (itemEl) {
|
|
69192
69321
|
scrollItemIntoView(itemEl);
|
|
69193
69322
|
return;
|
|
@@ -69195,7 +69324,7 @@ const useListScrollSync = ({
|
|
|
69195
69324
|
// Not in DOM — shift the render window. The item will read
|
|
69196
69325
|
// pendingScrollRef on mount and scroll into view.
|
|
69197
69326
|
pendingScrollRef.current = {
|
|
69198
|
-
id: item.
|
|
69327
|
+
id: item.itemId,
|
|
69199
69328
|
resolve: itemEl => {
|
|
69200
69329
|
pendingScrollRef.current = null;
|
|
69201
69330
|
scrollItemIntoView(itemEl);
|
|
@@ -69547,6 +69676,8 @@ const useListScrollSync = ({
|
|
|
69547
69676
|
// one was looking at is then somewhere else.
|
|
69548
69677
|
const onScrolledChangeRef = useRef(null);
|
|
69549
69678
|
onScrolledChangeRef.current = onScrolledChange;
|
|
69679
|
+
const rememberScrollRef = useRef(false);
|
|
69680
|
+
rememberScrollRef.current = rememberScroll;
|
|
69550
69681
|
// Where the list was at the last thing that moved it. Kept whether anyone
|
|
69551
69682
|
// asked for it or not: it is what a resize needs to put things back.
|
|
69552
69683
|
const positionRef = useRef(null);
|
|
@@ -69564,16 +69695,32 @@ const useListScrollSync = ({
|
|
|
69564
69695
|
return;
|
|
69565
69696
|
}
|
|
69566
69697
|
positionRef.current = position;
|
|
69567
|
-
|
|
69698
|
+
const remember = rememberScrollRef.current;
|
|
69699
|
+
const onScrolledChange = onScrolledChangeRef.current;
|
|
69700
|
+
if (!remember && !onScrolledChange) {
|
|
69568
69701
|
return;
|
|
69569
69702
|
}
|
|
69570
69703
|
const rowEl = findRowElement(getListEl(), position.id);
|
|
69571
|
-
|
|
69704
|
+
const scrolledNow = {
|
|
69572
69705
|
id: position.id,
|
|
69573
69706
|
index: position.index,
|
|
69574
69707
|
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal)
|
|
69575
|
-
}
|
|
69708
|
+
};
|
|
69709
|
+
if (remember) {
|
|
69710
|
+
rememberScrollerPosition(listId, scrolledNow);
|
|
69711
|
+
}
|
|
69712
|
+
if (onScrolledChange) {
|
|
69713
|
+
onScrolledChange(scrolledNow);
|
|
69714
|
+
}
|
|
69576
69715
|
};
|
|
69716
|
+
// Leaving: with its page, or alone (see forgetScrollerUnlessPageLeft).
|
|
69717
|
+
useLayoutEffect(() => {
|
|
69718
|
+
return () => {
|
|
69719
|
+
if (rememberScrollRef.current) {
|
|
69720
|
+
forgetScrollerUnlessPageLeft(listId);
|
|
69721
|
+
}
|
|
69722
|
+
};
|
|
69723
|
+
}, []);
|
|
69577
69724
|
|
|
69578
69725
|
// A list that gets narrower rewraps every row it holds, so everything below
|
|
69579
69726
|
// moves and the reader loses their place — the very thing scrolling a long
|
|
@@ -69646,7 +69793,7 @@ const useListScrollSync = ({
|
|
|
69646
69793
|
return;
|
|
69647
69794
|
}
|
|
69648
69795
|
const items = listRows.visibleItemsSignal.peek();
|
|
69649
|
-
const itemNow = items.find(i => i.
|
|
69796
|
+
const itemNow = items.find(i => i.itemId === anchor.id);
|
|
69650
69797
|
if (!itemNow) {
|
|
69651
69798
|
anchorRef.current = null;
|
|
69652
69799
|
return;
|
|
@@ -70345,17 +70492,22 @@ const resolveScrollInset = (value, viewportSize) => {
|
|
|
70345
70492
|
return number;
|
|
70346
70493
|
};
|
|
70347
70494
|
|
|
70348
|
-
// The row
|
|
70349
|
-
//
|
|
70350
|
-
//
|
|
70351
|
-
//
|
|
70352
|
-
//
|
|
70495
|
+
// The row of that name, IN THIS LIST — by the name the list knows it under
|
|
70496
|
+
// (see ListItemUI), not the element's id. Not document.getElementById either:
|
|
70497
|
+
// a name is only ever unique within a list — two lists on the same page can be
|
|
70498
|
+
// showing the same collection — and a list acting on a row that belongs to
|
|
70499
|
+
// another one is a spectacular kind of wrong (it scrolls to hold still
|
|
70500
|
+
// something it is not even showing).
|
|
70353
70501
|
const findRowElement = (listEl, id) => {
|
|
70354
|
-
return listEl.querySelector(`[
|
|
70502
|
+
return listEl.querySelector(`[navi-list-item-real="${CSS.escape(id)}"]`);
|
|
70355
70503
|
};
|
|
70504
|
+
const getRowName = rowEl => rowEl.getAttribute("navi-list-item-real");
|
|
70356
70505
|
|
|
70357
70506
|
// The row the user is looking at, and where it sits: what must not move when
|
|
70358
|
-
// the list is rebuilt around it.
|
|
70507
|
+
// the list is rebuilt around it. Read off the rows' own boxes, not by
|
|
70508
|
+
// hit-testing the screen: a scrolling list takes its rows out of hit-testing
|
|
70509
|
+
// (see the navi-scrolling rule in the css above), and the scroll event is
|
|
70510
|
+
// precisely when this is asked.
|
|
70359
70511
|
const captureScrollAnchor = ({
|
|
70360
70512
|
scrollerEl,
|
|
70361
70513
|
listEl,
|
|
@@ -70367,30 +70519,31 @@ const captureScrollAnchor = ({
|
|
|
70367
70519
|
}
|
|
70368
70520
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
70369
70521
|
const listRect = listEl.getBoundingClientRect();
|
|
70370
|
-
const
|
|
70371
|
-
if (!
|
|
70522
|
+
const range = getListVisibleRange(viewportRect, listRect, horizontal);
|
|
70523
|
+
if (!range) {
|
|
70372
70524
|
return null;
|
|
70373
70525
|
}
|
|
70526
|
+
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
|
|
70527
|
+
const {
|
|
70528
|
+
rowEls,
|
|
70529
|
+
index
|
|
70530
|
+
} = findRowsFrom(listEl, range.from, horizontal);
|
|
70374
70531
|
let fallbackAnchor = null;
|
|
70375
|
-
for (let
|
|
70376
|
-
const
|
|
70377
|
-
const
|
|
70378
|
-
const
|
|
70379
|
-
if (
|
|
70380
|
-
|
|
70381
|
-
}
|
|
70382
|
-
const itemEl = el.closest(REAL_LIST_ITEM_SELECTOR);
|
|
70383
|
-
if (!itemEl) {
|
|
70384
|
-
continue;
|
|
70532
|
+
for (let i = index; i < rowEls.length; i++) {
|
|
70533
|
+
const rowEl = rowEls[i];
|
|
70534
|
+
const rowRect = rowEl.getBoundingClientRect();
|
|
70535
|
+
const rowStart = horizontal ? rowRect.left : rowRect.top;
|
|
70536
|
+
if (rowStart >= range.to) {
|
|
70537
|
+
break;
|
|
70385
70538
|
}
|
|
70386
|
-
const
|
|
70539
|
+
const rowName = getRowName(rowEl);
|
|
70540
|
+
const item = items.find(i => i.itemId === rowName);
|
|
70387
70541
|
if (!item) {
|
|
70388
70542
|
continue;
|
|
70389
70543
|
}
|
|
70390
|
-
const
|
|
70391
|
-
const offset = horizontal ? itemRect.left - viewportRect.left : itemRect.top - viewportRect.top;
|
|
70544
|
+
const offset = rowStart - viewportFrom;
|
|
70392
70545
|
const anchor = {
|
|
70393
|
-
id: item.
|
|
70546
|
+
id: item.itemId,
|
|
70394
70547
|
index: item.index,
|
|
70395
70548
|
offset
|
|
70396
70549
|
};
|
|
@@ -70410,43 +70563,61 @@ const captureScrollAnchor = ({
|
|
|
70410
70563
|
// The part of the list that is on screen, along the scrolling axis. Both edges
|
|
70411
70564
|
// matter: the scroller may be larger than the list (scroller="parent") as well
|
|
70412
70565
|
// as smaller (the list scrolls inside its own box).
|
|
70413
|
-
const
|
|
70414
|
-
// The screen has a say too: what is asked here is answered by
|
|
70415
|
-
// elementFromPoint, which only knows about points that are actually on it. A
|
|
70416
|
-
// list whose scroll box hangs below the fold of the page would otherwise be
|
|
70417
|
-
// probed where nothing can be hit — and would silently stop keeping its rows
|
|
70418
|
-
// still, which is exactly when it matters.
|
|
70419
|
-
const screenTo = horizontal ? document.documentElement.clientWidth : document.documentElement.clientHeight;
|
|
70566
|
+
const getListVisibleRange = (viewportRect, listRect, horizontal) => {
|
|
70420
70567
|
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
|
|
70421
70568
|
const viewportTo = horizontal ? viewportRect.right : viewportRect.bottom;
|
|
70422
70569
|
const listFrom = horizontal ? listRect.left : listRect.top;
|
|
70423
70570
|
const listTo = horizontal ? listRect.right : listRect.bottom;
|
|
70424
|
-
|
|
70425
|
-
|
|
70426
|
-
if (from < 0) {
|
|
70427
|
-
from = 0;
|
|
70428
|
-
}
|
|
70429
|
-
if (to > screenTo) {
|
|
70430
|
-
to = screenTo;
|
|
70431
|
-
}
|
|
70571
|
+
const from = listFrom > viewportFrom ? listFrom : viewportFrom;
|
|
70572
|
+
const to = listTo < viewportTo ? listTo : viewportTo;
|
|
70432
70573
|
if (to - from < 2) {
|
|
70433
70574
|
return null;
|
|
70434
70575
|
}
|
|
70435
|
-
// Where to put the probe on the other axis: inside the list, inside the
|
|
70436
|
-
// viewport.
|
|
70437
|
-
const crossFrom = horizontal ? listRect.top : listRect.left;
|
|
70438
|
-
const crossViewportFrom = horizontal ? viewportRect.top : viewportRect.left;
|
|
70439
|
-
const crossPos = (crossFrom > crossViewportFrom ? crossFrom : crossViewportFrom) + 1;
|
|
70440
70576
|
return {
|
|
70441
70577
|
from,
|
|
70442
|
-
to
|
|
70443
|
-
crossPos
|
|
70578
|
+
to
|
|
70444
70579
|
};
|
|
70445
70580
|
};
|
|
70581
|
+
// The real rows of the list, and the first of them reaching past `from` along
|
|
70582
|
+
// the scroll axis. A binary search over their boxes: the rows stand in
|
|
70583
|
+
// document order along that axis, so their far edges only grow.
|
|
70584
|
+
const findRowsFrom = (listEl, from, horizontal) => {
|
|
70585
|
+
const rowEls = listEl.querySelectorAll(REAL_LIST_ITEM_SELECTOR);
|
|
70586
|
+
let low = 0;
|
|
70587
|
+
let high = rowEls.length;
|
|
70588
|
+
while (low < high) {
|
|
70589
|
+
const mid = low + high >> 1;
|
|
70590
|
+
const rect = rowEls[mid].getBoundingClientRect();
|
|
70591
|
+
const end = horizontal ? rect.right : rect.bottom;
|
|
70592
|
+
if (end > from) {
|
|
70593
|
+
high = mid;
|
|
70594
|
+
} else {
|
|
70595
|
+
low = mid + 1;
|
|
70596
|
+
}
|
|
70597
|
+
}
|
|
70598
|
+
return {
|
|
70599
|
+
rowEls,
|
|
70600
|
+
index: low
|
|
70601
|
+
};
|
|
70602
|
+
};
|
|
70603
|
+
// Whether a filler (the room held for rows outside the window) is what stands
|
|
70604
|
+
// at that position along the scroll axis.
|
|
70605
|
+
const isFillerAt = (listEl, position, horizontal) => {
|
|
70606
|
+
for (const fillerEl of listEl.querySelectorAll("[navi-virtual-filler]")) {
|
|
70607
|
+
const rect = fillerEl.getBoundingClientRect();
|
|
70608
|
+
const from = horizontal ? rect.left : rect.top;
|
|
70609
|
+
const to = horizontal ? rect.right : rect.bottom;
|
|
70610
|
+
if (position >= from && position < to) {
|
|
70611
|
+
return true;
|
|
70612
|
+
}
|
|
70613
|
+
}
|
|
70614
|
+
return false;
|
|
70615
|
+
};
|
|
70446
70616
|
|
|
70447
|
-
// Which row of the collection sits at the current scroll position.
|
|
70448
|
-
//
|
|
70449
|
-
// on screen is only
|
|
70617
|
+
// Which row of the collection sits at the current scroll position. Read off
|
|
70618
|
+
// the rows' boxes when a real row is there (see captureScrollAnchor for why
|
|
70619
|
+
// not hit-testing), and from the row size when what is on screen is only
|
|
70620
|
+
// reserved room.
|
|
70450
70621
|
// Returns { index, item, reason } or null if nothing can be determined.
|
|
70451
70622
|
const getScrollInfo = ({
|
|
70452
70623
|
scrollValues,
|
|
@@ -70460,34 +70631,31 @@ const getScrollInfo = ({
|
|
|
70460
70631
|
const items = listRows.itemsSignal.peek();
|
|
70461
70632
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
70462
70633
|
const listRect = listEl.getBoundingClientRect();
|
|
70463
|
-
|
|
70464
|
-
|
|
70465
|
-
const scanRange = getListVisibleScanRange(viewportRect, listRect, horizontal);
|
|
70466
|
-
if (!scanRange) {
|
|
70634
|
+
const range = getListVisibleRange(viewportRect, listRect, horizontal);
|
|
70635
|
+
if (!range) {
|
|
70467
70636
|
return null;
|
|
70468
70637
|
}
|
|
70469
|
-
//
|
|
70470
|
-
//
|
|
70471
|
-
//
|
|
70472
|
-
//
|
|
70473
|
-
const scanStart = (
|
|
70474
|
-
|
|
70475
|
-
|
|
70476
|
-
|
|
70477
|
-
|
|
70478
|
-
|
|
70479
|
-
|
|
70480
|
-
|
|
70481
|
-
|
|
70482
|
-
|
|
70483
|
-
|
|
70484
|
-
|
|
70485
|
-
|
|
70486
|
-
|
|
70487
|
-
|
|
70488
|
-
|
|
70489
|
-
|
|
70490
|
-
break;
|
|
70638
|
+
// Read from the center of the visible part of the list along the main axis.
|
|
70639
|
+
// The render window places half its budget before and half after the hit
|
|
70640
|
+
// index. Anchoring to the center maximises how many rendered items fall
|
|
70641
|
+
// within the visible area.
|
|
70642
|
+
const scanStart = (range.from + range.to) / 2;
|
|
70643
|
+
let hitEl = null;
|
|
70644
|
+
const hitFiller = isFillerAt(listEl, scanStart, horizontal);
|
|
70645
|
+
if (!hitFiller) {
|
|
70646
|
+
// The first real row from the center down, the way a probe walking down
|
|
70647
|
+
// from it would meet one — past a separator or a group label in between.
|
|
70648
|
+
const {
|
|
70649
|
+
rowEls,
|
|
70650
|
+
index
|
|
70651
|
+
} = findRowsFrom(listEl, scanStart, horizontal);
|
|
70652
|
+
const rowEl = rowEls[index];
|
|
70653
|
+
if (rowEl) {
|
|
70654
|
+
const rowRect = rowEl.getBoundingClientRect();
|
|
70655
|
+
const rowStart = horizontal ? rowRect.left : rowRect.top;
|
|
70656
|
+
if (rowStart < range.to) {
|
|
70657
|
+
hitEl = rowEl;
|
|
70658
|
+
}
|
|
70491
70659
|
}
|
|
70492
70660
|
}
|
|
70493
70661
|
// Shared by the "hit a filler" and "hit nothing at all" cases below: both
|
|
@@ -70518,8 +70686,8 @@ const getScrollInfo = ({
|
|
|
70518
70686
|
return estimateFromScrollPos("hit filler");
|
|
70519
70687
|
}
|
|
70520
70688
|
if (hitEl) {
|
|
70521
|
-
const
|
|
70522
|
-
const item = items.find(i => i.
|
|
70689
|
+
const hitName = getRowName(hitEl);
|
|
70690
|
+
const item = items.find(i => i.itemId === hitName);
|
|
70523
70691
|
if (!item) {
|
|
70524
70692
|
return null;
|
|
70525
70693
|
}
|
|
@@ -70529,13 +70697,11 @@ const getScrollInfo = ({
|
|
|
70529
70697
|
reason: `hit item at ${item.index} (${item.value})`
|
|
70530
70698
|
};
|
|
70531
70699
|
}
|
|
70532
|
-
//
|
|
70533
|
-
// the
|
|
70534
|
-
//
|
|
70535
|
-
//
|
|
70536
|
-
//
|
|
70537
|
-
// estimate as the hitFiller case is a safe fallback: it only needs the
|
|
70538
|
-
// scroll position, not a successful hit-test.
|
|
70700
|
+
// No real row stands between the center and the end of what is visible.
|
|
70701
|
+
// Keeping the stale renderWindow here means the DOM never gets asked to
|
|
70702
|
+
// catch up with a scrollTop that may have jumped far away — the user ends
|
|
70703
|
+
// up staring at blank space. Same estimate as the hitFiller case is a safe
|
|
70704
|
+
// fallback: it only needs the scroll position.
|
|
70539
70705
|
const estimated = estimateFromScrollPos("no hit");
|
|
70540
70706
|
if (estimated) {
|
|
70541
70707
|
return estimated;
|
|
@@ -70970,6 +71136,12 @@ const ListItemUI = props => {
|
|
|
70970
71136
|
// gave the row its place and decided it is inside the render window.
|
|
70971
71137
|
const row = useContext(ListRowContext);
|
|
70972
71138
|
const slotId = useContext(ListSlotContext);
|
|
71139
|
+
// What the row is called in the list — the run's name for a row it draws,
|
|
71140
|
+
// whatever `id` the caller put on the element (a run row may need a DOM id of
|
|
71141
|
+
// its own, to keep clear of another element's). A position names a row by
|
|
71142
|
+
// this (see captureScrollAnchor), and a row asked for by name is found by
|
|
71143
|
+
// this (locateRow, findRowElement); the DOM id is the caller's.
|
|
71144
|
+
props.itemId = row ? row.id : props.id;
|
|
70973
71145
|
// There is no standalone match/matchScore/highlight prop — participation
|
|
70974
71146
|
// in a matching system (search, filter…) only goes through `matchInfo`
|
|
70975
71147
|
// (e.g. useSearchText's getItemMatchInfo(item): { match, matchScore,
|
|
@@ -71050,6 +71222,7 @@ const ListItemReal = props => {
|
|
|
71050
71222
|
const {
|
|
71051
71223
|
ref,
|
|
71052
71224
|
id,
|
|
71225
|
+
itemId,
|
|
71053
71226
|
hidden,
|
|
71054
71227
|
muted,
|
|
71055
71228
|
loading,
|
|
@@ -71069,7 +71242,7 @@ const ListItemReal = props => {
|
|
|
71069
71242
|
// see the state change, which only the caller can arrange).
|
|
71070
71243
|
const pendingScrollRef = useContext(PendingScrollRefContext);
|
|
71071
71244
|
const pendingScroll = pendingScrollRef.current;
|
|
71072
|
-
const needScrollOnMount = pendingScroll && pendingScroll.id ===
|
|
71245
|
+
const needScrollOnMount = pendingScroll && pendingScroll.id === itemId;
|
|
71073
71246
|
useLayoutEffect(() => {
|
|
71074
71247
|
if (!needScrollOnMount) {
|
|
71075
71248
|
return;
|
|
@@ -71168,7 +71341,7 @@ const ListItemReal = props => {
|
|
|
71168
71341
|
baseClassName: "navi_list_item",
|
|
71169
71342
|
styleCSSVars: LIST_ITEM_STYLE_CSS_VARS,
|
|
71170
71343
|
id: id,
|
|
71171
|
-
"navi-list-item-real":
|
|
71344
|
+
"navi-list-item-real": itemId,
|
|
71172
71345
|
...rest,
|
|
71173
71346
|
...itemColumnsOverrideProps,
|
|
71174
71347
|
index: undefined,
|
|
@@ -72549,6 +72722,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
72549
72722
|
* onScrolledChange?: (scrolled: {id: string, index: number, offset: number}) => void,
|
|
72550
72723
|
* scroller?: "self" | "parent" | "document" | Element | {current: Element},
|
|
72551
72724
|
* hoverWhileScrolling?: boolean,
|
|
72725
|
+
* scrollResetOnNavigation?: boolean,
|
|
72552
72726
|
* fallback?: import("ignore:preact").ComponentChildren,
|
|
72553
72727
|
* searchFallback?: import("ignore:preact").ComponentChildren,
|
|
72554
72728
|
* searchText?: string,
|
|
@@ -72640,7 +72814,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
72640
72814
|
* list is already known to be empty: the empty `fallback` shows right away
|
|
72641
72815
|
* rather than an empty frame, so nothing moves when the response arrives.
|
|
72642
72816
|
* @param {"start"|"end"|number|{id: string, offset?: number}} [props.defaultScrolled="start"]
|
|
72643
|
-
* Where the list opens, after which the user owns the scroll
|
|
72817
|
+
* Where the list opens, after which the user owns the scroll — unless it is
|
|
72818
|
+
* being come back to (see `scrollResetOnNavigation`). `"end"` is a
|
|
72644
72819
|
* thread read backwards — the last rows are the ones to show, and the ones
|
|
72645
72820
|
* asked for first. A number opens on that row of the collection. `{id,
|
|
72646
72821
|
* offset}` — what `onScrolledChange` hands out — opens on a NAMED row,
|
|
@@ -72723,6 +72898,15 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
72723
72898
|
* Pass `true` for a list whose rows must stay live under the pointer while
|
|
72724
72899
|
* it scrolls. The trade of the default is the mirror one: right after a
|
|
72725
72900
|
* scroll, the row under the pointer lights up only once the pointer moves.
|
|
72901
|
+
* @param {boolean} [props.scrollResetOnNavigation=false]
|
|
72902
|
+
* A list that opens the same way every time. Without it the list comes back
|
|
72903
|
+
* where it was when its screen is left and come back to — the way the page
|
|
72904
|
+
* does, and for a list that scrolls itself the page's own restoration cannot
|
|
72905
|
+
* see. The position is kept under the list's `id` and the page's url, for
|
|
72906
|
+
* the session (a reload comes back too); a list without an `id` of its own
|
|
72907
|
+
* has nothing to be remembered by. A fresh arrival at the page opens at
|
|
72908
|
+
* `defaultScrolled` either way, and so does a list the caller holds through
|
|
72909
|
+
* `scrolled`.
|
|
72726
72910
|
* @param {boolean} [props.deselectable]
|
|
72727
72911
|
* A single-select list allowed to hold nothing: the selected row, pressed
|
|
72728
72912
|
* again, lets go. Without it the list is a radio group — a choice, once
|