@jsenv/navi 0.29.361 → 0.29.362
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 +289 -104
- package/dist/dev/jsenv_navi.js.map +4 -4
- package/dist/jsenv_navi.js +289 -104
- package/dist/jsenv_navi.js.map +4 -4
- package/docs/scroll.md +6 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -26978,6 +26978,18 @@ const takeoverRoutingRenderingHold = () => {
|
|
|
26978
26978
|
* to a reader. Kept in the session too, so a reload lands where the browser
|
|
26979
26979
|
* would have landed — the flag above is a promise to do the whole job.
|
|
26980
26980
|
*
|
|
26981
|
+
* The document is not the only scrollport of a page. A list scrolling itself
|
|
26982
|
+
* (a `<List expandY>` under a search field that stays put) is left and come
|
|
26983
|
+
* back to the same way, and the browser never knew it was a scrollport at
|
|
26984
|
+
* all: those say where they are by name (rememberScrollerPosition), under the
|
|
26985
|
+
* same URL and for the same session, and ask it back when they mount again
|
|
26986
|
+
* (recallScrollerPosition). What a push means for them is what it means for
|
|
26987
|
+
* the document — an arrival opens at the top (see startAtTop): the page
|
|
26988
|
+
* arrived at has its named positions dropped before its lists render, so a
|
|
26989
|
+
* list recalls only on the way back. A scroller that goes while its page
|
|
26990
|
+
* stays — a popup closing over the same address — has nothing to come back
|
|
26991
|
+
* to, and says so as it leaves (forgetScrollerUnlessPageLeft).
|
|
26992
|
+
*
|
|
26981
26993
|
* What is NOT covered, and cannot be from here: a page whose height depends on
|
|
26982
26994
|
* something still loading. Its content is not there at the moment it is put
|
|
26983
26995
|
* back, so a position beyond what has arrived is clamped as before. Only the
|
|
@@ -26995,7 +27007,23 @@ const takeoverRoutingRenderingHold = () => {
|
|
|
26995
27007
|
const STORAGE_KEY = "navi_scroll_positions";
|
|
26996
27008
|
|
|
26997
27009
|
const positionByUrl = new Map();
|
|
26998
|
-
|
|
27010
|
+
// url -> (scroller name -> position). The position is whatever the scroller
|
|
27011
|
+
// handed in: what it can put itself back on, in its own terms.
|
|
27012
|
+
const scrollerPositionsByUrl = new Map();
|
|
27013
|
+
// The url each named scroller last spoke under: what tells a scroller leaving
|
|
27014
|
+
// a page that stays from one leaving with its page.
|
|
27015
|
+
const urlByScrollerName = new Map();
|
|
27016
|
+
|
|
27017
|
+
// Read once, the first time anyone needs the positions: the document's
|
|
27018
|
+
// restoration is installed by the routing, and a list remembering itself may
|
|
27019
|
+
// mount in an app that never routes.
|
|
27020
|
+
let storeLoaded = false;
|
|
27021
|
+
const loadStore = () => {
|
|
27022
|
+
if (storeLoaded) {
|
|
27023
|
+
return;
|
|
27024
|
+
}
|
|
27025
|
+
storeLoaded = true;
|
|
27026
|
+
window.addEventListener("pagehide", storePositions);
|
|
26999
27027
|
let stored;
|
|
27000
27028
|
try {
|
|
27001
27029
|
stored = window.sessionStorage.getItem(STORAGE_KEY);
|
|
@@ -27008,18 +27036,31 @@ const readStoredPositions = () => {
|
|
|
27008
27036
|
return;
|
|
27009
27037
|
}
|
|
27010
27038
|
try {
|
|
27011
|
-
|
|
27039
|
+
const { document: documentPositions, scrollers } = JSON.parse(stored);
|
|
27040
|
+
for (const [url, position] of Object.entries(documentPositions || {})) {
|
|
27012
27041
|
positionByUrl.set(url, position);
|
|
27013
27042
|
}
|
|
27043
|
+
for (const [url, positionByName] of Object.entries(scrollers || {})) {
|
|
27044
|
+
scrollerPositionsByUrl.set(url, new Map(Object.entries(positionByName)));
|
|
27045
|
+
}
|
|
27014
27046
|
} catch {
|
|
27015
27047
|
// Something else wrote there, or it was truncated.
|
|
27016
27048
|
}
|
|
27017
27049
|
};
|
|
27018
27050
|
const storePositions = () => {
|
|
27051
|
+
const scrollers = {};
|
|
27052
|
+
for (const [url, positionByName] of scrollerPositionsByUrl) {
|
|
27053
|
+
if (positionByName.size > 0) {
|
|
27054
|
+
scrollers[url] = Object.fromEntries(positionByName);
|
|
27055
|
+
}
|
|
27056
|
+
}
|
|
27019
27057
|
try {
|
|
27020
27058
|
window.sessionStorage.setItem(
|
|
27021
27059
|
STORAGE_KEY,
|
|
27022
|
-
JSON.stringify(
|
|
27060
|
+
JSON.stringify({
|
|
27061
|
+
document: Object.fromEntries(positionByUrl),
|
|
27062
|
+
scrollers,
|
|
27063
|
+
}),
|
|
27023
27064
|
);
|
|
27024
27065
|
} catch {
|
|
27025
27066
|
// Full, or refused: the session is the only thing lost.
|
|
@@ -27059,7 +27100,7 @@ const installScrollRestoration = () => {
|
|
|
27059
27100
|
return;
|
|
27060
27101
|
}
|
|
27061
27102
|
window.history.scrollRestoration = "manual";
|
|
27062
|
-
|
|
27103
|
+
loadStore();
|
|
27063
27104
|
// Read as it happens rather than when leaving: a traverse changes the url
|
|
27064
27105
|
// before anything here is told, so a position read then would be read for
|
|
27065
27106
|
// the wrong page.
|
|
@@ -27076,7 +27117,6 @@ const installScrollRestoration = () => {
|
|
|
27076
27117
|
},
|
|
27077
27118
|
{ passive: true },
|
|
27078
27119
|
);
|
|
27079
|
-
window.addEventListener("pagehide", storePositions);
|
|
27080
27120
|
// What a reload asks for, now that the browser has been told not to do it.
|
|
27081
27121
|
// Once, and at the first render of a route: the position is only meaningful
|
|
27082
27122
|
// once there is a page under it.
|
|
@@ -27119,18 +27159,35 @@ const restoreScrollPosition = (url) => {
|
|
|
27119
27159
|
// The document, because the document is the scrollport in the common case. An
|
|
27120
27160
|
// app that scrolls an element of its own scrolls it itself.
|
|
27121
27161
|
const startAtTop = (url, { from } = {}) => {
|
|
27162
|
+
if (!isArrival(url, { from })) {
|
|
27163
|
+
return;
|
|
27164
|
+
}
|
|
27165
|
+
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
27166
|
+
};
|
|
27167
|
+
const isArrival = (url, { from }) => {
|
|
27122
27168
|
const urlObject = new URL(url, window.location.href);
|
|
27123
27169
|
// A fragment names where to land, and the browser is the one that finds it.
|
|
27124
27170
|
if (urlObject.hash) {
|
|
27125
|
-
return;
|
|
27171
|
+
return false;
|
|
27126
27172
|
}
|
|
27127
27173
|
if (
|
|
27128
27174
|
from !== undefined &&
|
|
27129
27175
|
new URL(from, window.location.href).pathname === urlObject.pathname
|
|
27130
27176
|
) {
|
|
27177
|
+
return false;
|
|
27178
|
+
}
|
|
27179
|
+
return true;
|
|
27180
|
+
};
|
|
27181
|
+
|
|
27182
|
+
// The same arrival, for the page's own scrollers. The document is scrolled to
|
|
27183
|
+
// its top once the page is there; a list opens where it decides to in its
|
|
27184
|
+
// first render, so what it must not find is dropped before the routing
|
|
27185
|
+
// renders anything.
|
|
27186
|
+
const forgetScrollersOnArrival = (url, { from } = {}) => {
|
|
27187
|
+
if (!isArrival(url, { from })) {
|
|
27131
27188
|
return;
|
|
27132
27189
|
}
|
|
27133
|
-
|
|
27190
|
+
scrollerPositionsByUrl.delete(new URL(url, window.location.href).href);
|
|
27134
27191
|
};
|
|
27135
27192
|
|
|
27136
27193
|
// An arrival at a page whose scrollport is already showing another one: the
|
|
@@ -27149,6 +27206,49 @@ const scrollTo = ({ x, y }) => {
|
|
|
27149
27206
|
window.scrollTo({ top: y, left: x, behavior: "instant" });
|
|
27150
27207
|
};
|
|
27151
27208
|
|
|
27209
|
+
const rememberScrollerPosition = (name, position) => {
|
|
27210
|
+
loadStore();
|
|
27211
|
+
const url = window.location.href;
|
|
27212
|
+
let positionByName = scrollerPositionsByUrl.get(url);
|
|
27213
|
+
if (!positionByName) {
|
|
27214
|
+
positionByName = new Map();
|
|
27215
|
+
scrollerPositionsByUrl.set(url, positionByName);
|
|
27216
|
+
}
|
|
27217
|
+
positionByName.set(name, position);
|
|
27218
|
+
urlByScrollerName.set(name, url);
|
|
27219
|
+
};
|
|
27220
|
+
|
|
27221
|
+
const recallScrollerPosition = (name) => {
|
|
27222
|
+
loadStore();
|
|
27223
|
+
const positionByName = scrollerPositionsByUrl.get(window.location.href);
|
|
27224
|
+
if (!positionByName) {
|
|
27225
|
+
return undefined;
|
|
27226
|
+
}
|
|
27227
|
+
return positionByName.get(name);
|
|
27228
|
+
};
|
|
27229
|
+
|
|
27230
|
+
// Said by a scroller as it unmounts. Its page is being left when the url is
|
|
27231
|
+
// already another one — the history is written before the page it names is
|
|
27232
|
+
// taken down — and then its position is kept for the way back. The url still
|
|
27233
|
+
// being the one it spoke under means the page stays and the scroller alone
|
|
27234
|
+
// goes (a popup closing, a section folding): there is no coming back to a
|
|
27235
|
+
// place that was not left, and a position kept would greet the next mount
|
|
27236
|
+
// under this address as a return.
|
|
27237
|
+
const forgetScrollerUnlessPageLeft = (name) => {
|
|
27238
|
+
const url = urlByScrollerName.get(name);
|
|
27239
|
+
if (url === undefined) {
|
|
27240
|
+
return;
|
|
27241
|
+
}
|
|
27242
|
+
urlByScrollerName.delete(name);
|
|
27243
|
+
if (url !== window.location.href) {
|
|
27244
|
+
return;
|
|
27245
|
+
}
|
|
27246
|
+
const positionByName = scrollerPositionsByUrl.get(url);
|
|
27247
|
+
if (positionByName) {
|
|
27248
|
+
positionByName.delete(name);
|
|
27249
|
+
}
|
|
27250
|
+
};
|
|
27251
|
+
|
|
27152
27252
|
/**
|
|
27153
27253
|
* A navigation is ABOUT to be applied — said before its very first write.
|
|
27154
27254
|
*
|
|
@@ -27511,6 +27611,13 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
27511
27611
|
return undefined;
|
|
27512
27612
|
}
|
|
27513
27613
|
|
|
27614
|
+
// The page's own scrollers are told of an arrival before the routing
|
|
27615
|
+
// renders anything: a list arriving decides where it opens in its first
|
|
27616
|
+
// render (see scroll_restoration.js). The document itself is moved once
|
|
27617
|
+
// the page is there, below.
|
|
27618
|
+
if (navigationType === "push") {
|
|
27619
|
+
forgetScrollersOnArrival(url, { from: urlLeft });
|
|
27620
|
+
}
|
|
27514
27621
|
if (abortController) {
|
|
27515
27622
|
abortController.abort(`navigating to ${url}`);
|
|
27516
27623
|
}
|
|
@@ -67205,10 +67312,11 @@ const ListUI = props => {
|
|
|
67205
67312
|
onListVisibleItemsChange,
|
|
67206
67313
|
virtualItemSize,
|
|
67207
67314
|
scrolled,
|
|
67208
|
-
defaultScrolled = "start",
|
|
67315
|
+
defaultScrolled: defaultScrolledProp = "start",
|
|
67209
67316
|
onScrolledChange,
|
|
67210
67317
|
scroller = "self",
|
|
67211
67318
|
hoverWhileScrolling = false,
|
|
67319
|
+
scrollResetOnNavigation = false,
|
|
67212
67320
|
lockSize,
|
|
67213
67321
|
columns,
|
|
67214
67322
|
itemColumns,
|
|
@@ -67227,6 +67335,20 @@ const ListUI = props => {
|
|
|
67227
67335
|
listRows,
|
|
67228
67336
|
...rest
|
|
67229
67337
|
} = props;
|
|
67338
|
+
// Remembered by name, and a name made up at render (see ListFirstResolver)
|
|
67339
|
+
// names no list a later mount would recognize.
|
|
67340
|
+
const rememberScroll = !scrollResetOnNavigation && !isLikelyPreactGeneratedId(rest.id);
|
|
67341
|
+
// Where the list was when its screen was left, when this is the way back
|
|
67342
|
+
// (see scroll_restoration.js). Read once: `defaultScrolled` is held by
|
|
67343
|
+
// reference, and a place read again each render would be a list moved each
|
|
67344
|
+
// render. A list held by its caller (`scrolled`) is where the caller says.
|
|
67345
|
+
const [scrolledRemembered] = useState(() => {
|
|
67346
|
+
if (!rememberScroll || scrolled !== undefined && scrolled !== null) {
|
|
67347
|
+
return undefined;
|
|
67348
|
+
}
|
|
67349
|
+
return recallScrollerPosition(rest.id);
|
|
67350
|
+
});
|
|
67351
|
+
const defaultScrolled = scrolledRemembered || defaultScrolledProp;
|
|
67230
67352
|
const scrollBoxPaddingProps = {};
|
|
67231
67353
|
for (const name of LIST_PADDING_PROP_SET) {
|
|
67232
67354
|
if (name in rest) {
|
|
@@ -67321,6 +67443,8 @@ const ListUI = props => {
|
|
|
67321
67443
|
scrolled,
|
|
67322
67444
|
defaultScrolled,
|
|
67323
67445
|
onScrolledChange,
|
|
67446
|
+
rememberScroll,
|
|
67447
|
+
listId: rest.id,
|
|
67324
67448
|
scroller,
|
|
67325
67449
|
searchText,
|
|
67326
67450
|
horizontal
|
|
@@ -67353,7 +67477,7 @@ const ListUI = props => {
|
|
|
67353
67477
|
// locator). Both answer here, so a row is reachable whether or not the
|
|
67354
67478
|
// window happens to frame it.
|
|
67355
67479
|
const getItemById = itemId => {
|
|
67356
|
-
const itemDrawn = listRows.itemsSignal.peek().find(item => item.
|
|
67480
|
+
const itemDrawn = listRows.itemsSignal.peek().find(item => item.itemId === itemId);
|
|
67357
67481
|
if (itemDrawn) {
|
|
67358
67482
|
return itemDrawn;
|
|
67359
67483
|
}
|
|
@@ -67363,6 +67487,7 @@ const ListUI = props => {
|
|
|
67363
67487
|
}
|
|
67364
67488
|
return {
|
|
67365
67489
|
id: itemId,
|
|
67490
|
+
itemId,
|
|
67366
67491
|
index: rowIndex
|
|
67367
67492
|
};
|
|
67368
67493
|
};
|
|
@@ -67645,6 +67770,8 @@ const useListScrollSync = ({
|
|
|
67645
67770
|
scrolled,
|
|
67646
67771
|
defaultScrolled,
|
|
67647
67772
|
onScrolledChange,
|
|
67773
|
+
rememberScroll,
|
|
67774
|
+
listId,
|
|
67648
67775
|
scroller,
|
|
67649
67776
|
searchText,
|
|
67650
67777
|
horizontal
|
|
@@ -67857,15 +67984,18 @@ const useListScrollSync = ({
|
|
|
67857
67984
|
`"${event.type}" on ${getElementSignature(event.target)} (${reason})`;
|
|
67858
67985
|
// When we display the list we prefer to have selected item at the center
|
|
67859
67986
|
// 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: "${
|
|
67987
|
+
const align = blockRequested || (event.type === "navi_displayed" ? "center" : "nearest");
|
|
67988
|
+
`${getElementSignature(itemEl)}.scrollIntoView({ block: "${align}", inline: "${align}", container: "nearest" })`;
|
|
67862
67989
|
// The list is going somewhere on purpose, so there is no view to hold
|
|
67863
67990
|
// still any more: an anchor captured before this drop it, or it would
|
|
67864
67991
|
// put the list back where it was the moment the rows move under it.
|
|
67865
67992
|
anchorRef.current = null;
|
|
67993
|
+
// One alignment, said on both axes: the axis the list scrolls on is the
|
|
67994
|
+
// one that reads it, and the other has nothing to move.
|
|
67866
67995
|
scrollIntoViewScoped(itemEl, {
|
|
67867
67996
|
container: getScroller(),
|
|
67868
|
-
block
|
|
67997
|
+
block: align,
|
|
67998
|
+
inline: align
|
|
67869
67999
|
});
|
|
67870
68000
|
const listEl = getListEl();
|
|
67871
68001
|
dispatchPublicCustomEvent(listEl, "navi_scroll", {
|
|
@@ -67877,7 +68007,7 @@ const useListScrollSync = ({
|
|
|
67877
68007
|
// Whether the row is drawn is asked of the dom, not of the render window:
|
|
67878
68008
|
// the window says what a run draws, and a list whose rows are declared one
|
|
67879
68009
|
// by one has them all in the dom whatever the window says.
|
|
67880
|
-
const itemEl = findRowElement(getListEl(), item.
|
|
68010
|
+
const itemEl = findRowElement(getListEl(), item.itemId);
|
|
67881
68011
|
if (itemEl) {
|
|
67882
68012
|
scrollItemIntoView(itemEl);
|
|
67883
68013
|
return;
|
|
@@ -67885,7 +68015,7 @@ const useListScrollSync = ({
|
|
|
67885
68015
|
// Not in DOM — shift the render window. The item will read
|
|
67886
68016
|
// pendingScrollRef on mount and scroll into view.
|
|
67887
68017
|
pendingScrollRef.current = {
|
|
67888
|
-
id: item.
|
|
68018
|
+
id: item.itemId,
|
|
67889
68019
|
resolve: itemEl => {
|
|
67890
68020
|
pendingScrollRef.current = null;
|
|
67891
68021
|
scrollItemIntoView(itemEl);
|
|
@@ -68236,6 +68366,8 @@ const useListScrollSync = ({
|
|
|
68236
68366
|
// one was looking at is then somewhere else.
|
|
68237
68367
|
const onScrolledChangeRef = useRef(null);
|
|
68238
68368
|
onScrolledChangeRef.current = onScrolledChange;
|
|
68369
|
+
const rememberScrollRef = useRef(false);
|
|
68370
|
+
rememberScrollRef.current = rememberScroll;
|
|
68239
68371
|
// Where the list was at the last thing that moved it. Kept whether anyone
|
|
68240
68372
|
// asked for it or not: it is what a resize needs to put things back.
|
|
68241
68373
|
const positionRef = useRef(null);
|
|
@@ -68253,16 +68385,32 @@ const useListScrollSync = ({
|
|
|
68253
68385
|
return;
|
|
68254
68386
|
}
|
|
68255
68387
|
positionRef.current = position;
|
|
68256
|
-
|
|
68388
|
+
const remember = rememberScrollRef.current;
|
|
68389
|
+
const onScrolledChange = onScrolledChangeRef.current;
|
|
68390
|
+
if (!remember && !onScrolledChange) {
|
|
68257
68391
|
return;
|
|
68258
68392
|
}
|
|
68259
68393
|
const rowEl = findRowElement(getListEl(), position.id);
|
|
68260
|
-
|
|
68394
|
+
const scrolledNow = {
|
|
68261
68395
|
id: position.id,
|
|
68262
68396
|
index: position.index,
|
|
68263
68397
|
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal)
|
|
68264
|
-
}
|
|
68398
|
+
};
|
|
68399
|
+
if (remember) {
|
|
68400
|
+
rememberScrollerPosition(listId, scrolledNow);
|
|
68401
|
+
}
|
|
68402
|
+
if (onScrolledChange) {
|
|
68403
|
+
onScrolledChange(scrolledNow);
|
|
68404
|
+
}
|
|
68265
68405
|
};
|
|
68406
|
+
// Leaving: with its page, or alone (see forgetScrollerUnlessPageLeft).
|
|
68407
|
+
useLayoutEffect(() => {
|
|
68408
|
+
return () => {
|
|
68409
|
+
if (rememberScrollRef.current) {
|
|
68410
|
+
forgetScrollerUnlessPageLeft(listId);
|
|
68411
|
+
}
|
|
68412
|
+
};
|
|
68413
|
+
}, []);
|
|
68266
68414
|
|
|
68267
68415
|
// A list that gets narrower rewraps every row it holds, so everything below
|
|
68268
68416
|
// moves and the reader loses their place — the very thing scrolling a long
|
|
@@ -68335,7 +68483,7 @@ const useListScrollSync = ({
|
|
|
68335
68483
|
return;
|
|
68336
68484
|
}
|
|
68337
68485
|
const items = listRows.visibleItemsSignal.peek();
|
|
68338
|
-
const itemNow = items.find(i => i.
|
|
68486
|
+
const itemNow = items.find(i => i.itemId === anchor.id);
|
|
68339
68487
|
if (!itemNow) {
|
|
68340
68488
|
anchorRef.current = null;
|
|
68341
68489
|
return;
|
|
@@ -68938,17 +69086,22 @@ const resolveScrollInset = (value, viewportSize) => {
|
|
|
68938
69086
|
return number;
|
|
68939
69087
|
};
|
|
68940
69088
|
|
|
68941
|
-
// The row
|
|
68942
|
-
//
|
|
68943
|
-
//
|
|
68944
|
-
//
|
|
68945
|
-
//
|
|
69089
|
+
// The row of that name, IN THIS LIST — by the name the list knows it under
|
|
69090
|
+
// (see ListItemUI), not the element's id. Not document.getElementById either:
|
|
69091
|
+
// a name is only ever unique within a list — two lists on the same page can be
|
|
69092
|
+
// showing the same collection — and a list acting on a row that belongs to
|
|
69093
|
+
// another one is a spectacular kind of wrong (it scrolls to hold still
|
|
69094
|
+
// something it is not even showing).
|
|
68946
69095
|
const findRowElement = (listEl, id) => {
|
|
68947
|
-
return listEl.querySelector(`[
|
|
69096
|
+
return listEl.querySelector(`[navi-list-item-real="${CSS.escape(id)}"]`);
|
|
68948
69097
|
};
|
|
69098
|
+
const getRowName = rowEl => rowEl.getAttribute("navi-list-item-real");
|
|
68949
69099
|
|
|
68950
69100
|
// The row the user is looking at, and where it sits: what must not move when
|
|
68951
|
-
// the list is rebuilt around it.
|
|
69101
|
+
// the list is rebuilt around it. Read off the rows' own boxes, not by
|
|
69102
|
+
// hit-testing the screen: a scrolling list takes its rows out of hit-testing
|
|
69103
|
+
// (see the navi-scrolling rule in the css above), and the scroll event is
|
|
69104
|
+
// precisely when this is asked.
|
|
68952
69105
|
const captureScrollAnchor = ({
|
|
68953
69106
|
scrollerEl,
|
|
68954
69107
|
listEl,
|
|
@@ -68960,30 +69113,31 @@ const captureScrollAnchor = ({
|
|
|
68960
69113
|
}
|
|
68961
69114
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
68962
69115
|
const listRect = listEl.getBoundingClientRect();
|
|
68963
|
-
const
|
|
68964
|
-
if (!
|
|
69116
|
+
const range = getListVisibleRange(viewportRect, listRect, horizontal);
|
|
69117
|
+
if (!range) {
|
|
68965
69118
|
return null;
|
|
68966
69119
|
}
|
|
69120
|
+
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
|
|
69121
|
+
const {
|
|
69122
|
+
rowEls,
|
|
69123
|
+
index
|
|
69124
|
+
} = findRowsFrom(listEl, range.from, horizontal);
|
|
68967
69125
|
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;
|
|
69126
|
+
for (let i = index; i < rowEls.length; i++) {
|
|
69127
|
+
const rowEl = rowEls[i];
|
|
69128
|
+
const rowRect = rowEl.getBoundingClientRect();
|
|
69129
|
+
const rowStart = horizontal ? rowRect.left : rowRect.top;
|
|
69130
|
+
if (rowStart >= range.to) {
|
|
69131
|
+
break;
|
|
68978
69132
|
}
|
|
68979
|
-
const
|
|
69133
|
+
const rowName = getRowName(rowEl);
|
|
69134
|
+
const item = items.find(i => i.itemId === rowName);
|
|
68980
69135
|
if (!item) {
|
|
68981
69136
|
continue;
|
|
68982
69137
|
}
|
|
68983
|
-
const
|
|
68984
|
-
const offset = horizontal ? itemRect.left - viewportRect.left : itemRect.top - viewportRect.top;
|
|
69138
|
+
const offset = rowStart - viewportFrom;
|
|
68985
69139
|
const anchor = {
|
|
68986
|
-
id: item.
|
|
69140
|
+
id: item.itemId,
|
|
68987
69141
|
index: item.index,
|
|
68988
69142
|
offset
|
|
68989
69143
|
};
|
|
@@ -69003,43 +69157,61 @@ const captureScrollAnchor = ({
|
|
|
69003
69157
|
// The part of the list that is on screen, along the scrolling axis. Both edges
|
|
69004
69158
|
// matter: the scroller may be larger than the list (scroller="parent") as well
|
|
69005
69159
|
// 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;
|
|
69160
|
+
const getListVisibleRange = (viewportRect, listRect, horizontal) => {
|
|
69013
69161
|
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
|
|
69014
69162
|
const viewportTo = horizontal ? viewportRect.right : viewportRect.bottom;
|
|
69015
69163
|
const listFrom = horizontal ? listRect.left : listRect.top;
|
|
69016
69164
|
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
|
-
}
|
|
69165
|
+
const from = listFrom > viewportFrom ? listFrom : viewportFrom;
|
|
69166
|
+
const to = listTo < viewportTo ? listTo : viewportTo;
|
|
69025
69167
|
if (to - from < 2) {
|
|
69026
69168
|
return null;
|
|
69027
69169
|
}
|
|
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
69170
|
return {
|
|
69034
69171
|
from,
|
|
69035
|
-
to
|
|
69036
|
-
crossPos
|
|
69172
|
+
to
|
|
69037
69173
|
};
|
|
69038
69174
|
};
|
|
69175
|
+
// The real rows of the list, and the first of them reaching past `from` along
|
|
69176
|
+
// the scroll axis. A binary search over their boxes: the rows stand in
|
|
69177
|
+
// document order along that axis, so their far edges only grow.
|
|
69178
|
+
const findRowsFrom = (listEl, from, horizontal) => {
|
|
69179
|
+
const rowEls = listEl.querySelectorAll(REAL_LIST_ITEM_SELECTOR);
|
|
69180
|
+
let low = 0;
|
|
69181
|
+
let high = rowEls.length;
|
|
69182
|
+
while (low < high) {
|
|
69183
|
+
const mid = low + high >> 1;
|
|
69184
|
+
const rect = rowEls[mid].getBoundingClientRect();
|
|
69185
|
+
const end = horizontal ? rect.right : rect.bottom;
|
|
69186
|
+
if (end > from) {
|
|
69187
|
+
high = mid;
|
|
69188
|
+
} else {
|
|
69189
|
+
low = mid + 1;
|
|
69190
|
+
}
|
|
69191
|
+
}
|
|
69192
|
+
return {
|
|
69193
|
+
rowEls,
|
|
69194
|
+
index: low
|
|
69195
|
+
};
|
|
69196
|
+
};
|
|
69197
|
+
// Whether a filler (the room held for rows outside the window) is what stands
|
|
69198
|
+
// at that position along the scroll axis.
|
|
69199
|
+
const isFillerAt = (listEl, position, horizontal) => {
|
|
69200
|
+
for (const fillerEl of listEl.querySelectorAll("[navi-virtual-filler]")) {
|
|
69201
|
+
const rect = fillerEl.getBoundingClientRect();
|
|
69202
|
+
const from = horizontal ? rect.left : rect.top;
|
|
69203
|
+
const to = horizontal ? rect.right : rect.bottom;
|
|
69204
|
+
if (position >= from && position < to) {
|
|
69205
|
+
return true;
|
|
69206
|
+
}
|
|
69207
|
+
}
|
|
69208
|
+
return false;
|
|
69209
|
+
};
|
|
69039
69210
|
|
|
69040
|
-
// Which row of the collection sits at the current scroll position.
|
|
69041
|
-
//
|
|
69042
|
-
// on screen is only
|
|
69211
|
+
// Which row of the collection sits at the current scroll position. Read off
|
|
69212
|
+
// the rows' boxes when a real row is there (see captureScrollAnchor for why
|
|
69213
|
+
// not hit-testing), and from the row size when what is on screen is only
|
|
69214
|
+
// reserved room.
|
|
69043
69215
|
// Returns { index, item, reason } or null if nothing can be determined.
|
|
69044
69216
|
const getScrollInfo = ({
|
|
69045
69217
|
scrollValues,
|
|
@@ -69053,34 +69225,31 @@ const getScrollInfo = ({
|
|
|
69053
69225
|
const items = listRows.itemsSignal.peek();
|
|
69054
69226
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
69055
69227
|
const listRect = listEl.getBoundingClientRect();
|
|
69056
|
-
|
|
69057
|
-
|
|
69058
|
-
const scanRange = getListVisibleScanRange(viewportRect, listRect, horizontal);
|
|
69059
|
-
if (!scanRange) {
|
|
69228
|
+
const range = getListVisibleRange(viewportRect, listRect, horizontal);
|
|
69229
|
+
if (!range) {
|
|
69060
69230
|
return null;
|
|
69061
69231
|
}
|
|
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;
|
|
69232
|
+
// Read from the center of the visible part of the list along the main axis.
|
|
69233
|
+
// The render window places half its budget before and half after the hit
|
|
69234
|
+
// index. Anchoring to the center maximises how many rendered items fall
|
|
69235
|
+
// within the visible area.
|
|
69236
|
+
const scanStart = (range.from + range.to) / 2;
|
|
69237
|
+
let hitEl = null;
|
|
69238
|
+
const hitFiller = isFillerAt(listEl, scanStart, horizontal);
|
|
69239
|
+
if (!hitFiller) {
|
|
69240
|
+
// The first real row from the center down, the way a probe walking down
|
|
69241
|
+
// from it would meet one — past a separator or a group label in between.
|
|
69242
|
+
const {
|
|
69243
|
+
rowEls,
|
|
69244
|
+
index
|
|
69245
|
+
} = findRowsFrom(listEl, scanStart, horizontal);
|
|
69246
|
+
const rowEl = rowEls[index];
|
|
69247
|
+
if (rowEl) {
|
|
69248
|
+
const rowRect = rowEl.getBoundingClientRect();
|
|
69249
|
+
const rowStart = horizontal ? rowRect.left : rowRect.top;
|
|
69250
|
+
if (rowStart < range.to) {
|
|
69251
|
+
hitEl = rowEl;
|
|
69252
|
+
}
|
|
69084
69253
|
}
|
|
69085
69254
|
}
|
|
69086
69255
|
// Shared by the "hit a filler" and "hit nothing at all" cases below: both
|
|
@@ -69111,8 +69280,8 @@ const getScrollInfo = ({
|
|
|
69111
69280
|
return estimateFromScrollPos("hit filler");
|
|
69112
69281
|
}
|
|
69113
69282
|
if (hitEl) {
|
|
69114
|
-
const
|
|
69115
|
-
const item = items.find(i => i.
|
|
69283
|
+
const hitName = getRowName(hitEl);
|
|
69284
|
+
const item = items.find(i => i.itemId === hitName);
|
|
69116
69285
|
if (!item) {
|
|
69117
69286
|
return null;
|
|
69118
69287
|
}
|
|
@@ -69122,13 +69291,11 @@ const getScrollInfo = ({
|
|
|
69122
69291
|
reason: `hit item at ${item.index} (${item.value})`
|
|
69123
69292
|
};
|
|
69124
69293
|
}
|
|
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.
|
|
69294
|
+
// No real row stands between the center and the end of what is visible.
|
|
69295
|
+
// Keeping the stale renderWindow here means the DOM never gets asked to
|
|
69296
|
+
// catch up with a scrollTop that may have jumped far away — the user ends
|
|
69297
|
+
// up staring at blank space. Same estimate as the hitFiller case is a safe
|
|
69298
|
+
// fallback: it only needs the scroll position.
|
|
69132
69299
|
const estimated = estimateFromScrollPos("no hit");
|
|
69133
69300
|
if (estimated) {
|
|
69134
69301
|
return estimated;
|
|
@@ -69563,6 +69730,12 @@ const ListItemUI = props => {
|
|
|
69563
69730
|
// gave the row its place and decided it is inside the render window.
|
|
69564
69731
|
const row = useContext(ListRowContext);
|
|
69565
69732
|
const slotId = useContext(ListSlotContext);
|
|
69733
|
+
// What the row is called in the list — the run's name for a row it draws,
|
|
69734
|
+
// whatever `id` the caller put on the element (a run row may need a DOM id of
|
|
69735
|
+
// its own, to keep clear of another element's). A position names a row by
|
|
69736
|
+
// this (see captureScrollAnchor), and a row asked for by name is found by
|
|
69737
|
+
// this (locateRow, findRowElement); the DOM id is the caller's.
|
|
69738
|
+
props.itemId = row ? row.id : props.id;
|
|
69566
69739
|
// There is no standalone match/matchScore/highlight prop — participation
|
|
69567
69740
|
// in a matching system (search, filter…) only goes through `matchInfo`
|
|
69568
69741
|
// (e.g. useSearchText's getItemMatchInfo(item): { match, matchScore,
|
|
@@ -69643,6 +69816,7 @@ const ListItemReal = props => {
|
|
|
69643
69816
|
const {
|
|
69644
69817
|
ref,
|
|
69645
69818
|
id,
|
|
69819
|
+
itemId,
|
|
69646
69820
|
hidden,
|
|
69647
69821
|
muted,
|
|
69648
69822
|
loading,
|
|
@@ -69662,7 +69836,7 @@ const ListItemReal = props => {
|
|
|
69662
69836
|
// see the state change, which only the caller can arrange).
|
|
69663
69837
|
const pendingScrollRef = useContext(PendingScrollRefContext);
|
|
69664
69838
|
const pendingScroll = pendingScrollRef.current;
|
|
69665
|
-
const needScrollOnMount = pendingScroll && pendingScroll.id ===
|
|
69839
|
+
const needScrollOnMount = pendingScroll && pendingScroll.id === itemId;
|
|
69666
69840
|
useLayoutEffect(() => {
|
|
69667
69841
|
if (!needScrollOnMount) {
|
|
69668
69842
|
return;
|
|
@@ -69761,7 +69935,7 @@ const ListItemReal = props => {
|
|
|
69761
69935
|
baseClassName: "navi_list_item",
|
|
69762
69936
|
styleCSSVars: LIST_ITEM_STYLE_CSS_VARS,
|
|
69763
69937
|
id: id,
|
|
69764
|
-
"navi-list-item-real":
|
|
69938
|
+
"navi-list-item-real": itemId,
|
|
69765
69939
|
...rest,
|
|
69766
69940
|
...itemColumnsOverrideProps,
|
|
69767
69941
|
index: undefined,
|
|
@@ -71133,6 +71307,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
71133
71307
|
* onScrolledChange?: (scrolled: {id: string, index: number, offset: number}) => void,
|
|
71134
71308
|
* scroller?: "self" | "parent" | "document" | Element | {current: Element},
|
|
71135
71309
|
* hoverWhileScrolling?: boolean,
|
|
71310
|
+
* scrollResetOnNavigation?: boolean,
|
|
71136
71311
|
* fallback?: import("ignore:preact").ComponentChildren,
|
|
71137
71312
|
* searchFallback?: import("ignore:preact").ComponentChildren,
|
|
71138
71313
|
* searchText?: string,
|
|
@@ -71224,7 +71399,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
71224
71399
|
* list is already known to be empty: the empty `fallback` shows right away
|
|
71225
71400
|
* rather than an empty frame, so nothing moves when the response arrives.
|
|
71226
71401
|
* @param {"start"|"end"|number|{id: string, offset?: number}} [props.defaultScrolled="start"]
|
|
71227
|
-
* Where the list opens, after which the user owns the scroll
|
|
71402
|
+
* Where the list opens, after which the user owns the scroll — unless it is
|
|
71403
|
+
* being come back to (see `scrollResetOnNavigation`). `"end"` is a
|
|
71228
71404
|
* thread read backwards — the last rows are the ones to show, and the ones
|
|
71229
71405
|
* asked for first. A number opens on that row of the collection. `{id,
|
|
71230
71406
|
* offset}` — what `onScrolledChange` hands out — opens on a NAMED row,
|
|
@@ -71307,6 +71483,15 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
71307
71483
|
* Pass `true` for a list whose rows must stay live under the pointer while
|
|
71308
71484
|
* it scrolls. The trade of the default is the mirror one: right after a
|
|
71309
71485
|
* scroll, the row under the pointer lights up only once the pointer moves.
|
|
71486
|
+
* @param {boolean} [props.scrollResetOnNavigation=false]
|
|
71487
|
+
* A list that opens the same way every time. Without it the list comes back
|
|
71488
|
+
* where it was when its screen is left and come back to — the way the page
|
|
71489
|
+
* does, and for a list that scrolls itself the page's own restoration cannot
|
|
71490
|
+
* see. The position is kept under the list's `id` and the page's url, for
|
|
71491
|
+
* the session (a reload comes back too); a list without an `id` of its own
|
|
71492
|
+
* has nothing to be remembered by. A fresh arrival at the page opens at
|
|
71493
|
+
* `defaultScrolled` either way, and so does a list the caller holds through
|
|
71494
|
+
* `scrolled`.
|
|
71310
71495
|
* @param {boolean} [props.deselectable]
|
|
71311
71496
|
* A single-select list allowed to hold nothing: the selected row, pressed
|
|
71312
71497
|
* again, lets go. Without it the list is a radio group — a choice, once
|