@jsenv/navi 0.29.426 → 0.29.428
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 +372 -125
- package/dist/dev/jsenv_navi.js.map +11 -11
- package/dist/dev/jsenv_navi_side_effects.js +1 -5
- package/dist/dev/jsenv_navi_side_effects.js.map +2 -2
- package/dist/jsenv_navi.js +369 -122
- package/dist/jsenv_navi.js.map +11 -11
- package/dist/jsenv_navi_side_effects.js +1 -5
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/css_architecture.md +1 -1
- package/docs/list_refresh.md +5 -4
- package/package.json +2 -2
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -16881,9 +16881,9 @@ const armOutsidePressClose = (
|
|
|
16881
16881
|
* `font-family` is deliberately absent: a surface keeps the face of what
|
|
16882
16882
|
* opened it, so a section written in a display font gets its tooltips in that
|
|
16883
16883
|
* font. `color`, `font-size` and `font-weight` are absent too — each surface
|
|
16884
|
-
* answers those its own way (a popup
|
|
16885
|
-
* the size of what opened it
|
|
16886
|
-
* size).
|
|
16884
|
+
* answers those its own way (a popup and a callout write in
|
|
16885
|
+
* --navi-popup-color; a popup follows the size of what opened it, a callout
|
|
16886
|
+
* resets to the document's size).
|
|
16887
16887
|
*
|
|
16888
16888
|
* In `@layer navi` so an app that wants one of these back says so on the
|
|
16889
16889
|
* surface and wins, without having to out-specify anything.
|
|
@@ -17113,8 +17113,7 @@ const css$14 = /* css */ `
|
|
|
17113
17113
|
--callout-error-color: var(--navi-callout-error-color);
|
|
17114
17114
|
--callout-neutral-color: var(--navi-callout-neutral-color);
|
|
17115
17115
|
|
|
17116
|
-
--callout-background-color:
|
|
17117
|
-
--callout-icon-color: black;
|
|
17116
|
+
--callout-background-color: var(--navi-popup-background-color);
|
|
17118
17117
|
--callout-padding: 8px;
|
|
17119
17118
|
--callout-z-index: var(--navi-z-index-callout);
|
|
17120
17119
|
/* The page's line, said here rather than inherited: the icon and the
|
|
@@ -17168,11 +17167,11 @@ const css$14 = /* css */ `
|
|
|
17168
17167
|
height: auto; /* User agent reset */
|
|
17169
17168
|
margin: 0;
|
|
17170
17169
|
padding: 0; /* User agent reset */
|
|
17171
|
-
/*
|
|
17172
|
-
|
|
17173
|
-
color
|
|
17174
|
-
navi_css_vars.js
|
|
17175
|
-
color:
|
|
17170
|
+
/* A callout sits inside the element but writes on its own paper: the
|
|
17171
|
+
popup's ink, paired with the popup's paper it is drawn on, so the two
|
|
17172
|
+
follow color-scheme and the theme together. The color keywords are
|
|
17173
|
+
re-declared against that same ink in navi_css_vars.js. */
|
|
17174
|
+
color: var(--navi-popup-color);
|
|
17176
17175
|
font-weight: initial; /* Callout fells disconnected from the element, font weight should be predictible and stable */
|
|
17177
17176
|
font-size: initial; /* Callout fells disconnected from the element, font size should be predictible and stable */
|
|
17178
17177
|
line-height: var(
|
|
@@ -19484,16 +19483,18 @@ const findBusySource = (field) => {
|
|
|
19484
19483
|
// a new interaction is queued behind the run (see the action queue in
|
|
19485
19484
|
// control_hooks.jsx) rather than refused.
|
|
19486
19485
|
if (!field.optimistic && boundAction) {
|
|
19487
|
-
// The
|
|
19488
|
-
//
|
|
19489
|
-
//
|
|
19490
|
-
//
|
|
19491
|
-
//
|
|
19492
|
-
//
|
|
19493
|
-
//
|
|
19494
|
-
//
|
|
19495
|
-
|
|
19496
|
-
|
|
19486
|
+
// The instance the run in flight uses, as the render reads it (see
|
|
19487
|
+
// runningActionSignal in control_hooks.jsx): the state a proxy follows can
|
|
19488
|
+
// move mid-run — a popup content unmounting takes the group's value — and
|
|
19489
|
+
// the proxy would resolve to an idle instance. With no run out, the
|
|
19490
|
+
// instance the proxy resolves to right now, never the proxy's own signal:
|
|
19491
|
+
// a MIRROR synced by an effect the settling batch defers, which still says
|
|
19492
|
+
// RUNNING mid-batch (a state echo, an automatic follow-up) for an action
|
|
19493
|
+
// already over. An instance is COMPLETED the moment it settles.
|
|
19494
|
+
const liveAction =
|
|
19495
|
+
field.runningActionSignal.value ??
|
|
19496
|
+
boundAction.getCurrentAction?.() ??
|
|
19497
|
+
boundAction;
|
|
19497
19498
|
if (liveAction.runningStateSignal.value === RUNNING) {
|
|
19498
19499
|
return { field, action: liveAction };
|
|
19499
19500
|
}
|
|
@@ -20531,11 +20532,10 @@ const READONLY_CONSTRAINT = {
|
|
|
20531
20532
|
name: "readonly",
|
|
20532
20533
|
messageAttribute: "data-readonly-message",
|
|
20533
20534
|
check: (field, { intent } = {}) => {
|
|
20534
|
-
|
|
20535
|
-
|
|
20536
|
-
|
|
20537
|
-
)
|
|
20538
|
-
if (!readOnly) {
|
|
20535
|
+
// Not off the rendered `readOnly`/`aria-readonly`: they also carry the run
|
|
20536
|
+
// of the control's own action, a frame behind (see readOnlyWithoutRun in
|
|
20537
|
+
// control_hooks.jsx). That run is BUSY_CONSTRAINT's, which reads it live.
|
|
20538
|
+
if (!field.readOnlyWithoutRun) {
|
|
20539
20539
|
return null;
|
|
20540
20540
|
}
|
|
20541
20541
|
|
|
@@ -26867,10 +26867,11 @@ const createRangeReader = (
|
|
|
26867
26867
|
});
|
|
26868
26868
|
return { byIndex, count: composition.count };
|
|
26869
26869
|
};
|
|
26870
|
-
// `replace`
|
|
26871
|
-
//
|
|
26872
|
-
// so two lists reading the same
|
|
26873
|
-
// to one composition instead of
|
|
26870
|
+
// `replace` says the list dropped ranks it held (an item the collection moved
|
|
26871
|
+
// elsewhere, ranks past its end): kept here, they would come back at the next
|
|
26872
|
+
// mount. Otherwise the ranks are merged, so two lists reading the same
|
|
26873
|
+
// collection through their own windows add up to one composition instead of
|
|
26874
|
+
// taking turns erasing each other.
|
|
26874
26875
|
readRange.writeComposition = ({ byIndex, count, replace }) => {
|
|
26875
26876
|
const params = currentParams();
|
|
26876
26877
|
let composition = findComposition(params);
|
|
@@ -44065,6 +44066,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
44065
44066
|
ref: realUIStateController.ref,
|
|
44066
44067
|
uiStateSignal: realUIStateController.uiStateSignal,
|
|
44067
44068
|
controlHostProps: realUIStateController.controlHostProps,
|
|
44069
|
+
readOnlyWithoutRun: realUIStateController.readOnlyWithoutRun,
|
|
44068
44070
|
registerChild: (child) => {
|
|
44069
44071
|
if (!canRegisterAsFacadeChild(child)) {
|
|
44070
44072
|
return;
|
|
@@ -44194,6 +44196,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
44194
44196
|
s.controller.ref = realUIStateController.ref;
|
|
44195
44197
|
s.controller.uiStateSignal = realUIStateController.uiStateSignal;
|
|
44196
44198
|
s.controller.controlHostProps = realUIStateController.controlHostProps;
|
|
44199
|
+
s.controller.readOnlyWithoutRun =
|
|
44200
|
+
realUIStateController.readOnlyWithoutRun;
|
|
44197
44201
|
realUIStateController.pushStateDownToFacadeChild =
|
|
44198
44202
|
s.controller.pushStateDownToChild;
|
|
44199
44203
|
|
|
@@ -46102,6 +46106,11 @@ const useInteractiveProps = (props, {
|
|
|
46102
46106
|
const actionLoading = optimistic ? false : actionRunning;
|
|
46103
46107
|
const loadingResolved = loadingBase || actionLoading;
|
|
46104
46108
|
const readOnlyResolved = readOnlyBase || actionLoading;
|
|
46109
|
+
// What READONLY_CONSTRAINT refuses for. The run of the control's own action
|
|
46110
|
+
// is BUSY_CONSTRAINT's, read live: readOnlyResolved is a render snapshot,
|
|
46111
|
+
// still read-only in the tick the run settles — the tick a form's
|
|
46112
|
+
// after-send `--navi-reset` asks the form to empty itself.
|
|
46113
|
+
uiStateController.readOnlyWithoutRun = Boolean(readOnlyBase);
|
|
46105
46114
|
// Read-only, and what this control opens still opens: reading what is in
|
|
46106
46115
|
// there changes nothing. Read by READONLY_CONSTRAINT, which lets an
|
|
46107
46116
|
// interaction that only reads through on it.
|
|
@@ -46556,10 +46565,10 @@ const useInteractiveProps = (props, {
|
|
|
46556
46565
|
});
|
|
46557
46566
|
}
|
|
46558
46567
|
// controlHostProps is a curated subset of props with resolved values applied
|
|
46559
|
-
// (e.g. readOnly resolved from context + action loading). The
|
|
46560
|
-
//
|
|
46561
|
-
//
|
|
46562
|
-
//
|
|
46568
|
+
// (e.g. readOnly resolved from context + action loading). The constraints
|
|
46569
|
+
// read off uiStateController.controlHostProps at runtime (e.g. DISABLED_CONSTRAINT,
|
|
46570
|
+
// the readonly reason), so pointing the controller at controlHostProps keeps
|
|
46571
|
+
// those reads current without any extra bookkeeping.
|
|
46563
46572
|
const firstRender = uiStateController.controlHostProps === undefined;
|
|
46564
46573
|
uiStateController.controlHostProps = controlHostProps;
|
|
46565
46574
|
// The action itself, not just what the last render made of it: its running
|
|
@@ -55616,14 +55625,34 @@ const ButtonConfirm = ({
|
|
|
55616
55625
|
}
|
|
55617
55626
|
return jsxs("span", {
|
|
55618
55627
|
className: "navi_button_confirm",
|
|
55619
|
-
ref: askingRef
|
|
55628
|
+
ref: askingRef
|
|
55629
|
+
// A place for the focus, so a press anywhere in the question keeps it
|
|
55630
|
+
// inside: WebKit does not focus a pressed button, and a press on
|
|
55631
|
+
// "Confirmer" would otherwise blur toward <body> — a focusout with no
|
|
55632
|
+
// relatedTarget, read below as the person leaving. The callout answers
|
|
55633
|
+
// the same ambiguity the same way.
|
|
55634
|
+
,
|
|
55635
|
+
tabIndex: -1
|
|
55636
|
+
// "not me, unless you have nothing else": the ladder in the effect above
|
|
55637
|
+
// walks past the question to what it holds.
|
|
55638
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
55639
|
+
,
|
|
55640
|
+
"navi-autofocus": "last-resort",
|
|
55620
55641
|
onKeyDown: e => {
|
|
55621
55642
|
if (e.key === "Escape") {
|
|
55622
55643
|
cancel(true);
|
|
55623
55644
|
}
|
|
55624
55645
|
},
|
|
55625
55646
|
onFocusOut: e => {
|
|
55626
|
-
|
|
55647
|
+
// Chrome also fires this from the question's own removal, once preact
|
|
55648
|
+
// has cleared the ref: nobody left, and cancel(false) would undo the
|
|
55649
|
+
// focus the answer gives back. That focus cannot go back before the
|
|
55650
|
+
// removal: the button it lands on is mounted by the same commit.
|
|
55651
|
+
const askingEl = askingRef.current;
|
|
55652
|
+
if (!askingEl) {
|
|
55653
|
+
return;
|
|
55654
|
+
}
|
|
55655
|
+
if (!askingEl.contains(e.relatedTarget)) {
|
|
55627
55656
|
cancel(false);
|
|
55628
55657
|
}
|
|
55629
55658
|
},
|
|
@@ -73247,7 +73276,7 @@ const css$x = /* css */`@property --list-group-label-height {
|
|
|
73247
73276
|
}
|
|
73248
73277
|
}
|
|
73249
73278
|
|
|
73250
|
-
.navi_list[navi-box-flow="grid"] > .navi_list_virtual_filler, .navi_list[navi-box-flow="grid"] > .navi_separator {
|
|
73279
|
+
.navi_list[navi-box-flow="grid"] > .navi_list_virtual_filler, .navi_list[navi-box-flow="grid"] > .navi_list_failed_items, .navi_list[navi-box-flow="grid"] > .navi_separator {
|
|
73251
73280
|
grid-column: 1 / -1;
|
|
73252
73281
|
}
|
|
73253
73282
|
|
|
@@ -73624,7 +73653,8 @@ const ListUI = props => {
|
|
|
73624
73653
|
listId: rest.id,
|
|
73625
73654
|
scroller,
|
|
73626
73655
|
searchText,
|
|
73627
|
-
horizontal
|
|
73656
|
+
horizontal,
|
|
73657
|
+
columns
|
|
73628
73658
|
});
|
|
73629
73659
|
|
|
73630
73660
|
// renderBudget frames the items of a run; a list whose items are all declared
|
|
@@ -73939,7 +73969,8 @@ const useListScrollSync = ({
|
|
|
73939
73969
|
listId,
|
|
73940
73970
|
scroller,
|
|
73941
73971
|
searchText,
|
|
73942
|
-
horizontal
|
|
73972
|
+
horizontal,
|
|
73973
|
+
columns
|
|
73943
73974
|
}) => {
|
|
73944
73975
|
const debugScroll = useDebugScroll();
|
|
73945
73976
|
// The items drawn, [start, end) among the list's own. A ref as well as a
|
|
@@ -73954,6 +73985,7 @@ const useListScrollSync = ({
|
|
|
73954
73985
|
return renderWindow.start > 0 || renderWindow.end < listItems.totalSignal.peek();
|
|
73955
73986
|
};
|
|
73956
73987
|
const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
|
|
73988
|
+
inLines: Boolean(columns),
|
|
73957
73989
|
windowLeavesItemsOut,
|
|
73958
73990
|
scrolledWanted: scrolled ?? defaultScrolled,
|
|
73959
73991
|
// The fillers hold the room of items above the screen at this size: a
|
|
@@ -74022,6 +74054,34 @@ const useListScrollSync = ({
|
|
|
74022
74054
|
});
|
|
74023
74055
|
};
|
|
74024
74056
|
|
|
74057
|
+
// How many items share a line: the tracks a grid of items (`columns`)
|
|
74058
|
+
// resolves to, one otherwise. A layout result under auto-fill, so it is read
|
|
74059
|
+
// off the list once laid out, at each commit and each resize — for the runs
|
|
74060
|
+
// alone, which draw in lines: items declared one by one are all drawn. A
|
|
74061
|
+
// line of another length rewraps every item drawn: the item at the top of
|
|
74062
|
+
// the view is held where it is across it.
|
|
74063
|
+
const itemsPerLineRef = useRef(1);
|
|
74064
|
+
const [itemsPerLine, setItemsPerLine] = useState(1);
|
|
74065
|
+
const updateItemsPerLine = () => {
|
|
74066
|
+
if (!ref.current) {
|
|
74067
|
+
return;
|
|
74068
|
+
}
|
|
74069
|
+
const itemsPerLineNow = columns && listItems.hasRuns() ? readItemsPerLine(getListEl()) : 1;
|
|
74070
|
+
if (itemsPerLineNow === null || itemsPerLineNow === itemsPerLineRef.current) {
|
|
74071
|
+
return;
|
|
74072
|
+
}
|
|
74073
|
+
captureAnchor();
|
|
74074
|
+
itemsPerLineRef.current = itemsPerLineNow;
|
|
74075
|
+
setItemsPerLine(itemsPerLineNow);
|
|
74076
|
+
};
|
|
74077
|
+
const updateItemsPerLineRef = useRef(null);
|
|
74078
|
+
updateItemsPerLineRef.current = updateItemsPerLine;
|
|
74079
|
+
useLayoutEffect(() => {
|
|
74080
|
+
if (columns || itemsPerLineRef.current !== 1) {
|
|
74081
|
+
updateItemsPerLine();
|
|
74082
|
+
}
|
|
74083
|
+
});
|
|
74084
|
+
|
|
74025
74085
|
// What the window holds goes through three stages (see settleWindow):
|
|
74026
74086
|
// - "first": the first commit, a count of items since nothing is laid out
|
|
74027
74087
|
// yet to measure a size with — `initial` or the budget when they are one,
|
|
@@ -74423,6 +74483,7 @@ const useListScrollSync = ({
|
|
|
74423
74483
|
listEl: getListEl(),
|
|
74424
74484
|
listItems,
|
|
74425
74485
|
virtualItemSizeSignal,
|
|
74486
|
+
itemsPerLine: itemsPerLineRef.current,
|
|
74426
74487
|
renderWindowRef,
|
|
74427
74488
|
horizontal
|
|
74428
74489
|
});
|
|
@@ -74763,20 +74824,14 @@ const useListScrollSync = ({
|
|
|
74763
74824
|
}
|
|
74764
74825
|
const scrollerEl = getScroller();
|
|
74765
74826
|
const listEl = getListEl();
|
|
74766
|
-
|
|
74767
|
-
|
|
74768
|
-
|
|
74769
|
-
|
|
74770
|
-
|
|
74771
|
-
|
|
74772
|
-
|
|
74773
|
-
|
|
74774
|
-
// growing is its own content settling: only a list holding itself
|
|
74775
|
-
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
74776
|
-
// reading must not be touched — its items are held still by the anchoring,
|
|
74777
|
-
// which this would undo. The SCROLLER resizing is the window around it
|
|
74778
|
-
// changing shape, and then the item that was at the top goes back where it
|
|
74779
|
-
// was.
|
|
74827
|
+
// Two things resize here, and they call for opposite answers. The LIST
|
|
74828
|
+
// growing is its own content settling: only a list holding itself
|
|
74829
|
+
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
74830
|
+
// reading must not be touched — its items are held still by the anchoring,
|
|
74831
|
+
// which this would undo. The SCROLLER resizing is the window around it
|
|
74832
|
+
// changing shape, and then the item that was at the top goes back where it
|
|
74833
|
+
// was.
|
|
74834
|
+
const putTopItemBack = entries => {
|
|
74780
74835
|
const listResized = entries.some(entry => entry.target === listEl);
|
|
74781
74836
|
if (listResized && onGeometryChangeRef.current()) {
|
|
74782
74837
|
return;
|
|
@@ -74805,6 +74860,19 @@ const useListScrollSync = ({
|
|
|
74805
74860
|
} else {
|
|
74806
74861
|
scrollerEl.scrollTop += delta;
|
|
74807
74862
|
}
|
|
74863
|
+
};
|
|
74864
|
+
const observer = new ResizeObserver(entries => {
|
|
74865
|
+
// A box that grows may be a box that starts to scroll — the scroller is
|
|
74866
|
+
// resolved again before anything is done about the resize, so that a
|
|
74867
|
+
// list which has outgrown a bounded ancestor stops holding on to the
|
|
74868
|
+
// page.
|
|
74869
|
+
resolveScroller();
|
|
74870
|
+
rememberPosition();
|
|
74871
|
+
putTopItemBack(entries);
|
|
74872
|
+
// Once the top item is back where it was: that is the place a line of
|
|
74873
|
+
// another length has to keep, and putting it back drops any anchor
|
|
74874
|
+
// captured before.
|
|
74875
|
+
updateItemsPerLineRef.current();
|
|
74808
74876
|
});
|
|
74809
74877
|
observer.observe(scrollerEl);
|
|
74810
74878
|
observer.observe(listEl);
|
|
@@ -74960,7 +75028,10 @@ const useListScrollSync = ({
|
|
|
74960
75028
|
start,
|
|
74961
75029
|
end
|
|
74962
75030
|
} = renderWindowRef.current;
|
|
74963
|
-
|
|
75031
|
+
// What an item not drawn weighs: its share of a line, which the items side
|
|
75032
|
+
// by side on it take together.
|
|
75033
|
+
const itemsPerLineNow = itemsPerLineRef.current;
|
|
75034
|
+
const itemSize = virtualItemSizeSignal.peek() / itemsPerLineNow;
|
|
74964
75035
|
const pixelsOf = createItemSizeReader(items, start, end, itemSize);
|
|
74965
75036
|
const walkBefore = (from, sizeWanted, sizeOf) => {
|
|
74966
75037
|
let index = from;
|
|
@@ -75028,10 +75099,10 @@ const useListScrollSync = ({
|
|
|
75028
75099
|
const behindSize = countsItems ? Math.floor(spare / 4) : spare / 4;
|
|
75029
75100
|
const aheadSize = spare - behindSize;
|
|
75030
75101
|
if (stage === "steady" && !budgetWarnedRef.current && bandEnd > bandStart) {
|
|
75031
|
-
const
|
|
75032
|
-
if (spare <
|
|
75102
|
+
const twoLines = (countsItems ? 2 : 2 * (bandTo - bandFrom) / (bandEnd - bandStart)) * itemsPerLineNow;
|
|
75103
|
+
if (spare < twoLines) {
|
|
75033
75104
|
budgetWarnedRef.current = true;
|
|
75034
|
-
console.warn(`List: renderBudget=${renderBudget.value}${renderBudget.unit} leaves less than two items beyond what the screen shows (${bandEnd - bandStart} items): items will go blank as it scrolls. Give it room for a screen ahead or more — "300%" of the box that scrolls it, say.`);
|
|
75105
|
+
console.warn(`List: renderBudget=${renderBudget.value}${renderBudget.unit} leaves less than two ${itemsPerLineNow > 1 ? "lines of items" : "items"} beyond what the screen shows (${bandEnd - bandStart} items): items will go blank as it scrolls. Give it room for a screen ahead or more — "300%" of the box that scrolls it, say.`);
|
|
75035
75106
|
}
|
|
75036
75107
|
}
|
|
75037
75108
|
if (!force) {
|
|
@@ -75237,9 +75308,28 @@ const useListScrollSync = ({
|
|
|
75237
75308
|
ifChanged: true
|
|
75238
75309
|
});
|
|
75239
75310
|
}, [windowStart, windowEnd]);
|
|
75311
|
+
// What the runs draw: the window widened to whole lines. The window itself
|
|
75312
|
+
// moves an item at a time, and a grid lays the first item drawn in its first
|
|
75313
|
+
// column — started anywhere but on a line, every item drawn would sit in the
|
|
75314
|
+
// column of another. Started on one, an item is in the column its place says
|
|
75315
|
+
// whatever the window frames. The same object while the numbers hold, so the
|
|
75316
|
+
// runs are not told about a window that did not move.
|
|
75317
|
+
const windowDrawnRef = useRef(null);
|
|
75318
|
+
const drawnStart = windowStart - windowStart % itemsPerLine;
|
|
75319
|
+
const endPastLine = windowEnd % itemsPerLine;
|
|
75320
|
+
const drawnEnd = endPastLine === 0 ? windowEnd : windowEnd + itemsPerLine - endPastLine;
|
|
75321
|
+
let windowDrawn = windowDrawnRef.current;
|
|
75322
|
+
if (!windowDrawn || windowDrawn.start !== drawnStart || windowDrawn.end !== drawnEnd || windowDrawn.itemsPerLine !== itemsPerLine) {
|
|
75323
|
+
windowDrawn = {
|
|
75324
|
+
start: drawnStart,
|
|
75325
|
+
end: drawnEnd,
|
|
75326
|
+
itemsPerLine
|
|
75327
|
+
};
|
|
75328
|
+
windowDrawnRef.current = windowDrawn;
|
|
75329
|
+
}
|
|
75240
75330
|
return {
|
|
75241
75331
|
virtualItemSizeSignal,
|
|
75242
|
-
renderWindow:
|
|
75332
|
+
renderWindow: windowDrawn,
|
|
75243
75333
|
pendingScrollRef,
|
|
75244
75334
|
scrollToItem,
|
|
75245
75335
|
captureAnchor
|
|
@@ -75263,6 +75353,21 @@ const getScrollerViewportRect = scrollerEl => {
|
|
|
75263
75353
|
}
|
|
75264
75354
|
return scrollerEl.getBoundingClientRect();
|
|
75265
75355
|
};
|
|
75356
|
+
// The tracks a grid resolved its template to, as its computed
|
|
75357
|
+
// grid-template-columns lists them once laid out: "150px 150px [end] 150px".
|
|
75358
|
+
// Not laid out (a closed popup), that is the template as written, which says
|
|
75359
|
+
// nothing yet under auto-fill: `null`.
|
|
75360
|
+
const readItemsPerLine = listEl => {
|
|
75361
|
+
const tracks = getComputedStyle(listEl).gridTemplateColumns;
|
|
75362
|
+
if (tracks === "none" || tracks.includes("(")) {
|
|
75363
|
+
return null;
|
|
75364
|
+
}
|
|
75365
|
+
const sizes = tracks.replace(/\[[^\]]*\]/g, " ").trim();
|
|
75366
|
+
if (!sizes) {
|
|
75367
|
+
return null;
|
|
75368
|
+
}
|
|
75369
|
+
return sizes.split(/\s+/).length;
|
|
75370
|
+
};
|
|
75266
75371
|
// A window that cannot move is the one failure of a virtualized list that
|
|
75267
75372
|
// looks like nothing: the items outside it are fillers holding their room, so
|
|
75268
75373
|
// the list simply ends on blank space, at exactly the height of the items never
|
|
@@ -76150,6 +76255,7 @@ const getScrollInfo = ({
|
|
|
76150
76255
|
listEl,
|
|
76151
76256
|
listItems,
|
|
76152
76257
|
virtualItemSizeSignal,
|
|
76258
|
+
itemsPerLine,
|
|
76153
76259
|
renderWindowRef,
|
|
76154
76260
|
horizontal
|
|
76155
76261
|
}) => {
|
|
@@ -76187,7 +76293,9 @@ const getScrollInfo = ({
|
|
|
76187
76293
|
// mean we don't know the real on-screen index, only the scroll position,
|
|
76188
76294
|
// so estimate from it rather than assume nothing changed.
|
|
76189
76295
|
const estimateFromScrollPos = reasonPrefix => {
|
|
76190
|
-
|
|
76296
|
+
// An item's share of its line: the items side by side on it take it
|
|
76297
|
+
// together.
|
|
76298
|
+
const virtualItemSize = virtualItemSizeSignal.peek() / itemsPerLine;
|
|
76191
76299
|
if (virtualItemSize === 0) {
|
|
76192
76300
|
return null;
|
|
76193
76301
|
}
|
|
@@ -76243,8 +76351,10 @@ const getScrollInfo = ({
|
|
|
76243
76351
|
const VIRTUAL_ITEM_SIZE_EPSILON = 0.5;
|
|
76244
76352
|
// Measures the items currently in the DOM, edge to edge: what a filler stands in
|
|
76245
76353
|
// for is the room a run of items takes together — separators and group labels
|
|
76246
|
-
// included — not the height of one <li>.
|
|
76247
|
-
|
|
76354
|
+
// included — not the height of one <li>. The size of a line of them: one item
|
|
76355
|
+
// in a list, the items side by side in a grid (`inLines`), counted where a new
|
|
76356
|
+
// line starts.
|
|
76357
|
+
const measureItemSize = (listEl, horizontal, inLines) => {
|
|
76248
76358
|
let fromSkeletons = false;
|
|
76249
76359
|
let itemEls = listEl.querySelectorAll(REAL_LIST_ITEM_SELECTOR);
|
|
76250
76360
|
if (itemEls.length === 0) {
|
|
@@ -76266,13 +76376,27 @@ const measureItemSize = (listEl, horizontal) => {
|
|
|
76266
76376
|
if (span <= 0) {
|
|
76267
76377
|
return null;
|
|
76268
76378
|
}
|
|
76379
|
+
let lineCount = itemEls.length;
|
|
76380
|
+
if (inLines) {
|
|
76381
|
+
lineCount = 0;
|
|
76382
|
+
let lineFrom = -Infinity;
|
|
76383
|
+
for (const itemEl of itemEls) {
|
|
76384
|
+
const rect = itemEl.getBoundingClientRect();
|
|
76385
|
+
const from = horizontal ? rect.left : rect.top;
|
|
76386
|
+
if (from > lineFrom + 0.5) {
|
|
76387
|
+
lineCount++;
|
|
76388
|
+
lineFrom = from;
|
|
76389
|
+
}
|
|
76390
|
+
}
|
|
76391
|
+
}
|
|
76269
76392
|
return {
|
|
76270
|
-
size: span /
|
|
76271
|
-
|
|
76393
|
+
size: span / lineCount,
|
|
76394
|
+
lineCount,
|
|
76272
76395
|
fromSkeletons
|
|
76273
76396
|
};
|
|
76274
76397
|
};
|
|
76275
76398
|
const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
76399
|
+
inLines,
|
|
76276
76400
|
windowLeavesItemsOut,
|
|
76277
76401
|
scrolledWanted,
|
|
76278
76402
|
beforeSizeChange
|
|
@@ -76316,8 +76440,8 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
76316
76440
|
return;
|
|
76317
76441
|
}
|
|
76318
76442
|
}
|
|
76319
|
-
samples.sum += measure.size * measure.
|
|
76320
|
-
samples.count += measure.
|
|
76443
|
+
samples.sum += measure.size * measure.lineCount;
|
|
76444
|
+
samples.count += measure.lineCount;
|
|
76321
76445
|
const next = samples.sum / samples.count;
|
|
76322
76446
|
const current = virtualSizeSignal.peek();
|
|
76323
76447
|
if (Math.abs(next - current) > VIRTUAL_ITEM_SIZE_EPSILON) {
|
|
@@ -76337,7 +76461,7 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
76337
76461
|
const itemsHeldOffScreen = windowLeavesItemsOut();
|
|
76338
76462
|
if (!virtualItemSizeProp && sizeAlreadyKnown && itemsHeldOffScreen && ref.current) {
|
|
76339
76463
|
const listEl = ref.current.querySelector(".navi_list");
|
|
76340
|
-
const measure = listEl ? measureItemSize(listEl, horizontal) : null;
|
|
76464
|
+
const measure = listEl ? measureItemSize(listEl, horizontal, inLines) : null;
|
|
76341
76465
|
if (measure) {
|
|
76342
76466
|
feedSample(measure);
|
|
76343
76467
|
}
|
|
@@ -76358,11 +76482,11 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
76358
76482
|
if (!listEl) {
|
|
76359
76483
|
return undefined;
|
|
76360
76484
|
}
|
|
76361
|
-
const measure = measureItemSize(listEl, horizontal);
|
|
76485
|
+
const measure = measureItemSize(listEl, horizontal, inLines);
|
|
76362
76486
|
if (measure) {
|
|
76363
76487
|
const samples = samplesRef.current;
|
|
76364
|
-
samples.sum = measure.size * measure.
|
|
76365
|
-
samples.count = measure.
|
|
76488
|
+
samples.sum = measure.size * measure.lineCount;
|
|
76489
|
+
samples.count = measure.lineCount;
|
|
76366
76490
|
samples.fromSkeletons = measure.fromSkeletons;
|
|
76367
76491
|
virtualSizeSignal.value = measure.size;
|
|
76368
76492
|
return undefined;
|
|
@@ -76507,14 +76631,17 @@ const Fallback = ({
|
|
|
76507
76631
|
// Reads the item size itself: it is what the size is for, and a run holding
|
|
76508
76632
|
// every item it draws must not be redrawn — every item of it — because the size
|
|
76509
76633
|
// settled after the first commit.
|
|
76634
|
+
// It holds lines: the items side by side on one take its room together, and a
|
|
76635
|
+
// last line left short takes it all the same.
|
|
76510
76636
|
const VirtualFiller = ({
|
|
76511
76637
|
edge,
|
|
76512
76638
|
itemCount,
|
|
76639
|
+
itemsPerLine,
|
|
76513
76640
|
findChunks
|
|
76514
76641
|
}) => {
|
|
76515
76642
|
const listItems = useContext(ListItemsContext);
|
|
76516
|
-
const
|
|
76517
|
-
const sizeToFill = itemCount *
|
|
76643
|
+
const lineSize = listItems.virtualItemSizeSignal.value;
|
|
76644
|
+
const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize;
|
|
76518
76645
|
// A filler resizing moves what stands below it — the items on screen, when it
|
|
76519
76646
|
// holds the room of items above them — and it resizes in a commit of its own
|
|
76520
76647
|
// when the item size settles after the list has rendered: the list puts its
|
|
@@ -76533,7 +76660,7 @@ const VirtualFiller = ({
|
|
|
76533
76660
|
"aria-hidden": true,
|
|
76534
76661
|
style: {
|
|
76535
76662
|
"--size-to-fill": `${sizeToFill}px`,
|
|
76536
|
-
"--x-find-line-size": findChunks ? `${
|
|
76663
|
+
"--x-find-line-size": findChunks ? `${lineSize / itemsPerLine}px` : undefined
|
|
76537
76664
|
},
|
|
76538
76665
|
children: findChunks && findChunks.map(chunk => jsx("div", {
|
|
76539
76666
|
className: "navi_list_find_stand_in",
|
|
@@ -77416,6 +77543,9 @@ const useRunItems = (store, {
|
|
|
77416
77543
|
const rankOf = itemIndex => itemIndex - runStart;
|
|
77417
77544
|
const indexOfRank = rank => rank + runStart;
|
|
77418
77545
|
const getItemAt = itemIndex => store.getItem(rankOf(itemIndex));
|
|
77546
|
+
const {
|
|
77547
|
+
itemsPerLine
|
|
77548
|
+
} = renderWindow;
|
|
77419
77549
|
const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;
|
|
77420
77550
|
const windowTo = renderWindow.end < runEnd ? renderWindow.end : runEnd;
|
|
77421
77551
|
store.forget(rankOf(windowFrom), rankOf(windowTo));
|
|
@@ -77523,8 +77653,20 @@ const useRunItems = (store, {
|
|
|
77523
77653
|
// band is drawn among the items. A negative start is not a rank but a count
|
|
77524
77654
|
// back from the end (the very first ask, before the count is known) — there
|
|
77525
77655
|
// is no item to convert it to, and the band falls back to the window.
|
|
77526
|
-
|
|
77527
|
-
|
|
77656
|
+
let failureFrom = store.failure === null ? -1 : store.failure.start < 0 || indexOfRank(store.failure.start) < windowFrom ? windowFrom : indexOfRank(store.failure.start);
|
|
77657
|
+
let failureTo = store.failure === null ? -1 : store.failure.end < 0 || indexOfRank(store.failure.end) > windowTo - 1 ? windowTo - 1 : indexOfRank(store.failure.end);
|
|
77658
|
+
if (store.failure !== null && itemsPerLine > 1) {
|
|
77659
|
+
// In a grid the band takes whole lines, like the window: begun mid-line,
|
|
77660
|
+
// it would push every item after it into another column.
|
|
77661
|
+
failureFrom -= failureFrom % itemsPerLine;
|
|
77662
|
+
if (failureFrom < windowFrom) {
|
|
77663
|
+
failureFrom = windowFrom;
|
|
77664
|
+
}
|
|
77665
|
+
failureTo += itemsPerLine - 1 - failureTo % itemsPerLine;
|
|
77666
|
+
if (failureTo > windowTo - 1) {
|
|
77667
|
+
failureTo = windowTo - 1;
|
|
77668
|
+
}
|
|
77669
|
+
}
|
|
77528
77670
|
const nodes = [];
|
|
77529
77671
|
// Items that belong together, as the data says (a day of messages, a month of
|
|
77530
77672
|
// games): consecutive items sharing a group key are wrapped in one group, so
|
|
@@ -77532,19 +77674,57 @@ const useRunItems = (store, {
|
|
|
77532
77674
|
// rebuilt as the window slides — a group holds the items of its day that are
|
|
77533
77675
|
// currently drawn, which is exactly the span its label has to survive.
|
|
77534
77676
|
let group = null;
|
|
77677
|
+
// Two groups can still share a key (a failure band standing mid-group, data
|
|
77678
|
+
// not sorted by group): the later ones are told apart by their rank among
|
|
77679
|
+
// them, so the first keeps its element. A group keyed by where it starts
|
|
77680
|
+
// would lose its element each time the window slides past its first item.
|
|
77681
|
+
const groupCountByKey = new Map();
|
|
77535
77682
|
const closeGroup = () => {
|
|
77536
77683
|
if (!group) {
|
|
77537
77684
|
return;
|
|
77538
77685
|
}
|
|
77686
|
+
const groupCount = (groupCountByKey.get(group.key) || 0) + 1;
|
|
77687
|
+
groupCountByKey.set(group.key, groupCount);
|
|
77539
77688
|
nodes.push(jsx(ListItemGroup, {
|
|
77540
77689
|
label: group.label,
|
|
77541
77690
|
labelProps: group.labelProps,
|
|
77542
77691
|
children: group.children
|
|
77543
|
-
}, `${ownerId}_group_${group.key}`));
|
|
77692
|
+
}, groupCount === 1 ? `${ownerId}_group_${group.key}` : `${ownerId}_group_${group.key}_${groupCount}`));
|
|
77544
77693
|
group = null;
|
|
77545
77694
|
};
|
|
77695
|
+
// An item not held has no group of its own. Between two items of the open
|
|
77696
|
+
// group it is a line of that group whose content has not arrived: drawn
|
|
77697
|
+
// outside, it would cut the group in two and show its label twice. A hole at
|
|
77698
|
+
// a group's edge stays outside — nothing says which side it belongs to.
|
|
77699
|
+
let holeTo = -1;
|
|
77700
|
+
let holeGroupKey;
|
|
77701
|
+
const groupKeyOfHole = itemIndex => {
|
|
77702
|
+
if (itemIndex < holeTo) {
|
|
77703
|
+
return holeGroupKey;
|
|
77704
|
+
}
|
|
77705
|
+
holeTo = itemIndex + 1;
|
|
77706
|
+
while (holeTo < windowTo && holeTo !== failureFrom && getItemAt(holeTo) === undefined) {
|
|
77707
|
+
holeTo++;
|
|
77708
|
+
}
|
|
77709
|
+
holeGroupKey = undefined;
|
|
77710
|
+
if (group && holeTo < windowTo && holeTo !== failureFrom) {
|
|
77711
|
+
const keyAfterHole = groupBy(getItemAt(holeTo), holeTo);
|
|
77712
|
+
if (keyAfterHole === group.key) {
|
|
77713
|
+
holeGroupKey = keyAfterHole;
|
|
77714
|
+
}
|
|
77715
|
+
}
|
|
77716
|
+
return holeGroupKey;
|
|
77717
|
+
};
|
|
77546
77718
|
// Which group an item belongs to, or undefined when it belongs to none.
|
|
77547
|
-
const groupKeyOf = (item, itemIndex) =>
|
|
77719
|
+
const groupKeyOf = (item, itemIndex) => {
|
|
77720
|
+
if (!groupBy) {
|
|
77721
|
+
return undefined;
|
|
77722
|
+
}
|
|
77723
|
+
if (item === undefined) {
|
|
77724
|
+
return groupKeyOfHole(itemIndex);
|
|
77725
|
+
}
|
|
77726
|
+
return groupBy(item, itemIndex);
|
|
77727
|
+
};
|
|
77548
77728
|
const pushItem = (itemNode, item, itemIndex, groupKey) => {
|
|
77549
77729
|
if (groupKey === undefined) {
|
|
77550
77730
|
closeGroup();
|
|
@@ -77601,6 +77781,7 @@ const useRunItems = (store, {
|
|
|
77601
77781
|
nodes.push(jsx(VirtualFiller, {
|
|
77602
77782
|
edge: "before",
|
|
77603
77783
|
itemCount: windowFrom - runStart,
|
|
77784
|
+
itemsPerLine: itemsPerLine,
|
|
77604
77785
|
findChunks: getFindChunks(runStart, windowFrom)
|
|
77605
77786
|
}, "navi-list-filler-before"));
|
|
77606
77787
|
}
|
|
@@ -77615,7 +77796,7 @@ const useRunItems = (store, {
|
|
|
77615
77796
|
nodes.push(jsx("li", {
|
|
77616
77797
|
className: "navi_list_failed_items",
|
|
77617
77798
|
style: {
|
|
77618
|
-
"--size-to-fill": `${failedItemCount * listItems.virtualItemSizeSignal.value}px`
|
|
77799
|
+
"--size-to-fill": `${Math.ceil(failedItemCount / itemsPerLine) * listItems.virtualItemSizeSignal.value}px`
|
|
77619
77800
|
},
|
|
77620
77801
|
children: renderError ? renderError({
|
|
77621
77802
|
error: store.failure.error,
|
|
@@ -77705,6 +77886,7 @@ const useRunItems = (store, {
|
|
|
77705
77886
|
nodes.push(jsx(VirtualFiller, {
|
|
77706
77887
|
edge: "after",
|
|
77707
77888
|
itemCount: runEnd - windowTo,
|
|
77889
|
+
itemsPerLine: itemsPerLine,
|
|
77708
77890
|
findChunks: getFindChunks(windowTo, runEnd)
|
|
77709
77891
|
}, "navi-list-filler-after"));
|
|
77710
77892
|
}
|
|
@@ -77879,10 +78061,15 @@ const useItemStore = ({
|
|
|
77879
78061
|
}
|
|
77880
78062
|
const pages = pagesRef.current;
|
|
77881
78063
|
const [, setPageVersion] = useState(0);
|
|
77882
|
-
// The
|
|
77883
|
-
//
|
|
77884
|
-
//
|
|
77885
|
-
|
|
78064
|
+
// The ranks whose item is from before: the run came back to the screen, or
|
|
78065
|
+
// the source said the collection moved. They stay drawn until a page confirms
|
|
78066
|
+
// or replaces them, rank by rank. A page covers what the window framed when it
|
|
78067
|
+
// was asked for, and the window keeps growing meanwhile: dropping what the
|
|
78068
|
+
// page did not cover empties items on screen.
|
|
78069
|
+
const staleRanksRef = useRef(null);
|
|
78070
|
+
if (staleRanksRef.current === null) {
|
|
78071
|
+
staleRanksRef.current = restored ? new Set(pages.byIndex.keys()) : new Set();
|
|
78072
|
+
}
|
|
77886
78073
|
const [refreshing, setRefreshing] = useState(false);
|
|
77887
78074
|
// A source that says when what it reads has moved (a resource range reader
|
|
77888
78075
|
// does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
|
|
@@ -77893,7 +78080,7 @@ const useItemStore = ({
|
|
|
77893
78080
|
const invalidationRef = useRef(invalidation);
|
|
77894
78081
|
if (invalidationRef.current !== invalidation) {
|
|
77895
78082
|
invalidationRef.current = invalidation;
|
|
77896
|
-
|
|
78083
|
+
staleRanksRef.current = new Set(pages.byIndex.keys());
|
|
77897
78084
|
}
|
|
77898
78085
|
// The one request in flight, with the means to call it off: a page asked for
|
|
77899
78086
|
// a window the list has left is work the server and the browser are doing for
|
|
@@ -77926,11 +78113,6 @@ const useItemStore = ({
|
|
|
77926
78113
|
// It stands for a page of them: a list that is about to be filled looks
|
|
77927
78114
|
// like items on their way, not like an empty list.
|
|
77928
78115
|
const itemCount = pages.count ?? count ?? listItems.pageSize;
|
|
77929
|
-
// A run that never received anything has nothing to keep on screen: asking
|
|
77930
|
-
// again is its first ask, not a refresh.
|
|
77931
|
-
if (staleRef.current && pages.count === undefined) {
|
|
77932
|
-
staleRef.current = false;
|
|
77933
|
-
}
|
|
77934
78116
|
useLayoutEffect(() => {
|
|
77935
78117
|
if (!refreshing) {
|
|
77936
78118
|
return null;
|
|
@@ -78003,9 +78185,11 @@ const useItemStore = ({
|
|
|
78003
78185
|
const keepFrom = windowFrom - ITEM_STORE_KEEP_AROUND;
|
|
78004
78186
|
const keepTo = windowTo + ITEM_STORE_KEEP_AROUND;
|
|
78005
78187
|
if (pages.byIndex.size > budget) {
|
|
78188
|
+
const staleRanks = staleRanksRef.current;
|
|
78006
78189
|
for (const index of pages.byIndex.keys()) {
|
|
78007
78190
|
if (index < keepFrom || index > keepTo) {
|
|
78008
78191
|
pages.byIndex.delete(index);
|
|
78192
|
+
staleRanks.delete(index);
|
|
78009
78193
|
}
|
|
78010
78194
|
}
|
|
78011
78195
|
}
|
|
@@ -78040,10 +78224,24 @@ const useItemStore = ({
|
|
|
78040
78224
|
let start = missingStart;
|
|
78041
78225
|
let end = missingEnd;
|
|
78042
78226
|
let around;
|
|
78043
|
-
//
|
|
78044
|
-
//
|
|
78045
|
-
|
|
78046
|
-
|
|
78227
|
+
// The window draws items from before: they are asked for again, and stay
|
|
78228
|
+
// drawn meanwhile.
|
|
78229
|
+
const staleRanks = staleRanksRef.current;
|
|
78230
|
+
let staleFrom = -1;
|
|
78231
|
+
let staleTo = -1;
|
|
78232
|
+
if (staleRanks.size > 0) {
|
|
78233
|
+
let rank = windowFrom;
|
|
78234
|
+
while (rank < windowTo) {
|
|
78235
|
+
if (staleRanks.has(rank)) {
|
|
78236
|
+
if (staleFrom === -1) {
|
|
78237
|
+
staleFrom = rank;
|
|
78238
|
+
}
|
|
78239
|
+
staleTo = rank;
|
|
78240
|
+
}
|
|
78241
|
+
rank++;
|
|
78242
|
+
}
|
|
78243
|
+
}
|
|
78244
|
+
const revalidating = staleFrom !== -1;
|
|
78047
78245
|
// The list is held on an item nothing on screen leads to: the items it holds
|
|
78048
78246
|
// do not contain it, so no window it could draw will ever bring it. Only
|
|
78049
78247
|
// asking for it by name does.
|
|
@@ -78060,18 +78258,36 @@ const useItemStore = ({
|
|
|
78060
78258
|
start = from < 0 ? 0 : from;
|
|
78061
78259
|
end = start + budget - 1;
|
|
78062
78260
|
} else if (revalidating) {
|
|
78063
|
-
|
|
78064
|
-
|
|
78065
|
-
if (
|
|
78066
|
-
|
|
78067
|
-
|
|
78068
|
-
|
|
78069
|
-
|
|
78070
|
-
|
|
78071
|
-
|
|
78072
|
-
|
|
78073
|
-
|
|
78074
|
-
|
|
78261
|
+
// The items from before that the window draws, and the missing ones
|
|
78262
|
+
// with them: one ask.
|
|
78263
|
+
if (start === -1 || staleFrom < start) {
|
|
78264
|
+
start = staleFrom;
|
|
78265
|
+
}
|
|
78266
|
+
if (staleTo > end) {
|
|
78267
|
+
end = staleTo;
|
|
78268
|
+
}
|
|
78269
|
+
// Grown over the items from before on either side, up to a page: the
|
|
78270
|
+
// window grows past its first picture right after it is painted, and
|
|
78271
|
+
// what it grows onto would otherwise be asked for once more.
|
|
78272
|
+
while (end - start + 1 < budget) {
|
|
78273
|
+
const staleBefore = staleRanks.has(start - 1);
|
|
78274
|
+
const staleAfter = staleRanks.has(end + 1);
|
|
78275
|
+
if (!staleBefore && !staleAfter) {
|
|
78276
|
+
break;
|
|
78277
|
+
}
|
|
78278
|
+
if (staleBefore) {
|
|
78279
|
+
start--;
|
|
78280
|
+
}
|
|
78281
|
+
if (staleAfter && end - start + 1 < budget) {
|
|
78282
|
+
end++;
|
|
78283
|
+
}
|
|
78284
|
+
}
|
|
78285
|
+
// Anchored on the first item from before on screen: a source
|
|
78286
|
+
// paginating by cursor gets an item to count from, and the reading
|
|
78287
|
+
// position is what must survive.
|
|
78288
|
+
const firstStale = pages.byIndex.get(staleFrom);
|
|
78289
|
+
if (firstStale && firstStale.id !== undefined) {
|
|
78290
|
+
around = firstStale.id;
|
|
78075
78291
|
}
|
|
78076
78292
|
} else if (pages.count === undefined) {
|
|
78077
78293
|
const scrolled = listItems.scrolled;
|
|
@@ -78125,7 +78341,8 @@ const useItemStore = ({
|
|
|
78125
78341
|
debugAsk("already revalidating");
|
|
78126
78342
|
return;
|
|
78127
78343
|
}
|
|
78128
|
-
// A page for
|
|
78344
|
+
// A page asked for before the items went stale answers for the
|
|
78345
|
+
// collection as it was.
|
|
78129
78346
|
request.controller?.abort();
|
|
78130
78347
|
request.busy = false;
|
|
78131
78348
|
}
|
|
@@ -78146,11 +78363,11 @@ const useItemStore = ({
|
|
|
78146
78363
|
request.controller?.abort();
|
|
78147
78364
|
request.busy = false;
|
|
78148
78365
|
}
|
|
78149
|
-
const held = pages.byIndex.size;
|
|
78150
78366
|
// Asking again for a range that was already asked for, having received
|
|
78151
|
-
// nothing since, can only produce the same answer
|
|
78152
|
-
//
|
|
78153
|
-
|
|
78367
|
+
// nothing since, can only produce the same answer — unless the
|
|
78368
|
+
// collection moved since, which is what an invalidation says.
|
|
78369
|
+
const held = `${pages.byIndex.size}/${staleRanks.size}/${invalidation}`;
|
|
78370
|
+
if (request.start === start && request.end === end && request.held === held) {
|
|
78154
78371
|
debugAsk("this range was asked for already");
|
|
78155
78372
|
return;
|
|
78156
78373
|
}
|
|
@@ -78186,7 +78403,6 @@ const useItemStore = ({
|
|
|
78186
78403
|
request.revalidating = false;
|
|
78187
78404
|
setFailure(null);
|
|
78188
78405
|
if (revalidating) {
|
|
78189
|
-
staleRef.current = false;
|
|
78190
78406
|
setRefreshing(false);
|
|
78191
78407
|
}
|
|
78192
78408
|
publishRequestState();
|
|
@@ -78205,15 +78421,35 @@ const useItemStore = ({
|
|
|
78205
78421
|
// Before the items land: what is on screen has to stay where it is,
|
|
78206
78422
|
// and the DOM still shows the state to hold onto.
|
|
78207
78423
|
listItems.captureAnchor();
|
|
78208
|
-
|
|
78209
|
-
|
|
78210
|
-
|
|
78211
|
-
|
|
78212
|
-
|
|
78424
|
+
// An item from before is gone from where it stood when the page puts
|
|
78425
|
+
// it at another rank (it would be drawn twice), or when its rank is
|
|
78426
|
+
// past the end of the collection.
|
|
78427
|
+
const staleRanksNow = staleRanksRef.current;
|
|
78428
|
+
const pageEnd = pageStart + pageItems.length;
|
|
78429
|
+
let dropped = false;
|
|
78430
|
+
if (staleRanksNow.size > 0) {
|
|
78431
|
+
const pageIds = new Set();
|
|
78432
|
+
for (const pageItem of pageItems) {
|
|
78433
|
+
if (pageItem && pageItem.id !== undefined) {
|
|
78434
|
+
pageIds.add(pageItem.id);
|
|
78435
|
+
}
|
|
78436
|
+
}
|
|
78437
|
+
for (const rank of staleRanksNow) {
|
|
78438
|
+
if (rank >= pageStart && rank < pageEnd) {
|
|
78439
|
+
continue;
|
|
78440
|
+
}
|
|
78441
|
+
const staleItem = pages.byIndex.get(rank);
|
|
78442
|
+
if (rank >= pageCount || staleItem && pageIds.has(staleItem.id)) {
|
|
78443
|
+
staleRanksNow.delete(rank);
|
|
78444
|
+
pages.byIndex.delete(rank);
|
|
78445
|
+
dropped = true;
|
|
78446
|
+
}
|
|
78447
|
+
}
|
|
78213
78448
|
}
|
|
78214
78449
|
let i = 0;
|
|
78215
78450
|
while (i < pageItems.length) {
|
|
78216
78451
|
pages.byIndex.set(pageStart + i, pageItems[i]);
|
|
78452
|
+
staleRanksNow.delete(pageStart + i);
|
|
78217
78453
|
i++;
|
|
78218
78454
|
}
|
|
78219
78455
|
pages.count = pageCount;
|
|
@@ -78223,7 +78459,7 @@ const useItemStore = ({
|
|
|
78223
78459
|
itemsAction.writeComposition({
|
|
78224
78460
|
byIndex: pages.byIndex,
|
|
78225
78461
|
count: pageCount,
|
|
78226
|
-
replace:
|
|
78462
|
+
replace: dropped
|
|
78227
78463
|
});
|
|
78228
78464
|
}
|
|
78229
78465
|
listItems.pagesSignal.value = listItems.pagesSignal.peek() + 1;
|
|
@@ -78238,9 +78474,14 @@ const useItemStore = ({
|
|
|
78238
78474
|
request.revalidating = false;
|
|
78239
78475
|
publishRequestState();
|
|
78240
78476
|
if (revalidating) {
|
|
78241
|
-
// The items from before stay
|
|
78477
|
+
// The items from before stay, and are not asked for again until the
|
|
78478
|
+
// source says the collection moved: a revalidation that failed has
|
|
78242
78479
|
// nothing better to put in their place.
|
|
78243
|
-
|
|
78480
|
+
let rank = start;
|
|
78481
|
+
while (rank <= end) {
|
|
78482
|
+
staleRanksRef.current.delete(rank);
|
|
78483
|
+
rank++;
|
|
78484
|
+
}
|
|
78244
78485
|
setRefreshing(false);
|
|
78245
78486
|
return;
|
|
78246
78487
|
}
|
|
@@ -78437,12 +78678,16 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
78437
78678
|
* @param {string} [props.columns]
|
|
78438
78679
|
* The list's own columns: a `grid-template-columns` value the ITEMS are laid
|
|
78439
78680
|
* into — a sheet of icons (`repeat(auto-fill, minmax(2.5rem, 1fr))`), a row of
|
|
78440
|
-
* choices (`repeat(3, minmax(0, 1fr))`)
|
|
78441
|
-
*
|
|
78442
|
-
*
|
|
78443
|
-
*
|
|
78444
|
-
*
|
|
78445
|
-
*
|
|
78681
|
+
* choices (`repeat(3, minmax(0, 1fr))`), a feed of covers
|
|
78682
|
+
* (`repeat(auto-fill, minmax(150px, 1fr))` over a `<List.Items>`). Each item
|
|
78683
|
+
* takes one cell, and an item meant to take a whole line says so for itself
|
|
78684
|
+
* (`style={{ gridColumn: "1 / -1" }}`). A `<List.Items>` run is drawn in whole
|
|
78685
|
+
* lines — as many items to a line as the template resolves to, read again
|
|
78686
|
+
* when the list's width changes: its window starts and ends on a line, so an
|
|
78687
|
+
* item is always in the column its place says, and the fillers hold the room
|
|
78688
|
+
* of the lines it does not draw (`virtualItemSize` is then a line's size).
|
|
78689
|
+
* That count holds only while every item of the run takes one cell: an item
|
|
78690
|
+
* spanning a line, or a `groupBy`, puts the ones after it in other columns.
|
|
78446
78691
|
* @param {string} [props.itemColumns]
|
|
78447
78692
|
* The columns inside an ITEM: a `grid-template-columns` value each item fills
|
|
78448
78693
|
* with its own children, a table whose cells line up down the list. Every item
|
|
@@ -78458,8 +78703,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
78458
78703
|
* scroll box, which fills it and has nothing to arrange.
|
|
78459
78704
|
* @param {boolean} [props.flexWrap]
|
|
78460
78705
|
* Lets a horizontal list's items fall to the next line instead of running
|
|
78461
|
-
* past the edge — a row of choices under a `maxWidth`, say.
|
|
78462
|
-
*
|
|
78706
|
+
* past the edge — a row of choices under a `maxWidth`, say. A wrapped line
|
|
78707
|
+
* holding several items is not virtualizable: a long collection laid several
|
|
78708
|
+
* to a line goes in `columns`.
|
|
78463
78709
|
* @param {string} [props.overflow]
|
|
78464
78710
|
* `"visible"` lets the items paint outside the list — a check in an item's
|
|
78465
78711
|
* corner, a badge crossing the edge. A list clips by default, which is what
|
|
@@ -78578,7 +78824,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
78578
78824
|
* one-line size) — an item guessed too small builds one more, an item
|
|
78579
78825
|
* guessed too big leaves a blank (see docs/scroll.md, "What the list knows,
|
|
78580
78826
|
* and what it guesses"). The items drawn are measured either way: the render
|
|
78581
|
-
* window is sized on them.
|
|
78827
|
+
* window is sized on them. In a grid (`columns`), the size is a line's: a
|
|
78828
|
+
* card's, which the items side by side on it share.
|
|
78582
78829
|
* @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
|
|
78583
78830
|
* Which box scrolls — and with it, which box the render window follows and
|
|
78584
78831
|
* which box a scroll position is read from (`onScrolledChange`). `"self"`
|