@jsenv/navi 0.29.360 → 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 +316 -113
- package/dist/dev/jsenv_navi.js.map +6 -6
- package/dist/jsenv_navi.js +315 -112
- package/dist/jsenv_navi.js.map +6 -6
- package/docs/interactions.md +8 -2
- package/docs/scroll.md +6 -0
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -4635,9 +4635,12 @@ const findControlHost = (el) => {
|
|
|
4635
4635
|
* `findControlHost` above answers for a control's own DOM (itself, or the native
|
|
4636
4636
|
* element it wraps). This one is for something that is not part of a control but
|
|
4637
4637
|
* has to reach one — an interaction declared on a box (see
|
|
4638
|
-
* interaction/interactions.js), which may be the control, may
|
|
4639
|
-
*
|
|
4640
|
-
* control
|
|
4638
|
+
* interaction/interactions.js), which may be the control, may sit inside it, or
|
|
4639
|
+
* may be a wrapper around exactly one. Nearest wins in that order, and the answer
|
|
4640
|
+
* is null when there is no control the box belongs to: not every box lives in
|
|
4641
|
+
* one, and a box laying out several controls (a row of badges, a toolbar) is
|
|
4642
|
+
* held by none of them — its interactions are its own, and a press on its empty
|
|
4643
|
+
* part is not a press on the first control it happens to contain.
|
|
4641
4644
|
*/
|
|
4642
4645
|
const findNearestControlHost = (el) => {
|
|
4643
4646
|
// Itself, then upwards — the box is inside a button, or is one.
|
|
@@ -4645,8 +4648,22 @@ const findNearestControlHost = (el) => {
|
|
|
4645
4648
|
if (selfOrAncestor) {
|
|
4646
4649
|
return selfOrAncestor;
|
|
4647
4650
|
}
|
|
4648
|
-
// …then downwards
|
|
4649
|
-
|
|
4651
|
+
// …then downwards, only when the box wraps a single control. Counted by
|
|
4652
|
+
// control, not by host: a picker holds the hosts of its popup content, and a
|
|
4653
|
+
// box around that one picker is still around one control.
|
|
4654
|
+
let single = null;
|
|
4655
|
+
for (const host of el.querySelectorAll("[navi-control-host]")) {
|
|
4656
|
+
const controlRoot = host.closest("[navi-control]") || host;
|
|
4657
|
+
const controlAbove = controlRoot.parentElement.closest("[navi-control]");
|
|
4658
|
+
if (controlAbove && el.contains(controlAbove)) {
|
|
4659
|
+
continue;
|
|
4660
|
+
}
|
|
4661
|
+
if (single && single !== controlRoot) {
|
|
4662
|
+
return null;
|
|
4663
|
+
}
|
|
4664
|
+
single = controlRoot;
|
|
4665
|
+
}
|
|
4666
|
+
return single ? findControlHost(single) : null;
|
|
4650
4667
|
};
|
|
4651
4668
|
const isControlRoot = (el) => {
|
|
4652
4669
|
return el.hasAttribute("navi-control");
|
|
@@ -10957,9 +10974,10 @@ const interactionsDisputeThePress = (interactions) => {
|
|
|
10957
10974
|
*
|
|
10958
10975
|
* The control is not passed in: it is found from the element, which is what lets
|
|
10959
10976
|
* `interactions` live on a Box rather than on the control itself. A Box that IS a
|
|
10960
|
-
* control (a Button) is its own; a Box
|
|
10961
|
-
* with no control anywhere near it
|
|
10962
|
-
*
|
|
10977
|
+
* control (a Button) is its own; a Box inside one, or wrapping exactly one,
|
|
10978
|
+
* reaches it; a Box with no control anywhere near it — or laying out several,
|
|
10979
|
+
* which belongs to none of them — still answers with a callback of the caller's,
|
|
10980
|
+
* and only "request_action" has nothing to ask.
|
|
10963
10981
|
*
|
|
10964
10982
|
* Set up once per element rather than on every render, which is what lets a
|
|
10965
10983
|
* detector be a plain `setup`/teardown pair. So the interactions themselves are
|
|
@@ -11007,7 +11025,7 @@ const useInteractionsEffect = (ref, interactionsRef) => {
|
|
|
11007
11025
|
if (!controlHost) {
|
|
11008
11026
|
{
|
|
11009
11027
|
console.warn(
|
|
11010
|
-
`interactions: "${type}" asks for an action, but there is no control around it to ask. Put the interaction on a control (or on a box that
|
|
11028
|
+
`interactions: "${type}" asks for an action, but there is no control around it to ask. Put the interaction on a control (or on a box that wraps exactly one), or give it a callback.`,
|
|
11011
11029
|
);
|
|
11012
11030
|
}
|
|
11013
11031
|
return null;
|
|
@@ -27715,6 +27733,18 @@ const takeoverRoutingRenderingHold = () => {
|
|
|
27715
27733
|
* to a reader. Kept in the session too, so a reload lands where the browser
|
|
27716
27734
|
* would have landed — the flag above is a promise to do the whole job.
|
|
27717
27735
|
*
|
|
27736
|
+
* The document is not the only scrollport of a page. A list scrolling itself
|
|
27737
|
+
* (a `<List expandY>` under a search field that stays put) is left and come
|
|
27738
|
+
* back to the same way, and the browser never knew it was a scrollport at
|
|
27739
|
+
* all: those say where they are by name (rememberScrollerPosition), under the
|
|
27740
|
+
* same URL and for the same session, and ask it back when they mount again
|
|
27741
|
+
* (recallScrollerPosition). What a push means for them is what it means for
|
|
27742
|
+
* the document — an arrival opens at the top (see startAtTop): the page
|
|
27743
|
+
* arrived at has its named positions dropped before its lists render, so a
|
|
27744
|
+
* list recalls only on the way back. A scroller that goes while its page
|
|
27745
|
+
* stays — a popup closing over the same address — has nothing to come back
|
|
27746
|
+
* to, and says so as it leaves (forgetScrollerUnlessPageLeft).
|
|
27747
|
+
*
|
|
27718
27748
|
* What is NOT covered, and cannot be from here: a page whose height depends on
|
|
27719
27749
|
* something still loading. Its content is not there at the moment it is put
|
|
27720
27750
|
* back, so a position beyond what has arrived is clamped as before. Only the
|
|
@@ -27732,7 +27762,23 @@ const takeoverRoutingRenderingHold = () => {
|
|
|
27732
27762
|
const STORAGE_KEY = "navi_scroll_positions";
|
|
27733
27763
|
|
|
27734
27764
|
const positionByUrl = new Map();
|
|
27735
|
-
|
|
27765
|
+
// url -> (scroller name -> position). The position is whatever the scroller
|
|
27766
|
+
// handed in: what it can put itself back on, in its own terms.
|
|
27767
|
+
const scrollerPositionsByUrl = new Map();
|
|
27768
|
+
// The url each named scroller last spoke under: what tells a scroller leaving
|
|
27769
|
+
// a page that stays from one leaving with its page.
|
|
27770
|
+
const urlByScrollerName = new Map();
|
|
27771
|
+
|
|
27772
|
+
// Read once, the first time anyone needs the positions: the document's
|
|
27773
|
+
// restoration is installed by the routing, and a list remembering itself may
|
|
27774
|
+
// mount in an app that never routes.
|
|
27775
|
+
let storeLoaded = false;
|
|
27776
|
+
const loadStore = () => {
|
|
27777
|
+
if (storeLoaded) {
|
|
27778
|
+
return;
|
|
27779
|
+
}
|
|
27780
|
+
storeLoaded = true;
|
|
27781
|
+
window.addEventListener("pagehide", storePositions);
|
|
27736
27782
|
let stored;
|
|
27737
27783
|
try {
|
|
27738
27784
|
stored = window.sessionStorage.getItem(STORAGE_KEY);
|
|
@@ -27745,18 +27791,31 @@ const readStoredPositions = () => {
|
|
|
27745
27791
|
return;
|
|
27746
27792
|
}
|
|
27747
27793
|
try {
|
|
27748
|
-
|
|
27794
|
+
const { document: documentPositions, scrollers } = JSON.parse(stored);
|
|
27795
|
+
for (const [url, position] of Object.entries(documentPositions || {})) {
|
|
27749
27796
|
positionByUrl.set(url, position);
|
|
27750
27797
|
}
|
|
27798
|
+
for (const [url, positionByName] of Object.entries(scrollers || {})) {
|
|
27799
|
+
scrollerPositionsByUrl.set(url, new Map(Object.entries(positionByName)));
|
|
27800
|
+
}
|
|
27751
27801
|
} catch {
|
|
27752
27802
|
// Something else wrote there, or it was truncated.
|
|
27753
27803
|
}
|
|
27754
27804
|
};
|
|
27755
27805
|
const storePositions = () => {
|
|
27806
|
+
const scrollers = {};
|
|
27807
|
+
for (const [url, positionByName] of scrollerPositionsByUrl) {
|
|
27808
|
+
if (positionByName.size > 0) {
|
|
27809
|
+
scrollers[url] = Object.fromEntries(positionByName);
|
|
27810
|
+
}
|
|
27811
|
+
}
|
|
27756
27812
|
try {
|
|
27757
27813
|
window.sessionStorage.setItem(
|
|
27758
27814
|
STORAGE_KEY,
|
|
27759
|
-
JSON.stringify(
|
|
27815
|
+
JSON.stringify({
|
|
27816
|
+
document: Object.fromEntries(positionByUrl),
|
|
27817
|
+
scrollers,
|
|
27818
|
+
}),
|
|
27760
27819
|
);
|
|
27761
27820
|
} catch {
|
|
27762
27821
|
// Full, or refused: the session is the only thing lost.
|
|
@@ -27796,7 +27855,7 @@ const installScrollRestoration = () => {
|
|
|
27796
27855
|
return;
|
|
27797
27856
|
}
|
|
27798
27857
|
window.history.scrollRestoration = "manual";
|
|
27799
|
-
|
|
27858
|
+
loadStore();
|
|
27800
27859
|
// Read as it happens rather than when leaving: a traverse changes the url
|
|
27801
27860
|
// before anything here is told, so a position read then would be read for
|
|
27802
27861
|
// the wrong page.
|
|
@@ -27813,7 +27872,6 @@ const installScrollRestoration = () => {
|
|
|
27813
27872
|
},
|
|
27814
27873
|
{ passive: true },
|
|
27815
27874
|
);
|
|
27816
|
-
window.addEventListener("pagehide", storePositions);
|
|
27817
27875
|
// What a reload asks for, now that the browser has been told not to do it.
|
|
27818
27876
|
// Once, and at the first render of a route: the position is only meaningful
|
|
27819
27877
|
// once there is a page under it.
|
|
@@ -27856,18 +27914,35 @@ const restoreScrollPosition = (url) => {
|
|
|
27856
27914
|
// The document, because the document is the scrollport in the common case. An
|
|
27857
27915
|
// app that scrolls an element of its own scrolls it itself.
|
|
27858
27916
|
const startAtTop = (url, { from } = {}) => {
|
|
27917
|
+
if (!isArrival(url, { from })) {
|
|
27918
|
+
return;
|
|
27919
|
+
}
|
|
27920
|
+
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
27921
|
+
};
|
|
27922
|
+
const isArrival = (url, { from }) => {
|
|
27859
27923
|
const urlObject = new URL(url, window.location.href);
|
|
27860
27924
|
// A fragment names where to land, and the browser is the one that finds it.
|
|
27861
27925
|
if (urlObject.hash) {
|
|
27862
|
-
return;
|
|
27926
|
+
return false;
|
|
27863
27927
|
}
|
|
27864
27928
|
if (
|
|
27865
27929
|
from !== undefined &&
|
|
27866
27930
|
new URL(from, window.location.href).pathname === urlObject.pathname
|
|
27867
27931
|
) {
|
|
27932
|
+
return false;
|
|
27933
|
+
}
|
|
27934
|
+
return true;
|
|
27935
|
+
};
|
|
27936
|
+
|
|
27937
|
+
// The same arrival, for the page's own scrollers. The document is scrolled to
|
|
27938
|
+
// its top once the page is there; a list opens where it decides to in its
|
|
27939
|
+
// first render, so what it must not find is dropped before the routing
|
|
27940
|
+
// renders anything.
|
|
27941
|
+
const forgetScrollersOnArrival = (url, { from } = {}) => {
|
|
27942
|
+
if (!isArrival(url, { from })) {
|
|
27868
27943
|
return;
|
|
27869
27944
|
}
|
|
27870
|
-
|
|
27945
|
+
scrollerPositionsByUrl.delete(new URL(url, window.location.href).href);
|
|
27871
27946
|
};
|
|
27872
27947
|
|
|
27873
27948
|
// An arrival at a page whose scrollport is already showing another one: the
|
|
@@ -27886,6 +27961,49 @@ const scrollTo = ({ x, y }) => {
|
|
|
27886
27961
|
window.scrollTo({ top: y, left: x, behavior: "instant" });
|
|
27887
27962
|
};
|
|
27888
27963
|
|
|
27964
|
+
const rememberScrollerPosition = (name, position) => {
|
|
27965
|
+
loadStore();
|
|
27966
|
+
const url = window.location.href;
|
|
27967
|
+
let positionByName = scrollerPositionsByUrl.get(url);
|
|
27968
|
+
if (!positionByName) {
|
|
27969
|
+
positionByName = new Map();
|
|
27970
|
+
scrollerPositionsByUrl.set(url, positionByName);
|
|
27971
|
+
}
|
|
27972
|
+
positionByName.set(name, position);
|
|
27973
|
+
urlByScrollerName.set(name, url);
|
|
27974
|
+
};
|
|
27975
|
+
|
|
27976
|
+
const recallScrollerPosition = (name) => {
|
|
27977
|
+
loadStore();
|
|
27978
|
+
const positionByName = scrollerPositionsByUrl.get(window.location.href);
|
|
27979
|
+
if (!positionByName) {
|
|
27980
|
+
return undefined;
|
|
27981
|
+
}
|
|
27982
|
+
return positionByName.get(name);
|
|
27983
|
+
};
|
|
27984
|
+
|
|
27985
|
+
// Said by a scroller as it unmounts. Its page is being left when the url is
|
|
27986
|
+
// already another one — the history is written before the page it names is
|
|
27987
|
+
// taken down — and then its position is kept for the way back. The url still
|
|
27988
|
+
// being the one it spoke under means the page stays and the scroller alone
|
|
27989
|
+
// goes (a popup closing, a section folding): there is no coming back to a
|
|
27990
|
+
// place that was not left, and a position kept would greet the next mount
|
|
27991
|
+
// under this address as a return.
|
|
27992
|
+
const forgetScrollerUnlessPageLeft = (name) => {
|
|
27993
|
+
const url = urlByScrollerName.get(name);
|
|
27994
|
+
if (url === undefined) {
|
|
27995
|
+
return;
|
|
27996
|
+
}
|
|
27997
|
+
urlByScrollerName.delete(name);
|
|
27998
|
+
if (url !== window.location.href) {
|
|
27999
|
+
return;
|
|
28000
|
+
}
|
|
28001
|
+
const positionByName = scrollerPositionsByUrl.get(url);
|
|
28002
|
+
if (positionByName) {
|
|
28003
|
+
positionByName.delete(name);
|
|
28004
|
+
}
|
|
28005
|
+
};
|
|
28006
|
+
|
|
27889
28007
|
/**
|
|
27890
28008
|
* A navigation is ABOUT to be applied — said before its very first write.
|
|
27891
28009
|
*
|
|
@@ -28248,6 +28366,13 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28248
28366
|
return undefined;
|
|
28249
28367
|
}
|
|
28250
28368
|
|
|
28369
|
+
// The page's own scrollers are told of an arrival before the routing
|
|
28370
|
+
// renders anything: a list arriving decides where it opens in its first
|
|
28371
|
+
// render (see scroll_restoration.js). The document itself is moved once
|
|
28372
|
+
// the page is there, below.
|
|
28373
|
+
if (navigationType === "push") {
|
|
28374
|
+
forgetScrollersOnArrival(url, { from: urlLeft });
|
|
28375
|
+
}
|
|
28251
28376
|
if (abortController) {
|
|
28252
28377
|
abortController.abort(`navigating to ${url}`);
|
|
28253
28378
|
}
|
|
@@ -68495,10 +68620,11 @@ const ListUI = props => {
|
|
|
68495
68620
|
onListVisibleItemsChange,
|
|
68496
68621
|
virtualItemSize,
|
|
68497
68622
|
scrolled,
|
|
68498
|
-
defaultScrolled = "start",
|
|
68623
|
+
defaultScrolled: defaultScrolledProp = "start",
|
|
68499
68624
|
onScrolledChange,
|
|
68500
68625
|
scroller = "self",
|
|
68501
68626
|
hoverWhileScrolling = false,
|
|
68627
|
+
scrollResetOnNavigation = false,
|
|
68502
68628
|
lockSize,
|
|
68503
68629
|
columns,
|
|
68504
68630
|
itemColumns,
|
|
@@ -68517,6 +68643,20 @@ const ListUI = props => {
|
|
|
68517
68643
|
listRows,
|
|
68518
68644
|
...rest
|
|
68519
68645
|
} = props;
|
|
68646
|
+
// Remembered by name, and a name made up at render (see ListFirstResolver)
|
|
68647
|
+
// names no list a later mount would recognize.
|
|
68648
|
+
const rememberScroll = !scrollResetOnNavigation && !isLikelyPreactGeneratedId(rest.id);
|
|
68649
|
+
// Where the list was when its screen was left, when this is the way back
|
|
68650
|
+
// (see scroll_restoration.js). Read once: `defaultScrolled` is held by
|
|
68651
|
+
// reference, and a place read again each render would be a list moved each
|
|
68652
|
+
// render. A list held by its caller (`scrolled`) is where the caller says.
|
|
68653
|
+
const [scrolledRemembered] = useState(() => {
|
|
68654
|
+
if (!rememberScroll || scrolled !== undefined && scrolled !== null) {
|
|
68655
|
+
return undefined;
|
|
68656
|
+
}
|
|
68657
|
+
return recallScrollerPosition(rest.id);
|
|
68658
|
+
});
|
|
68659
|
+
const defaultScrolled = scrolledRemembered || defaultScrolledProp;
|
|
68520
68660
|
const scrollBoxPaddingProps = {};
|
|
68521
68661
|
for (const name of LIST_PADDING_PROP_SET) {
|
|
68522
68662
|
if (name in rest) {
|
|
@@ -68611,6 +68751,8 @@ const ListUI = props => {
|
|
|
68611
68751
|
scrolled,
|
|
68612
68752
|
defaultScrolled,
|
|
68613
68753
|
onScrolledChange,
|
|
68754
|
+
rememberScroll,
|
|
68755
|
+
listId: rest.id,
|
|
68614
68756
|
scroller,
|
|
68615
68757
|
searchText,
|
|
68616
68758
|
horizontal
|
|
@@ -68643,7 +68785,7 @@ const ListUI = props => {
|
|
|
68643
68785
|
// locator). Both answer here, so a row is reachable whether or not the
|
|
68644
68786
|
// window happens to frame it.
|
|
68645
68787
|
const getItemById = itemId => {
|
|
68646
|
-
const itemDrawn = listRows.itemsSignal.peek().find(item => item.
|
|
68788
|
+
const itemDrawn = listRows.itemsSignal.peek().find(item => item.itemId === itemId);
|
|
68647
68789
|
if (itemDrawn) {
|
|
68648
68790
|
return itemDrawn;
|
|
68649
68791
|
}
|
|
@@ -68653,6 +68795,7 @@ const ListUI = props => {
|
|
|
68653
68795
|
}
|
|
68654
68796
|
return {
|
|
68655
68797
|
id: itemId,
|
|
68798
|
+
itemId,
|
|
68656
68799
|
index: rowIndex
|
|
68657
68800
|
};
|
|
68658
68801
|
};
|
|
@@ -68935,6 +69078,8 @@ const useListScrollSync = ({
|
|
|
68935
69078
|
scrolled,
|
|
68936
69079
|
defaultScrolled,
|
|
68937
69080
|
onScrolledChange,
|
|
69081
|
+
rememberScroll,
|
|
69082
|
+
listId,
|
|
68938
69083
|
scroller,
|
|
68939
69084
|
searchText,
|
|
68940
69085
|
horizontal
|
|
@@ -69148,16 +69293,19 @@ const useListScrollSync = ({
|
|
|
69148
69293
|
const trigger = `"${event.type}" on ${getElementSignature(event.target)} (${reason})`;
|
|
69149
69294
|
// When we display the list we prefer to have selected item at the center
|
|
69150
69295
|
// otherwise, usually when focused by arrow nav, we want to keep it into view close to the nearest edge
|
|
69151
|
-
const
|
|
69152
|
-
const scrollToItemCall = `${getElementSignature(itemEl)}.scrollIntoView({ block: "${
|
|
69296
|
+
const align = blockRequested || (event.type === "navi_displayed" ? "center" : "nearest");
|
|
69297
|
+
const scrollToItemCall = `${getElementSignature(itemEl)}.scrollIntoView({ block: "${align}", inline: "${align}", container: "nearest" })`;
|
|
69153
69298
|
debugScroll(`${trigger} -> ${scrollToItemCall}`);
|
|
69154
69299
|
// The list is going somewhere on purpose, so there is no view to hold
|
|
69155
69300
|
// still any more: an anchor captured before this drop it, or it would
|
|
69156
69301
|
// put the list back where it was the moment the rows move under it.
|
|
69157
69302
|
anchorRef.current = null;
|
|
69303
|
+
// One alignment, said on both axes: the axis the list scrolls on is the
|
|
69304
|
+
// one that reads it, and the other has nothing to move.
|
|
69158
69305
|
scrollIntoViewScoped(itemEl, {
|
|
69159
69306
|
container: getScroller(),
|
|
69160
|
-
block
|
|
69307
|
+
block: align,
|
|
69308
|
+
inline: align
|
|
69161
69309
|
});
|
|
69162
69310
|
const listEl = getListEl();
|
|
69163
69311
|
dispatchPublicCustomEvent(listEl, "navi_scroll", {
|
|
@@ -69169,7 +69317,7 @@ const useListScrollSync = ({
|
|
|
69169
69317
|
// Whether the row is drawn is asked of the dom, not of the render window:
|
|
69170
69318
|
// the window says what a run draws, and a list whose rows are declared one
|
|
69171
69319
|
// by one has them all in the dom whatever the window says.
|
|
69172
|
-
const itemEl = findRowElement(getListEl(), item.
|
|
69320
|
+
const itemEl = findRowElement(getListEl(), item.itemId);
|
|
69173
69321
|
if (itemEl) {
|
|
69174
69322
|
scrollItemIntoView(itemEl);
|
|
69175
69323
|
return;
|
|
@@ -69177,7 +69325,7 @@ const useListScrollSync = ({
|
|
|
69177
69325
|
// Not in DOM — shift the render window. The item will read
|
|
69178
69326
|
// pendingScrollRef on mount and scroll into view.
|
|
69179
69327
|
pendingScrollRef.current = {
|
|
69180
|
-
id: item.
|
|
69328
|
+
id: item.itemId,
|
|
69181
69329
|
resolve: itemEl => {
|
|
69182
69330
|
pendingScrollRef.current = null;
|
|
69183
69331
|
scrollItemIntoView(itemEl);
|
|
@@ -69529,6 +69677,8 @@ const useListScrollSync = ({
|
|
|
69529
69677
|
// one was looking at is then somewhere else.
|
|
69530
69678
|
const onScrolledChangeRef = useRef(null);
|
|
69531
69679
|
onScrolledChangeRef.current = onScrolledChange;
|
|
69680
|
+
const rememberScrollRef = useRef(false);
|
|
69681
|
+
rememberScrollRef.current = rememberScroll;
|
|
69532
69682
|
// Where the list was at the last thing that moved it. Kept whether anyone
|
|
69533
69683
|
// asked for it or not: it is what a resize needs to put things back.
|
|
69534
69684
|
const positionRef = useRef(null);
|
|
@@ -69546,16 +69696,32 @@ const useListScrollSync = ({
|
|
|
69546
69696
|
return;
|
|
69547
69697
|
}
|
|
69548
69698
|
positionRef.current = position;
|
|
69549
|
-
|
|
69699
|
+
const remember = rememberScrollRef.current;
|
|
69700
|
+
const onScrolledChange = onScrolledChangeRef.current;
|
|
69701
|
+
if (!remember && !onScrolledChange) {
|
|
69550
69702
|
return;
|
|
69551
69703
|
}
|
|
69552
69704
|
const rowEl = findRowElement(getListEl(), position.id);
|
|
69553
|
-
|
|
69705
|
+
const scrolledNow = {
|
|
69554
69706
|
id: position.id,
|
|
69555
69707
|
index: position.index,
|
|
69556
69708
|
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal)
|
|
69557
|
-
}
|
|
69709
|
+
};
|
|
69710
|
+
if (remember) {
|
|
69711
|
+
rememberScrollerPosition(listId, scrolledNow);
|
|
69712
|
+
}
|
|
69713
|
+
if (onScrolledChange) {
|
|
69714
|
+
onScrolledChange(scrolledNow);
|
|
69715
|
+
}
|
|
69558
69716
|
};
|
|
69717
|
+
// Leaving: with its page, or alone (see forgetScrollerUnlessPageLeft).
|
|
69718
|
+
useLayoutEffect(() => {
|
|
69719
|
+
return () => {
|
|
69720
|
+
if (rememberScrollRef.current) {
|
|
69721
|
+
forgetScrollerUnlessPageLeft(listId);
|
|
69722
|
+
}
|
|
69723
|
+
};
|
|
69724
|
+
}, []);
|
|
69559
69725
|
|
|
69560
69726
|
// A list that gets narrower rewraps every row it holds, so everything below
|
|
69561
69727
|
// moves and the reader loses their place — the very thing scrolling a long
|
|
@@ -69628,7 +69794,7 @@ const useListScrollSync = ({
|
|
|
69628
69794
|
return;
|
|
69629
69795
|
}
|
|
69630
69796
|
const items = listRows.visibleItemsSignal.peek();
|
|
69631
|
-
const itemNow = items.find(i => i.
|
|
69797
|
+
const itemNow = items.find(i => i.itemId === anchor.id);
|
|
69632
69798
|
if (!itemNow) {
|
|
69633
69799
|
anchorRef.current = null;
|
|
69634
69800
|
return;
|
|
@@ -70327,17 +70493,22 @@ const resolveScrollInset = (value, viewportSize) => {
|
|
|
70327
70493
|
return number;
|
|
70328
70494
|
};
|
|
70329
70495
|
|
|
70330
|
-
// The row
|
|
70331
|
-
//
|
|
70332
|
-
//
|
|
70333
|
-
//
|
|
70334
|
-
//
|
|
70496
|
+
// The row of that name, IN THIS LIST — by the name the list knows it under
|
|
70497
|
+
// (see ListItemUI), not the element's id. Not document.getElementById either:
|
|
70498
|
+
// a name is only ever unique within a list — two lists on the same page can be
|
|
70499
|
+
// showing the same collection — and a list acting on a row that belongs to
|
|
70500
|
+
// another one is a spectacular kind of wrong (it scrolls to hold still
|
|
70501
|
+
// something it is not even showing).
|
|
70335
70502
|
const findRowElement = (listEl, id) => {
|
|
70336
|
-
return listEl.querySelector(`[
|
|
70503
|
+
return listEl.querySelector(`[navi-list-item-real="${CSS.escape(id)}"]`);
|
|
70337
70504
|
};
|
|
70505
|
+
const getRowName = rowEl => rowEl.getAttribute("navi-list-item-real");
|
|
70338
70506
|
|
|
70339
70507
|
// The row the user is looking at, and where it sits: what must not move when
|
|
70340
|
-
// the list is rebuilt around it.
|
|
70508
|
+
// the list is rebuilt around it. Read off the rows' own boxes, not by
|
|
70509
|
+
// hit-testing the screen: a scrolling list takes its rows out of hit-testing
|
|
70510
|
+
// (see the navi-scrolling rule in the css above), and the scroll event is
|
|
70511
|
+
// precisely when this is asked.
|
|
70341
70512
|
const captureScrollAnchor = ({
|
|
70342
70513
|
scrollerEl,
|
|
70343
70514
|
listEl,
|
|
@@ -70349,30 +70520,31 @@ const captureScrollAnchor = ({
|
|
|
70349
70520
|
}
|
|
70350
70521
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
70351
70522
|
const listRect = listEl.getBoundingClientRect();
|
|
70352
|
-
const
|
|
70353
|
-
if (!
|
|
70523
|
+
const range = getListVisibleRange(viewportRect, listRect, horizontal);
|
|
70524
|
+
if (!range) {
|
|
70354
70525
|
return null;
|
|
70355
70526
|
}
|
|
70527
|
+
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
|
|
70528
|
+
const {
|
|
70529
|
+
rowEls,
|
|
70530
|
+
index
|
|
70531
|
+
} = findRowsFrom(listEl, range.from, horizontal);
|
|
70356
70532
|
let fallbackAnchor = null;
|
|
70357
|
-
for (let
|
|
70358
|
-
const
|
|
70359
|
-
const
|
|
70360
|
-
const
|
|
70361
|
-
if (
|
|
70362
|
-
|
|
70363
|
-
}
|
|
70364
|
-
const itemEl = el.closest(REAL_LIST_ITEM_SELECTOR);
|
|
70365
|
-
if (!itemEl) {
|
|
70366
|
-
continue;
|
|
70533
|
+
for (let i = index; i < rowEls.length; i++) {
|
|
70534
|
+
const rowEl = rowEls[i];
|
|
70535
|
+
const rowRect = rowEl.getBoundingClientRect();
|
|
70536
|
+
const rowStart = horizontal ? rowRect.left : rowRect.top;
|
|
70537
|
+
if (rowStart >= range.to) {
|
|
70538
|
+
break;
|
|
70367
70539
|
}
|
|
70368
|
-
const
|
|
70540
|
+
const rowName = getRowName(rowEl);
|
|
70541
|
+
const item = items.find(i => i.itemId === rowName);
|
|
70369
70542
|
if (!item) {
|
|
70370
70543
|
continue;
|
|
70371
70544
|
}
|
|
70372
|
-
const
|
|
70373
|
-
const offset = horizontal ? itemRect.left - viewportRect.left : itemRect.top - viewportRect.top;
|
|
70545
|
+
const offset = rowStart - viewportFrom;
|
|
70374
70546
|
const anchor = {
|
|
70375
|
-
id: item.
|
|
70547
|
+
id: item.itemId,
|
|
70376
70548
|
index: item.index,
|
|
70377
70549
|
offset
|
|
70378
70550
|
};
|
|
@@ -70392,43 +70564,61 @@ const captureScrollAnchor = ({
|
|
|
70392
70564
|
// The part of the list that is on screen, along the scrolling axis. Both edges
|
|
70393
70565
|
// matter: the scroller may be larger than the list (scroller="parent") as well
|
|
70394
70566
|
// as smaller (the list scrolls inside its own box).
|
|
70395
|
-
const
|
|
70396
|
-
// The screen has a say too: what is asked here is answered by
|
|
70397
|
-
// elementFromPoint, which only knows about points that are actually on it. A
|
|
70398
|
-
// list whose scroll box hangs below the fold of the page would otherwise be
|
|
70399
|
-
// probed where nothing can be hit — and would silently stop keeping its rows
|
|
70400
|
-
// still, which is exactly when it matters.
|
|
70401
|
-
const screenTo = horizontal ? document.documentElement.clientWidth : document.documentElement.clientHeight;
|
|
70567
|
+
const getListVisibleRange = (viewportRect, listRect, horizontal) => {
|
|
70402
70568
|
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
|
|
70403
70569
|
const viewportTo = horizontal ? viewportRect.right : viewportRect.bottom;
|
|
70404
70570
|
const listFrom = horizontal ? listRect.left : listRect.top;
|
|
70405
70571
|
const listTo = horizontal ? listRect.right : listRect.bottom;
|
|
70406
|
-
|
|
70407
|
-
|
|
70408
|
-
if (from < 0) {
|
|
70409
|
-
from = 0;
|
|
70410
|
-
}
|
|
70411
|
-
if (to > screenTo) {
|
|
70412
|
-
to = screenTo;
|
|
70413
|
-
}
|
|
70572
|
+
const from = listFrom > viewportFrom ? listFrom : viewportFrom;
|
|
70573
|
+
const to = listTo < viewportTo ? listTo : viewportTo;
|
|
70414
70574
|
if (to - from < 2) {
|
|
70415
70575
|
return null;
|
|
70416
70576
|
}
|
|
70417
|
-
// Where to put the probe on the other axis: inside the list, inside the
|
|
70418
|
-
// viewport.
|
|
70419
|
-
const crossFrom = horizontal ? listRect.top : listRect.left;
|
|
70420
|
-
const crossViewportFrom = horizontal ? viewportRect.top : viewportRect.left;
|
|
70421
|
-
const crossPos = (crossFrom > crossViewportFrom ? crossFrom : crossViewportFrom) + 1;
|
|
70422
70577
|
return {
|
|
70423
70578
|
from,
|
|
70424
|
-
to
|
|
70425
|
-
crossPos
|
|
70579
|
+
to
|
|
70426
70580
|
};
|
|
70427
70581
|
};
|
|
70582
|
+
// The real rows of the list, and the first of them reaching past `from` along
|
|
70583
|
+
// the scroll axis. A binary search over their boxes: the rows stand in
|
|
70584
|
+
// document order along that axis, so their far edges only grow.
|
|
70585
|
+
const findRowsFrom = (listEl, from, horizontal) => {
|
|
70586
|
+
const rowEls = listEl.querySelectorAll(REAL_LIST_ITEM_SELECTOR);
|
|
70587
|
+
let low = 0;
|
|
70588
|
+
let high = rowEls.length;
|
|
70589
|
+
while (low < high) {
|
|
70590
|
+
const mid = low + high >> 1;
|
|
70591
|
+
const rect = rowEls[mid].getBoundingClientRect();
|
|
70592
|
+
const end = horizontal ? rect.right : rect.bottom;
|
|
70593
|
+
if (end > from) {
|
|
70594
|
+
high = mid;
|
|
70595
|
+
} else {
|
|
70596
|
+
low = mid + 1;
|
|
70597
|
+
}
|
|
70598
|
+
}
|
|
70599
|
+
return {
|
|
70600
|
+
rowEls,
|
|
70601
|
+
index: low
|
|
70602
|
+
};
|
|
70603
|
+
};
|
|
70604
|
+
// Whether a filler (the room held for rows outside the window) is what stands
|
|
70605
|
+
// at that position along the scroll axis.
|
|
70606
|
+
const isFillerAt = (listEl, position, horizontal) => {
|
|
70607
|
+
for (const fillerEl of listEl.querySelectorAll("[navi-virtual-filler]")) {
|
|
70608
|
+
const rect = fillerEl.getBoundingClientRect();
|
|
70609
|
+
const from = horizontal ? rect.left : rect.top;
|
|
70610
|
+
const to = horizontal ? rect.right : rect.bottom;
|
|
70611
|
+
if (position >= from && position < to) {
|
|
70612
|
+
return true;
|
|
70613
|
+
}
|
|
70614
|
+
}
|
|
70615
|
+
return false;
|
|
70616
|
+
};
|
|
70428
70617
|
|
|
70429
|
-
// Which row of the collection sits at the current scroll position.
|
|
70430
|
-
//
|
|
70431
|
-
// on screen is only
|
|
70618
|
+
// Which row of the collection sits at the current scroll position. Read off
|
|
70619
|
+
// the rows' boxes when a real row is there (see captureScrollAnchor for why
|
|
70620
|
+
// not hit-testing), and from the row size when what is on screen is only
|
|
70621
|
+
// reserved room.
|
|
70432
70622
|
// Returns { index, item, reason } or null if nothing can be determined.
|
|
70433
70623
|
const getScrollInfo = ({
|
|
70434
70624
|
scrollValues,
|
|
@@ -70442,34 +70632,31 @@ const getScrollInfo = ({
|
|
|
70442
70632
|
const items = listRows.itemsSignal.peek();
|
|
70443
70633
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
70444
70634
|
const listRect = listEl.getBoundingClientRect();
|
|
70445
|
-
|
|
70446
|
-
|
|
70447
|
-
const scanRange = getListVisibleScanRange(viewportRect, listRect, horizontal);
|
|
70448
|
-
if (!scanRange) {
|
|
70635
|
+
const range = getListVisibleRange(viewportRect, listRect, horizontal);
|
|
70636
|
+
if (!range) {
|
|
70449
70637
|
return null;
|
|
70450
70638
|
}
|
|
70451
|
-
//
|
|
70452
|
-
//
|
|
70453
|
-
//
|
|
70454
|
-
//
|
|
70455
|
-
const scanStart = (
|
|
70456
|
-
|
|
70457
|
-
|
|
70458
|
-
|
|
70459
|
-
|
|
70460
|
-
|
|
70461
|
-
|
|
70462
|
-
|
|
70463
|
-
|
|
70464
|
-
|
|
70465
|
-
|
|
70466
|
-
|
|
70467
|
-
|
|
70468
|
-
|
|
70469
|
-
|
|
70470
|
-
|
|
70471
|
-
|
|
70472
|
-
break;
|
|
70639
|
+
// Read from the center of the visible part of the list along the main axis.
|
|
70640
|
+
// The render window places half its budget before and half after the hit
|
|
70641
|
+
// index. Anchoring to the center maximises how many rendered items fall
|
|
70642
|
+
// within the visible area.
|
|
70643
|
+
const scanStart = (range.from + range.to) / 2;
|
|
70644
|
+
let hitEl = null;
|
|
70645
|
+
const hitFiller = isFillerAt(listEl, scanStart, horizontal);
|
|
70646
|
+
if (!hitFiller) {
|
|
70647
|
+
// The first real row from the center down, the way a probe walking down
|
|
70648
|
+
// from it would meet one — past a separator or a group label in between.
|
|
70649
|
+
const {
|
|
70650
|
+
rowEls,
|
|
70651
|
+
index
|
|
70652
|
+
} = findRowsFrom(listEl, scanStart, horizontal);
|
|
70653
|
+
const rowEl = rowEls[index];
|
|
70654
|
+
if (rowEl) {
|
|
70655
|
+
const rowRect = rowEl.getBoundingClientRect();
|
|
70656
|
+
const rowStart = horizontal ? rowRect.left : rowRect.top;
|
|
70657
|
+
if (rowStart < range.to) {
|
|
70658
|
+
hitEl = rowEl;
|
|
70659
|
+
}
|
|
70473
70660
|
}
|
|
70474
70661
|
}
|
|
70475
70662
|
// Shared by the "hit a filler" and "hit nothing at all" cases below: both
|
|
@@ -70500,8 +70687,8 @@ const getScrollInfo = ({
|
|
|
70500
70687
|
return estimateFromScrollPos("hit filler");
|
|
70501
70688
|
}
|
|
70502
70689
|
if (hitEl) {
|
|
70503
|
-
const
|
|
70504
|
-
const item = items.find(i => i.
|
|
70690
|
+
const hitName = getRowName(hitEl);
|
|
70691
|
+
const item = items.find(i => i.itemId === hitName);
|
|
70505
70692
|
if (!item) {
|
|
70506
70693
|
return null;
|
|
70507
70694
|
}
|
|
@@ -70511,13 +70698,11 @@ const getScrollInfo = ({
|
|
|
70511
70698
|
reason: `hit item at ${item.index} (${item.value})`
|
|
70512
70699
|
};
|
|
70513
70700
|
}
|
|
70514
|
-
//
|
|
70515
|
-
// the
|
|
70516
|
-
//
|
|
70517
|
-
//
|
|
70518
|
-
//
|
|
70519
|
-
// estimate as the hitFiller case is a safe fallback: it only needs the
|
|
70520
|
-
// scroll position, not a successful hit-test.
|
|
70701
|
+
// No real row stands between the center and the end of what is visible.
|
|
70702
|
+
// Keeping the stale renderWindow here means the DOM never gets asked to
|
|
70703
|
+
// catch up with a scrollTop that may have jumped far away — the user ends
|
|
70704
|
+
// up staring at blank space. Same estimate as the hitFiller case is a safe
|
|
70705
|
+
// fallback: it only needs the scroll position.
|
|
70521
70706
|
const estimated = estimateFromScrollPos("no hit");
|
|
70522
70707
|
if (estimated) {
|
|
70523
70708
|
return estimated;
|
|
@@ -70952,6 +71137,12 @@ const ListItemUI = props => {
|
|
|
70952
71137
|
// gave the row its place and decided it is inside the render window.
|
|
70953
71138
|
const row = useContext(ListRowContext);
|
|
70954
71139
|
const slotId = useContext(ListSlotContext);
|
|
71140
|
+
// What the row is called in the list — the run's name for a row it draws,
|
|
71141
|
+
// whatever `id` the caller put on the element (a run row may need a DOM id of
|
|
71142
|
+
// its own, to keep clear of another element's). A position names a row by
|
|
71143
|
+
// this (see captureScrollAnchor), and a row asked for by name is found by
|
|
71144
|
+
// this (locateRow, findRowElement); the DOM id is the caller's.
|
|
71145
|
+
props.itemId = row ? row.id : props.id;
|
|
70955
71146
|
// There is no standalone match/matchScore/highlight prop — participation
|
|
70956
71147
|
// in a matching system (search, filter…) only goes through `matchInfo`
|
|
70957
71148
|
// (e.g. useSearchText's getItemMatchInfo(item): { match, matchScore,
|
|
@@ -71032,6 +71223,7 @@ const ListItemReal = props => {
|
|
|
71032
71223
|
const {
|
|
71033
71224
|
ref,
|
|
71034
71225
|
id,
|
|
71226
|
+
itemId,
|
|
71035
71227
|
hidden,
|
|
71036
71228
|
muted,
|
|
71037
71229
|
loading,
|
|
@@ -71051,7 +71243,7 @@ const ListItemReal = props => {
|
|
|
71051
71243
|
// see the state change, which only the caller can arrange).
|
|
71052
71244
|
const pendingScrollRef = useContext(PendingScrollRefContext);
|
|
71053
71245
|
const pendingScroll = pendingScrollRef.current;
|
|
71054
|
-
const needScrollOnMount = pendingScroll && pendingScroll.id ===
|
|
71246
|
+
const needScrollOnMount = pendingScroll && pendingScroll.id === itemId;
|
|
71055
71247
|
useLayoutEffect(() => {
|
|
71056
71248
|
if (!needScrollOnMount) {
|
|
71057
71249
|
return;
|
|
@@ -71150,7 +71342,7 @@ const ListItemReal = props => {
|
|
|
71150
71342
|
baseClassName: "navi_list_item",
|
|
71151
71343
|
styleCSSVars: LIST_ITEM_STYLE_CSS_VARS,
|
|
71152
71344
|
id: id,
|
|
71153
|
-
"navi-list-item-real":
|
|
71345
|
+
"navi-list-item-real": itemId,
|
|
71154
71346
|
...rest,
|
|
71155
71347
|
...itemColumnsOverrideProps,
|
|
71156
71348
|
index: undefined,
|
|
@@ -72531,6 +72723,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
72531
72723
|
* onScrolledChange?: (scrolled: {id: string, index: number, offset: number}) => void,
|
|
72532
72724
|
* scroller?: "self" | "parent" | "document" | Element | {current: Element},
|
|
72533
72725
|
* hoverWhileScrolling?: boolean,
|
|
72726
|
+
* scrollResetOnNavigation?: boolean,
|
|
72534
72727
|
* fallback?: import("ignore:preact").ComponentChildren,
|
|
72535
72728
|
* searchFallback?: import("ignore:preact").ComponentChildren,
|
|
72536
72729
|
* searchText?: string,
|
|
@@ -72622,7 +72815,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
72622
72815
|
* list is already known to be empty: the empty `fallback` shows right away
|
|
72623
72816
|
* rather than an empty frame, so nothing moves when the response arrives.
|
|
72624
72817
|
* @param {"start"|"end"|number|{id: string, offset?: number}} [props.defaultScrolled="start"]
|
|
72625
|
-
* Where the list opens, after which the user owns the scroll
|
|
72818
|
+
* Where the list opens, after which the user owns the scroll — unless it is
|
|
72819
|
+
* being come back to (see `scrollResetOnNavigation`). `"end"` is a
|
|
72626
72820
|
* thread read backwards — the last rows are the ones to show, and the ones
|
|
72627
72821
|
* asked for first. A number opens on that row of the collection. `{id,
|
|
72628
72822
|
* offset}` — what `onScrolledChange` hands out — opens on a NAMED row,
|
|
@@ -72705,6 +72899,15 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
72705
72899
|
* Pass `true` for a list whose rows must stay live under the pointer while
|
|
72706
72900
|
* it scrolls. The trade of the default is the mirror one: right after a
|
|
72707
72901
|
* scroll, the row under the pointer lights up only once the pointer moves.
|
|
72902
|
+
* @param {boolean} [props.scrollResetOnNavigation=false]
|
|
72903
|
+
* A list that opens the same way every time. Without it the list comes back
|
|
72904
|
+
* where it was when its screen is left and come back to — the way the page
|
|
72905
|
+
* does, and for a list that scrolls itself the page's own restoration cannot
|
|
72906
|
+
* see. The position is kept under the list's `id` and the page's url, for
|
|
72907
|
+
* the session (a reload comes back too); a list without an `id` of its own
|
|
72908
|
+
* has nothing to be remembered by. A fresh arrival at the page opens at
|
|
72909
|
+
* `defaultScrolled` either way, and so does a list the caller holds through
|
|
72910
|
+
* `scrolled`.
|
|
72708
72911
|
* @param {boolean} [props.deselectable]
|
|
72709
72912
|
* A single-select list allowed to hold nothing: the selected row, pressed
|
|
72710
72913
|
* again, lets go. Without it the list is a radio group — a choice, once
|