@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/jsenv_navi.js
CHANGED
|
@@ -16238,9 +16238,9 @@ const armOutsidePressClose = (
|
|
|
16238
16238
|
* `font-family` is deliberately absent: a surface keeps the face of what
|
|
16239
16239
|
* opened it, so a section written in a display font gets its tooltips in that
|
|
16240
16240
|
* font. `color`, `font-size` and `font-weight` are absent too — each surface
|
|
16241
|
-
* answers those its own way (a popup
|
|
16242
|
-
* the size of what opened it
|
|
16243
|
-
* size).
|
|
16241
|
+
* answers those its own way (a popup and a callout write in
|
|
16242
|
+
* --navi-popup-color; a popup follows the size of what opened it, a callout
|
|
16243
|
+
* resets to the document's size).
|
|
16244
16244
|
*
|
|
16245
16245
|
* In `@layer navi` so an app that wants one of these back says so on the
|
|
16246
16246
|
* surface and wins, without having to out-specify anything.
|
|
@@ -16470,8 +16470,7 @@ const css$14 = /* css */ `
|
|
|
16470
16470
|
--callout-error-color: var(--navi-callout-error-color);
|
|
16471
16471
|
--callout-neutral-color: var(--navi-callout-neutral-color);
|
|
16472
16472
|
|
|
16473
|
-
--callout-background-color:
|
|
16474
|
-
--callout-icon-color: black;
|
|
16473
|
+
--callout-background-color: var(--navi-popup-background-color);
|
|
16475
16474
|
--callout-padding: 8px;
|
|
16476
16475
|
--callout-z-index: var(--navi-z-index-callout);
|
|
16477
16476
|
/* The page's line, said here rather than inherited: the icon and the
|
|
@@ -16525,11 +16524,11 @@ const css$14 = /* css */ `
|
|
|
16525
16524
|
height: auto; /* User agent reset */
|
|
16526
16525
|
margin: 0;
|
|
16527
16526
|
padding: 0; /* User agent reset */
|
|
16528
|
-
/*
|
|
16529
|
-
|
|
16530
|
-
color
|
|
16531
|
-
navi_css_vars.js
|
|
16532
|
-
color:
|
|
16527
|
+
/* A callout sits inside the element but writes on its own paper: the
|
|
16528
|
+
popup's ink, paired with the popup's paper it is drawn on, so the two
|
|
16529
|
+
follow color-scheme and the theme together. The color keywords are
|
|
16530
|
+
re-declared against that same ink in navi_css_vars.js. */
|
|
16531
|
+
color: var(--navi-popup-color);
|
|
16533
16532
|
font-weight: initial; /* Callout fells disconnected from the element, font weight should be predictible and stable */
|
|
16534
16533
|
font-size: initial; /* Callout fells disconnected from the element, font size should be predictible and stable */
|
|
16535
16534
|
line-height: var(
|
|
@@ -18752,16 +18751,18 @@ const findBusySource = (field) => {
|
|
|
18752
18751
|
// a new interaction is queued behind the run (see the action queue in
|
|
18753
18752
|
// control_hooks.jsx) rather than refused.
|
|
18754
18753
|
if (!field.optimistic && boundAction) {
|
|
18755
|
-
// The
|
|
18756
|
-
//
|
|
18757
|
-
//
|
|
18758
|
-
//
|
|
18759
|
-
//
|
|
18760
|
-
//
|
|
18761
|
-
//
|
|
18762
|
-
//
|
|
18763
|
-
|
|
18764
|
-
|
|
18754
|
+
// The instance the run in flight uses, as the render reads it (see
|
|
18755
|
+
// runningActionSignal in control_hooks.jsx): the state a proxy follows can
|
|
18756
|
+
// move mid-run — a popup content unmounting takes the group's value — and
|
|
18757
|
+
// the proxy would resolve to an idle instance. With no run out, the
|
|
18758
|
+
// instance the proxy resolves to right now, never the proxy's own signal:
|
|
18759
|
+
// a MIRROR synced by an effect the settling batch defers, which still says
|
|
18760
|
+
// RUNNING mid-batch (a state echo, an automatic follow-up) for an action
|
|
18761
|
+
// already over. An instance is COMPLETED the moment it settles.
|
|
18762
|
+
const liveAction =
|
|
18763
|
+
field.runningActionSignal.value ??
|
|
18764
|
+
boundAction.getCurrentAction?.() ??
|
|
18765
|
+
boundAction;
|
|
18765
18766
|
if (liveAction.runningStateSignal.value === RUNNING) {
|
|
18766
18767
|
return { field, action: liveAction };
|
|
18767
18768
|
}
|
|
@@ -19799,11 +19800,10 @@ const READONLY_CONSTRAINT = {
|
|
|
19799
19800
|
name: "readonly",
|
|
19800
19801
|
messageAttribute: "data-readonly-message",
|
|
19801
19802
|
check: (field, { intent } = {}) => {
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
19805
|
-
)
|
|
19806
|
-
if (!readOnly) {
|
|
19803
|
+
// Not off the rendered `readOnly`/`aria-readonly`: they also carry the run
|
|
19804
|
+
// of the control's own action, a frame behind (see readOnlyWithoutRun in
|
|
19805
|
+
// control_hooks.jsx). That run is BUSY_CONSTRAINT's, which reads it live.
|
|
19806
|
+
if (!field.readOnlyWithoutRun) {
|
|
19807
19807
|
return null;
|
|
19808
19808
|
}
|
|
19809
19809
|
|
|
@@ -25978,10 +25978,11 @@ const createRangeReader = (
|
|
|
25978
25978
|
});
|
|
25979
25979
|
return { byIndex, count: composition.count };
|
|
25980
25980
|
};
|
|
25981
|
-
// `replace`
|
|
25982
|
-
//
|
|
25983
|
-
// so two lists reading the same
|
|
25984
|
-
// to one composition instead of
|
|
25981
|
+
// `replace` says the list dropped ranks it held (an item the collection moved
|
|
25982
|
+
// elsewhere, ranks past its end): kept here, they would come back at the next
|
|
25983
|
+
// mount. Otherwise the ranks are merged, so two lists reading the same
|
|
25984
|
+
// collection through their own windows add up to one composition instead of
|
|
25985
|
+
// taking turns erasing each other.
|
|
25985
25986
|
readRange.writeComposition = ({ byIndex, count, replace }) => {
|
|
25986
25987
|
const params = currentParams();
|
|
25987
25988
|
let composition = findComposition(params);
|
|
@@ -42988,6 +42989,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
42988
42989
|
ref: realUIStateController.ref,
|
|
42989
42990
|
uiStateSignal: realUIStateController.uiStateSignal,
|
|
42990
42991
|
controlHostProps: realUIStateController.controlHostProps,
|
|
42992
|
+
readOnlyWithoutRun: realUIStateController.readOnlyWithoutRun,
|
|
42991
42993
|
registerChild: (child) => {
|
|
42992
42994
|
if (!canRegisterAsFacadeChild(child)) {
|
|
42993
42995
|
return;
|
|
@@ -43116,6 +43118,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
43116
43118
|
s.controller.ref = realUIStateController.ref;
|
|
43117
43119
|
s.controller.uiStateSignal = realUIStateController.uiStateSignal;
|
|
43118
43120
|
s.controller.controlHostProps = realUIStateController.controlHostProps;
|
|
43121
|
+
s.controller.readOnlyWithoutRun =
|
|
43122
|
+
realUIStateController.readOnlyWithoutRun;
|
|
43119
43123
|
realUIStateController.pushStateDownToFacadeChild =
|
|
43120
43124
|
s.controller.pushStateDownToChild;
|
|
43121
43125
|
|
|
@@ -44922,6 +44926,11 @@ const useInteractiveProps = (props, {
|
|
|
44922
44926
|
const actionLoading = optimistic ? false : actionRunning;
|
|
44923
44927
|
const loadingResolved = loadingBase || actionLoading;
|
|
44924
44928
|
const readOnlyResolved = readOnlyBase || actionLoading;
|
|
44929
|
+
// What READONLY_CONSTRAINT refuses for. The run of the control's own action
|
|
44930
|
+
// is BUSY_CONSTRAINT's, read live: readOnlyResolved is a render snapshot,
|
|
44931
|
+
// still read-only in the tick the run settles — the tick a form's
|
|
44932
|
+
// after-send `--navi-reset` asks the form to empty itself.
|
|
44933
|
+
uiStateController.readOnlyWithoutRun = Boolean(readOnlyBase);
|
|
44925
44934
|
// Read-only, and what this control opens still opens: reading what is in
|
|
44926
44935
|
// there changes nothing. Read by READONLY_CONSTRAINT, which lets an
|
|
44927
44936
|
// interaction that only reads through on it.
|
|
@@ -45374,10 +45383,10 @@ const useInteractiveProps = (props, {
|
|
|
45374
45383
|
});
|
|
45375
45384
|
}
|
|
45376
45385
|
// controlHostProps is a curated subset of props with resolved values applied
|
|
45377
|
-
// (e.g. readOnly resolved from context + action loading). The
|
|
45378
|
-
//
|
|
45379
|
-
//
|
|
45380
|
-
//
|
|
45386
|
+
// (e.g. readOnly resolved from context + action loading). The constraints
|
|
45387
|
+
// read off uiStateController.controlHostProps at runtime (e.g. DISABLED_CONSTRAINT,
|
|
45388
|
+
// the readonly reason), so pointing the controller at controlHostProps keeps
|
|
45389
|
+
// those reads current without any extra bookkeeping.
|
|
45381
45390
|
const firstRender = uiStateController.controlHostProps === undefined;
|
|
45382
45391
|
uiStateController.controlHostProps = controlHostProps;
|
|
45383
45392
|
// The action itself, not just what the last render made of it: its running
|
|
@@ -54421,14 +54430,34 @@ const ButtonConfirm = ({
|
|
|
54421
54430
|
}
|
|
54422
54431
|
return jsxs("span", {
|
|
54423
54432
|
className: "navi_button_confirm",
|
|
54424
|
-
ref: askingRef
|
|
54433
|
+
ref: askingRef
|
|
54434
|
+
// A place for the focus, so a press anywhere in the question keeps it
|
|
54435
|
+
// inside: WebKit does not focus a pressed button, and a press on
|
|
54436
|
+
// "Confirmer" would otherwise blur toward <body> — a focusout with no
|
|
54437
|
+
// relatedTarget, read below as the person leaving. The callout answers
|
|
54438
|
+
// the same ambiguity the same way.
|
|
54439
|
+
,
|
|
54440
|
+
tabIndex: -1
|
|
54441
|
+
// "not me, unless you have nothing else": the ladder in the effect above
|
|
54442
|
+
// walks past the question to what it holds.
|
|
54443
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
54444
|
+
,
|
|
54445
|
+
"navi-autofocus": "last-resort",
|
|
54425
54446
|
onKeyDown: e => {
|
|
54426
54447
|
if (e.key === "Escape") {
|
|
54427
54448
|
cancel(true);
|
|
54428
54449
|
}
|
|
54429
54450
|
},
|
|
54430
54451
|
onFocusOut: e => {
|
|
54431
|
-
|
|
54452
|
+
// Chrome also fires this from the question's own removal, once preact
|
|
54453
|
+
// has cleared the ref: nobody left, and cancel(false) would undo the
|
|
54454
|
+
// focus the answer gives back. That focus cannot go back before the
|
|
54455
|
+
// removal: the button it lands on is mounted by the same commit.
|
|
54456
|
+
const askingEl = askingRef.current;
|
|
54457
|
+
if (!askingEl) {
|
|
54458
|
+
return;
|
|
54459
|
+
}
|
|
54460
|
+
if (!askingEl.contains(e.relatedTarget)) {
|
|
54432
54461
|
cancel(false);
|
|
54433
54462
|
}
|
|
54434
54463
|
},
|
|
@@ -71864,7 +71893,7 @@ const css$x = /* css */`@property --list-group-label-height {
|
|
|
71864
71893
|
}
|
|
71865
71894
|
}
|
|
71866
71895
|
|
|
71867
|
-
.navi_list[navi-box-flow="grid"] > .navi_list_virtual_filler, .navi_list[navi-box-flow="grid"] > .navi_separator {
|
|
71896
|
+
.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 {
|
|
71868
71897
|
grid-column: 1 / -1;
|
|
71869
71898
|
}
|
|
71870
71899
|
|
|
@@ -72241,7 +72270,8 @@ const ListUI = props => {
|
|
|
72241
72270
|
listId: rest.id,
|
|
72242
72271
|
scroller,
|
|
72243
72272
|
searchText,
|
|
72244
|
-
horizontal
|
|
72273
|
+
horizontal,
|
|
72274
|
+
columns
|
|
72245
72275
|
});
|
|
72246
72276
|
|
|
72247
72277
|
// renderBudget frames the items of a run; a list whose items are all declared
|
|
@@ -72556,7 +72586,8 @@ const useListScrollSync = ({
|
|
|
72556
72586
|
listId,
|
|
72557
72587
|
scroller,
|
|
72558
72588
|
searchText,
|
|
72559
|
-
horizontal
|
|
72589
|
+
horizontal,
|
|
72590
|
+
columns
|
|
72560
72591
|
}) => {
|
|
72561
72592
|
const debugScroll = useDebugScroll();
|
|
72562
72593
|
// The items drawn, [start, end) among the list's own. A ref as well as a
|
|
@@ -72571,6 +72602,7 @@ const useListScrollSync = ({
|
|
|
72571
72602
|
return renderWindow.start > 0 || renderWindow.end < listItems.totalSignal.peek();
|
|
72572
72603
|
};
|
|
72573
72604
|
const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
|
|
72605
|
+
inLines: Boolean(columns),
|
|
72574
72606
|
windowLeavesItemsOut,
|
|
72575
72607
|
scrolledWanted: scrolled ?? defaultScrolled,
|
|
72576
72608
|
// The fillers hold the room of items above the screen at this size: a
|
|
@@ -72639,6 +72671,34 @@ const useListScrollSync = ({
|
|
|
72639
72671
|
});
|
|
72640
72672
|
};
|
|
72641
72673
|
|
|
72674
|
+
// How many items share a line: the tracks a grid of items (`columns`)
|
|
72675
|
+
// resolves to, one otherwise. A layout result under auto-fill, so it is read
|
|
72676
|
+
// off the list once laid out, at each commit and each resize — for the runs
|
|
72677
|
+
// alone, which draw in lines: items declared one by one are all drawn. A
|
|
72678
|
+
// line of another length rewraps every item drawn: the item at the top of
|
|
72679
|
+
// the view is held where it is across it.
|
|
72680
|
+
const itemsPerLineRef = useRef(1);
|
|
72681
|
+
const [itemsPerLine, setItemsPerLine] = useState(1);
|
|
72682
|
+
const updateItemsPerLine = () => {
|
|
72683
|
+
if (!ref.current) {
|
|
72684
|
+
return;
|
|
72685
|
+
}
|
|
72686
|
+
const itemsPerLineNow = columns && listItems.hasRuns() ? readItemsPerLine(getListEl()) : 1;
|
|
72687
|
+
if (itemsPerLineNow === null || itemsPerLineNow === itemsPerLineRef.current) {
|
|
72688
|
+
return;
|
|
72689
|
+
}
|
|
72690
|
+
captureAnchor();
|
|
72691
|
+
itemsPerLineRef.current = itemsPerLineNow;
|
|
72692
|
+
setItemsPerLine(itemsPerLineNow);
|
|
72693
|
+
};
|
|
72694
|
+
const updateItemsPerLineRef = useRef(null);
|
|
72695
|
+
updateItemsPerLineRef.current = updateItemsPerLine;
|
|
72696
|
+
useLayoutEffect(() => {
|
|
72697
|
+
if (columns || itemsPerLineRef.current !== 1) {
|
|
72698
|
+
updateItemsPerLine();
|
|
72699
|
+
}
|
|
72700
|
+
});
|
|
72701
|
+
|
|
72642
72702
|
// What the window holds goes through three stages (see settleWindow):
|
|
72643
72703
|
// - "first": the first commit, a count of items since nothing is laid out
|
|
72644
72704
|
// yet to measure a size with — `initial` or the budget when they are one,
|
|
@@ -73037,6 +73097,7 @@ const useListScrollSync = ({
|
|
|
73037
73097
|
listEl: getListEl(),
|
|
73038
73098
|
listItems,
|
|
73039
73099
|
virtualItemSizeSignal,
|
|
73100
|
+
itemsPerLine: itemsPerLineRef.current,
|
|
73040
73101
|
renderWindowRef,
|
|
73041
73102
|
horizontal
|
|
73042
73103
|
});
|
|
@@ -73377,20 +73438,14 @@ const useListScrollSync = ({
|
|
|
73377
73438
|
}
|
|
73378
73439
|
const scrollerEl = getScroller();
|
|
73379
73440
|
const listEl = getListEl();
|
|
73380
|
-
|
|
73381
|
-
|
|
73382
|
-
|
|
73383
|
-
|
|
73384
|
-
|
|
73385
|
-
|
|
73386
|
-
|
|
73387
|
-
|
|
73388
|
-
// growing is its own content settling: only a list holding itself
|
|
73389
|
-
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
73390
|
-
// reading must not be touched — its items are held still by the anchoring,
|
|
73391
|
-
// which this would undo. The SCROLLER resizing is the window around it
|
|
73392
|
-
// changing shape, and then the item that was at the top goes back where it
|
|
73393
|
-
// was.
|
|
73441
|
+
// Two things resize here, and they call for opposite answers. The LIST
|
|
73442
|
+
// growing is its own content settling: only a list holding itself
|
|
73443
|
+
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
73444
|
+
// reading must not be touched — its items are held still by the anchoring,
|
|
73445
|
+
// which this would undo. The SCROLLER resizing is the window around it
|
|
73446
|
+
// changing shape, and then the item that was at the top goes back where it
|
|
73447
|
+
// was.
|
|
73448
|
+
const putTopItemBack = entries => {
|
|
73394
73449
|
const listResized = entries.some(entry => entry.target === listEl);
|
|
73395
73450
|
if (listResized && onGeometryChangeRef.current()) {
|
|
73396
73451
|
return;
|
|
@@ -73419,6 +73474,19 @@ const useListScrollSync = ({
|
|
|
73419
73474
|
} else {
|
|
73420
73475
|
scrollerEl.scrollTop += delta;
|
|
73421
73476
|
}
|
|
73477
|
+
};
|
|
73478
|
+
const observer = new ResizeObserver(entries => {
|
|
73479
|
+
// A box that grows may be a box that starts to scroll — the scroller is
|
|
73480
|
+
// resolved again before anything is done about the resize, so that a
|
|
73481
|
+
// list which has outgrown a bounded ancestor stops holding on to the
|
|
73482
|
+
// page.
|
|
73483
|
+
resolveScroller();
|
|
73484
|
+
rememberPosition();
|
|
73485
|
+
putTopItemBack(entries);
|
|
73486
|
+
// Once the top item is back where it was: that is the place a line of
|
|
73487
|
+
// another length has to keep, and putting it back drops any anchor
|
|
73488
|
+
// captured before.
|
|
73489
|
+
updateItemsPerLineRef.current();
|
|
73422
73490
|
});
|
|
73423
73491
|
observer.observe(scrollerEl);
|
|
73424
73492
|
observer.observe(listEl);
|
|
@@ -73574,7 +73642,10 @@ const useListScrollSync = ({
|
|
|
73574
73642
|
start,
|
|
73575
73643
|
end
|
|
73576
73644
|
} = renderWindowRef.current;
|
|
73577
|
-
|
|
73645
|
+
// What an item not drawn weighs: its share of a line, which the items side
|
|
73646
|
+
// by side on it take together.
|
|
73647
|
+
const itemsPerLineNow = itemsPerLineRef.current;
|
|
73648
|
+
const itemSize = virtualItemSizeSignal.peek() / itemsPerLineNow;
|
|
73578
73649
|
const pixelsOf = createItemSizeReader(items, start, end, itemSize);
|
|
73579
73650
|
const walkBefore = (from, sizeWanted, sizeOf) => {
|
|
73580
73651
|
let index = from;
|
|
@@ -73844,9 +73915,28 @@ const useListScrollSync = ({
|
|
|
73844
73915
|
ifChanged: true
|
|
73845
73916
|
});
|
|
73846
73917
|
}, [windowStart, windowEnd]);
|
|
73918
|
+
// What the runs draw: the window widened to whole lines. The window itself
|
|
73919
|
+
// moves an item at a time, and a grid lays the first item drawn in its first
|
|
73920
|
+
// column — started anywhere but on a line, every item drawn would sit in the
|
|
73921
|
+
// column of another. Started on one, an item is in the column its place says
|
|
73922
|
+
// whatever the window frames. The same object while the numbers hold, so the
|
|
73923
|
+
// runs are not told about a window that did not move.
|
|
73924
|
+
const windowDrawnRef = useRef(null);
|
|
73925
|
+
const drawnStart = windowStart - windowStart % itemsPerLine;
|
|
73926
|
+
const endPastLine = windowEnd % itemsPerLine;
|
|
73927
|
+
const drawnEnd = endPastLine === 0 ? windowEnd : windowEnd + itemsPerLine - endPastLine;
|
|
73928
|
+
let windowDrawn = windowDrawnRef.current;
|
|
73929
|
+
if (!windowDrawn || windowDrawn.start !== drawnStart || windowDrawn.end !== drawnEnd || windowDrawn.itemsPerLine !== itemsPerLine) {
|
|
73930
|
+
windowDrawn = {
|
|
73931
|
+
start: drawnStart,
|
|
73932
|
+
end: drawnEnd,
|
|
73933
|
+
itemsPerLine
|
|
73934
|
+
};
|
|
73935
|
+
windowDrawnRef.current = windowDrawn;
|
|
73936
|
+
}
|
|
73847
73937
|
return {
|
|
73848
73938
|
virtualItemSizeSignal,
|
|
73849
|
-
renderWindow:
|
|
73939
|
+
renderWindow: windowDrawn,
|
|
73850
73940
|
pendingScrollRef,
|
|
73851
73941
|
scrollToItem,
|
|
73852
73942
|
captureAnchor
|
|
@@ -73870,6 +73960,21 @@ const getScrollerViewportRect = scrollerEl => {
|
|
|
73870
73960
|
}
|
|
73871
73961
|
return scrollerEl.getBoundingClientRect();
|
|
73872
73962
|
};
|
|
73963
|
+
// The tracks a grid resolved its template to, as its computed
|
|
73964
|
+
// grid-template-columns lists them once laid out: "150px 150px [end] 150px".
|
|
73965
|
+
// Not laid out (a closed popup), that is the template as written, which says
|
|
73966
|
+
// nothing yet under auto-fill: `null`.
|
|
73967
|
+
const readItemsPerLine = listEl => {
|
|
73968
|
+
const tracks = getComputedStyle(listEl).gridTemplateColumns;
|
|
73969
|
+
if (tracks === "none" || tracks.includes("(")) {
|
|
73970
|
+
return null;
|
|
73971
|
+
}
|
|
73972
|
+
const sizes = tracks.replace(/\[[^\]]*\]/g, " ").trim();
|
|
73973
|
+
if (!sizes) {
|
|
73974
|
+
return null;
|
|
73975
|
+
}
|
|
73976
|
+
return sizes.split(/\s+/).length;
|
|
73977
|
+
};
|
|
73873
73978
|
// A window that cannot move is the one failure of a virtualized list that
|
|
73874
73979
|
// looks like nothing: the items outside it are fillers holding their room, so
|
|
73875
73980
|
// the list simply ends on blank space, at exactly the height of the items never
|
|
@@ -74672,6 +74777,7 @@ const getScrollInfo = ({
|
|
|
74672
74777
|
listEl,
|
|
74673
74778
|
listItems,
|
|
74674
74779
|
virtualItemSizeSignal,
|
|
74780
|
+
itemsPerLine,
|
|
74675
74781
|
renderWindowRef,
|
|
74676
74782
|
horizontal
|
|
74677
74783
|
}) => {
|
|
@@ -74709,7 +74815,9 @@ const getScrollInfo = ({
|
|
|
74709
74815
|
// mean we don't know the real on-screen index, only the scroll position,
|
|
74710
74816
|
// so estimate from it rather than assume nothing changed.
|
|
74711
74817
|
const estimateFromScrollPos = reasonPrefix => {
|
|
74712
|
-
|
|
74818
|
+
// An item's share of its line: the items side by side on it take it
|
|
74819
|
+
// together.
|
|
74820
|
+
const virtualItemSize = virtualItemSizeSignal.peek() / itemsPerLine;
|
|
74713
74821
|
if (virtualItemSize === 0) {
|
|
74714
74822
|
return null;
|
|
74715
74823
|
}
|
|
@@ -74765,8 +74873,10 @@ const getScrollInfo = ({
|
|
|
74765
74873
|
const VIRTUAL_ITEM_SIZE_EPSILON = 0.5;
|
|
74766
74874
|
// Measures the items currently in the DOM, edge to edge: what a filler stands in
|
|
74767
74875
|
// for is the room a run of items takes together — separators and group labels
|
|
74768
|
-
// included — not the height of one <li>.
|
|
74769
|
-
|
|
74876
|
+
// included — not the height of one <li>. The size of a line of them: one item
|
|
74877
|
+
// in a list, the items side by side in a grid (`inLines`), counted where a new
|
|
74878
|
+
// line starts.
|
|
74879
|
+
const measureItemSize = (listEl, horizontal, inLines) => {
|
|
74770
74880
|
let fromSkeletons = false;
|
|
74771
74881
|
let itemEls = listEl.querySelectorAll(REAL_LIST_ITEM_SELECTOR);
|
|
74772
74882
|
if (itemEls.length === 0) {
|
|
@@ -74788,13 +74898,27 @@ const measureItemSize = (listEl, horizontal) => {
|
|
|
74788
74898
|
if (span <= 0) {
|
|
74789
74899
|
return null;
|
|
74790
74900
|
}
|
|
74901
|
+
let lineCount = itemEls.length;
|
|
74902
|
+
if (inLines) {
|
|
74903
|
+
lineCount = 0;
|
|
74904
|
+
let lineFrom = -Infinity;
|
|
74905
|
+
for (const itemEl of itemEls) {
|
|
74906
|
+
const rect = itemEl.getBoundingClientRect();
|
|
74907
|
+
const from = horizontal ? rect.left : rect.top;
|
|
74908
|
+
if (from > lineFrom + 0.5) {
|
|
74909
|
+
lineCount++;
|
|
74910
|
+
lineFrom = from;
|
|
74911
|
+
}
|
|
74912
|
+
}
|
|
74913
|
+
}
|
|
74791
74914
|
return {
|
|
74792
|
-
size: span /
|
|
74793
|
-
|
|
74915
|
+
size: span / lineCount,
|
|
74916
|
+
lineCount,
|
|
74794
74917
|
fromSkeletons
|
|
74795
74918
|
};
|
|
74796
74919
|
};
|
|
74797
74920
|
const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
74921
|
+
inLines,
|
|
74798
74922
|
windowLeavesItemsOut,
|
|
74799
74923
|
scrolledWanted,
|
|
74800
74924
|
beforeSizeChange
|
|
@@ -74838,8 +74962,8 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
74838
74962
|
return;
|
|
74839
74963
|
}
|
|
74840
74964
|
}
|
|
74841
|
-
samples.sum += measure.size * measure.
|
|
74842
|
-
samples.count += measure.
|
|
74965
|
+
samples.sum += measure.size * measure.lineCount;
|
|
74966
|
+
samples.count += measure.lineCount;
|
|
74843
74967
|
const next = samples.sum / samples.count;
|
|
74844
74968
|
const current = virtualSizeSignal.peek();
|
|
74845
74969
|
if (Math.abs(next - current) > VIRTUAL_ITEM_SIZE_EPSILON) {
|
|
@@ -74859,7 +74983,7 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
74859
74983
|
const itemsHeldOffScreen = windowLeavesItemsOut();
|
|
74860
74984
|
if (!virtualItemSizeProp && sizeAlreadyKnown && itemsHeldOffScreen && ref.current) {
|
|
74861
74985
|
const listEl = ref.current.querySelector(".navi_list");
|
|
74862
|
-
const measure = listEl ? measureItemSize(listEl, horizontal) : null;
|
|
74986
|
+
const measure = listEl ? measureItemSize(listEl, horizontal, inLines) : null;
|
|
74863
74987
|
if (measure) {
|
|
74864
74988
|
feedSample(measure);
|
|
74865
74989
|
}
|
|
@@ -74880,11 +75004,11 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
74880
75004
|
if (!listEl) {
|
|
74881
75005
|
return undefined;
|
|
74882
75006
|
}
|
|
74883
|
-
const measure = measureItemSize(listEl, horizontal);
|
|
75007
|
+
const measure = measureItemSize(listEl, horizontal, inLines);
|
|
74884
75008
|
if (measure) {
|
|
74885
75009
|
const samples = samplesRef.current;
|
|
74886
|
-
samples.sum = measure.size * measure.
|
|
74887
|
-
samples.count = measure.
|
|
75010
|
+
samples.sum = measure.size * measure.lineCount;
|
|
75011
|
+
samples.count = measure.lineCount;
|
|
74888
75012
|
samples.fromSkeletons = measure.fromSkeletons;
|
|
74889
75013
|
virtualSizeSignal.value = measure.size;
|
|
74890
75014
|
return undefined;
|
|
@@ -75029,14 +75153,17 @@ const Fallback = ({
|
|
|
75029
75153
|
// Reads the item size itself: it is what the size is for, and a run holding
|
|
75030
75154
|
// every item it draws must not be redrawn — every item of it — because the size
|
|
75031
75155
|
// settled after the first commit.
|
|
75156
|
+
// It holds lines: the items side by side on one take its room together, and a
|
|
75157
|
+
// last line left short takes it all the same.
|
|
75032
75158
|
const VirtualFiller = ({
|
|
75033
75159
|
edge,
|
|
75034
75160
|
itemCount,
|
|
75161
|
+
itemsPerLine,
|
|
75035
75162
|
findChunks
|
|
75036
75163
|
}) => {
|
|
75037
75164
|
const listItems = useContext(ListItemsContext);
|
|
75038
|
-
const
|
|
75039
|
-
const sizeToFill = itemCount *
|
|
75165
|
+
const lineSize = listItems.virtualItemSizeSignal.value;
|
|
75166
|
+
const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize;
|
|
75040
75167
|
// A filler resizing moves what stands below it — the items on screen, when it
|
|
75041
75168
|
// holds the room of items above them — and it resizes in a commit of its own
|
|
75042
75169
|
// when the item size settles after the list has rendered: the list puts its
|
|
@@ -75055,7 +75182,7 @@ const VirtualFiller = ({
|
|
|
75055
75182
|
"aria-hidden": true,
|
|
75056
75183
|
style: {
|
|
75057
75184
|
"--size-to-fill": `${sizeToFill}px`,
|
|
75058
|
-
"--x-find-line-size": findChunks ? `${
|
|
75185
|
+
"--x-find-line-size": findChunks ? `${lineSize / itemsPerLine}px` : undefined
|
|
75059
75186
|
},
|
|
75060
75187
|
children: findChunks && findChunks.map(chunk => jsx("div", {
|
|
75061
75188
|
className: "navi_list_find_stand_in",
|
|
@@ -75935,6 +76062,9 @@ const useRunItems = (store, {
|
|
|
75935
76062
|
const rankOf = itemIndex => itemIndex - runStart;
|
|
75936
76063
|
const indexOfRank = rank => rank + runStart;
|
|
75937
76064
|
const getItemAt = itemIndex => store.getItem(rankOf(itemIndex));
|
|
76065
|
+
const {
|
|
76066
|
+
itemsPerLine
|
|
76067
|
+
} = renderWindow;
|
|
75938
76068
|
const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;
|
|
75939
76069
|
const windowTo = renderWindow.end < runEnd ? renderWindow.end : runEnd;
|
|
75940
76070
|
store.forget(rankOf(windowFrom), rankOf(windowTo));
|
|
@@ -76042,8 +76172,20 @@ const useRunItems = (store, {
|
|
|
76042
76172
|
// band is drawn among the items. A negative start is not a rank but a count
|
|
76043
76173
|
// back from the end (the very first ask, before the count is known) — there
|
|
76044
76174
|
// is no item to convert it to, and the band falls back to the window.
|
|
76045
|
-
|
|
76046
|
-
|
|
76175
|
+
let failureFrom = store.failure === null ? -1 : store.failure.start < 0 || indexOfRank(store.failure.start) < windowFrom ? windowFrom : indexOfRank(store.failure.start);
|
|
76176
|
+
let failureTo = store.failure === null ? -1 : store.failure.end < 0 || indexOfRank(store.failure.end) > windowTo - 1 ? windowTo - 1 : indexOfRank(store.failure.end);
|
|
76177
|
+
if (store.failure !== null && itemsPerLine > 1) {
|
|
76178
|
+
// In a grid the band takes whole lines, like the window: begun mid-line,
|
|
76179
|
+
// it would push every item after it into another column.
|
|
76180
|
+
failureFrom -= failureFrom % itemsPerLine;
|
|
76181
|
+
if (failureFrom < windowFrom) {
|
|
76182
|
+
failureFrom = windowFrom;
|
|
76183
|
+
}
|
|
76184
|
+
failureTo += itemsPerLine - 1 - failureTo % itemsPerLine;
|
|
76185
|
+
if (failureTo > windowTo - 1) {
|
|
76186
|
+
failureTo = windowTo - 1;
|
|
76187
|
+
}
|
|
76188
|
+
}
|
|
76047
76189
|
const nodes = [];
|
|
76048
76190
|
// Items that belong together, as the data says (a day of messages, a month of
|
|
76049
76191
|
// games): consecutive items sharing a group key are wrapped in one group, so
|
|
@@ -76051,19 +76193,57 @@ const useRunItems = (store, {
|
|
|
76051
76193
|
// rebuilt as the window slides — a group holds the items of its day that are
|
|
76052
76194
|
// currently drawn, which is exactly the span its label has to survive.
|
|
76053
76195
|
let group = null;
|
|
76196
|
+
// Two groups can still share a key (a failure band standing mid-group, data
|
|
76197
|
+
// not sorted by group): the later ones are told apart by their rank among
|
|
76198
|
+
// them, so the first keeps its element. A group keyed by where it starts
|
|
76199
|
+
// would lose its element each time the window slides past its first item.
|
|
76200
|
+
const groupCountByKey = new Map();
|
|
76054
76201
|
const closeGroup = () => {
|
|
76055
76202
|
if (!group) {
|
|
76056
76203
|
return;
|
|
76057
76204
|
}
|
|
76205
|
+
const groupCount = (groupCountByKey.get(group.key) || 0) + 1;
|
|
76206
|
+
groupCountByKey.set(group.key, groupCount);
|
|
76058
76207
|
nodes.push(jsx(ListItemGroup, {
|
|
76059
76208
|
label: group.label,
|
|
76060
76209
|
labelProps: group.labelProps,
|
|
76061
76210
|
children: group.children
|
|
76062
|
-
}, `${ownerId}_group_${group.key}`));
|
|
76211
|
+
}, groupCount === 1 ? `${ownerId}_group_${group.key}` : `${ownerId}_group_${group.key}_${groupCount}`));
|
|
76063
76212
|
group = null;
|
|
76064
76213
|
};
|
|
76214
|
+
// An item not held has no group of its own. Between two items of the open
|
|
76215
|
+
// group it is a line of that group whose content has not arrived: drawn
|
|
76216
|
+
// outside, it would cut the group in two and show its label twice. A hole at
|
|
76217
|
+
// a group's edge stays outside — nothing says which side it belongs to.
|
|
76218
|
+
let holeTo = -1;
|
|
76219
|
+
let holeGroupKey;
|
|
76220
|
+
const groupKeyOfHole = itemIndex => {
|
|
76221
|
+
if (itemIndex < holeTo) {
|
|
76222
|
+
return holeGroupKey;
|
|
76223
|
+
}
|
|
76224
|
+
holeTo = itemIndex + 1;
|
|
76225
|
+
while (holeTo < windowTo && holeTo !== failureFrom && getItemAt(holeTo) === undefined) {
|
|
76226
|
+
holeTo++;
|
|
76227
|
+
}
|
|
76228
|
+
holeGroupKey = undefined;
|
|
76229
|
+
if (group && holeTo < windowTo && holeTo !== failureFrom) {
|
|
76230
|
+
const keyAfterHole = groupBy(getItemAt(holeTo), holeTo);
|
|
76231
|
+
if (keyAfterHole === group.key) {
|
|
76232
|
+
holeGroupKey = keyAfterHole;
|
|
76233
|
+
}
|
|
76234
|
+
}
|
|
76235
|
+
return holeGroupKey;
|
|
76236
|
+
};
|
|
76065
76237
|
// Which group an item belongs to, or undefined when it belongs to none.
|
|
76066
|
-
const groupKeyOf = (item, itemIndex) =>
|
|
76238
|
+
const groupKeyOf = (item, itemIndex) => {
|
|
76239
|
+
if (!groupBy) {
|
|
76240
|
+
return undefined;
|
|
76241
|
+
}
|
|
76242
|
+
if (item === undefined) {
|
|
76243
|
+
return groupKeyOfHole(itemIndex);
|
|
76244
|
+
}
|
|
76245
|
+
return groupBy(item, itemIndex);
|
|
76246
|
+
};
|
|
76067
76247
|
const pushItem = (itemNode, item, itemIndex, groupKey) => {
|
|
76068
76248
|
if (groupKey === undefined) {
|
|
76069
76249
|
closeGroup();
|
|
@@ -76120,6 +76300,7 @@ const useRunItems = (store, {
|
|
|
76120
76300
|
nodes.push(jsx(VirtualFiller, {
|
|
76121
76301
|
edge: "before",
|
|
76122
76302
|
itemCount: windowFrom - runStart,
|
|
76303
|
+
itemsPerLine: itemsPerLine,
|
|
76123
76304
|
findChunks: getFindChunks(runStart, windowFrom)
|
|
76124
76305
|
}, "navi-list-filler-before"));
|
|
76125
76306
|
}
|
|
@@ -76134,7 +76315,7 @@ const useRunItems = (store, {
|
|
|
76134
76315
|
nodes.push(jsx("li", {
|
|
76135
76316
|
className: "navi_list_failed_items",
|
|
76136
76317
|
style: {
|
|
76137
|
-
"--size-to-fill": `${failedItemCount * listItems.virtualItemSizeSignal.value}px`
|
|
76318
|
+
"--size-to-fill": `${Math.ceil(failedItemCount / itemsPerLine) * listItems.virtualItemSizeSignal.value}px`
|
|
76138
76319
|
},
|
|
76139
76320
|
children: renderError ? renderError({
|
|
76140
76321
|
error: store.failure.error,
|
|
@@ -76224,6 +76405,7 @@ const useRunItems = (store, {
|
|
|
76224
76405
|
nodes.push(jsx(VirtualFiller, {
|
|
76225
76406
|
edge: "after",
|
|
76226
76407
|
itemCount: runEnd - windowTo,
|
|
76408
|
+
itemsPerLine: itemsPerLine,
|
|
76227
76409
|
findChunks: getFindChunks(windowTo, runEnd)
|
|
76228
76410
|
}, "navi-list-filler-after"));
|
|
76229
76411
|
}
|
|
@@ -76389,10 +76571,15 @@ const useItemStore = ({
|
|
|
76389
76571
|
}
|
|
76390
76572
|
const pages = pagesRef.current;
|
|
76391
76573
|
const [, setPageVersion] = useState(0);
|
|
76392
|
-
// The
|
|
76393
|
-
//
|
|
76394
|
-
//
|
|
76395
|
-
|
|
76574
|
+
// The ranks whose item is from before: the run came back to the screen, or
|
|
76575
|
+
// the source said the collection moved. They stay drawn until a page confirms
|
|
76576
|
+
// or replaces them, rank by rank. A page covers what the window framed when it
|
|
76577
|
+
// was asked for, and the window keeps growing meanwhile: dropping what the
|
|
76578
|
+
// page did not cover empties items on screen.
|
|
76579
|
+
const staleRanksRef = useRef(null);
|
|
76580
|
+
if (staleRanksRef.current === null) {
|
|
76581
|
+
staleRanksRef.current = restored ? new Set(pages.byIndex.keys()) : new Set();
|
|
76582
|
+
}
|
|
76396
76583
|
const [refreshing, setRefreshing] = useState(false);
|
|
76397
76584
|
// A source that says when what it reads has moved (a resource range reader
|
|
76398
76585
|
// does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
|
|
@@ -76403,7 +76590,7 @@ const useItemStore = ({
|
|
|
76403
76590
|
const invalidationRef = useRef(invalidation);
|
|
76404
76591
|
if (invalidationRef.current !== invalidation) {
|
|
76405
76592
|
invalidationRef.current = invalidation;
|
|
76406
|
-
|
|
76593
|
+
staleRanksRef.current = new Set(pages.byIndex.keys());
|
|
76407
76594
|
}
|
|
76408
76595
|
// The one request in flight, with the means to call it off: a page asked for
|
|
76409
76596
|
// a window the list has left is work the server and the browser are doing for
|
|
@@ -76436,11 +76623,6 @@ const useItemStore = ({
|
|
|
76436
76623
|
// It stands for a page of them: a list that is about to be filled looks
|
|
76437
76624
|
// like items on their way, not like an empty list.
|
|
76438
76625
|
const itemCount = pages.count ?? count ?? listItems.pageSize;
|
|
76439
|
-
// A run that never received anything has nothing to keep on screen: asking
|
|
76440
|
-
// again is its first ask, not a refresh.
|
|
76441
|
-
if (staleRef.current && pages.count === undefined) {
|
|
76442
|
-
staleRef.current = false;
|
|
76443
|
-
}
|
|
76444
76626
|
useLayoutEffect(() => {
|
|
76445
76627
|
if (!refreshing) {
|
|
76446
76628
|
return null;
|
|
@@ -76513,9 +76695,11 @@ const useItemStore = ({
|
|
|
76513
76695
|
const keepFrom = windowFrom - ITEM_STORE_KEEP_AROUND;
|
|
76514
76696
|
const keepTo = windowTo + ITEM_STORE_KEEP_AROUND;
|
|
76515
76697
|
if (pages.byIndex.size > budget) {
|
|
76698
|
+
const staleRanks = staleRanksRef.current;
|
|
76516
76699
|
for (const index of pages.byIndex.keys()) {
|
|
76517
76700
|
if (index < keepFrom || index > keepTo) {
|
|
76518
76701
|
pages.byIndex.delete(index);
|
|
76702
|
+
staleRanks.delete(index);
|
|
76519
76703
|
}
|
|
76520
76704
|
}
|
|
76521
76705
|
}
|
|
@@ -76550,10 +76734,24 @@ const useItemStore = ({
|
|
|
76550
76734
|
let start = missingStart;
|
|
76551
76735
|
let end = missingEnd;
|
|
76552
76736
|
let around;
|
|
76553
|
-
//
|
|
76554
|
-
//
|
|
76555
|
-
|
|
76556
|
-
|
|
76737
|
+
// The window draws items from before: they are asked for again, and stay
|
|
76738
|
+
// drawn meanwhile.
|
|
76739
|
+
const staleRanks = staleRanksRef.current;
|
|
76740
|
+
let staleFrom = -1;
|
|
76741
|
+
let staleTo = -1;
|
|
76742
|
+
if (staleRanks.size > 0) {
|
|
76743
|
+
let rank = windowFrom;
|
|
76744
|
+
while (rank < windowTo) {
|
|
76745
|
+
if (staleRanks.has(rank)) {
|
|
76746
|
+
if (staleFrom === -1) {
|
|
76747
|
+
staleFrom = rank;
|
|
76748
|
+
}
|
|
76749
|
+
staleTo = rank;
|
|
76750
|
+
}
|
|
76751
|
+
rank++;
|
|
76752
|
+
}
|
|
76753
|
+
}
|
|
76754
|
+
const revalidating = staleFrom !== -1;
|
|
76557
76755
|
// The list is held on an item nothing on screen leads to: the items it holds
|
|
76558
76756
|
// do not contain it, so no window it could draw will ever bring it. Only
|
|
76559
76757
|
// asking for it by name does.
|
|
@@ -76570,18 +76768,36 @@ const useItemStore = ({
|
|
|
76570
76768
|
start = from < 0 ? 0 : from;
|
|
76571
76769
|
end = start + budget - 1;
|
|
76572
76770
|
} else if (revalidating) {
|
|
76573
|
-
|
|
76574
|
-
|
|
76575
|
-
if (
|
|
76576
|
-
|
|
76577
|
-
|
|
76578
|
-
|
|
76579
|
-
|
|
76580
|
-
|
|
76581
|
-
|
|
76582
|
-
|
|
76583
|
-
|
|
76584
|
-
|
|
76771
|
+
// The items from before that the window draws, and the missing ones
|
|
76772
|
+
// with them: one ask.
|
|
76773
|
+
if (start === -1 || staleFrom < start) {
|
|
76774
|
+
start = staleFrom;
|
|
76775
|
+
}
|
|
76776
|
+
if (staleTo > end) {
|
|
76777
|
+
end = staleTo;
|
|
76778
|
+
}
|
|
76779
|
+
// Grown over the items from before on either side, up to a page: the
|
|
76780
|
+
// window grows past its first picture right after it is painted, and
|
|
76781
|
+
// what it grows onto would otherwise be asked for once more.
|
|
76782
|
+
while (end - start + 1 < budget) {
|
|
76783
|
+
const staleBefore = staleRanks.has(start - 1);
|
|
76784
|
+
const staleAfter = staleRanks.has(end + 1);
|
|
76785
|
+
if (!staleBefore && !staleAfter) {
|
|
76786
|
+
break;
|
|
76787
|
+
}
|
|
76788
|
+
if (staleBefore) {
|
|
76789
|
+
start--;
|
|
76790
|
+
}
|
|
76791
|
+
if (staleAfter && end - start + 1 < budget) {
|
|
76792
|
+
end++;
|
|
76793
|
+
}
|
|
76794
|
+
}
|
|
76795
|
+
// Anchored on the first item from before on screen: a source
|
|
76796
|
+
// paginating by cursor gets an item to count from, and the reading
|
|
76797
|
+
// position is what must survive.
|
|
76798
|
+
const firstStale = pages.byIndex.get(staleFrom);
|
|
76799
|
+
if (firstStale && firstStale.id !== undefined) {
|
|
76800
|
+
around = firstStale.id;
|
|
76585
76801
|
}
|
|
76586
76802
|
} else if (pages.count === undefined) {
|
|
76587
76803
|
const scrolled = listItems.scrolled;
|
|
@@ -76635,7 +76851,8 @@ const useItemStore = ({
|
|
|
76635
76851
|
debugAsk("already revalidating");
|
|
76636
76852
|
return;
|
|
76637
76853
|
}
|
|
76638
|
-
// A page for
|
|
76854
|
+
// A page asked for before the items went stale answers for the
|
|
76855
|
+
// collection as it was.
|
|
76639
76856
|
request.controller?.abort();
|
|
76640
76857
|
request.busy = false;
|
|
76641
76858
|
}
|
|
@@ -76656,11 +76873,11 @@ const useItemStore = ({
|
|
|
76656
76873
|
request.controller?.abort();
|
|
76657
76874
|
request.busy = false;
|
|
76658
76875
|
}
|
|
76659
|
-
const held = pages.byIndex.size;
|
|
76660
76876
|
// Asking again for a range that was already asked for, having received
|
|
76661
|
-
// nothing since, can only produce the same answer
|
|
76662
|
-
//
|
|
76663
|
-
|
|
76877
|
+
// nothing since, can only produce the same answer — unless the
|
|
76878
|
+
// collection moved since, which is what an invalidation says.
|
|
76879
|
+
const held = `${pages.byIndex.size}/${staleRanks.size}/${invalidation}`;
|
|
76880
|
+
if (request.start === start && request.end === end && request.held === held) {
|
|
76664
76881
|
debugAsk("this range was asked for already");
|
|
76665
76882
|
return;
|
|
76666
76883
|
}
|
|
@@ -76696,7 +76913,6 @@ const useItemStore = ({
|
|
|
76696
76913
|
request.revalidating = false;
|
|
76697
76914
|
setFailure(null);
|
|
76698
76915
|
if (revalidating) {
|
|
76699
|
-
staleRef.current = false;
|
|
76700
76916
|
setRefreshing(false);
|
|
76701
76917
|
}
|
|
76702
76918
|
publishRequestState();
|
|
@@ -76715,15 +76931,35 @@ const useItemStore = ({
|
|
|
76715
76931
|
// Before the items land: what is on screen has to stay where it is,
|
|
76716
76932
|
// and the DOM still shows the state to hold onto.
|
|
76717
76933
|
listItems.captureAnchor();
|
|
76718
|
-
|
|
76719
|
-
|
|
76720
|
-
|
|
76721
|
-
|
|
76722
|
-
|
|
76934
|
+
// An item from before is gone from where it stood when the page puts
|
|
76935
|
+
// it at another rank (it would be drawn twice), or when its rank is
|
|
76936
|
+
// past the end of the collection.
|
|
76937
|
+
const staleRanksNow = staleRanksRef.current;
|
|
76938
|
+
const pageEnd = pageStart + pageItems.length;
|
|
76939
|
+
let dropped = false;
|
|
76940
|
+
if (staleRanksNow.size > 0) {
|
|
76941
|
+
const pageIds = new Set();
|
|
76942
|
+
for (const pageItem of pageItems) {
|
|
76943
|
+
if (pageItem && pageItem.id !== undefined) {
|
|
76944
|
+
pageIds.add(pageItem.id);
|
|
76945
|
+
}
|
|
76946
|
+
}
|
|
76947
|
+
for (const rank of staleRanksNow) {
|
|
76948
|
+
if (rank >= pageStart && rank < pageEnd) {
|
|
76949
|
+
continue;
|
|
76950
|
+
}
|
|
76951
|
+
const staleItem = pages.byIndex.get(rank);
|
|
76952
|
+
if (rank >= pageCount || staleItem && pageIds.has(staleItem.id)) {
|
|
76953
|
+
staleRanksNow.delete(rank);
|
|
76954
|
+
pages.byIndex.delete(rank);
|
|
76955
|
+
dropped = true;
|
|
76956
|
+
}
|
|
76957
|
+
}
|
|
76723
76958
|
}
|
|
76724
76959
|
let i = 0;
|
|
76725
76960
|
while (i < pageItems.length) {
|
|
76726
76961
|
pages.byIndex.set(pageStart + i, pageItems[i]);
|
|
76962
|
+
staleRanksNow.delete(pageStart + i);
|
|
76727
76963
|
i++;
|
|
76728
76964
|
}
|
|
76729
76965
|
pages.count = pageCount;
|
|
@@ -76733,7 +76969,7 @@ const useItemStore = ({
|
|
|
76733
76969
|
itemsAction.writeComposition({
|
|
76734
76970
|
byIndex: pages.byIndex,
|
|
76735
76971
|
count: pageCount,
|
|
76736
|
-
replace:
|
|
76972
|
+
replace: dropped
|
|
76737
76973
|
});
|
|
76738
76974
|
}
|
|
76739
76975
|
listItems.pagesSignal.value = listItems.pagesSignal.peek() + 1;
|
|
@@ -76748,9 +76984,14 @@ const useItemStore = ({
|
|
|
76748
76984
|
request.revalidating = false;
|
|
76749
76985
|
publishRequestState();
|
|
76750
76986
|
if (revalidating) {
|
|
76751
|
-
// The items from before stay
|
|
76987
|
+
// The items from before stay, and are not asked for again until the
|
|
76988
|
+
// source says the collection moved: a revalidation that failed has
|
|
76752
76989
|
// nothing better to put in their place.
|
|
76753
|
-
|
|
76990
|
+
let rank = start;
|
|
76991
|
+
while (rank <= end) {
|
|
76992
|
+
staleRanksRef.current.delete(rank);
|
|
76993
|
+
rank++;
|
|
76994
|
+
}
|
|
76754
76995
|
setRefreshing(false);
|
|
76755
76996
|
return;
|
|
76756
76997
|
}
|
|
@@ -76947,12 +77188,16 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76947
77188
|
* @param {string} [props.columns]
|
|
76948
77189
|
* The list's own columns: a `grid-template-columns` value the ITEMS are laid
|
|
76949
77190
|
* into — a sheet of icons (`repeat(auto-fill, minmax(2.5rem, 1fr))`), a row of
|
|
76950
|
-
* choices (`repeat(3, minmax(0, 1fr))`)
|
|
76951
|
-
*
|
|
76952
|
-
*
|
|
76953
|
-
*
|
|
76954
|
-
*
|
|
76955
|
-
*
|
|
77191
|
+
* choices (`repeat(3, minmax(0, 1fr))`), a feed of covers
|
|
77192
|
+
* (`repeat(auto-fill, minmax(150px, 1fr))` over a `<List.Items>`). Each item
|
|
77193
|
+
* takes one cell, and an item meant to take a whole line says so for itself
|
|
77194
|
+
* (`style={{ gridColumn: "1 / -1" }}`). A `<List.Items>` run is drawn in whole
|
|
77195
|
+
* lines — as many items to a line as the template resolves to, read again
|
|
77196
|
+
* when the list's width changes: its window starts and ends on a line, so an
|
|
77197
|
+
* item is always in the column its place says, and the fillers hold the room
|
|
77198
|
+
* of the lines it does not draw (`virtualItemSize` is then a line's size).
|
|
77199
|
+
* That count holds only while every item of the run takes one cell: an item
|
|
77200
|
+
* spanning a line, or a `groupBy`, puts the ones after it in other columns.
|
|
76956
77201
|
* @param {string} [props.itemColumns]
|
|
76957
77202
|
* The columns inside an ITEM: a `grid-template-columns` value each item fills
|
|
76958
77203
|
* with its own children, a table whose cells line up down the list. Every item
|
|
@@ -76968,8 +77213,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76968
77213
|
* scroll box, which fills it and has nothing to arrange.
|
|
76969
77214
|
* @param {boolean} [props.flexWrap]
|
|
76970
77215
|
* Lets a horizontal list's items fall to the next line instead of running
|
|
76971
|
-
* past the edge — a row of choices under a `maxWidth`, say.
|
|
76972
|
-
*
|
|
77216
|
+
* past the edge — a row of choices under a `maxWidth`, say. A wrapped line
|
|
77217
|
+
* holding several items is not virtualizable: a long collection laid several
|
|
77218
|
+
* to a line goes in `columns`.
|
|
76973
77219
|
* @param {string} [props.overflow]
|
|
76974
77220
|
* `"visible"` lets the items paint outside the list — a check in an item's
|
|
76975
77221
|
* corner, a badge crossing the edge. A list clips by default, which is what
|
|
@@ -77088,7 +77334,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
77088
77334
|
* one-line size) — an item guessed too small builds one more, an item
|
|
77089
77335
|
* guessed too big leaves a blank (see docs/scroll.md, "What the list knows,
|
|
77090
77336
|
* and what it guesses"). The items drawn are measured either way: the render
|
|
77091
|
-
* window is sized on them.
|
|
77337
|
+
* window is sized on them. In a grid (`columns`), the size is a line's: a
|
|
77338
|
+
* card's, which the items side by side on it share.
|
|
77092
77339
|
* @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
|
|
77093
77340
|
* Which box scrolls — and with it, which box the render window follows and
|
|
77094
77341
|
* which box a scroll position is read from (`onScrolledChange`). `"self"`
|