@jsenv/navi 0.29.343 → 0.29.345
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 +1652 -1350
- package/dist/dev/jsenv_navi.js.map +6 -4
- package/dist/jsenv_navi.js +1652 -1350
- package/dist/jsenv_navi.js.map +6 -4
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -64746,452 +64746,666 @@ const cssVars = vars => {
|
|
|
64746
64746
|
};
|
|
64747
64747
|
const lengthValue = value => typeof value === "number" ? `${value}px` : value;
|
|
64748
64748
|
|
|
64749
|
-
|
|
64750
|
-
|
|
64751
|
-
|
|
64752
|
-
|
|
64753
|
-
* USAGE:
|
|
64754
|
-
* ```jsx
|
|
64755
|
-
* function ListControlled({ items }) {
|
|
64756
|
-
* const tracker = useItemTracker({
|
|
64757
|
-
* onChange: () => console.log("items changed"),
|
|
64758
|
-
* });
|
|
64759
|
-
*
|
|
64760
|
-
* return (
|
|
64761
|
-
* <ul>
|
|
64762
|
-
* {items.map((item, i) => (
|
|
64763
|
-
* <Row key={item.id} id={item.id} index={i} hidden={item.hidden} value={item.value} tracker={tracker} />
|
|
64764
|
-
* ))}
|
|
64765
|
-
* <Count tracker={tracker} />
|
|
64766
|
-
* </ul>
|
|
64767
|
-
* );
|
|
64768
|
-
* }
|
|
64769
|
-
*
|
|
64770
|
-
* function Row({ id, index, hidden, value, tracker }) {
|
|
64771
|
-
* const visibleIndex = tracker.useTrackItem({ id, index, hidden, value });
|
|
64772
|
-
* if (visibleIndex === -1) return null;
|
|
64773
|
-
* return <li>{value}</li>;
|
|
64774
|
-
* }
|
|
64775
|
-
*
|
|
64776
|
-
* function Count({ tracker }) {
|
|
64777
|
-
* const count = tracker.visibleCountSignal.value; // re-renders only when count changes
|
|
64778
|
-
* return <span>{count} items</span>;
|
|
64779
|
-
* }
|
|
64780
|
-
* ```
|
|
64781
|
-
*
|
|
64782
|
-
* INTERNALS:
|
|
64783
|
-
* - registrations: Map key → data, contains only visible items
|
|
64784
|
-
* - idToKey: Map id → key, stable across renders
|
|
64785
|
-
* - orderedKeys: number[] of visible item keys sorted by explicit order
|
|
64786
|
-
* - keyToOrderedIndex: Map key → orderedKeys index, gives O(1) indexOf equivalent
|
|
64787
|
-
* - keyToExplicitOrder: Map key → explicitly passed index, used to maintain sort order
|
|
64788
|
-
* - allItemsSignal: signal(array), all items including hidden, ordered by explicit index
|
|
64789
|
-
* - visibleItemsSignal: signal(array), non-hidden items only
|
|
64790
|
-
* - countSignal: signal(number), count of all items including hidden
|
|
64791
|
-
* - visibleCountSignal: signal(number), updated in microtask batch, only when count changes
|
|
64792
|
-
* - propSignals: Map propName → signal(array), updated in microtask batch with element equality
|
|
64793
|
-
* - onChangeRef: holds the latest onChange callback, called once per microtask batch
|
|
64794
|
-
*
|
|
64795
|
-
* useTrackItem(id, data, index): registers the item with an explicitly provided index
|
|
64796
|
-
* that determines its position among siblings. The caller (e.g. items.map) knows the
|
|
64797
|
-
* correct order and passes it directly — no render-sequence deduction needed.
|
|
64798
|
-
* Returns the visible rank (position among non-hidden items), or -1 when hidden.
|
|
64799
|
-
* Signals and onChange are deferred to a microtask so multiple items updating
|
|
64800
|
-
* in one commit cause only one notification.
|
|
64801
|
-
*
|
|
64802
|
-
* getTrackedItemByIndex(index): synchronous O(1) lookup of a visible item by
|
|
64803
|
-
* its visible rank. Returns undefined when index is out of range.
|
|
64804
|
-
*
|
|
64805
|
-
* peekItems(): the items as they stand right now, without waiting for the
|
|
64806
|
-
* deferred notification — what a sibling rendering after the items must read
|
|
64807
|
-
* to paint them in the same commit.
|
|
64808
|
-
*/
|
|
64809
|
-
|
|
64810
|
-
const useItemTracker = ({ onChange } = {}) => {
|
|
64811
|
-
const onChangeRef = useRef(onChange);
|
|
64812
|
-
onChangeRef.current = onChange;
|
|
64813
|
-
const trackerRef = useRef(null);
|
|
64814
|
-
let tracker = trackerRef.current;
|
|
64815
|
-
if (!tracker) {
|
|
64816
|
-
trackerRef.current = tracker = createItemTracker((items) => {
|
|
64817
|
-
onChangeRef.current?.(items);
|
|
64818
|
-
});
|
|
64749
|
+
const ListItemHeaderOrFooterResolver = props => {
|
|
64750
|
+
const Next = useNextResolver();
|
|
64751
|
+
if (props.header) {
|
|
64752
|
+
return renderResolver(ListItemHeader, props);
|
|
64819
64753
|
}
|
|
64820
|
-
|
|
64821
|
-
|
|
64822
|
-
|
|
64823
|
-
|
|
64824
|
-
|
|
64754
|
+
if (props.footer) {
|
|
64755
|
+
return renderResolver(ListItemFooter, props);
|
|
64756
|
+
}
|
|
64757
|
+
return jsx(Next, {
|
|
64758
|
+
...props
|
|
64759
|
+
});
|
|
64760
|
+
};
|
|
64761
|
+
const ListItemHeader = props => {
|
|
64762
|
+
const Next = useNextResolver();
|
|
64763
|
+
const {
|
|
64764
|
+
ref
|
|
64765
|
+
} = props;
|
|
64766
|
+
useDisplayedLayoutEffect(ref, headerEl => {
|
|
64767
|
+
const listContainerEl = headerEl.closest(".navi_list_container");
|
|
64768
|
+
const rect = headerEl.getBoundingClientRect();
|
|
64769
|
+
listContainerEl.style.setProperty("--list-header-height", `${rect.height}px`);
|
|
64770
|
+
listContainerEl.style.setProperty("--list-header-width", `${rect.width}px`);
|
|
64771
|
+
}, []);
|
|
64772
|
+
return jsx(Next, {
|
|
64773
|
+
...props,
|
|
64774
|
+
header: undefined,
|
|
64775
|
+
role: "presentation",
|
|
64776
|
+
baseClassName: "navi_list_item_header"
|
|
64777
|
+
});
|
|
64778
|
+
};
|
|
64779
|
+
const ListItemFooter = props => {
|
|
64780
|
+
const Next = useNextResolver();
|
|
64781
|
+
const {
|
|
64782
|
+
ref
|
|
64783
|
+
} = props;
|
|
64784
|
+
useDisplayedLayoutEffect(ref, footerEl => {
|
|
64785
|
+
const listContainerEl = footerEl.closest(".navi_list_container");
|
|
64786
|
+
const rect = footerEl.getBoundingClientRect();
|
|
64787
|
+
listContainerEl.style.setProperty("--list-footer-height", `${rect.height}px`);
|
|
64788
|
+
listContainerEl.style.setProperty("--list-footer-width", `${rect.width}px`);
|
|
64789
|
+
}, []);
|
|
64790
|
+
return jsx(Next, {
|
|
64791
|
+
...props,
|
|
64792
|
+
footer: undefined,
|
|
64793
|
+
role: "presentation",
|
|
64794
|
+
baseClassName: "navi_list_item_footer"
|
|
64825
64795
|
});
|
|
64826
|
-
return tracker;
|
|
64827
64796
|
};
|
|
64828
64797
|
|
|
64829
|
-
|
|
64830
|
-
|
|
64831
|
-
|
|
64832
|
-
|
|
64833
|
-
|
|
64834
|
-
|
|
64835
|
-
|
|
64836
|
-
|
|
64837
|
-
|
|
64838
|
-
|
|
64839
|
-
|
|
64840
|
-
|
|
64841
|
-
|
|
64842
|
-
|
|
64843
|
-
|
|
64844
|
-
|
|
64845
|
-
|
|
64846
|
-
|
|
64847
|
-
|
|
64848
|
-
|
|
64849
|
-
|
|
64850
|
-
|
|
64851
|
-
|
|
64852
|
-
|
|
64853
|
-
|
|
64798
|
+
// Everything the list knows about its rows, in one place: how many the
|
|
64799
|
+
// collection has and where each child's rows start (the places), which rows
|
|
64800
|
+
// are drawn and which of those mount and show (the rows), and what each says
|
|
64801
|
+
// about itself (the items). Two clocks. Places and "is anything standing
|
|
64802
|
+
// above me" answer synchronously, in the middle of a render pass — a row asks
|
|
64803
|
+
// about the rows before it, and those have rendered already. The items and
|
|
64804
|
+
// the counts settle once per frame (a microtask): many rows change in one
|
|
64805
|
+
// commit, and what reads them wants one notification.
|
|
64806
|
+
//
|
|
64807
|
+
// Why places are read off the walk and not off the renders: a child knows how
|
|
64808
|
+
// many rows it stands for but not what was declared before it, and it cannot
|
|
64809
|
+
// deduce that from when it renders — a render is free to skip it. A child that
|
|
64810
|
+
// draws from signals and whose props are all referentially === the previous
|
|
64811
|
+
// ones does not render again (@preact/signals installs a shouldComponentUpdate
|
|
64812
|
+
// that says so), which is what any child nobody rebuilt this frame is — and
|
|
64813
|
+
// children numbered as they render would then slide up into the place of the
|
|
64814
|
+
// one that was skipped. So the list names a slot for each of its children and
|
|
64815
|
+
// declares them here, in order, before any of them renders (see
|
|
64816
|
+
// ListDeclaredChildren). A child then takes its place BY SLOT, and the place
|
|
64817
|
+
// is a signal: it moves when what stands before it changes — a row filtered
|
|
64818
|
+
// out, a run taking in rows, a slot added or moved — and the child follows,
|
|
64819
|
+
// rendered again for it whether or not anything else would have rendered it.
|
|
64820
|
+
//
|
|
64821
|
+
// Why "first" is a signal too: only the row itself knows, once it renders,
|
|
64822
|
+
// that it renders nothing (filtered out by a search), and the rows after it
|
|
64823
|
+
// may have been handed back unchanged. The first mounted row of a scope is
|
|
64824
|
+
// kept as a signal each of them reads; when it leaves, they are rendered again.
|
|
64854
64825
|
|
|
64855
|
-
|
|
64856
|
-
const countModified = countSignal.peek() !== newCount;
|
|
64857
|
-
if (countModified) {
|
|
64858
|
-
countSignal.value = newCount;
|
|
64859
|
-
someChange = true;
|
|
64860
|
-
}
|
|
64826
|
+
const UNGROUPED = Symbol("ungrouped");
|
|
64861
64827
|
|
|
64862
|
-
|
|
64863
|
-
|
|
64864
|
-
|
|
64865
|
-
|
|
64866
|
-
|
|
64867
|
-
|
|
64868
|
-
|
|
64869
|
-
|
|
64870
|
-
|
|
64871
|
-
|
|
64872
|
-
|
|
64873
|
-
|
|
64874
|
-
|
|
64875
|
-
|
|
64876
|
-
|
|
64877
|
-
|
|
64878
|
-
|
|
64879
|
-
|
|
64880
|
-
|
|
64881
|
-
|
|
64882
|
-
|
|
64883
|
-
|
|
64884
|
-
|
|
64885
|
-
|
|
64886
|
-
|
|
64887
|
-
|
|
64888
|
-
|
|
64889
|
-
|
|
64890
|
-
|
|
64891
|
-
|
|
64892
|
-
|
|
64893
|
-
|
|
64828
|
+
const createListRows = () => {
|
|
64829
|
+
const totalSignal = signal(0);
|
|
64830
|
+
// Bumped whenever a run takes in rows. The list itself has to hear about it:
|
|
64831
|
+
// rows arriving outside the render window change nothing it can see (nothing
|
|
64832
|
+
// registers, nothing is drawn), and yet they are what it may have been
|
|
64833
|
+
// waiting for — the row it was told to open on, for one.
|
|
64834
|
+
const pagesSignal = signal(0);
|
|
64835
|
+
// How many runs are re-reading rows they already show. The list wears it as
|
|
64836
|
+
// an attribute: what is drawn is from before, and the app may want to say so
|
|
64837
|
+
// without taking anything away.
|
|
64838
|
+
const refreshingSignal = signal(0);
|
|
64839
|
+
// The slots each walk declared, in order, by the slot the walk stands in
|
|
64840
|
+
// (null for the list's own children). Together they are a tree: a group's
|
|
64841
|
+
// rows live inside the group's slot.
|
|
64842
|
+
const slotIdsByParent = new Map();
|
|
64843
|
+
// Who took a place — a row, or a run of rows — and how many rows of the
|
|
64844
|
+
// collection it stands for. The place itself is a signal, see take.
|
|
64845
|
+
const ownerById = new Map();
|
|
64846
|
+
// The owners standing in each slot, in the order they took their place.
|
|
64847
|
+
// One, as a rule; a child that renders several rows keeps them in the order
|
|
64848
|
+
// they first rendered, which is all it can be told.
|
|
64849
|
+
const ownerIdsBySlot = new Map();
|
|
64850
|
+
const locatorByOwner = new Map();
|
|
64851
|
+
// The slots as the tree reads, first to last, and where each stands in it.
|
|
64852
|
+
// Rebuilt once a walk has changed the tree, read to place the owners.
|
|
64853
|
+
const slotWalk = [];
|
|
64854
|
+
const rankBySlot = new Map();
|
|
64855
|
+
let rowTotal = 0;
|
|
64856
|
+
// Owners have left and the others have not been moved up yet. Done on the
|
|
64857
|
+
// next ask rather than on the spot: rows leave many at a time (a search, a
|
|
64858
|
+
// list unmounting), and moving the others up once is enough.
|
|
64859
|
+
let placesStale = false;
|
|
64860
|
+
// Where the last slot holding an owner stands: an owner arriving at or after
|
|
64861
|
+
// it is placed at the end without going over the others — a whole first
|
|
64862
|
+
// render, rows arriving in order, costs each row nothing but itself.
|
|
64863
|
+
let rankOwnedLast = -1;
|
|
64894
64864
|
|
|
64895
|
-
|
|
64896
|
-
|
|
64897
|
-
|
|
64898
|
-
|
|
64899
|
-
|
|
64900
|
-
|
|
64901
|
-
|
|
64902
|
-
if (allItemsChanged) {
|
|
64903
|
-
itemsSignal.value = allItems;
|
|
64904
|
-
someChange = true;
|
|
64905
|
-
}
|
|
64906
|
-
if (visibleItemsChanged) {
|
|
64907
|
-
visibleItemsSignal.value = visibleItems;
|
|
64908
|
-
someChange = true;
|
|
64909
|
-
}
|
|
64910
|
-
const noMatchCountModified =
|
|
64911
|
-
noMatchCountSignal.peek() !== newNoMatchCount;
|
|
64912
|
-
if (noMatchCountModified) {
|
|
64913
|
-
noMatchCountSignal.value = newNoMatchCount;
|
|
64914
|
-
someChange = true;
|
|
64865
|
+
const rebuildWalk = () => {
|
|
64866
|
+
slotWalk.length = 0;
|
|
64867
|
+
rankBySlot.clear();
|
|
64868
|
+
const visit = (parentSlotId) => {
|
|
64869
|
+
const slotIds = slotIdsByParent.get(parentSlotId);
|
|
64870
|
+
if (!slotIds) {
|
|
64871
|
+
return;
|
|
64915
64872
|
}
|
|
64916
|
-
|
|
64917
|
-
|
|
64873
|
+
for (const slotId of slotIds) {
|
|
64874
|
+
rankBySlot.set(slotId, slotWalk.length);
|
|
64875
|
+
slotWalk.push(slotId);
|
|
64876
|
+
visit(slotId);
|
|
64918
64877
|
}
|
|
64919
|
-
}
|
|
64878
|
+
};
|
|
64879
|
+
visit(null);
|
|
64920
64880
|
};
|
|
64921
|
-
|
|
64922
|
-
|
|
64923
|
-
|
|
64924
|
-
|
|
64925
|
-
|
|
64926
|
-
|
|
64927
|
-
|
|
64928
|
-
|
|
64929
|
-
|
|
64881
|
+
// Every place, in one go: a place is the sum of what stands before it, so
|
|
64882
|
+
// there is nothing to hand out one at a time. Writing a place that did not
|
|
64883
|
+
// change wakes nobody — a signal ignores a value equal to its own.
|
|
64884
|
+
const refreshPlaces = () => {
|
|
64885
|
+
placesStale = false;
|
|
64886
|
+
let index = 0;
|
|
64887
|
+
let rank = 0;
|
|
64888
|
+
rankOwnedLast = -1;
|
|
64889
|
+
while (rank < slotWalk.length) {
|
|
64890
|
+
const ownerIds = ownerIdsBySlot.get(slotWalk[rank]);
|
|
64891
|
+
if (ownerIds) {
|
|
64892
|
+
for (const ownerId of ownerIds) {
|
|
64893
|
+
const owner = ownerById.get(ownerId);
|
|
64894
|
+
owner.placeSignal.value = index;
|
|
64895
|
+
index += owner.rowCount;
|
|
64896
|
+
}
|
|
64897
|
+
rankOwnedLast = rank;
|
|
64930
64898
|
}
|
|
64931
|
-
|
|
64932
|
-
|
|
64933
|
-
|
|
64899
|
+
rank++;
|
|
64900
|
+
}
|
|
64901
|
+
rowTotal = index;
|
|
64902
|
+
totalSignal.value = index;
|
|
64903
|
+
// A run's edges are places too (see refreshFirst).
|
|
64904
|
+
markStale(UNGROUPED);
|
|
64934
64905
|
};
|
|
64935
|
-
|
|
64936
|
-
|
|
64937
|
-
if (
|
|
64906
|
+
const addToSlot = (slotId, ownerId) => {
|
|
64907
|
+
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
64908
|
+
if (ownerIds) {
|
|
64909
|
+
ownerIds.push(ownerId);
|
|
64910
|
+
} else {
|
|
64911
|
+
ownerIdsBySlot.set(slotId, [ownerId]);
|
|
64912
|
+
}
|
|
64913
|
+
warnIfEveryRowInOneSlot(slotId);
|
|
64914
|
+
};
|
|
64915
|
+
// Rows that all stand in the same slot keep the order they first mounted in:
|
|
64916
|
+
// the walk is over the children the list is given, and a component holding
|
|
64917
|
+
// them is one child however many rows it renders. Everything about a place
|
|
64918
|
+
// then stops following what the caller writes — a search reordering the rows
|
|
64919
|
+
// moves nothing. Said once, and only for the shape that can be nothing else:
|
|
64920
|
+
// the list's whole content is one child, and several rows came out of it.
|
|
64921
|
+
let everyRowInOneSlotWarned = false;
|
|
64922
|
+
const warnIfEveryRowInOneSlot = (slotId) => {
|
|
64923
|
+
if (everyRowInOneSlotWarned) {
|
|
64938
64924
|
return;
|
|
64939
64925
|
}
|
|
64940
|
-
|
|
64941
|
-
|
|
64926
|
+
const rootSlotIds = slotIdsByParent.get(null);
|
|
64927
|
+
if (!rootSlotIds || rootSlotIds.length !== 1 || rootSlotIds[0] !== slotId) {
|
|
64928
|
+
return;
|
|
64929
|
+
}
|
|
64930
|
+
if (ownerIdsBySlot.get(slotId).length < 2) {
|
|
64931
|
+
return;
|
|
64932
|
+
}
|
|
64933
|
+
everyRowInOneSlotWarned = true;
|
|
64934
|
+
console.warn(
|
|
64935
|
+
`List: every row stands in the same slot, so they keep the order they first rendered in — reordering them (a search, a sort) will not move them. The list's rows must be its own children: give it the rows (or a <List.Items>), not a component rendering them.`,
|
|
64936
|
+
);
|
|
64942
64937
|
};
|
|
64943
|
-
|
|
64944
|
-
|
|
64945
|
-
|
|
64946
|
-
|
|
64947
|
-
let lo = 0;
|
|
64948
|
-
let hi = orderedKeys.length;
|
|
64949
|
-
while (lo < hi) {
|
|
64950
|
-
const mid = (lo + hi) >> 1;
|
|
64951
|
-
if (keyToExplicitOrder.get(orderedKeys[mid]) <= explicitOrder) {
|
|
64952
|
-
lo = mid + 1;
|
|
64953
|
-
} else {
|
|
64954
|
-
hi = mid;
|
|
64955
|
-
}
|
|
64938
|
+
const removeFromSlot = (slotId, ownerId) => {
|
|
64939
|
+
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
64940
|
+
if (!ownerIds) {
|
|
64941
|
+
return;
|
|
64956
64942
|
}
|
|
64957
|
-
|
|
64958
|
-
|
|
64959
|
-
|
|
64943
|
+
const index = ownerIds.indexOf(ownerId);
|
|
64944
|
+
if (index !== -1) {
|
|
64945
|
+
ownerIds.splice(index, 1);
|
|
64946
|
+
}
|
|
64947
|
+
if (ownerIds.length === 0) {
|
|
64948
|
+
ownerIdsBySlot.delete(slotId);
|
|
64960
64949
|
}
|
|
64961
64950
|
};
|
|
64962
|
-
|
|
64963
|
-
|
|
64964
|
-
|
|
64965
|
-
|
|
64966
|
-
|
|
64967
|
-
const
|
|
64968
|
-
|
|
64969
|
-
lo = mid + 1;
|
|
64970
|
-
} else {
|
|
64971
|
-
hi = mid;
|
|
64951
|
+
// A slot the walk no longer names: whatever stood in it is gone, and so is
|
|
64952
|
+
// whatever a walk inside it had declared.
|
|
64953
|
+
const dropSlot = (slotId) => {
|
|
64954
|
+
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
64955
|
+
if (ownerIds) {
|
|
64956
|
+
for (const ownerId of ownerIds) {
|
|
64957
|
+
ownerById.delete(ownerId);
|
|
64972
64958
|
}
|
|
64959
|
+
ownerIdsBySlot.delete(slotId);
|
|
64973
64960
|
}
|
|
64974
|
-
|
|
64975
|
-
|
|
64976
|
-
|
|
64961
|
+
const childSlotIds = slotIdsByParent.get(slotId);
|
|
64962
|
+
if (childSlotIds) {
|
|
64963
|
+
slotIdsByParent.delete(slotId);
|
|
64964
|
+
for (const childSlotId of childSlotIds) {
|
|
64965
|
+
dropSlot(childSlotId);
|
|
64966
|
+
}
|
|
64977
64967
|
}
|
|
64978
64968
|
};
|
|
64979
64969
|
|
|
64980
|
-
|
|
64981
|
-
|
|
64982
|
-
|
|
64983
|
-
|
|
64984
|
-
|
|
64985
|
-
|
|
64986
|
-
|
|
64987
|
-
|
|
64970
|
+
// ---- the rows drawn ----
|
|
64971
|
+
// rowId → { ownerId, place, groupId, data, mounted, visible, item }
|
|
64972
|
+
const rowById = new Map();
|
|
64973
|
+
// groupId → { firstSignal, countSignal, noMatchCountSignal }
|
|
64974
|
+
const groupById = new Map();
|
|
64975
|
+
// ownerId → { from, to }, for the runs (see declareWindow).
|
|
64976
|
+
const windowByOwner = new Map();
|
|
64977
|
+
// The first place something stands at, outside any group: the mounted rows
|
|
64978
|
+
// and the rows a run holds above its window. Per group, the group's first
|
|
64979
|
+
// mounted row.
|
|
64980
|
+
const firstStandingSignal = signal(-1);
|
|
64981
|
+
// The scopes whose first row left: recounted on the next ask, or at the end
|
|
64982
|
+
// of the frame, whichever comes first. A row mounting before the first one
|
|
64983
|
+
// moves it at once, no recount needed — the first can only ever move up.
|
|
64984
|
+
const staleScopes = new Set();
|
|
64985
|
+
const scopeOf = (groupId) => (groupId === undefined ? UNGROUPED : groupId);
|
|
64986
|
+
const groupOf = (groupId) => {
|
|
64987
|
+
let group = groupById.get(groupId);
|
|
64988
|
+
if (!group) {
|
|
64989
|
+
group = {
|
|
64990
|
+
firstSignal: signal(-1),
|
|
64991
|
+
countSignal: signal(0),
|
|
64992
|
+
noMatchCountSignal: signal(0),
|
|
64993
|
+
};
|
|
64994
|
+
groupById.set(groupId, group);
|
|
64988
64995
|
}
|
|
64996
|
+
return group;
|
|
64989
64997
|
};
|
|
64990
|
-
|
|
64991
|
-
|
|
64992
|
-
|
|
64993
|
-
|
|
64994
|
-
|
|
64995
|
-
|
|
64996
|
-
|
|
64997
|
-
|
|
64998
|
-
orderedKeys.splice(idx, 1);
|
|
64999
|
-
keyToOrderedIndex.delete(key);
|
|
65000
|
-
for (let i = idx; i < orderedKeys.length; i++) {
|
|
65001
|
-
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
65002
|
-
}
|
|
64998
|
+
const firstSignalOf = (scope) =>
|
|
64999
|
+
scope === UNGROUPED ? firstStandingSignal : groupOf(scope).firstSignal;
|
|
65000
|
+
const refreshFirst = (scope) => {
|
|
65001
|
+
staleScopes.delete(scope);
|
|
65002
|
+
let first = -1;
|
|
65003
|
+
const consider = (place) => {
|
|
65004
|
+
if (place !== undefined && (first === -1 || place < first)) {
|
|
65005
|
+
first = place;
|
|
65003
65006
|
}
|
|
65004
|
-
|
|
65005
|
-
|
|
65006
|
-
|
|
65007
|
-
|
|
65008
|
-
return;
|
|
65009
|
-
}
|
|
65010
|
-
|
|
65011
|
-
// Maintain allRegistrations and allOrderedKeys for all non-presentation items.
|
|
65012
|
-
allRegistrations.set(key, data);
|
|
65013
|
-
allKeys.add(key);
|
|
65014
|
-
const currentAllIdx = keyToAllOrderedIndex.get(key);
|
|
65015
|
-
const previousOrder = keyToExplicitOrder.get(key);
|
|
65016
|
-
keyToExplicitOrder.set(key, index);
|
|
65017
|
-
if (currentAllIdx === undefined) {
|
|
65018
|
-
insertAllKey(key, index);
|
|
65019
|
-
} else if (previousOrder !== index) {
|
|
65020
|
-
allOrderedKeys.splice(currentAllIdx, 1);
|
|
65021
|
-
keyToAllOrderedIndex.delete(key);
|
|
65022
|
-
for (let i = currentAllIdx; i < allOrderedKeys.length; i++) {
|
|
65023
|
-
keyToAllOrderedIndex.set(allOrderedKeys[i], i);
|
|
65007
|
+
};
|
|
65008
|
+
for (const row of rowById.values()) {
|
|
65009
|
+
if (row.mounted && scopeOf(row.groupId) === scope) {
|
|
65010
|
+
consider(row.place);
|
|
65024
65011
|
}
|
|
65025
|
-
insertAllKey(key, index);
|
|
65026
65012
|
}
|
|
65027
|
-
|
|
65028
|
-
|
|
65029
|
-
|
|
65030
|
-
|
|
65031
|
-
|
|
65032
|
-
|
|
65033
|
-
|
|
65034
|
-
|
|
65035
|
-
|
|
65013
|
+
if (scope === UNGROUPED) {
|
|
65014
|
+
for (const [ownerId, window] of windowByOwner) {
|
|
65015
|
+
const owner = ownerById.get(ownerId);
|
|
65016
|
+
if (!owner) {
|
|
65017
|
+
continue;
|
|
65018
|
+
}
|
|
65019
|
+
const start = owner.placeSignal.peek();
|
|
65020
|
+
if (window.from > start) {
|
|
65021
|
+
consider(start);
|
|
65022
|
+
}
|
|
65023
|
+
if (window.to < start + owner.rowCount) {
|
|
65024
|
+
consider(window.to);
|
|
65036
65025
|
}
|
|
65037
65026
|
}
|
|
65038
|
-
return;
|
|
65039
|
-
}
|
|
65040
|
-
|
|
65041
|
-
registrations.set(key, data);
|
|
65042
|
-
const currentIdx = keyToOrderedIndex.get(key);
|
|
65043
|
-
if (currentIdx === undefined) {
|
|
65044
|
-
insertKey(key, index);
|
|
65045
|
-
return;
|
|
65046
65027
|
}
|
|
65047
|
-
|
|
65028
|
+
firstSignalOf(scope).value = first;
|
|
65029
|
+
};
|
|
65030
|
+
const markStale = (scope) => {
|
|
65031
|
+
if (staleScopes.has(scope)) {
|
|
65048
65032
|
return;
|
|
65049
65033
|
}
|
|
65050
|
-
|
|
65051
|
-
|
|
65052
|
-
|
|
65053
|
-
|
|
65034
|
+
staleScopes.add(scope);
|
|
65035
|
+
queueMicrotask(() => {
|
|
65036
|
+
if (staleScopes.has(scope)) {
|
|
65037
|
+
refreshFirst(scope);
|
|
65038
|
+
}
|
|
65039
|
+
});
|
|
65040
|
+
};
|
|
65041
|
+
const leaveFirst = (row) => {
|
|
65042
|
+
const scope = scopeOf(row.groupId);
|
|
65043
|
+
if (firstSignalOf(scope).peek() === row.place) {
|
|
65044
|
+
markStale(scope);
|
|
65054
65045
|
}
|
|
65055
|
-
insertKey(key, index);
|
|
65056
65046
|
};
|
|
65057
65047
|
|
|
65058
|
-
|
|
65059
|
-
|
|
65060
|
-
|
|
65061
|
-
|
|
65062
|
-
|
|
65063
|
-
|
|
65064
|
-
|
|
65065
|
-
|
|
65048
|
+
// ---- the items, settled once per frame ----
|
|
65049
|
+
const itemsSignal = signal([]);
|
|
65050
|
+
const visibleItemsSignal = signal([]);
|
|
65051
|
+
const countSignal = signal(0);
|
|
65052
|
+
const visibleCountSignal = signal(0);
|
|
65053
|
+
const noMatchCountSignal = signal(0);
|
|
65054
|
+
let notifyScheduled = false;
|
|
65055
|
+
const runNotify = () => {
|
|
65056
|
+
batch(() => {
|
|
65057
|
+
const itemRows = [];
|
|
65058
|
+
for (const row of rowById.values()) {
|
|
65059
|
+
if (row.item) {
|
|
65060
|
+
itemRows.push(row);
|
|
65061
|
+
}
|
|
65062
|
+
}
|
|
65063
|
+
itemRows.sort(compareRowPlaces);
|
|
65064
|
+
const items = [];
|
|
65065
|
+
const visibleItems = [];
|
|
65066
|
+
let noMatchCount = 0;
|
|
65067
|
+
const countByGroup = new Map();
|
|
65068
|
+
const noMatchCountByGroup = new Map();
|
|
65069
|
+
const prevItems = itemsSignal.peek();
|
|
65070
|
+
const prevVisibleItems = visibleItemsSignal.peek();
|
|
65071
|
+
let itemsChanged = prevItems.length !== itemRows.length;
|
|
65072
|
+
let visibleItemsChanged = false;
|
|
65073
|
+
for (const row of itemRows) {
|
|
65074
|
+
const item = row.data;
|
|
65075
|
+
// Compared by reference: any prop change (selected, disabled, …) is a
|
|
65076
|
+
// new props object.
|
|
65077
|
+
if (!itemsChanged && item !== prevItems[items.length]) {
|
|
65078
|
+
itemsChanged = true;
|
|
65079
|
+
}
|
|
65080
|
+
items.push(item);
|
|
65081
|
+
const noMatch = item.match === false;
|
|
65082
|
+
if (noMatch) {
|
|
65083
|
+
noMatchCount++;
|
|
65084
|
+
}
|
|
65085
|
+
if (row.groupId !== undefined) {
|
|
65086
|
+
countByGroup.set(
|
|
65087
|
+
row.groupId,
|
|
65088
|
+
(countByGroup.get(row.groupId) || 0) + 1,
|
|
65089
|
+
);
|
|
65090
|
+
if (noMatch) {
|
|
65091
|
+
noMatchCountByGroup.set(
|
|
65092
|
+
row.groupId,
|
|
65093
|
+
(noMatchCountByGroup.get(row.groupId) || 0) + 1,
|
|
65094
|
+
);
|
|
65095
|
+
}
|
|
65096
|
+
}
|
|
65097
|
+
if (row.visible) {
|
|
65098
|
+
if (
|
|
65099
|
+
!visibleItemsChanged &&
|
|
65100
|
+
item !== prevVisibleItems[visibleItems.length]
|
|
65101
|
+
) {
|
|
65102
|
+
visibleItemsChanged = true;
|
|
65103
|
+
}
|
|
65104
|
+
visibleItems.push(item);
|
|
65105
|
+
}
|
|
65106
|
+
}
|
|
65107
|
+
if (visibleItems.length !== prevVisibleItems.length) {
|
|
65108
|
+
visibleItemsChanged = true;
|
|
65066
65109
|
}
|
|
65110
|
+
let someChange = false;
|
|
65111
|
+
if (countSignal.peek() !== items.length) {
|
|
65112
|
+
countSignal.value = items.length;
|
|
65113
|
+
someChange = true;
|
|
65114
|
+
}
|
|
65115
|
+
if (visibleCountSignal.peek() !== visibleItems.length) {
|
|
65116
|
+
visibleCountSignal.value = visibleItems.length;
|
|
65117
|
+
someChange = true;
|
|
65118
|
+
}
|
|
65119
|
+
if (itemsChanged) {
|
|
65120
|
+
itemsSignal.value = items;
|
|
65121
|
+
someChange = true;
|
|
65122
|
+
}
|
|
65123
|
+
if (visibleItemsChanged) {
|
|
65124
|
+
visibleItemsSignal.value = visibleItems;
|
|
65125
|
+
someChange = true;
|
|
65126
|
+
}
|
|
65127
|
+
if (noMatchCountSignal.peek() !== noMatchCount) {
|
|
65128
|
+
noMatchCountSignal.value = noMatchCount;
|
|
65129
|
+
someChange = true;
|
|
65130
|
+
}
|
|
65131
|
+
for (const [groupId, group] of groupById) {
|
|
65132
|
+
group.countSignal.value = countByGroup.get(groupId) || 0;
|
|
65133
|
+
group.noMatchCountSignal.value = noMatchCountByGroup.get(groupId) || 0;
|
|
65134
|
+
}
|
|
65135
|
+
if (someChange && listRows.onChange) {
|
|
65136
|
+
listRows.onChange();
|
|
65137
|
+
}
|
|
65138
|
+
});
|
|
65139
|
+
};
|
|
65140
|
+
const notify = () => {
|
|
65141
|
+
if (notifyScheduled) {
|
|
65142
|
+
return;
|
|
65067
65143
|
}
|
|
65068
|
-
|
|
65069
|
-
|
|
65070
|
-
|
|
65071
|
-
|
|
65144
|
+
notifyScheduled = true;
|
|
65145
|
+
queueMicrotask(() => {
|
|
65146
|
+
if (!notifyScheduled) {
|
|
65147
|
+
return;
|
|
65148
|
+
}
|
|
65149
|
+
notifyScheduled = false;
|
|
65150
|
+
runNotify();
|
|
65151
|
+
});
|
|
65072
65152
|
};
|
|
65073
65153
|
|
|
65074
|
-
const
|
|
65075
|
-
|
|
65076
|
-
|
|
65077
|
-
|
|
65078
|
-
|
|
65079
|
-
|
|
65080
|
-
|
|
65081
|
-
// Register an item. data.hidden controls visibility.
|
|
65082
|
-
// explicitOrder is the caller-provided index (e.g. from items.map((item, i) => ...))
|
|
65083
|
-
// that determines this item's position among siblings.
|
|
65084
|
-
// Returns the item's visible rank among non-hidden items, or -1 when hidden.
|
|
65085
|
-
const useTrackItem = (data) => {
|
|
65086
|
-
const { id, index } = data;
|
|
65087
|
-
const key = keyForId(id);
|
|
65088
|
-
|
|
65089
|
-
syncItem(key, index, data);
|
|
65090
|
-
notify();
|
|
65091
|
-
|
|
65092
|
-
useLayoutEffect(() => {
|
|
65093
|
-
return () => {
|
|
65094
|
-
unregisterKey(key);
|
|
65095
|
-
notify();
|
|
65096
|
-
};
|
|
65097
|
-
}, []);
|
|
65098
|
-
|
|
65099
|
-
if (data.filtered || data.hidden || data.role === "presentation") {
|
|
65100
|
-
return -1;
|
|
65101
|
-
}
|
|
65102
|
-
return keyToOrderedIndex.get(key) ?? -1;
|
|
65103
|
-
};
|
|
65104
|
-
|
|
65105
|
-
const getTrackedItemByIndex = (index) => {
|
|
65106
|
-
const key = orderedKeys[index];
|
|
65107
|
-
if (key === undefined) {
|
|
65108
|
-
return undefined;
|
|
65109
|
-
}
|
|
65110
|
-
return registrations.get(key);
|
|
65111
|
-
};
|
|
65112
|
-
|
|
65113
|
-
// The items as they stand right now, notification pending or not — same
|
|
65114
|
-
// content as itemsSignal, minus the wait.
|
|
65115
|
-
//
|
|
65116
|
-
// Items register during their own render, while the signal is only updated
|
|
65117
|
-
// on a deferred microtask (see notify): a sibling rendering after them would
|
|
65118
|
-
// otherwise paint from an empty list and correct itself a frame later. That
|
|
65119
|
-
// frame is visible whenever the painted size feeds a layout decision — a
|
|
65120
|
-
// dialog sizing itself on its content measures the empty version and shifts
|
|
65121
|
-
// once the real one lands. Reading this instead makes the first paint the
|
|
65122
|
-
// right one. Callers must still subscribe to itemsSignal to re-render on
|
|
65123
|
-
// LATER changes; this is the value to display, not the notification.
|
|
65124
|
-
const peekItems = () => {
|
|
65125
|
-
if (!notifyScheduled) {
|
|
65126
|
-
return itemsSignal.peek();
|
|
65127
|
-
}
|
|
65128
|
-
const items = [];
|
|
65129
|
-
for (const key of allOrderedKeys) {
|
|
65130
|
-
items.push(allRegistrations.get(key));
|
|
65131
|
-
}
|
|
65132
|
-
return items;
|
|
65133
|
-
};
|
|
65134
|
-
|
|
65135
|
-
return {
|
|
65136
|
-
useTrackItem,
|
|
65137
|
-
getTrackedItemByIndex,
|
|
65138
|
-
peekItems,
|
|
65154
|
+
const listRows = {
|
|
65155
|
+
totalSignal,
|
|
65156
|
+
pagesSignal,
|
|
65157
|
+
refreshingSignal,
|
|
65158
|
+
// The rows that are items, in place order — every one drawn, and the ones
|
|
65159
|
+
// that show — and what the search made of them. Written once per frame.
|
|
65139
65160
|
itemsSignal,
|
|
65140
65161
|
visibleItemsSignal,
|
|
65141
65162
|
countSignal,
|
|
65142
65163
|
visibleCountSignal,
|
|
65143
65164
|
noMatchCountSignal,
|
|
65144
|
-
|
|
65165
|
+
// Called once per frame in which the items changed. Set by the list.
|
|
65166
|
+
onChange: null,
|
|
65167
|
+
// What a run needs to know about the list it lives in: how many rows the
|
|
65168
|
+
// list is willing to draw at once, which end it opens on, and how much
|
|
65169
|
+
// room one row is given — a row whose content has not arrived must take
|
|
65170
|
+
// exactly that, or the rows drawn would not reach where the list says they
|
|
65171
|
+
// are.
|
|
65172
|
+
renderBudget: 0,
|
|
65173
|
+
scrolled: "start",
|
|
65174
|
+
// The list is on its way somewhere: what the window frames is not what it
|
|
65175
|
+
// is about to frame, so a run must not fetch for it (see holdWindow).
|
|
65176
|
+
holdPending: false,
|
|
65177
|
+
// Called by a run just before rows land in it: what is on screen must not
|
|
65178
|
+
// move because something arrived above it. Set by the list itself.
|
|
65179
|
+
captureAnchor: () => {},
|
|
65180
|
+
horizontal: false,
|
|
65181
|
+
virtualItemSizeSignal: null,
|
|
65182
|
+
renderSkeleton: undefined,
|
|
65183
|
+
// The children a walk stands over, in order — said in one call, before any
|
|
65184
|
+
// of them renders, so that what a child asks next is answered against the
|
|
65185
|
+
// whole picture and not against the children that happened to render
|
|
65186
|
+
// first. Said again on every render of the walk, and heard only when
|
|
65187
|
+
// something moved.
|
|
65188
|
+
declareSlots: (parentSlotId, slotIds) => {
|
|
65189
|
+
const slotIdsPrevious = slotIdsByParent.get(parentSlotId);
|
|
65190
|
+
if (slotIdsPrevious && sameSlotIds(slotIdsPrevious, slotIds)) {
|
|
65191
|
+
return;
|
|
65192
|
+
}
|
|
65193
|
+
if (slotIdsPrevious) {
|
|
65194
|
+
const slotIdSet = new Set(slotIds);
|
|
65195
|
+
for (const slotId of slotIdsPrevious) {
|
|
65196
|
+
if (!slotIdSet.has(slotId)) {
|
|
65197
|
+
dropSlot(slotId);
|
|
65198
|
+
}
|
|
65199
|
+
}
|
|
65200
|
+
}
|
|
65201
|
+
slotIdsByParent.set(parentSlotId, slotIds);
|
|
65202
|
+
rebuildWalk();
|
|
65203
|
+
refreshPlaces();
|
|
65204
|
+
},
|
|
65205
|
+
// Whether something has taken this slot for its own: what it renders
|
|
65206
|
+
// inside is then its to place (a run draws its groups with their rows
|
|
65207
|
+
// already placed), and no walk inside it has anything to declare.
|
|
65208
|
+
slotHasOwner: (slotId) => ownerIdsBySlot.has(slotId),
|
|
65209
|
+
// Whether any run of rows lives in this list: what makes a render window
|
|
65210
|
+
// mean anything (see List's renderBudget).
|
|
65211
|
+
hasRuns: () => locatorByOwner.size > 0,
|
|
65212
|
+
setRowLocator: (ownerId, locate) => {
|
|
65213
|
+
locatorByOwner.set(ownerId, locate);
|
|
65214
|
+
},
|
|
65215
|
+
dropRowLocator: (ownerId) => {
|
|
65216
|
+
locatorByOwner.delete(ownerId);
|
|
65217
|
+
},
|
|
65218
|
+
// Where the row named by that id sits, asked of whoever holds it.
|
|
65219
|
+
locateRow: (id) => {
|
|
65220
|
+
for (const locate of locatorByOwner.values()) {
|
|
65221
|
+
const index = locate(id);
|
|
65222
|
+
if (index !== null) {
|
|
65223
|
+
return index;
|
|
65224
|
+
}
|
|
65225
|
+
}
|
|
65226
|
+
return null;
|
|
65227
|
+
},
|
|
65228
|
+
// The place the owner's rows start at — read from a signal, so that the
|
|
65229
|
+
// owner is rendered again when it moves (see the top of this file). Asked on
|
|
65230
|
+
// every render, and answered without a second look for as long as the
|
|
65231
|
+
// owner stands in the same slot for the same number of rows.
|
|
65232
|
+
take: (ownerId, rowCount, slotId) => {
|
|
65233
|
+
let owner = ownerById.get(ownerId);
|
|
65234
|
+
if (owner) {
|
|
65235
|
+
if (owner.slotId !== slotId || owner.rowCount !== rowCount) {
|
|
65236
|
+
removeFromSlot(owner.slotId, ownerId);
|
|
65237
|
+
addToSlot(slotId, ownerId);
|
|
65238
|
+
owner.slotId = slotId;
|
|
65239
|
+
owner.rowCount = rowCount;
|
|
65240
|
+
placesStale = true;
|
|
65241
|
+
}
|
|
65242
|
+
if (placesStale) {
|
|
65243
|
+
refreshPlaces();
|
|
65244
|
+
}
|
|
65245
|
+
return owner.placeSignal.value;
|
|
65246
|
+
}
|
|
65247
|
+
if (placesStale) {
|
|
65248
|
+
refreshPlaces();
|
|
65249
|
+
}
|
|
65250
|
+
const rank = rankBySlot.get(slotId);
|
|
65251
|
+
addToSlot(slotId, ownerId);
|
|
65252
|
+
if (rank !== undefined && rank >= rankOwnedLast) {
|
|
65253
|
+
owner = { slotId, rowCount, placeSignal: signal(rowTotal) };
|
|
65254
|
+
ownerById.set(ownerId, owner);
|
|
65255
|
+
rowTotal += rowCount;
|
|
65256
|
+
rankOwnedLast = rank;
|
|
65257
|
+
totalSignal.value = rowTotal;
|
|
65258
|
+
return owner.placeSignal.value;
|
|
65259
|
+
}
|
|
65260
|
+
owner = { slotId, rowCount, placeSignal: signal(0) };
|
|
65261
|
+
ownerById.set(ownerId, owner);
|
|
65262
|
+
refreshPlaces();
|
|
65263
|
+
return owner.placeSignal.value;
|
|
65264
|
+
},
|
|
65265
|
+
// The owner stands for no row of the collection: it was filtered out by a
|
|
65266
|
+
// search, or it is gone.
|
|
65267
|
+
drop: (ownerId) => {
|
|
65268
|
+
const owner = ownerById.get(ownerId);
|
|
65269
|
+
if (!owner) {
|
|
65270
|
+
return;
|
|
65271
|
+
}
|
|
65272
|
+
ownerById.delete(ownerId);
|
|
65273
|
+
removeFromSlot(owner.slotId, ownerId);
|
|
65274
|
+
windowByOwner.delete(ownerId);
|
|
65275
|
+
if (placesStale) {
|
|
65276
|
+
return;
|
|
65277
|
+
}
|
|
65278
|
+
placesStale = true;
|
|
65279
|
+
queueMicrotask(() => {
|
|
65280
|
+
if (placesStale) {
|
|
65281
|
+
refreshPlaces();
|
|
65282
|
+
}
|
|
65283
|
+
});
|
|
65284
|
+
},
|
|
65285
|
+
|
|
65286
|
+
// ---- the rows drawn ----
|
|
65287
|
+
|
|
65288
|
+
// A run says which of its rows it draws. The others stand: rows above the
|
|
65289
|
+
// window are above every row drawn, whether or not any of the drawn ones
|
|
65290
|
+
// mounts (see refreshFirst).
|
|
65291
|
+
declareWindow: (ownerId, from, to) => {
|
|
65292
|
+
const window = windowByOwner.get(ownerId);
|
|
65293
|
+
if (window && window.from === from && window.to === to) {
|
|
65294
|
+
return;
|
|
65295
|
+
}
|
|
65296
|
+
windowByOwner.set(ownerId, { from, to });
|
|
65297
|
+
markStale(UNGROUPED);
|
|
65298
|
+
},
|
|
65299
|
+
// A row says what it is, where it renders: its place, the group it is
|
|
65300
|
+
// in, and its data — from which follows whether it mounts at all
|
|
65301
|
+
// (filtered out), whether it shows (hidden keeps the room, not the
|
|
65302
|
+
// content), and whether it is an item (a group wrapper, a skeleton, are
|
|
65303
|
+
// rows of the list but items of nobody). Said in the name of the
|
|
65304
|
+
// component, not of the item id: two components may stand for one item for
|
|
65305
|
+
// a moment, one leaving as the other arrives.
|
|
65306
|
+
draw: (rowId, { ownerId, place, groupId, data }) => {
|
|
65307
|
+
const mounted = !data.filtered;
|
|
65308
|
+
const visible = mounted && !data.hidden;
|
|
65309
|
+
const item = !data.skeleton && data.role !== "presentation";
|
|
65310
|
+
let row = rowById.get(rowId);
|
|
65311
|
+
if (row) {
|
|
65312
|
+
if (
|
|
65313
|
+
row.mounted &&
|
|
65314
|
+
(row.place !== place || row.groupId !== groupId || !mounted)
|
|
65315
|
+
) {
|
|
65316
|
+
leaveFirst(row);
|
|
65317
|
+
}
|
|
65318
|
+
row.ownerId = ownerId;
|
|
65319
|
+
row.place = place;
|
|
65320
|
+
row.groupId = groupId;
|
|
65321
|
+
row.data = data;
|
|
65322
|
+
row.mounted = mounted;
|
|
65323
|
+
row.visible = visible;
|
|
65324
|
+
row.item = item;
|
|
65325
|
+
} else {
|
|
65326
|
+
row = { ownerId, place, groupId, data, mounted, visible, item };
|
|
65327
|
+
rowById.set(rowId, row);
|
|
65328
|
+
}
|
|
65329
|
+
if (mounted) {
|
|
65330
|
+
const firstSignal = firstSignalOf(scopeOf(groupId));
|
|
65331
|
+
const first = firstSignal.peek();
|
|
65332
|
+
if (first === -1 || place < first) {
|
|
65333
|
+
firstSignal.value = place;
|
|
65334
|
+
}
|
|
65335
|
+
}
|
|
65336
|
+
notify();
|
|
65337
|
+
},
|
|
65338
|
+
// The row is gone. A declared row is its own owner and gives its place
|
|
65339
|
+
// back with it; a run's row leaves the run's places alone.
|
|
65340
|
+
erase: (rowId) => {
|
|
65341
|
+
const row = rowById.get(rowId);
|
|
65342
|
+
if (!row) {
|
|
65343
|
+
return;
|
|
65344
|
+
}
|
|
65345
|
+
rowById.delete(rowId);
|
|
65346
|
+
if (row.mounted) {
|
|
65347
|
+
leaveFirst(row);
|
|
65348
|
+
}
|
|
65349
|
+
if (row.ownerId === rowId) {
|
|
65350
|
+
listRows.drop(rowId);
|
|
65351
|
+
}
|
|
65352
|
+
notify();
|
|
65353
|
+
},
|
|
65354
|
+
// Whether nothing of the list stands above this row: in its group, no
|
|
65355
|
+
// other row of the group mounts before it; outside groups, no row mounts
|
|
65356
|
+
// before it and no run has rows above its window before it. Answered from
|
|
65357
|
+
// a signal, so a row rendered from a kept vnode is rendered again when the
|
|
65358
|
+
// row before it leaves or comes back.
|
|
65359
|
+
isFirst: (rowId) => {
|
|
65360
|
+
const row = rowById.get(rowId);
|
|
65361
|
+
const scope = scopeOf(row.groupId);
|
|
65362
|
+
if (staleScopes.has(scope)) {
|
|
65363
|
+
refreshFirst(scope);
|
|
65364
|
+
}
|
|
65365
|
+
return firstSignalOf(scope).value === row.place;
|
|
65366
|
+
},
|
|
65367
|
+
// What a group knows about its rows: how many, and how many of them the
|
|
65368
|
+
// search left out (see ListItemGroup).
|
|
65369
|
+
group: (groupId) => groupOf(groupId),
|
|
65370
|
+
dropGroup: (groupId) => {
|
|
65371
|
+
groupById.delete(groupId);
|
|
65372
|
+
staleScopes.delete(groupId);
|
|
65373
|
+
},
|
|
65374
|
+
// Written when a frame's rows have settled (see notify); the value to act
|
|
65375
|
+
// on is peeked from wherever the change is heard.
|
|
65376
|
+
flushSync: () => {
|
|
65377
|
+
if (!notifyScheduled) {
|
|
65378
|
+
return;
|
|
65379
|
+
}
|
|
65380
|
+
notifyScheduled = false;
|
|
65381
|
+
runNotify();
|
|
65382
|
+
},
|
|
65145
65383
|
};
|
|
65384
|
+
return listRows;
|
|
65146
65385
|
};
|
|
65147
|
-
|
|
65148
|
-
|
|
65149
|
-
|
|
65150
|
-
if (props.header) {
|
|
65151
|
-
return renderResolver(ListItemHeader, props);
|
|
65386
|
+
const sameSlotIds = (left, right) => {
|
|
65387
|
+
if (left.length !== right.length) {
|
|
65388
|
+
return false;
|
|
65152
65389
|
}
|
|
65153
|
-
|
|
65154
|
-
|
|
65390
|
+
let index = 0;
|
|
65391
|
+
while (index < left.length) {
|
|
65392
|
+
if (left[index] !== right[index]) {
|
|
65393
|
+
return false;
|
|
65394
|
+
}
|
|
65395
|
+
index++;
|
|
65155
65396
|
}
|
|
65156
|
-
return
|
|
65157
|
-
...props
|
|
65158
|
-
});
|
|
65397
|
+
return true;
|
|
65159
65398
|
};
|
|
65160
|
-
|
|
65161
|
-
|
|
65162
|
-
|
|
65163
|
-
|
|
65164
|
-
|
|
65165
|
-
|
|
65166
|
-
|
|
65167
|
-
|
|
65168
|
-
|
|
65169
|
-
|
|
65170
|
-
}, []);
|
|
65171
|
-
return jsx(Next, {
|
|
65172
|
-
...props,
|
|
65173
|
-
header: undefined,
|
|
65174
|
-
role: "presentation",
|
|
65175
|
-
baseClassName: "navi_list_item_header"
|
|
65176
|
-
});
|
|
65177
|
-
};
|
|
65178
|
-
const ListItemFooter = props => {
|
|
65179
|
-
const Next = useNextResolver();
|
|
65180
|
-
const {
|
|
65181
|
-
ref
|
|
65182
|
-
} = props;
|
|
65183
|
-
useDisplayedLayoutEffect(ref, footerEl => {
|
|
65184
|
-
const listContainerEl = footerEl.closest(".navi_list_container");
|
|
65185
|
-
const rect = footerEl.getBoundingClientRect();
|
|
65186
|
-
listContainerEl.style.setProperty("--list-footer-height", `${rect.height}px`);
|
|
65187
|
-
listContainerEl.style.setProperty("--list-footer-width", `${rect.width}px`);
|
|
65188
|
-
}, []);
|
|
65189
|
-
return jsx(Next, {
|
|
65190
|
-
...props,
|
|
65191
|
-
footer: undefined,
|
|
65192
|
-
role: "presentation",
|
|
65193
|
-
baseClassName: "navi_list_item_footer"
|
|
65194
|
-
});
|
|
65399
|
+
|
|
65400
|
+
// Rows in place order; a row with no place (a declared row filtered out) last.
|
|
65401
|
+
const compareRowPlaces = (left, right) => {
|
|
65402
|
+
if (left.place === undefined) {
|
|
65403
|
+
return right.place === undefined ? 0 : 1;
|
|
65404
|
+
}
|
|
65405
|
+
if (right.place === undefined) {
|
|
65406
|
+
return -1;
|
|
65407
|
+
}
|
|
65408
|
+
return left.place - right.place;
|
|
65195
65409
|
};
|
|
65196
65410
|
|
|
65197
65411
|
// What a row may say it is waiting on, all of them about the row as a thing
|
|
@@ -65999,8 +66213,10 @@ const applySearchHighlight = (el, highlight) => {
|
|
|
65999
66213
|
};
|
|
66000
66214
|
|
|
66001
66215
|
installImportMetaCssBuild(import.meta);
|
|
66002
|
-
const
|
|
66003
|
-
|
|
66216
|
+
const ListRowsContext = createContext(null);
|
|
66217
|
+
// The group a row is declared in, by id (see ListItemGroup): what its count
|
|
66218
|
+
// and its separator are scoped to.
|
|
66219
|
+
const ListGroupContext = createContext(undefined);
|
|
66004
66220
|
const PendingScrollRefContext = createContext(null);
|
|
66005
66221
|
// Controls how List.Item behaves when match=false (set via List searchNoMatchMode prop):
|
|
66006
66222
|
// "remove" — remove from DOM (default)
|
|
@@ -66039,20 +66255,15 @@ const SeparatorContext = createContext(null);
|
|
|
66039
66255
|
// Set by <List itemTransition>: each row then gets a view-transition-name of
|
|
66040
66256
|
// its own, so a change wrapped in a view transition animates row by row.
|
|
66041
66257
|
const ItemTransitionContext = createContext(false);
|
|
66042
|
-
// What the list knows about the collection as a whole: how many rows it has,
|
|
66043
|
-
// which of them it actually holds, and where each child's rows start. Filled in
|
|
66044
|
-
// by the children as they render (see createListVirtual), read by everything
|
|
66045
|
-
// that must reserve room for what is not rendered.
|
|
66046
|
-
const ListVirtualContext = createContext(null);
|
|
66047
66258
|
// Set around each row a run of items renders (see ListItems): which row of the
|
|
66048
|
-
// collection it is,
|
|
66049
|
-
// by context rather than injected into whatever
|
|
66050
|
-
// that returning a component of one's own —
|
|
66051
|
-
// works the same way.
|
|
66259
|
+
// collection it is, where it stands among the rows the list holds, and which
|
|
66260
|
+
// run it belongs to. Carried by context rather than injected into whatever
|
|
66261
|
+
// vnode renderItem returned, so that returning a component of one's own —
|
|
66262
|
+
// instead of a bare <List.Item> — works the same way.
|
|
66052
66263
|
const ListRowContext = createContext(null);
|
|
66053
66264
|
// The slot a child of the list stands in, by id (see ListDeclaredChildren). A
|
|
66054
66265
|
// row takes its place in the collection by slot: the place is then the list's
|
|
66055
|
-
// to move, and the row's to follow — see
|
|
66266
|
+
// to move, and the row's to follow — see list_rows.js.
|
|
66056
66267
|
const ListSlotContext = createContext(null);
|
|
66057
66268
|
const css$x = /* css */`@layer navi {
|
|
66058
66269
|
.navi_list_container {
|
|
@@ -66543,7 +66754,7 @@ const ListUI = props => {
|
|
|
66543
66754
|
overflow,
|
|
66544
66755
|
overflowX,
|
|
66545
66756
|
overflowY,
|
|
66546
|
-
|
|
66757
|
+
listRows,
|
|
66547
66758
|
...rest
|
|
66548
66759
|
} = props;
|
|
66549
66760
|
const scrollBoxPaddingProps = {};
|
|
@@ -66612,16 +66823,19 @@ const ListUI = props => {
|
|
|
66612
66823
|
observer.disconnect();
|
|
66613
66824
|
};
|
|
66614
66825
|
}, [lockSize]);
|
|
66615
|
-
|
|
66616
|
-
|
|
66617
|
-
|
|
66618
|
-
|
|
66826
|
+
listRows.onChange = () => {
|
|
66827
|
+
onListVisibleItemsChange?.(listRows.visibleItemsSignal.peek());
|
|
66828
|
+
};
|
|
66829
|
+
// Code in a layout effect of the list reads the rows as they stand after
|
|
66830
|
+
// the commit; the rows settle on a microtask, which preact does not wait for.
|
|
66831
|
+
useLayoutEffect(() => {
|
|
66832
|
+
listRows.flushSync();
|
|
66619
66833
|
});
|
|
66620
66834
|
// What the runs ask for and stand for: the steady budget, whatever the
|
|
66621
66835
|
// window of the first paint draws — a run asking for the rows of the first
|
|
66622
66836
|
// picture and then for the rest is two round trips for one opening.
|
|
66623
|
-
|
|
66624
|
-
|
|
66837
|
+
listRows.renderBudget = renderBudgetAfterPaint;
|
|
66838
|
+
listRows.scrolled = scrolled ?? defaultScrolled;
|
|
66625
66839
|
const {
|
|
66626
66840
|
virtualItemSizeSignal,
|
|
66627
66841
|
renderWindow,
|
|
@@ -66630,11 +66844,10 @@ const ListUI = props => {
|
|
|
66630
66844
|
captureAnchor
|
|
66631
66845
|
} = useListScrollSync({
|
|
66632
66846
|
ref,
|
|
66633
|
-
|
|
66847
|
+
listRows,
|
|
66634
66848
|
renderBudget,
|
|
66635
66849
|
renderBudgetSteady: renderBudgetAfterPaint,
|
|
66636
66850
|
virtualItemSize,
|
|
66637
|
-
virtual,
|
|
66638
66851
|
scrolled,
|
|
66639
66852
|
defaultScrolled,
|
|
66640
66853
|
onScrolledChange,
|
|
@@ -66653,28 +66866,28 @@ const ListUI = props => {
|
|
|
66653
66866
|
if (props.renderBudget === undefined || renderBudgetWarnedRef.current) {
|
|
66654
66867
|
return;
|
|
66655
66868
|
}
|
|
66656
|
-
if (
|
|
66869
|
+
if (listRows.hasRuns() || listRows.itemsSignal.peek().length === 0) {
|
|
66657
66870
|
return;
|
|
66658
66871
|
}
|
|
66659
66872
|
renderBudgetWarnedRef.current = true;
|
|
66660
66873
|
console.warn(`List: renderBudget=${renderBudget} has no effect here. The render window frames the rows a run draws (<List.Items itemsAction>); items declared one by one (<List.Item>) are all rendered. Move the items to <List.Items> to cap the number of DOM nodes, or drop the prop.`);
|
|
66661
66874
|
});
|
|
66662
|
-
|
|
66663
|
-
|
|
66664
|
-
|
|
66665
|
-
|
|
66875
|
+
listRows.captureAnchor = captureAnchor;
|
|
66876
|
+
listRows.virtualItemSizeSignal = virtualItemSizeSignal;
|
|
66877
|
+
listRows.horizontal = Boolean(horizontal);
|
|
66878
|
+
listRows.renderSkeleton = renderSkeleton;
|
|
66666
66879
|
|
|
66667
66880
|
// A row is addressed by id from outside (--navi-scroll, --navi-select): the
|
|
66668
|
-
// ones drawn have
|
|
66881
|
+
// ones drawn have said so (see list_rows.js), and the ones a run
|
|
66669
66882
|
// holds without drawing are known only to that run (see List.Items' row
|
|
66670
66883
|
// locator). Both answer here, so a row is reachable whether or not the
|
|
66671
66884
|
// window happens to frame it.
|
|
66672
66885
|
const getItemById = itemId => {
|
|
66673
|
-
const itemDrawn =
|
|
66886
|
+
const itemDrawn = listRows.itemsSignal.peek().find(item => item.id === itemId);
|
|
66674
66887
|
if (itemDrawn) {
|
|
66675
66888
|
return itemDrawn;
|
|
66676
66889
|
}
|
|
66677
|
-
const rowIndex =
|
|
66890
|
+
const rowIndex = listRows.locateRow(itemId);
|
|
66678
66891
|
if (rowIndex === null) {
|
|
66679
66892
|
return undefined;
|
|
66680
66893
|
}
|
|
@@ -66683,13 +66896,13 @@ const ListUI = props => {
|
|
|
66683
66896
|
index: rowIndex
|
|
66684
66897
|
};
|
|
66685
66898
|
};
|
|
66686
|
-
const noMatchCount =
|
|
66899
|
+
const noMatchCount = listRows.noMatchCountSignal.value;
|
|
66687
66900
|
// What the list stands for, which is not always what it holds: a run saying
|
|
66688
66901
|
// it covers 60 rows is not an empty list while it waits for the first of
|
|
66689
66902
|
// them (see List.Items).
|
|
66690
66903
|
// eslint-disable-next-line no-unused-expressions
|
|
66691
|
-
|
|
66692
|
-
const itemCount =
|
|
66904
|
+
listRows.pagesSignal.value;
|
|
66905
|
+
const itemCount = listRows.countSignal.value || listRows.totalSignal.value;
|
|
66693
66906
|
const allNoMatch = noMatchCount > 0 && noMatchCount === itemCount;
|
|
66694
66907
|
const searching = Boolean(searchText);
|
|
66695
66908
|
const fallbackDisabled = fallback !== undefined && !fallback;
|
|
@@ -66787,7 +67000,7 @@ const ListUI = props => {
|
|
|
66787
67000
|
expand: expand,
|
|
66788
67001
|
"navi-nothing-to-display": nothingToDisplay ? "" : undefined,
|
|
66789
67002
|
"navi-loading": loading ? "" : undefined,
|
|
66790
|
-
"navi-refreshing":
|
|
67003
|
+
"navi-refreshing": listRows.refreshingSignal.value ? "" : undefined,
|
|
66791
67004
|
"navi-error": error ? "" : undefined,
|
|
66792
67005
|
styleCSSVars: LIST_STYLE_CSS_VARS,
|
|
66793
67006
|
pseudoClasses: LIST_PSEUDO_CLASSES,
|
|
@@ -66823,9 +67036,8 @@ const ListUI = props => {
|
|
|
66823
67036
|
spacing: spacing,
|
|
66824
67037
|
columns: columns,
|
|
66825
67038
|
itemColumns: itemColumns,
|
|
66826
|
-
|
|
67039
|
+
listRows: listRows,
|
|
66827
67040
|
renderWindow: renderWindow,
|
|
66828
|
-
virtual: virtual,
|
|
66829
67041
|
pendingScrollRef: pendingScrollRef,
|
|
66830
67042
|
overflow: overflow,
|
|
66831
67043
|
overflowX: overflowX,
|
|
@@ -66846,11 +67058,11 @@ const ListFirstResolver = props => {
|
|
|
66846
67058
|
props.ref = props.ref || refDefault;
|
|
66847
67059
|
const idDefault = useId();
|
|
66848
67060
|
props.id = props.id || idDefault;
|
|
66849
|
-
const
|
|
66850
|
-
if (!
|
|
66851
|
-
|
|
67061
|
+
const listRowsRef = useRef(null);
|
|
67062
|
+
if (!listRowsRef.current) {
|
|
67063
|
+
listRowsRef.current = createListRows();
|
|
66852
67064
|
}
|
|
66853
|
-
props.
|
|
67065
|
+
props.listRows = listRowsRef.current;
|
|
66854
67066
|
const parallelGuard = useParallelGuard(props.parallelGuard ?? PARALLEL_GUARD_DEFAULT);
|
|
66855
67067
|
return jsx(ParallelGuardContext.Provider, {
|
|
66856
67068
|
value: parallelGuard,
|
|
@@ -66876,9 +67088,8 @@ const ListContent = ({
|
|
|
66876
67088
|
spacing,
|
|
66877
67089
|
columns,
|
|
66878
67090
|
itemColumns,
|
|
66879
|
-
|
|
67091
|
+
listRows,
|
|
66880
67092
|
renderWindow,
|
|
66881
|
-
virtual,
|
|
66882
67093
|
pendingScrollRef,
|
|
66883
67094
|
overflow,
|
|
66884
67095
|
overflowX,
|
|
@@ -66932,9 +67143,8 @@ const ListContent = ({
|
|
|
66932
67143
|
columns: columns,
|
|
66933
67144
|
itemColumns: itemColumns,
|
|
66934
67145
|
...listProps,
|
|
66935
|
-
|
|
67146
|
+
listRows: listRows,
|
|
66936
67147
|
renderWindow: renderWindow,
|
|
66937
|
-
virtual: virtual,
|
|
66938
67148
|
children: children
|
|
66939
67149
|
})
|
|
66940
67150
|
})
|
|
@@ -66958,11 +67168,10 @@ const LIST_STYLE_CSS_VARS = {
|
|
|
66958
67168
|
const LIST_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":focus-within", ":read-only", ":disabled", ":-navi-void", ":-navi-expanded"];
|
|
66959
67169
|
const useListScrollSync = ({
|
|
66960
67170
|
ref,
|
|
66961
|
-
|
|
67171
|
+
listRows,
|
|
66962
67172
|
renderBudget,
|
|
66963
67173
|
renderBudgetSteady,
|
|
66964
67174
|
virtualItemSize,
|
|
66965
|
-
virtual,
|
|
66966
67175
|
scrolled,
|
|
66967
67176
|
defaultScrolled,
|
|
66968
67177
|
onScrolledChange,
|
|
@@ -66972,7 +67181,7 @@ const useListScrollSync = ({
|
|
|
66972
67181
|
}) => {
|
|
66973
67182
|
const debugScroll = useDebugScroll();
|
|
66974
67183
|
const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
|
|
66975
|
-
|
|
67184
|
+
listRows,
|
|
66976
67185
|
renderBudget,
|
|
66977
67186
|
scrolledWanted: scrolled ?? defaultScrolled
|
|
66978
67187
|
});
|
|
@@ -66997,7 +67206,7 @@ const useListScrollSync = ({
|
|
|
66997
67206
|
ref,
|
|
66998
67207
|
scrollerElResolved,
|
|
66999
67208
|
renderBudget,
|
|
67000
|
-
totalSignal:
|
|
67209
|
+
totalSignal: listRows.totalSignal,
|
|
67001
67210
|
virtualItemSizeSignal,
|
|
67002
67211
|
horizontal
|
|
67003
67212
|
});
|
|
@@ -67022,7 +67231,7 @@ const useListScrollSync = ({
|
|
|
67022
67231
|
anchorRef.current = captureScrollAnchor({
|
|
67023
67232
|
scrollerEl: getScroller(),
|
|
67024
67233
|
listEl: getListEl(),
|
|
67025
|
-
items:
|
|
67234
|
+
items: listRows.visibleItemsSignal.peek(),
|
|
67026
67235
|
horizontal
|
|
67027
67236
|
});
|
|
67028
67237
|
};
|
|
@@ -67054,7 +67263,7 @@ const useListScrollSync = ({
|
|
|
67054
67263
|
start,
|
|
67055
67264
|
end
|
|
67056
67265
|
} = renderWindowRef.current;
|
|
67057
|
-
const total =
|
|
67266
|
+
const total = listRows.totalSignal.peek();
|
|
67058
67267
|
let framedStart = start;
|
|
67059
67268
|
let framedEnd = start + renderBudget;
|
|
67060
67269
|
if (total > 0 && framedEnd > total) {
|
|
@@ -67097,19 +67306,19 @@ const useListScrollSync = ({
|
|
|
67097
67306
|
// jumped.
|
|
67098
67307
|
const holdWindow = () => {
|
|
67099
67308
|
if (startPlaceRef.current.userTookOver) {
|
|
67100
|
-
|
|
67309
|
+
listRows.holdPending = false;
|
|
67101
67310
|
return;
|
|
67102
67311
|
}
|
|
67103
67312
|
// Held somewhere it has not reached yet: what the window frames right now
|
|
67104
67313
|
// is not what it will frame, so nothing should be fetched for it.
|
|
67105
|
-
|
|
67106
|
-
const total =
|
|
67314
|
+
listRows.holdPending = scrolledWanted !== "start" && scrolledWanted !== undefined;
|
|
67315
|
+
const total = listRows.totalSignal.peek();
|
|
67107
67316
|
if (total <= renderBudget) {
|
|
67108
67317
|
// The whole collection is what the list draws: wherever in it the list is
|
|
67109
67318
|
// held, the window is already its place. Nowhere to move to means nothing
|
|
67110
67319
|
// to wait for — a hold left standing here is a list that never asks for
|
|
67111
67320
|
// anything again.
|
|
67112
|
-
|
|
67321
|
+
listRows.holdPending = false;
|
|
67113
67322
|
return;
|
|
67114
67323
|
}
|
|
67115
67324
|
const half = Math.floor(renderBudget / 2);
|
|
@@ -67119,7 +67328,7 @@ const useListScrollSync = ({
|
|
|
67119
67328
|
} else if (typeof scrolledWanted === "number") {
|
|
67120
67329
|
wantedStart = scrolledWanted - half;
|
|
67121
67330
|
} else if (scrolledWanted && scrolledWanted.id !== undefined) {
|
|
67122
|
-
const rowIndex =
|
|
67331
|
+
const rowIndex = listRows.locateRow(scrolledWanted.id);
|
|
67123
67332
|
if (rowIndex !== null) {
|
|
67124
67333
|
wantedStart = rowIndex - half;
|
|
67125
67334
|
} else if (typeof scrolledWanted.index === "number") {
|
|
@@ -67146,14 +67355,14 @@ const useListScrollSync = ({
|
|
|
67146
67355
|
end
|
|
67147
67356
|
} = renderWindowRef.current;
|
|
67148
67357
|
if (wantedStart === start && end - start === renderBudget) {
|
|
67149
|
-
|
|
67358
|
+
listRows.holdPending = false;
|
|
67150
67359
|
return;
|
|
67151
67360
|
}
|
|
67152
67361
|
renderWindowRef.current = {
|
|
67153
67362
|
start: wantedStart,
|
|
67154
67363
|
end: wantedStart + renderBudget
|
|
67155
67364
|
};
|
|
67156
|
-
|
|
67365
|
+
listRows.holdPending = false;
|
|
67157
67366
|
};
|
|
67158
67367
|
const pendingScrollRef = useRef();
|
|
67159
67368
|
const scrollToItem = (item, {
|
|
@@ -67164,7 +67373,7 @@ const useListScrollSync = ({
|
|
|
67164
67373
|
if (!item) {
|
|
67165
67374
|
return;
|
|
67166
67375
|
}
|
|
67167
|
-
const items =
|
|
67376
|
+
const items = listRows.itemsSignal.peek();
|
|
67168
67377
|
const itemCount = items.length;
|
|
67169
67378
|
if (itemCount === 0) {
|
|
67170
67379
|
return;
|
|
@@ -67292,7 +67501,7 @@ const useListScrollSync = ({
|
|
|
67292
67501
|
return;
|
|
67293
67502
|
}
|
|
67294
67503
|
hasBeenDisplayedRef.current = true;
|
|
67295
|
-
const items =
|
|
67504
|
+
const items = listRows.itemsSignal.peek();
|
|
67296
67505
|
const firstSelected = items.find(i => {
|
|
67297
67506
|
if (i.selected) {
|
|
67298
67507
|
return true;
|
|
@@ -67376,7 +67585,7 @@ const useListScrollSync = ({
|
|
|
67376
67585
|
scrollValues: savedScroll,
|
|
67377
67586
|
scrollerEl: listScrollContainerEl,
|
|
67378
67587
|
listEl: getListEl(),
|
|
67379
|
-
|
|
67588
|
+
listRows,
|
|
67380
67589
|
virtualItemSizeSignal,
|
|
67381
67590
|
renderWindowRef,
|
|
67382
67591
|
horizontal
|
|
@@ -67393,7 +67602,7 @@ const useListScrollSync = ({
|
|
|
67393
67602
|
});
|
|
67394
67603
|
return undefined;
|
|
67395
67604
|
}
|
|
67396
|
-
const visibleItems =
|
|
67605
|
+
const visibleItems = listRows.visibleItemsSignal.peek();
|
|
67397
67606
|
const topItems = visibleItems.slice(0, renderBudget);
|
|
67398
67607
|
const topMatchScoresKey = topItems.map(i => `${i.id}:${i.matchInfo?.matchScore ?? ""}`).join(",");
|
|
67399
67608
|
const currentTopMatchScore = topMatchScoresKeyRef.current;
|
|
@@ -67431,7 +67640,7 @@ const useListScrollSync = ({
|
|
|
67431
67640
|
if (scrolledWanted === "start" || scrolledWanted === undefined || startPlaceRef.current.userTookOver || !ref.current) {
|
|
67432
67641
|
return;
|
|
67433
67642
|
}
|
|
67434
|
-
if (
|
|
67643
|
+
if (listRows.totalSignal.peek() === 0 || virtualItemSizeSignal.peek() === 0) {
|
|
67435
67644
|
return;
|
|
67436
67645
|
}
|
|
67437
67646
|
// Coming back to a named row: it has to be on screen to be put back where
|
|
@@ -67443,13 +67652,13 @@ const useListScrollSync = ({
|
|
|
67443
67652
|
// Only whoever holds the rows can say where that one sits: the list
|
|
67444
67653
|
// itself knows the rows it has drawn, and this one is precisely the one
|
|
67445
67654
|
// it has not drawn yet.
|
|
67446
|
-
const rowIndex =
|
|
67655
|
+
const rowIndex = listRows.locateRow(scrolledWanted.id);
|
|
67447
67656
|
if (rowIndex === null) {
|
|
67448
67657
|
// Not there yet. Where it stood is enough to be roughly right in the
|
|
67449
67658
|
// meantime — the scrollbar lands near its final place instead of at the
|
|
67450
67659
|
// top, and the exact position is taken once the row itself can be
|
|
67451
67660
|
// measured.
|
|
67452
|
-
if (
|
|
67661
|
+
if (listRows.pagesSignal.peek() === 0) {
|
|
67453
67662
|
if (typeof scrolledWanted.index === "number") {
|
|
67454
67663
|
const rowPosition = scrolledWanted.index * virtualItemSizeSignal.peek();
|
|
67455
67664
|
anchorRef.current = null;
|
|
@@ -67566,7 +67775,7 @@ const useListScrollSync = ({
|
|
|
67566
67775
|
const position = captureScrollAnchor({
|
|
67567
67776
|
scrollerEl: getScroller(),
|
|
67568
67777
|
listEl: getListEl(),
|
|
67569
|
-
items:
|
|
67778
|
+
items: listRows.visibleItemsSignal.peek(),
|
|
67570
67779
|
horizontal
|
|
67571
67780
|
});
|
|
67572
67781
|
if (!position) {
|
|
@@ -67654,7 +67863,7 @@ const useListScrollSync = ({
|
|
|
67654
67863
|
anchorRef.current = null;
|
|
67655
67864
|
return;
|
|
67656
67865
|
}
|
|
67657
|
-
const items =
|
|
67866
|
+
const items = listRows.visibleItemsSignal.peek();
|
|
67658
67867
|
const itemNow = items.find(i => i.id === anchor.id);
|
|
67659
67868
|
if (!itemNow) {
|
|
67660
67869
|
anchorRef.current = null;
|
|
@@ -67676,7 +67885,7 @@ const useListScrollSync = ({
|
|
|
67676
67885
|
const windowSize = end - start;
|
|
67677
67886
|
const startShifted = start + indexShift;
|
|
67678
67887
|
let startWanted = startShifted < 0 ? 0 : startShifted;
|
|
67679
|
-
const total =
|
|
67888
|
+
const total = listRows.totalSignal.peek();
|
|
67680
67889
|
// Same normalization as the scroll listener: a window running past the
|
|
67681
67890
|
// last row slides back instead of framing fewer rows than its budget
|
|
67682
67891
|
// allows — every row that fits in it must stay rendered.
|
|
@@ -67734,7 +67943,7 @@ const useListScrollSync = ({
|
|
|
67734
67943
|
const windowSlidRef = useRef(false);
|
|
67735
67944
|
useRef(false);
|
|
67736
67945
|
const evaluateWindow = reason => {
|
|
67737
|
-
const total =
|
|
67946
|
+
const total = listRows.totalSignal.peek();
|
|
67738
67947
|
if (total <= renderBudget) {
|
|
67739
67948
|
return;
|
|
67740
67949
|
}
|
|
@@ -67756,7 +67965,7 @@ const useListScrollSync = ({
|
|
|
67756
67965
|
},
|
|
67757
67966
|
scrollerEl,
|
|
67758
67967
|
listEl,
|
|
67759
|
-
|
|
67968
|
+
listRows,
|
|
67760
67969
|
virtualItemSizeSignal,
|
|
67761
67970
|
renderWindowRef,
|
|
67762
67971
|
horizontal
|
|
@@ -68355,12 +68564,12 @@ const getScrollInfo = ({
|
|
|
68355
68564
|
scrollValues,
|
|
68356
68565
|
scrollerEl,
|
|
68357
68566
|
listEl,
|
|
68358
|
-
|
|
68567
|
+
listRows,
|
|
68359
68568
|
virtualItemSizeSignal,
|
|
68360
68569
|
renderWindowRef,
|
|
68361
68570
|
horizontal
|
|
68362
68571
|
}) => {
|
|
68363
|
-
const items =
|
|
68572
|
+
const items = listRows.itemsSignal.peek();
|
|
68364
68573
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
68365
68574
|
const listRect = listEl.getBoundingClientRect();
|
|
68366
68575
|
let hitEl = null;
|
|
@@ -68485,7 +68694,7 @@ const measureItemSize = (listEl, horizontal) => {
|
|
|
68485
68694
|
};
|
|
68486
68695
|
};
|
|
68487
68696
|
const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
68488
|
-
|
|
68697
|
+
listRows,
|
|
68489
68698
|
renderBudget,
|
|
68490
68699
|
scrolledWanted
|
|
68491
68700
|
}) => {
|
|
@@ -68545,7 +68754,7 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
68545
68754
|
// size is for, and a list drawing every row it has would pay a layout on
|
|
68546
68755
|
// each of its renders for a number nothing reads.
|
|
68547
68756
|
const sizeAlreadyKnown = virtualSizeSignal.peek() !== 0;
|
|
68548
|
-
const rowsHeldOffScreen =
|
|
68757
|
+
const rowsHeldOffScreen = listRows.totalSignal.peek() > renderBudget;
|
|
68549
68758
|
if (!virtualItemSizeProp && sizeAlreadyKnown && rowsHeldOffScreen && ref.current) {
|
|
68550
68759
|
const listEl = ref.current.querySelector(".navi_list");
|
|
68551
68760
|
const measure = listEl ? measureItemSize(listEl, horizontal) : null;
|
|
@@ -68561,7 +68770,7 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
68561
68770
|
// screen, and a list held somewhere (placeWhereHeld) before it knows where
|
|
68562
68771
|
// that is. A list drawing every row it has, opening at its start, would
|
|
68563
68772
|
// pay a layout in every commit for a number nobody reads.
|
|
68564
|
-
const sizeRead =
|
|
68773
|
+
const sizeRead = listRows.totalSignal.peek() > renderBudget || scrolledWanted !== undefined && scrolledWanted !== "start";
|
|
68565
68774
|
if (!sizeRead) {
|
|
68566
68775
|
return undefined;
|
|
68567
68776
|
}
|
|
@@ -68613,9 +68822,8 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
68613
68822
|
// item after each commit and writes to the signal, causing only the fillers to
|
|
68614
68823
|
// re-render.
|
|
68615
68824
|
const UnorderedList = ({
|
|
68616
|
-
|
|
68825
|
+
listRows,
|
|
68617
68826
|
renderWindow,
|
|
68618
|
-
virtual,
|
|
68619
68827
|
fallback,
|
|
68620
68828
|
fallbackShown,
|
|
68621
68829
|
searchFallback,
|
|
@@ -68665,17 +68873,14 @@ const UnorderedList = ({
|
|
|
68665
68873
|
value: separator ?? null,
|
|
68666
68874
|
children: jsx(ItemTransitionContext.Provider, {
|
|
68667
68875
|
value: Boolean(itemTransition),
|
|
68668
|
-
children: jsx(
|
|
68669
|
-
value:
|
|
68670
|
-
children: jsx(
|
|
68671
|
-
value:
|
|
68672
|
-
children: jsx(
|
|
68673
|
-
value: null,
|
|
68674
|
-
children: jsx(
|
|
68675
|
-
|
|
68676
|
-
children: jsx(ListDeclaredChildren, {
|
|
68677
|
-
children: children
|
|
68678
|
-
})
|
|
68876
|
+
children: jsx(ListRowsContext.Provider, {
|
|
68877
|
+
value: listRows,
|
|
68878
|
+
children: jsx(ListRowContext.Provider, {
|
|
68879
|
+
value: null,
|
|
68880
|
+
children: jsx(ListItemColumnsContext.Provider, {
|
|
68881
|
+
value: columns ? null : itemColumns || null,
|
|
68882
|
+
children: jsx(ListDeclaredChildren, {
|
|
68883
|
+
children: children
|
|
68679
68884
|
})
|
|
68680
68885
|
})
|
|
68681
68886
|
})
|
|
@@ -68726,8 +68931,8 @@ const VirtualFiller = ({
|
|
|
68726
68931
|
edge,
|
|
68727
68932
|
itemCount
|
|
68728
68933
|
}) => {
|
|
68729
|
-
const
|
|
68730
|
-
const sizeToFill = itemCount *
|
|
68934
|
+
const listRows = useContext(ListRowsContext);
|
|
68935
|
+
const sizeToFill = itemCount * listRows.virtualItemSizeSignal.value;
|
|
68731
68936
|
if (!sizeToFill) {
|
|
68732
68937
|
return null;
|
|
68733
68938
|
}
|
|
@@ -68791,22 +68996,12 @@ const ListItemRowResolver = props => {
|
|
|
68791
68996
|
...props
|
|
68792
68997
|
});
|
|
68793
68998
|
}
|
|
68794
|
-
// eslint-disable-next-line no-unused-vars
|
|
68795
|
-
const {
|
|
68796
|
-
id,
|
|
68797
|
-
index,
|
|
68798
|
-
item,
|
|
68799
|
-
rowMinHeight,
|
|
68800
|
-
rowMinWidth,
|
|
68801
|
-
...rowProps
|
|
68802
|
-
} = row;
|
|
68803
68999
|
return jsx(Next, {
|
|
68804
|
-
...rowProps,
|
|
68805
69000
|
...props,
|
|
68806
69001
|
id: props.id || row.id,
|
|
68807
69002
|
index: row.index,
|
|
68808
|
-
minHeight: props.minHeight === undefined ? rowMinHeight : props.minHeight,
|
|
68809
|
-
minWidth: props.minWidth === undefined ? rowMinWidth : props.minWidth
|
|
69003
|
+
minHeight: props.minHeight === undefined ? row.rowMinHeight : props.minHeight,
|
|
69004
|
+
minWidth: props.minWidth === undefined ? row.rowMinWidth : props.minWidth
|
|
68810
69005
|
});
|
|
68811
69006
|
};
|
|
68812
69007
|
const ListItemPresentationResolver = props => {
|
|
@@ -68880,12 +69075,11 @@ const ListItemUI = props => {
|
|
|
68880
69075
|
}
|
|
68881
69076
|
const idDefault = useId();
|
|
68882
69077
|
props.id = props.id || idDefault;
|
|
68883
|
-
const
|
|
68884
|
-
const
|
|
69078
|
+
const listRows = useContext(ListRowsContext);
|
|
69079
|
+
const groupId = useContext(ListGroupContext);
|
|
68885
69080
|
const searchNoMatchMode = useContext(SearchNoMatchModeContext);
|
|
68886
69081
|
// The run this row belongs to, when it comes from one (see ListItems): it
|
|
68887
|
-
//
|
|
68888
|
-
// separator. All that is left here is to draw it.
|
|
69082
|
+
// gave the row its place and decided it is inside the render window.
|
|
68889
69083
|
const row = useContext(ListRowContext);
|
|
68890
69084
|
const slotId = useContext(ListSlotContext);
|
|
68891
69085
|
// There is no standalone match/matchScore/highlight prop — participation
|
|
@@ -68893,7 +69087,7 @@ const ListItemUI = props => {
|
|
|
68893
69087
|
// (e.g. useSearchText's getItemMatchInfo(item): { match, matchScore,
|
|
68894
69088
|
// matchRanges }), so there is exactly one way to wire it up.
|
|
68895
69089
|
const matchInfo = props.matchInfo;
|
|
68896
|
-
// Expose match on the
|
|
69090
|
+
// Expose match on the row: the list counts non-matching rows via
|
|
68897
69091
|
// `item.match === false` (drives noMatchCount → allNoMatch → the searchFallback
|
|
68898
69092
|
// / hide-when-empty behavior). Without this a matchInfo-based search would
|
|
68899
69093
|
// filter items out but never register them as "no match".
|
|
@@ -68919,68 +69113,47 @@ const ListItemUI = props => {
|
|
|
68919
69113
|
// and the one leaving must give back its own place, not the newcomer's.
|
|
68920
69114
|
if (!row) {
|
|
68921
69115
|
if (props.filtered) {
|
|
68922
|
-
|
|
69116
|
+
listRows.drop(idDefault);
|
|
68923
69117
|
} else {
|
|
68924
|
-
props.index =
|
|
69118
|
+
props.index = listRows.take(idDefault, 1, slotId);
|
|
68925
69119
|
}
|
|
68926
69120
|
}
|
|
69121
|
+
// Every row that renders says so, whether it was declared one by one or
|
|
69122
|
+
// drawn by a run: what it is (its value, whether it is selected) and whether
|
|
69123
|
+
// it mounts at all are written where it renders, in one place.
|
|
69124
|
+
listRows.draw(idDefault, {
|
|
69125
|
+
ownerId: row ? row.ownerId : idDefault,
|
|
69126
|
+
place: props.index,
|
|
69127
|
+
groupId,
|
|
69128
|
+
data: props
|
|
69129
|
+
});
|
|
68927
69130
|
useLayoutEffect(() => {
|
|
68928
69131
|
return () => {
|
|
68929
|
-
|
|
69132
|
+
listRows.erase(idDefault);
|
|
68930
69133
|
};
|
|
68931
69134
|
}, []);
|
|
68932
|
-
// Every row that is drawn registers itself, whether it was declared one by
|
|
68933
|
-
// one or drawn by a run: what it says about itself (its value, whether it is
|
|
68934
|
-
// selected) is written where it is drawn, in one place.
|
|
68935
|
-
const item = props;
|
|
68936
|
-
tracker.useTrackItem(item);
|
|
68937
|
-
const groupTracker = useContext(GroupItemTrackerContext);
|
|
68938
|
-
const groupVisibleIndex = groupTracker ? groupTracker.useTrackItem(item) : null;
|
|
68939
69135
|
const separator = useContext(SeparatorContext);
|
|
68940
69136
|
if (props.filtered) {
|
|
68941
69137
|
return null;
|
|
68942
69138
|
}
|
|
68943
|
-
// html-hidden items: excluded from virtual scroll accounting but always in DOM
|
|
68944
|
-
if (props.hidden) {
|
|
68945
|
-
// Its separator stays too, and stays invisible with it: the point of
|
|
68946
|
-
// keeping a row that matches nothing is that nothing moves, and a divider
|
|
68947
|
-
// that leaves takes its own height away.
|
|
68948
|
-
if (!separator || props.index === 0) {
|
|
68949
|
-
return jsx(ListItemReal, {
|
|
68950
|
-
...props
|
|
68951
|
-
});
|
|
68952
|
-
}
|
|
68953
|
-
return jsxs(Fragment, {
|
|
68954
|
-
children: [cloneElement(resolveSeparatorVnode(separator, props.index - 1), {
|
|
68955
|
-
style: VISIBILITY_HIDDEN_STYLE
|
|
68956
|
-
}), jsx(ListItemReal, {
|
|
68957
|
-
...props
|
|
68958
|
-
})]
|
|
68959
|
-
});
|
|
68960
|
-
}
|
|
68961
|
-
if (row) {
|
|
68962
|
-
return jsx(ListItemReal, {
|
|
68963
|
-
...props
|
|
68964
|
-
});
|
|
68965
|
-
}
|
|
68966
|
-
const index = props.index;
|
|
68967
69139
|
const listItemVnode = jsx(ListItemReal, {
|
|
68968
69140
|
...props
|
|
68969
69141
|
});
|
|
68970
|
-
//
|
|
68971
|
-
//
|
|
68972
|
-
|
|
68973
|
-
// carry stale keyToExplicitOrder values, the binary search reads them, no
|
|
68974
|
-
// item comes out at 0 and a spurious separator appears at the top. Inside a
|
|
68975
|
-
// group, each group has its own tracker and its items do not reorder, so
|
|
68976
|
-
// groupVisibleIndex is reliable.
|
|
68977
|
-
const isFirstInList = groupVisibleIndex === null ? index === 0 : groupVisibleIndex === 0;
|
|
68978
|
-
if (!separator || isFirstInList) {
|
|
69142
|
+
// The separator a row wears is the one at the gap above it: none when
|
|
69143
|
+
// nothing of the list stands above it (see list_rows.js).
|
|
69144
|
+
if (!separator || listRows.isFirst(idDefault)) {
|
|
68979
69145
|
return listItemVnode;
|
|
68980
69146
|
}
|
|
68981
|
-
//
|
|
68982
|
-
|
|
68983
|
-
|
|
69147
|
+
// The gap index, only used as the function-form argument.
|
|
69148
|
+
let separatorVnode = resolveSeparatorVnode(separator, props.index - 1);
|
|
69149
|
+
if (props.hidden) {
|
|
69150
|
+
// A row kept in the DOM but hidden keeps its separator, hidden with it:
|
|
69151
|
+
// the point of keeping a row that matches nothing is that nothing moves,
|
|
69152
|
+
// and a divider that leaves takes its own height away.
|
|
69153
|
+
separatorVnode = cloneElement(separatorVnode, {
|
|
69154
|
+
style: VISIBILITY_HIDDEN_STYLE
|
|
69155
|
+
});
|
|
69156
|
+
}
|
|
68984
69157
|
return jsxs(Fragment, {
|
|
68985
69158
|
children: [separatorVnode, listItemVnode]
|
|
68986
69159
|
});
|
|
@@ -69301,346 +69474,45 @@ const ListItem = /*#__PURE__*/createComponentResolver([ListItemFirstResolver, Li
|
|
|
69301
69474
|
pure: true
|
|
69302
69475
|
});
|
|
69303
69476
|
|
|
69304
|
-
//
|
|
69305
|
-
//
|
|
69306
|
-
//
|
|
69307
|
-
//
|
|
69308
|
-
// A child knows how many rows it stands for but not what was declared before
|
|
69309
|
-
// it, and it cannot deduce that from when it renders: a render is free to skip
|
|
69310
|
-
// it. A child that draws from signals and whose props are all referentially
|
|
69311
|
-
// === the previous ones does not render again (@preact/signals installs a
|
|
69312
|
-
// shouldComponentUpdate that says so), which is what any child nobody rebuilt
|
|
69313
|
-
// this frame is — and children numbered as they render would then slide up
|
|
69314
|
-
// into the place of the one that was skipped.
|
|
69477
|
+
// The walk that gives the list's children their places: a slot for each of
|
|
69478
|
+
// them, declared to the list's rows all at once before any child renders,
|
|
69479
|
+
// and handed to the child through a provider of its own — which is what lets
|
|
69480
|
+
// the row reach it however deep the caller buried it in components of theirs.
|
|
69315
69481
|
//
|
|
69316
|
-
//
|
|
69317
|
-
//
|
|
69318
|
-
//
|
|
69319
|
-
//
|
|
69320
|
-
//
|
|
69321
|
-
|
|
69322
|
-
|
|
69323
|
-
|
|
69324
|
-
const
|
|
69325
|
-
|
|
69326
|
-
|
|
69327
|
-
|
|
69328
|
-
|
|
69329
|
-
const
|
|
69330
|
-
|
|
69331
|
-
|
|
69332
|
-
|
|
69333
|
-
|
|
69334
|
-
|
|
69335
|
-
|
|
69336
|
-
|
|
69337
|
-
|
|
69338
|
-
|
|
69339
|
-
|
|
69340
|
-
const
|
|
69341
|
-
|
|
69342
|
-
|
|
69343
|
-
|
|
69344
|
-
|
|
69345
|
-
|
|
69346
|
-
|
|
69347
|
-
|
|
69348
|
-
|
|
69349
|
-
|
|
69350
|
-
let rowTotal = 0;
|
|
69351
|
-
// Owners have left and the others have not been moved up yet. Done on the
|
|
69352
|
-
// next ask rather than on the spot: rows leave many at a time (a search, a
|
|
69353
|
-
// list unmounting), and moving the others up once is enough.
|
|
69354
|
-
let placesStale = false;
|
|
69355
|
-
// Where the last slot holding an owner stands: an owner arriving at or after
|
|
69356
|
-
// it is placed at the end without going over the others — a whole first
|
|
69357
|
-
// render, rows arriving in order, costs each row nothing but itself.
|
|
69358
|
-
let rankOwnedLast = -1;
|
|
69359
|
-
const rebuildWalk = () => {
|
|
69360
|
-
slotWalk.length = 0;
|
|
69361
|
-
rankBySlot.clear();
|
|
69362
|
-
const visit = parentSlotId => {
|
|
69363
|
-
const slotIds = slotIdsByParent.get(parentSlotId);
|
|
69364
|
-
if (!slotIds) {
|
|
69365
|
-
return;
|
|
69366
|
-
}
|
|
69367
|
-
for (const slotId of slotIds) {
|
|
69368
|
-
rankBySlot.set(slotId, slotWalk.length);
|
|
69369
|
-
slotWalk.push(slotId);
|
|
69370
|
-
visit(slotId);
|
|
69371
|
-
}
|
|
69372
|
-
};
|
|
69373
|
-
visit(null);
|
|
69374
|
-
};
|
|
69375
|
-
// Every place, in one go: a place is the sum of what stands before it, so
|
|
69376
|
-
// there is nothing to hand out one at a time. Writing a place that did not
|
|
69377
|
-
// change wakes nobody — a signal ignores a value equal to its own.
|
|
69378
|
-
const refreshPlaces = () => {
|
|
69379
|
-
placesStale = false;
|
|
69380
|
-
let index = 0;
|
|
69381
|
-
let rank = 0;
|
|
69382
|
-
rankOwnedLast = -1;
|
|
69383
|
-
while (rank < slotWalk.length) {
|
|
69384
|
-
const ownerIds = ownerIdsBySlot.get(slotWalk[rank]);
|
|
69385
|
-
if (ownerIds) {
|
|
69386
|
-
for (const ownerId of ownerIds) {
|
|
69387
|
-
const owner = ownerById.get(ownerId);
|
|
69388
|
-
owner.placeSignal.value = index;
|
|
69389
|
-
index += owner.rowCount;
|
|
69390
|
-
}
|
|
69391
|
-
rankOwnedLast = rank;
|
|
69392
|
-
}
|
|
69393
|
-
rank++;
|
|
69394
|
-
}
|
|
69395
|
-
rowTotal = index;
|
|
69396
|
-
totalSignal.value = index;
|
|
69397
|
-
};
|
|
69398
|
-
const addToSlot = (slotId, ownerId) => {
|
|
69399
|
-
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
69400
|
-
if (ownerIds) {
|
|
69401
|
-
ownerIds.push(ownerId);
|
|
69402
|
-
} else {
|
|
69403
|
-
ownerIdsBySlot.set(slotId, [ownerId]);
|
|
69404
|
-
}
|
|
69405
|
-
warnIfEveryRowInOneSlot(slotId);
|
|
69406
|
-
};
|
|
69407
|
-
// Rows that all stand in the same slot keep the order they first mounted in:
|
|
69408
|
-
// the walk is over the children the list is given, and a component holding
|
|
69409
|
-
// them is one child however many rows it renders. Everything about a place
|
|
69410
|
-
// then stops following what the caller writes — a search reordering the rows
|
|
69411
|
-
// moves nothing. Said once, and only for the shape that can be nothing else:
|
|
69412
|
-
// the list's whole content is one child, and several rows came out of it.
|
|
69413
|
-
let everyRowInOneSlotWarned = false;
|
|
69414
|
-
const warnIfEveryRowInOneSlot = slotId => {
|
|
69415
|
-
if (everyRowInOneSlotWarned) {
|
|
69416
|
-
return;
|
|
69417
|
-
}
|
|
69418
|
-
const rootSlotIds = slotIdsByParent.get(null);
|
|
69419
|
-
if (!rootSlotIds || rootSlotIds.length !== 1 || rootSlotIds[0] !== slotId) {
|
|
69420
|
-
return;
|
|
69421
|
-
}
|
|
69422
|
-
if (ownerIdsBySlot.get(slotId).length < 2) {
|
|
69423
|
-
return;
|
|
69424
|
-
}
|
|
69425
|
-
everyRowInOneSlotWarned = true;
|
|
69426
|
-
console.warn(`List: every row stands in the same slot, so they keep the order they first rendered in — reordering them (a search, a sort) will not move them. The list's rows must be its own children: give it the rows (or a <List.Items>), not a component rendering them.`);
|
|
69427
|
-
};
|
|
69428
|
-
const removeFromSlot = (slotId, ownerId) => {
|
|
69429
|
-
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
69430
|
-
if (!ownerIds) {
|
|
69431
|
-
return;
|
|
69432
|
-
}
|
|
69433
|
-
const index = ownerIds.indexOf(ownerId);
|
|
69434
|
-
if (index !== -1) {
|
|
69435
|
-
ownerIds.splice(index, 1);
|
|
69436
|
-
}
|
|
69437
|
-
if (ownerIds.length === 0) {
|
|
69438
|
-
ownerIdsBySlot.delete(slotId);
|
|
69439
|
-
}
|
|
69440
|
-
};
|
|
69441
|
-
// A slot the walk no longer names: whatever stood in it is gone, and so is
|
|
69442
|
-
// whatever a walk inside it had declared.
|
|
69443
|
-
const dropSlot = slotId => {
|
|
69444
|
-
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
69445
|
-
if (ownerIds) {
|
|
69446
|
-
for (const ownerId of ownerIds) {
|
|
69447
|
-
ownerById.delete(ownerId);
|
|
69448
|
-
}
|
|
69449
|
-
ownerIdsBySlot.delete(slotId);
|
|
69450
|
-
}
|
|
69451
|
-
const childSlotIds = slotIdsByParent.get(slotId);
|
|
69452
|
-
if (childSlotIds) {
|
|
69453
|
-
slotIdsByParent.delete(slotId);
|
|
69454
|
-
for (const childSlotId of childSlotIds) {
|
|
69455
|
-
dropSlot(childSlotId);
|
|
69456
|
-
}
|
|
69457
|
-
}
|
|
69458
|
-
};
|
|
69459
|
-
const virtual = {
|
|
69460
|
-
totalSignal,
|
|
69461
|
-
pagesSignal,
|
|
69462
|
-
refreshingSignal,
|
|
69463
|
-
// What a run needs to know about the list it lives in: how many rows the
|
|
69464
|
-
// list is willing to draw at once, which end it opens on, and how much
|
|
69465
|
-
// room one row is given — a row whose content has not arrived must take
|
|
69466
|
-
// exactly that, or the rows drawn would not reach where the list says they
|
|
69467
|
-
// are.
|
|
69468
|
-
renderBudget: 0,
|
|
69469
|
-
scrolled: "start",
|
|
69470
|
-
// The list is on its way somewhere: what the window frames is not what it
|
|
69471
|
-
// is about to frame, so a run must not fetch for it (see holdWindow).
|
|
69472
|
-
holdPending: false,
|
|
69473
|
-
// Called by a run just before rows land in it: what is on screen must not
|
|
69474
|
-
// move because something arrived above it. Set by the list itself.
|
|
69475
|
-
captureAnchor: () => {},
|
|
69476
|
-
horizontal: false,
|
|
69477
|
-
virtualItemSizeSignal: null,
|
|
69478
|
-
renderSkeleton: undefined,
|
|
69479
|
-
// The children a walk stands over, in order — said in one call, before any
|
|
69480
|
-
// of them renders, so that what a child asks next is answered against the
|
|
69481
|
-
// whole picture and not against the children that happened to render
|
|
69482
|
-
// first. Said again on every render of the walk, and heard only when
|
|
69483
|
-
// something moved.
|
|
69484
|
-
declareSlots: (parentSlotId, slotIds) => {
|
|
69485
|
-
const slotIdsPrevious = slotIdsByParent.get(parentSlotId);
|
|
69486
|
-
if (slotIdsPrevious && sameSlotIds(slotIdsPrevious, slotIds)) {
|
|
69487
|
-
return;
|
|
69488
|
-
}
|
|
69489
|
-
if (slotIdsPrevious) {
|
|
69490
|
-
const slotIdSet = new Set(slotIds);
|
|
69491
|
-
for (const slotId of slotIdsPrevious) {
|
|
69492
|
-
if (!slotIdSet.has(slotId)) {
|
|
69493
|
-
dropSlot(slotId);
|
|
69494
|
-
}
|
|
69495
|
-
}
|
|
69496
|
-
}
|
|
69497
|
-
slotIdsByParent.set(parentSlotId, slotIds);
|
|
69498
|
-
rebuildWalk();
|
|
69499
|
-
refreshPlaces();
|
|
69500
|
-
},
|
|
69501
|
-
// Whether something has taken this slot for its own: what it renders
|
|
69502
|
-
// inside is then its to place (a run draws its groups with their rows
|
|
69503
|
-
// already placed), and no walk inside it has anything to declare.
|
|
69504
|
-
slotHasOwner: slotId => ownerIdsBySlot.has(slotId),
|
|
69505
|
-
// Whether any run of rows lives in this list: what makes a render window
|
|
69506
|
-
// mean anything (see List's renderBudget).
|
|
69507
|
-
hasRuns: () => locatorByOwner.size > 0,
|
|
69508
|
-
setRowLocator: (ownerId, locate) => {
|
|
69509
|
-
locatorByOwner.set(ownerId, locate);
|
|
69510
|
-
},
|
|
69511
|
-
dropRowLocator: ownerId => {
|
|
69512
|
-
locatorByOwner.delete(ownerId);
|
|
69513
|
-
},
|
|
69514
|
-
// Where the row named by that id sits, asked of whoever holds it.
|
|
69515
|
-
locateRow: id => {
|
|
69516
|
-
for (const locate of locatorByOwner.values()) {
|
|
69517
|
-
const index = locate(id);
|
|
69518
|
-
if (index !== null) {
|
|
69519
|
-
return index;
|
|
69520
|
-
}
|
|
69521
|
-
}
|
|
69522
|
-
return null;
|
|
69523
|
-
},
|
|
69524
|
-
// The place the owner's rows start at — read from a signal, so that the
|
|
69525
|
-
// owner is rendered again when it moves (see createListVirtual). Asked on
|
|
69526
|
-
// every render, and answered without a second look for as long as the
|
|
69527
|
-
// owner stands in the same slot for the same number of rows.
|
|
69528
|
-
take: (ownerId, rowCount, slotId) => {
|
|
69529
|
-
let owner = ownerById.get(ownerId);
|
|
69530
|
-
if (owner) {
|
|
69531
|
-
if (owner.slotId !== slotId || owner.rowCount !== rowCount) {
|
|
69532
|
-
removeFromSlot(owner.slotId, ownerId);
|
|
69533
|
-
addToSlot(slotId, ownerId);
|
|
69534
|
-
owner.slotId = slotId;
|
|
69535
|
-
owner.rowCount = rowCount;
|
|
69536
|
-
placesStale = true;
|
|
69537
|
-
}
|
|
69538
|
-
if (placesStale) {
|
|
69539
|
-
refreshPlaces();
|
|
69540
|
-
}
|
|
69541
|
-
return owner.placeSignal.value;
|
|
69542
|
-
}
|
|
69543
|
-
if (placesStale) {
|
|
69544
|
-
refreshPlaces();
|
|
69545
|
-
}
|
|
69546
|
-
const rank = rankBySlot.get(slotId);
|
|
69547
|
-
addToSlot(slotId, ownerId);
|
|
69548
|
-
if (rank !== undefined && rank >= rankOwnedLast) {
|
|
69549
|
-
owner = {
|
|
69550
|
-
slotId,
|
|
69551
|
-
rowCount,
|
|
69552
|
-
placeSignal: signal(rowTotal)
|
|
69553
|
-
};
|
|
69554
|
-
ownerById.set(ownerId, owner);
|
|
69555
|
-
rowTotal += rowCount;
|
|
69556
|
-
rankOwnedLast = rank;
|
|
69557
|
-
totalSignal.value = rowTotal;
|
|
69558
|
-
return owner.placeSignal.value;
|
|
69559
|
-
}
|
|
69560
|
-
owner = {
|
|
69561
|
-
slotId,
|
|
69562
|
-
rowCount,
|
|
69563
|
-
placeSignal: signal(0)
|
|
69564
|
-
};
|
|
69565
|
-
ownerById.set(ownerId, owner);
|
|
69566
|
-
refreshPlaces();
|
|
69567
|
-
return owner.placeSignal.value;
|
|
69568
|
-
},
|
|
69569
|
-
// The owner stands for no row of the collection: it was filtered out by a
|
|
69570
|
-
// search, or it is gone.
|
|
69571
|
-
drop: ownerId => {
|
|
69572
|
-
const owner = ownerById.get(ownerId);
|
|
69573
|
-
if (!owner) {
|
|
69574
|
-
return;
|
|
69575
|
-
}
|
|
69576
|
-
ownerById.delete(ownerId);
|
|
69577
|
-
removeFromSlot(owner.slotId, ownerId);
|
|
69578
|
-
if (placesStale) {
|
|
69579
|
-
return;
|
|
69580
|
-
}
|
|
69581
|
-
placesStale = true;
|
|
69582
|
-
queueMicrotask(() => {
|
|
69583
|
-
if (placesStale) {
|
|
69584
|
-
refreshPlaces();
|
|
69585
|
-
}
|
|
69586
|
-
});
|
|
69587
|
-
}
|
|
69588
|
-
};
|
|
69589
|
-
return virtual;
|
|
69590
|
-
};
|
|
69591
|
-
const sameSlotIds = (left, right) => {
|
|
69592
|
-
if (left.length !== right.length) {
|
|
69593
|
-
return false;
|
|
69594
|
-
}
|
|
69595
|
-
let index = 0;
|
|
69596
|
-
while (index < left.length) {
|
|
69597
|
-
if (left[index] !== right[index]) {
|
|
69598
|
-
return false;
|
|
69599
|
-
}
|
|
69600
|
-
index++;
|
|
69601
|
-
}
|
|
69602
|
-
return true;
|
|
69603
|
-
};
|
|
69604
|
-
|
|
69605
|
-
// The walk that gives the list's children their places: a slot for each of
|
|
69606
|
-
// them, declared to the list's virtual all at once before any child renders,
|
|
69607
|
-
// and handed to the child through a provider of its own — which is what lets
|
|
69608
|
-
// the row reach it however deep the caller buried it in components of theirs.
|
|
69609
|
-
//
|
|
69610
|
-
// A slot is named the way preact tells the child apart: by key when it has
|
|
69611
|
-
// one, by position otherwise, and inside the array it was given in — a nested
|
|
69612
|
-
// array is one child to preact, so what follows the array keeps its name
|
|
69613
|
-
// however many rows the array holds. A child preact would not render (null,
|
|
69614
|
-
// a boolean) has no slot: it is not there.
|
|
69615
|
-
const ListDeclaredChildren = ({
|
|
69616
|
-
children
|
|
69617
|
-
}) => {
|
|
69618
|
-
const virtual = useContext(ListVirtualContext);
|
|
69619
|
-
const parentSlotId = useContext(ListSlotContext);
|
|
69620
|
-
if (parentSlotId !== null && virtual.slotHasOwner(parentSlotId)) {
|
|
69621
|
-
return children;
|
|
69622
|
-
}
|
|
69623
|
-
const slotIds = [];
|
|
69624
|
-
const declared = [];
|
|
69625
|
-
declareChildren(children, parentSlotId === null ? "" : `${parentSlotId}/`, slotIds, declared);
|
|
69626
|
-
virtual.declareSlots(parentSlotId, slotIds);
|
|
69627
|
-
return jsx(Fragment, {
|
|
69628
|
-
children: declared
|
|
69629
|
-
});
|
|
69630
|
-
};
|
|
69631
|
-
const declareChildren = (children, prefix, slotIds, declared) => {
|
|
69632
|
-
const childArray = Array.isArray(children) ? children : [children];
|
|
69633
|
-
let index = 0;
|
|
69634
|
-
for (const child of childArray) {
|
|
69635
|
-
if (Array.isArray(child)) {
|
|
69636
|
-
declareChildren(child, `${prefix}${index}/`, slotIds, declared);
|
|
69637
|
-
} else if (child !== null && child !== undefined && child !== false && child !== true) {
|
|
69638
|
-
const slotId = child.key === undefined || child.key === null ? `${prefix}i${index}` : `${prefix}k${child.key}`;
|
|
69639
|
-
slotIds.push(slotId);
|
|
69640
|
-
declared.push(jsx(ListSlotContext.Provider, {
|
|
69641
|
-
value: slotId,
|
|
69642
|
-
children: child
|
|
69643
|
-
}, slotId));
|
|
69482
|
+
// A slot is named the way preact tells the child apart: by key when it has
|
|
69483
|
+
// one, by position otherwise, and inside the array it was given in — a nested
|
|
69484
|
+
// array is one child to preact, so what follows the array keeps its name
|
|
69485
|
+
// however many rows the array holds. A child preact would not render (null,
|
|
69486
|
+
// a boolean) has no slot: it is not there.
|
|
69487
|
+
const ListDeclaredChildren = ({
|
|
69488
|
+
children
|
|
69489
|
+
}) => {
|
|
69490
|
+
const listRows = useContext(ListRowsContext);
|
|
69491
|
+
const parentSlotId = useContext(ListSlotContext);
|
|
69492
|
+
if (parentSlotId !== null && listRows.slotHasOwner(parentSlotId)) {
|
|
69493
|
+
return children;
|
|
69494
|
+
}
|
|
69495
|
+
const slotIds = [];
|
|
69496
|
+
const declared = [];
|
|
69497
|
+
declareChildren(children, parentSlotId === null ? "" : `${parentSlotId}/`, slotIds, declared);
|
|
69498
|
+
listRows.declareSlots(parentSlotId, slotIds);
|
|
69499
|
+
return jsx(Fragment, {
|
|
69500
|
+
children: declared
|
|
69501
|
+
});
|
|
69502
|
+
};
|
|
69503
|
+
const declareChildren = (children, prefix, slotIds, declared) => {
|
|
69504
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
69505
|
+
let index = 0;
|
|
69506
|
+
for (const child of childArray) {
|
|
69507
|
+
if (Array.isArray(child)) {
|
|
69508
|
+
declareChildren(child, `${prefix}${index}/`, slotIds, declared);
|
|
69509
|
+
} else if (child !== null && child !== undefined && child !== false && child !== true) {
|
|
69510
|
+
const slotId = child.key === undefined || child.key === null ? `${prefix}i${index}` : `${prefix}k${child.key}`;
|
|
69511
|
+
slotIds.push(slotId);
|
|
69512
|
+
declared.push(jsx(ListSlotContext.Provider, {
|
|
69513
|
+
value: slotId,
|
|
69514
|
+
children: child
|
|
69515
|
+
}, slotId));
|
|
69644
69516
|
}
|
|
69645
69517
|
index++;
|
|
69646
69518
|
}
|
|
@@ -69782,7 +69654,7 @@ const ListItems = ({
|
|
|
69782
69654
|
onRequestStateChange
|
|
69783
69655
|
}) => {
|
|
69784
69656
|
const ownerId = useId();
|
|
69785
|
-
const
|
|
69657
|
+
const listRows = useContext(ListRowsContext);
|
|
69786
69658
|
const slotId = useContext(ListSlotContext);
|
|
69787
69659
|
const renderWindow = useContext(RenderWindowContext);
|
|
69788
69660
|
const separator = useContext(SeparatorContext);
|
|
@@ -69794,10 +69666,9 @@ const ListItems = ({
|
|
|
69794
69666
|
// row at the same index, in the same refreshing state: everything the
|
|
69795
69667
|
// function is given.
|
|
69796
69668
|
const rowVnodesRef = useRef(null);
|
|
69797
|
-
if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem
|
|
69669
|
+
if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem) {
|
|
69798
69670
|
rowVnodesRef.current = {
|
|
69799
69671
|
renderItem,
|
|
69800
|
-
separator,
|
|
69801
69672
|
byItem: new Map()
|
|
69802
69673
|
};
|
|
69803
69674
|
}
|
|
@@ -69809,7 +69680,7 @@ const ListItems = ({
|
|
|
69809
69680
|
memoryBudget,
|
|
69810
69681
|
onRequestStateChange
|
|
69811
69682
|
});
|
|
69812
|
-
const renderRowSkeleton = renderSkeleton === undefined ?
|
|
69683
|
+
const renderRowSkeleton = renderSkeleton === undefined ? listRows.renderSkeleton : renderSkeleton;
|
|
69813
69684
|
// A row on its way takes the room the list reserves for it: anything else
|
|
69814
69685
|
// and the rows drawn stop short of where the scroll says they are. Read
|
|
69815
69686
|
// where a row is actually missing, and not before: the size settles after
|
|
@@ -69821,9 +69692,9 @@ const ListItems = ({
|
|
|
69821
69692
|
return skeletonRow;
|
|
69822
69693
|
}
|
|
69823
69694
|
skeletonRow = {};
|
|
69824
|
-
const virtualItemSize =
|
|
69695
|
+
const virtualItemSize = listRows.virtualItemSizeSignal.value;
|
|
69825
69696
|
if (virtualItemSize) {
|
|
69826
|
-
if (
|
|
69697
|
+
if (listRows.horizontal) {
|
|
69827
69698
|
skeletonRow.rowMinWidth = `${virtualItemSize}px`;
|
|
69828
69699
|
} else {
|
|
69829
69700
|
skeletonRow.rowMinHeight = `${virtualItemSize}px`;
|
|
@@ -69831,7 +69702,7 @@ const ListItems = ({
|
|
|
69831
69702
|
}
|
|
69832
69703
|
return skeletonRow;
|
|
69833
69704
|
};
|
|
69834
|
-
const runStart =
|
|
69705
|
+
const runStart = listRows.take(ownerId, store.rowCount, slotId);
|
|
69835
69706
|
const runEnd = runStart + store.rowCount;
|
|
69836
69707
|
// The two ways to count the same row. The list numbers its rows from its own
|
|
69837
69708
|
// first one, whatever draws it; the store numbers the collection's, straight
|
|
@@ -69846,6 +69717,7 @@ const ListItems = ({
|
|
|
69846
69717
|
const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;
|
|
69847
69718
|
const windowTo = renderWindow.end < runEnd ? renderWindow.end : runEnd;
|
|
69848
69719
|
store.forget(rankOf(windowFrom), rankOf(windowTo));
|
|
69720
|
+
listRows.declareWindow(ownerId, windowFrom, windowTo);
|
|
69849
69721
|
|
|
69850
69722
|
// The row answers to its own id when the item carries one — that is what
|
|
69851
69723
|
// addresses it from outside (--navi-select, --navi-scroll, startAt) — and
|
|
@@ -69855,7 +69727,7 @@ const ListItems = ({
|
|
|
69855
69727
|
// Where a row named from outside actually sits. Only the run can answer:
|
|
69856
69728
|
// rows it holds but does not draw are nowhere else — a list only knows the
|
|
69857
69729
|
// rows it has drawn (they register themselves, see ListItemUI).
|
|
69858
|
-
|
|
69730
|
+
listRows.setRowLocator(ownerId, id => {
|
|
69859
69731
|
let found = null;
|
|
69860
69732
|
store.eachHeld((item, rank) => {
|
|
69861
69733
|
const rowIndex = rowOf(rank);
|
|
@@ -69867,8 +69739,8 @@ const ListItems = ({
|
|
|
69867
69739
|
});
|
|
69868
69740
|
useLayoutEffect(() => {
|
|
69869
69741
|
return () => {
|
|
69870
|
-
|
|
69871
|
-
|
|
69742
|
+
listRows.dropRowLocator(ownerId);
|
|
69743
|
+
listRows.drop(ownerId);
|
|
69872
69744
|
};
|
|
69873
69745
|
}, []);
|
|
69874
69746
|
|
|
@@ -69894,7 +69766,7 @@ const ListItems = ({
|
|
|
69894
69766
|
let askStart = missingStart;
|
|
69895
69767
|
let askEnd = missingEnd;
|
|
69896
69768
|
if (missingStart !== -1) {
|
|
69897
|
-
const rowsPerPage = pageSize ||
|
|
69769
|
+
const rowsPerPage = pageSize || listRows.renderBudget;
|
|
69898
69770
|
const holeSize = missingEnd - missingStart + 1;
|
|
69899
69771
|
if (holeSize < rowsPerPage) {
|
|
69900
69772
|
// Which way the page grows: away from the rows already held, which is
|
|
@@ -69969,13 +69841,9 @@ const ListItems = ({
|
|
|
69969
69841
|
}, `${ownerId}_group_${group.key}`));
|
|
69970
69842
|
group = null;
|
|
69971
69843
|
};
|
|
69972
|
-
// Which group a row belongs to, or undefined when it belongs to none.
|
|
69973
|
-
// before the row is pushed as well as while pushing it: a row opening a
|
|
69974
|
-
// group is the one row that must not wear a separator (see below).
|
|
69844
|
+
// Which group a row belongs to, or undefined when it belongs to none.
|
|
69975
69845
|
const groupKeyOf = (item, rowIndex) => groupBy && item !== undefined ? groupBy(item, rowIndex) : undefined;
|
|
69976
|
-
const
|
|
69977
|
-
const pushRow = (rowNode, item, rowIndex) => {
|
|
69978
|
-
const groupKey = groupKeyOf(item, rowIndex);
|
|
69846
|
+
const pushRow = (rowNode, item, rowIndex, groupKey) => {
|
|
69979
69847
|
if (groupKey === undefined) {
|
|
69980
69848
|
closeGroup();
|
|
69981
69849
|
rows.push(rowNode);
|
|
@@ -70013,7 +69881,7 @@ const ListItems = ({
|
|
|
70013
69881
|
rows.push(jsx("li", {
|
|
70014
69882
|
className: "navi_list_failed_rows",
|
|
70015
69883
|
style: {
|
|
70016
|
-
"--size-to-fill": `${failedRowCount *
|
|
69884
|
+
"--size-to-fill": `${failedRowCount * listRows.virtualItemSizeSignal.value}px`
|
|
70017
69885
|
},
|
|
70018
69886
|
children: renderError ? renderError({
|
|
70019
69887
|
error: store.failure.error,
|
|
@@ -70030,78 +69898,70 @@ const ListItems = ({
|
|
|
70030
69898
|
}
|
|
70031
69899
|
const item = getItemAt(rowIndex);
|
|
70032
69900
|
const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
|
|
69901
|
+
const groupKey = groupKeyOf(item, rowIndex);
|
|
69902
|
+
if (item === undefined) {
|
|
69903
|
+
// A row on its way never reaches ListItemUI (see ListItemSkeletonResolver):
|
|
69904
|
+
// it is stood among the rows that mount, and given its separator, here.
|
|
69905
|
+
let rowVnode;
|
|
69906
|
+
if (renderRowSkeleton === false) {
|
|
69907
|
+
// The row must still take its room: without it the rows below would
|
|
69908
|
+
// climb up and slide back down as the answer arrives.
|
|
69909
|
+
rowVnode = jsx(ListItem, {
|
|
69910
|
+
skeleton: true,
|
|
69911
|
+
style: VISIBILITY_HIDDEN_STYLE
|
|
69912
|
+
});
|
|
69913
|
+
} else if (renderRowSkeleton) {
|
|
69914
|
+
rowVnode = renderRowSkeleton(rowIndex);
|
|
69915
|
+
} else {
|
|
69916
|
+
rowVnode = jsx(ListItem, {
|
|
69917
|
+
skeleton: true
|
|
69918
|
+
});
|
|
69919
|
+
}
|
|
69920
|
+
if (rowVnode) {
|
|
69921
|
+
pushRow(jsx(ListRunSkeletonRow, {
|
|
69922
|
+
row: {
|
|
69923
|
+
id: key,
|
|
69924
|
+
index: rowIndex,
|
|
69925
|
+
ownerId,
|
|
69926
|
+
...getSkeletonRow()
|
|
69927
|
+
},
|
|
69928
|
+
separator: separator,
|
|
69929
|
+
children: rowVnode
|
|
69930
|
+
}, key), item, rowIndex, groupKey);
|
|
69931
|
+
}
|
|
69932
|
+
rowIndex++;
|
|
69933
|
+
continue;
|
|
69934
|
+
}
|
|
70033
69935
|
let rowVnode;
|
|
70034
69936
|
let rowContextValue;
|
|
70035
|
-
|
|
70036
|
-
if (
|
|
70037
|
-
|
|
70038
|
-
|
|
70039
|
-
rowVnode = rowVnodeKept.vnode;
|
|
70040
|
-
rowContextValue = rowVnodeKept.rowContextValue;
|
|
70041
|
-
rowKept = rowVnodeKept;
|
|
70042
|
-
} else {
|
|
70043
|
-
rowVnode = renderItem(item, rowIndex, renderItemState);
|
|
70044
|
-
// Kept with the vnode, for the same reason: a context value that is a
|
|
70045
|
-
// fresh object on every render forces every consumer of it to render,
|
|
70046
|
-
// which is the row's own chain — the vnode handed back unchanged would
|
|
70047
|
-
// then buy nothing.
|
|
70048
|
-
rowContextValue = {
|
|
70049
|
-
id: key,
|
|
70050
|
-
index: rowIndex,
|
|
70051
|
-
item
|
|
70052
|
-
};
|
|
70053
|
-
rowKept = {
|
|
70054
|
-
vnode: rowVnode,
|
|
70055
|
-
rowContextValue,
|
|
70056
|
-
rowIndex,
|
|
70057
|
-
refreshing: renderItemState.refreshing,
|
|
70058
|
-
separatorVnode: null
|
|
70059
|
-
};
|
|
70060
|
-
rowVnodesByItem.set(item, rowKept);
|
|
70061
|
-
}
|
|
70062
|
-
} else if (renderRowSkeleton === false) {
|
|
70063
|
-
// The row must still take its room: without it the rows below would
|
|
70064
|
-
// climb up and slide back down as the answer arrives.
|
|
70065
|
-
rowVnode = jsx(ListItem, {
|
|
70066
|
-
skeleton: true,
|
|
70067
|
-
style: VISIBILITY_HIDDEN_STYLE
|
|
70068
|
-
});
|
|
70069
|
-
} else if (renderRowSkeleton) {
|
|
70070
|
-
rowVnode = renderRowSkeleton(rowIndex);
|
|
69937
|
+
const rowVnodeKept = rowVnodesByItem.get(item);
|
|
69938
|
+
if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing) {
|
|
69939
|
+
rowVnode = rowVnodeKept.vnode;
|
|
69940
|
+
rowContextValue = rowVnodeKept.rowContextValue;
|
|
70071
69941
|
} else {
|
|
70072
|
-
rowVnode =
|
|
70073
|
-
|
|
69942
|
+
rowVnode = renderItem(item, rowIndex, renderItemState);
|
|
69943
|
+
// Kept with the vnode, for the same reason: a context value that is a
|
|
69944
|
+
// fresh object on every render forces every consumer of it to render,
|
|
69945
|
+
// which is the row's own chain — the vnode handed back unchanged would
|
|
69946
|
+
// then buy nothing.
|
|
69947
|
+
rowContextValue = {
|
|
69948
|
+
id: key,
|
|
69949
|
+
index: rowIndex,
|
|
69950
|
+
item,
|
|
69951
|
+
ownerId
|
|
69952
|
+
};
|
|
69953
|
+
rowVnodesByItem.set(item, {
|
|
69954
|
+
vnode: rowVnode,
|
|
69955
|
+
rowContextValue,
|
|
69956
|
+
rowIndex,
|
|
69957
|
+
refreshing: renderItemState.refreshing
|
|
70074
69958
|
});
|
|
70075
69959
|
}
|
|
70076
69960
|
if (rowVnode) {
|
|
70077
|
-
// The first row of a group wears no separator: the gap it sits at is the
|
|
70078
|
-
// one between two groups, and that gap is the group wrapper's own — it
|
|
70079
|
-
// is a row of the list like any other and draws its separator itself
|
|
70080
|
-
// (see ListItemUI). Drawn here it would land inside the group instead,
|
|
70081
|
-
// as a hairline under the label.
|
|
70082
|
-
const drawSeparator = separator && rowIndex > 0 && !opensGroup(groupKeyOf(item, rowIndex));
|
|
70083
|
-
if (drawSeparator) {
|
|
70084
|
-
// Kept with the row too: a separator built again is a separator
|
|
70085
|
-
// rendered again.
|
|
70086
|
-
let separatorVnode = rowKept ? rowKept.separatorVnode : null;
|
|
70087
|
-
if (!separatorVnode) {
|
|
70088
|
-
separatorVnode = cloneElement(resolveSeparatorVnode(separator, rowIndex - 1), {
|
|
70089
|
-
key: `${key}_separator`
|
|
70090
|
-
});
|
|
70091
|
-
if (rowKept) {
|
|
70092
|
-
rowKept.separatorVnode = separatorVnode;
|
|
70093
|
-
}
|
|
70094
|
-
}
|
|
70095
|
-
pushRow(separatorVnode, item, rowIndex);
|
|
70096
|
-
}
|
|
70097
69961
|
pushRow(jsx(ListRowContext.Provider, {
|
|
70098
|
-
value:
|
|
70099
|
-
id: key,
|
|
70100
|
-
index: rowIndex,
|
|
70101
|
-
...getSkeletonRow()
|
|
70102
|
-
} : rowContextValue,
|
|
69962
|
+
value: rowContextValue,
|
|
70103
69963
|
children: rowVnode
|
|
70104
|
-
}, key), item, rowIndex);
|
|
69964
|
+
}, key), item, rowIndex, groupKey);
|
|
70105
69965
|
}
|
|
70106
69966
|
rowIndex++;
|
|
70107
69967
|
}
|
|
@@ -70115,6 +69975,44 @@ const ListItems = ({
|
|
|
70115
69975
|
return rows;
|
|
70116
69976
|
};
|
|
70117
69977
|
|
|
69978
|
+
// A run's row that has not arrived, standing where the real one will. It never
|
|
69979
|
+
// reaches ListItemUI (see ListItemSkeletonResolver), so it is drawn among the
|
|
69980
|
+
// rows here, and wears the separator of the gap above it the way a real row
|
|
69981
|
+
// does there.
|
|
69982
|
+
const SKELETON_ROW_DATA = {
|
|
69983
|
+
skeleton: true
|
|
69984
|
+
};
|
|
69985
|
+
const ListRunSkeletonRow = ({
|
|
69986
|
+
row,
|
|
69987
|
+
separator,
|
|
69988
|
+
children
|
|
69989
|
+
}) => {
|
|
69990
|
+
const listRows = useContext(ListRowsContext);
|
|
69991
|
+
const groupId = useContext(ListGroupContext);
|
|
69992
|
+
const rowId = useId();
|
|
69993
|
+
listRows.draw(rowId, {
|
|
69994
|
+
ownerId: row.ownerId,
|
|
69995
|
+
place: row.index,
|
|
69996
|
+
groupId,
|
|
69997
|
+
data: SKELETON_ROW_DATA
|
|
69998
|
+
});
|
|
69999
|
+
useLayoutEffect(() => {
|
|
70000
|
+
return () => {
|
|
70001
|
+
listRows.erase(rowId);
|
|
70002
|
+
};
|
|
70003
|
+
}, []);
|
|
70004
|
+
const rowVnode = jsx(ListRowContext.Provider, {
|
|
70005
|
+
value: row,
|
|
70006
|
+
children: children
|
|
70007
|
+
});
|
|
70008
|
+
if (!separator || listRows.isFirst(rowId)) {
|
|
70009
|
+
return rowVnode;
|
|
70010
|
+
}
|
|
70011
|
+
return jsxs(Fragment, {
|
|
70012
|
+
children: [resolveSeparatorVnode(separator, row.index - 1), rowVnode]
|
|
70013
|
+
});
|
|
70014
|
+
};
|
|
70015
|
+
|
|
70118
70016
|
// What is drawn where rows were asked for and never came: the sentence and the
|
|
70119
70017
|
// way out, in the row itself — the rest of the list is fine, so replacing all
|
|
70120
70018
|
// of it (List's own `error`) would be a lie.
|
|
@@ -70260,7 +70158,7 @@ const useItemStore = ({
|
|
|
70260
70158
|
// where the hole is, and cleared by a retry — which is what makes the same
|
|
70261
70159
|
// range askable again (see the request memory just above).
|
|
70262
70160
|
const [failure, setFailure] = useState(null);
|
|
70263
|
-
const
|
|
70161
|
+
const listRows = useContext(ListRowsContext);
|
|
70264
70162
|
// The rows are there, which is what the list waits for to place itself on the
|
|
70265
70163
|
// row it is held at (see placeWhereHeld). Said from an effect: a signal read
|
|
70266
70164
|
// during this very render must not be written during it.
|
|
@@ -70269,12 +70167,12 @@ const useItemStore = ({
|
|
|
70269
70167
|
return;
|
|
70270
70168
|
}
|
|
70271
70169
|
itemsHeldRef.current = true;
|
|
70272
|
-
|
|
70170
|
+
listRows.pagesSignal.value = listRows.pagesSignal.peek() + 1;
|
|
70273
70171
|
});
|
|
70274
70172
|
// Before the first answer a run does not know how many rows it stands for.
|
|
70275
70173
|
// It stands for a windowful of them: a list that is about to be filled looks
|
|
70276
70174
|
// like rows on their way, not like an empty list.
|
|
70277
|
-
const rowCount = pages.count ?? count ??
|
|
70175
|
+
const rowCount = pages.count ?? count ?? listRows.renderBudget;
|
|
70278
70176
|
// A run that never received anything has nothing to keep on screen: asking
|
|
70279
70177
|
// again is its first ask, not a refresh.
|
|
70280
70178
|
if (staleRef.current && pages.count === undefined) {
|
|
@@ -70284,9 +70182,9 @@ const useItemStore = ({
|
|
|
70284
70182
|
if (!refreshing) {
|
|
70285
70183
|
return null;
|
|
70286
70184
|
}
|
|
70287
|
-
|
|
70185
|
+
listRows.refreshingSignal.value = listRows.refreshingSignal.peek() + 1;
|
|
70288
70186
|
return () => {
|
|
70289
|
-
|
|
70187
|
+
listRows.refreshingSignal.value = listRows.refreshingSignal.peek() - 1;
|
|
70290
70188
|
};
|
|
70291
70189
|
}, [refreshing]);
|
|
70292
70190
|
|
|
@@ -70385,7 +70283,7 @@ const useItemStore = ({
|
|
|
70385
70283
|
// how many rows there are, so it asks for the rows the list would open
|
|
70386
70284
|
// on — counting back from the end when that is where it opens, the way
|
|
70387
70285
|
// an HTTP range does.
|
|
70388
|
-
const budget =
|
|
70286
|
+
const budget = listRows.renderBudget;
|
|
70389
70287
|
let start = missingStart;
|
|
70390
70288
|
let end = missingEnd;
|
|
70391
70289
|
let around;
|
|
@@ -70396,11 +70294,11 @@ const useItemStore = ({
|
|
|
70396
70294
|
// The list is held on a row nothing on screen leads to: the rows it holds
|
|
70397
70295
|
// do not contain it, so no window it could draw will ever bring it. Only
|
|
70398
70296
|
// asking for it by name does.
|
|
70399
|
-
const wanted =
|
|
70297
|
+
const wanted = listRows.scrolled;
|
|
70400
70298
|
const askingAroundWantedRow = revalidating &&
|
|
70401
70299
|
// Only while the hold stands: once the user has taken the list over,
|
|
70402
70300
|
// the reading position is where they are, not where it opened.
|
|
70403
|
-
|
|
70301
|
+
listRows.holdPending && wanted && typeof wanted === "object" && wanted.id !== undefined && listRows.locateRow(wanted.id) === null;
|
|
70404
70302
|
if (askingAroundWantedRow) {
|
|
70405
70303
|
around = wanted.id;
|
|
70406
70304
|
// Where it stood when it was written down is enough to frame the ask;
|
|
@@ -70423,7 +70321,7 @@ const useItemStore = ({
|
|
|
70423
70321
|
around = firstHeld.id;
|
|
70424
70322
|
}
|
|
70425
70323
|
} else if (pages.count === undefined) {
|
|
70426
|
-
const scrolled =
|
|
70324
|
+
const scrolled = listRows.scrolled;
|
|
70427
70325
|
if (scrolled === "end") {
|
|
70428
70326
|
// Counting back from the end, the way an HTTP range does: a list
|
|
70429
70327
|
// opening on its last rows asks for them before it knows how many
|
|
@@ -70454,7 +70352,7 @@ const useItemStore = ({
|
|
|
70454
70352
|
// way somewhere the window does not frame yet, `count` that it knows
|
|
70455
70353
|
// how many rows it stands for.
|
|
70456
70354
|
const debugAsk = outcome => {
|
|
70457
|
-
debugScroll(`ask ${start}-${end}: ${outcome}`, `(revalidating=${revalidating} holdPending=${
|
|
70355
|
+
debugScroll(`ask ${start}-${end}: ${outcome}`, `(revalidating=${revalidating} holdPending=${listRows.holdPending} count=${pages.count})`);
|
|
70458
70356
|
};
|
|
70459
70357
|
if (start === -1) {
|
|
70460
70358
|
// Nothing missing and nothing to revalidate: the run has what it
|
|
@@ -70462,7 +70360,7 @@ const useItemStore = ({
|
|
|
70462
70360
|
debugAsk("nothing missing");
|
|
70463
70361
|
return;
|
|
70464
70362
|
}
|
|
70465
|
-
if (
|
|
70363
|
+
if (listRows.holdPending && pages.count !== undefined && !askingAroundWantedRow) {
|
|
70466
70364
|
// The one ask a hold lets through: the row the list is held on is
|
|
70467
70365
|
// what would lift the hold, and nothing else is going to bring it.
|
|
70468
70366
|
debugAsk("held on a row not reached yet");
|
|
@@ -70553,7 +70451,7 @@ const useItemStore = ({
|
|
|
70553
70451
|
const pageCount = Array.isArray(page) ? pageItems.length : page.count ?? pageStart + pageItems.length;
|
|
70554
70452
|
// Before the rows land: what is on screen has to stay where it is,
|
|
70555
70453
|
// and the DOM still shows the state to hold onto.
|
|
70556
|
-
|
|
70454
|
+
listRows.captureAnchor();
|
|
70557
70455
|
if (revalidating) {
|
|
70558
70456
|
// The rows held stood for a composition that has moved on; the
|
|
70559
70457
|
// ones outside the window are forgotten and asked for again if the
|
|
@@ -70575,7 +70473,7 @@ const useItemStore = ({
|
|
|
70575
70473
|
replace: revalidating
|
|
70576
70474
|
});
|
|
70577
70475
|
}
|
|
70578
|
-
|
|
70476
|
+
listRows.pagesSignal.value = listRows.pagesSignal.peek() + 1;
|
|
70579
70477
|
setPageVersion(version => version + 1);
|
|
70580
70478
|
};
|
|
70581
70479
|
const failed = error => {
|
|
@@ -70643,10 +70541,16 @@ const ListItemGroup = ({
|
|
|
70643
70541
|
...rest
|
|
70644
70542
|
}) => {
|
|
70645
70543
|
const groupId = useId();
|
|
70646
|
-
const
|
|
70544
|
+
const listRows = useContext(ListRowsContext);
|
|
70545
|
+
const group = listRows.group(groupId);
|
|
70546
|
+
useLayoutEffect(() => {
|
|
70547
|
+
return () => {
|
|
70548
|
+
listRows.dropGroup(groupId);
|
|
70549
|
+
};
|
|
70550
|
+
}, []);
|
|
70647
70551
|
const searchNoMatchMode = useContext(SearchNoMatchModeContext);
|
|
70648
|
-
const groupItemCount =
|
|
70649
|
-
const groupNoMatchCount =
|
|
70552
|
+
const groupItemCount = group.countSignal.value;
|
|
70553
|
+
const groupNoMatchCount = group.noMatchCountSignal.value;
|
|
70650
70554
|
// Every row of this group failed the search: the label has nothing left to
|
|
70651
70555
|
// title. "remove" empties the group on its own (and hiddenWhileEmpty takes it
|
|
70652
70556
|
// out of the flow), "muted" keeps the rows readable so the label stays useful
|
|
@@ -70690,8 +70594,8 @@ const ListItemGroup = ({
|
|
|
70690
70594
|
className: "navi_list_item_group_list",
|
|
70691
70595
|
role: "group",
|
|
70692
70596
|
"aria-labelledby": groupId,
|
|
70693
|
-
children: jsx(
|
|
70694
|
-
value:
|
|
70597
|
+
children: jsx(ListGroupContext.Provider, {
|
|
70598
|
+
value: groupId,
|
|
70695
70599
|
children: jsx(ListDeclaredChildren, {
|
|
70696
70600
|
children: children
|
|
70697
70601
|
})
|
|
@@ -76080,414 +75984,812 @@ const SplitButton = props => {
|
|
|
76080
75984
|
});
|
|
76081
75985
|
};
|
|
76082
75986
|
|
|
76083
|
-
// What the Picker's popup answers to — Picker's own popup props, named here so
|
|
76084
|
-
// a caller reaches all of them through the split button (see picker.jsx's JSDoc
|
|
76085
|
-
// for what each one says).
|
|
76086
|
-
const POPUP_PROP_SET = new Set(["mode", "popupLayer", "popupTestId", "positionArea", "popoverMode", "popoverSpacing", "popupWidthFitContent", "popoverMaxHeight", "dialogMinWidth", "dialogMinHeight", "dialogMaxWidth", "dialogMaxHeight", "dialogExpand", "dialogExpandX", "dialogExpandY", "dockedOnSmallTouchScreen", "marginWithContainer", "backdrop", "backdropVariant", "backdropColor", "backdropFilter", "pointerInteractionOutsideEffect", "escapeEffect", "closeOnFocusOut", "scrollCapture", "focusCapture", "popupBackgroundColor", "popupBorderRadius", "animation"]);
|
|
76087
|
-
const splitPopupProps = props => {
|
|
76088
|
-
const popupProps = {};
|
|
76089
|
-
const boxProps = {};
|
|
76090
|
-
for (const key of Object.keys(props)) {
|
|
76091
|
-
if (POPUP_PROP_SET.has(key)) {
|
|
76092
|
-
popupProps[key] = props[key];
|
|
75987
|
+
// What the Picker's popup answers to — Picker's own popup props, named here so
|
|
75988
|
+
// a caller reaches all of them through the split button (see picker.jsx's JSDoc
|
|
75989
|
+
// for what each one says).
|
|
75990
|
+
const POPUP_PROP_SET = new Set(["mode", "popupLayer", "popupTestId", "positionArea", "popoverMode", "popoverSpacing", "popupWidthFitContent", "popoverMaxHeight", "dialogMinWidth", "dialogMinHeight", "dialogMaxWidth", "dialogMaxHeight", "dialogExpand", "dialogExpandX", "dialogExpandY", "dockedOnSmallTouchScreen", "marginWithContainer", "backdrop", "backdropVariant", "backdropColor", "backdropFilter", "pointerInteractionOutsideEffect", "escapeEffect", "closeOnFocusOut", "scrollCapture", "focusCapture", "popupBackgroundColor", "popupBorderRadius", "animation"]);
|
|
75991
|
+
const splitPopupProps = props => {
|
|
75992
|
+
const popupProps = {};
|
|
75993
|
+
const boxProps = {};
|
|
75994
|
+
for (const key of Object.keys(props)) {
|
|
75995
|
+
if (POPUP_PROP_SET.has(key)) {
|
|
75996
|
+
popupProps[key] = props[key];
|
|
75997
|
+
} else {
|
|
75998
|
+
boxProps[key] = props[key];
|
|
75999
|
+
}
|
|
76000
|
+
}
|
|
76001
|
+
return [popupProps, boxProps];
|
|
76002
|
+
};
|
|
76003
|
+
|
|
76004
|
+
/**
|
|
76005
|
+
* applySearch — matches value against searchText.
|
|
76006
|
+
*
|
|
76007
|
+
* Accent-insensitive: "gue" matches "Guérin", "e" matches "é".
|
|
76008
|
+
* Case-insensitive: "bob" matches "Bob", with a score bonus for case-exact matches.
|
|
76009
|
+
* Multi-word: if searchText contains spaces, each word must appear somewhere in
|
|
76010
|
+
* the value for it to match. Ranges for all words are returned.
|
|
76011
|
+
*
|
|
76012
|
+
* Score table:
|
|
76013
|
+
*
|
|
76014
|
+
* Situation Score
|
|
76015
|
+
* ─────────────────────────────────────── ───────────────────────────
|
|
76016
|
+
* phrase at start of value 1
|
|
76017
|
+
* multi-word, one word at start (all match) 0.75
|
|
76018
|
+
* phrase / word at word boundary 0.625
|
|
76019
|
+
* phrase / words mid-word 0.5
|
|
76020
|
+
* + case-exact bonus +0.125
|
|
76021
|
+
* multi-word partial: score × (matched/total)
|
|
76022
|
+
*
|
|
76023
|
+
* matchRanges: [start, end] pairs (exclusive end) for CSS Highlight API.
|
|
76024
|
+
* Intended to be passed to useSearch as the matchFn parameter.
|
|
76025
|
+
*/
|
|
76026
|
+
const applySearch = (searchText, value) => {
|
|
76027
|
+
if (!searchText) {
|
|
76028
|
+
return { match: true, matchScore: 0, matchRanges: [] };
|
|
76029
|
+
}
|
|
76030
|
+
if (searchText.length > 100) {
|
|
76031
|
+
searchText = searchText.slice(0, 100);
|
|
76032
|
+
}
|
|
76033
|
+
const str = String(value);
|
|
76034
|
+
const foldedStr = foldAccents(str).toLowerCase();
|
|
76035
|
+
const { foldedSearch, words, originalWords } = getSearchInfo(searchText);
|
|
76036
|
+
|
|
76037
|
+
// Try exact phrase match first (gives best score).
|
|
76038
|
+
const phraseRanges = [];
|
|
76039
|
+
let phraseIdx = foldedStr.indexOf(foldedSearch);
|
|
76040
|
+
while (phraseIdx !== -1) {
|
|
76041
|
+
phraseRanges.push([phraseIdx, phraseIdx + foldedSearch.length]);
|
|
76042
|
+
phraseIdx = foldedStr.indexOf(foldedSearch, phraseIdx + 1);
|
|
76043
|
+
}
|
|
76044
|
+
if (phraseRanges.length > 0) {
|
|
76045
|
+
const atStart = foldedStr.startsWith(foldedSearch);
|
|
76046
|
+
const atWordBoundary = phraseRanges.some(([start]) =>
|
|
76047
|
+
isWordBoundary(foldedStr, start),
|
|
76048
|
+
);
|
|
76049
|
+
const caseExact = str.includes(searchText);
|
|
76050
|
+
let baseScore;
|
|
76051
|
+
if (atStart) {
|
|
76052
|
+
baseScore = SCORE_PHRASE_AT_START;
|
|
76053
|
+
} else if (atWordBoundary) {
|
|
76054
|
+
baseScore = SCORE_AT_WORD_BOUNDARY;
|
|
76055
|
+
} else {
|
|
76056
|
+
baseScore = SCORE_MID_WORD;
|
|
76057
|
+
}
|
|
76058
|
+
const matchScore = baseScore + (caseExact ? SCORE_BONUS_CASE_EXACT : 0);
|
|
76059
|
+
return { match: true, matchScore, matchRanges: mergeRanges(phraseRanges) };
|
|
76060
|
+
}
|
|
76061
|
+
|
|
76062
|
+
// Multi-word OR: split on whitespace, any word matching contributes to the score.
|
|
76063
|
+
// Items where all words match rank higher than partial matches.
|
|
76064
|
+
// Note: words always has at least 1 element here (searchText is non-empty and
|
|
76065
|
+
// foldedSearch.split filters empty strings). This path also handles the case
|
|
76066
|
+
// where searchText has trailing/leading spaces: the phrase match above tries
|
|
76067
|
+
// the literal (e.g. "tc " in "tc adapter"), and if that fails we fall through
|
|
76068
|
+
// here to try each word individually (e.g. "tc" matches "tca").
|
|
76069
|
+
const matchRanges = [];
|
|
76070
|
+
let matchedWordCount = 0;
|
|
76071
|
+
let anyWordAtStart = false;
|
|
76072
|
+
let anyWordAtWordBoundary = false;
|
|
76073
|
+
let allMatchedWordsExact = true;
|
|
76074
|
+
for (let w = 0; w < words.length; w++) {
|
|
76075
|
+
const word = words[w];
|
|
76076
|
+
const originalWord = originalWords[w];
|
|
76077
|
+
let idx = foldedStr.indexOf(word);
|
|
76078
|
+
if (idx === -1) {
|
|
76079
|
+
continue;
|
|
76080
|
+
}
|
|
76081
|
+
matchedWordCount++;
|
|
76082
|
+
let wordHasExactMatch = false;
|
|
76083
|
+
while (idx !== -1) {
|
|
76084
|
+
matchRanges.push([idx, idx + word.length]);
|
|
76085
|
+
if (idx === 0) {
|
|
76086
|
+
anyWordAtStart = true;
|
|
76087
|
+
anyWordAtWordBoundary = true;
|
|
76088
|
+
} else if (isWordBoundary(foldedStr, idx)) {
|
|
76089
|
+
anyWordAtWordBoundary = true;
|
|
76090
|
+
}
|
|
76091
|
+
if (str.slice(idx, idx + word.length) === originalWord) {
|
|
76092
|
+
wordHasExactMatch = true;
|
|
76093
|
+
}
|
|
76094
|
+
idx = foldedStr.indexOf(word, idx + 1);
|
|
76095
|
+
}
|
|
76096
|
+
if (!wordHasExactMatch) {
|
|
76097
|
+
allMatchedWordsExact = false;
|
|
76098
|
+
}
|
|
76099
|
+
}
|
|
76100
|
+
if (matchedWordCount === 0) {
|
|
76101
|
+
return tryAcronymMatch(foldedStr, str, searchText);
|
|
76102
|
+
}
|
|
76103
|
+
const wordRatio = matchedWordCount / words.length;
|
|
76104
|
+
let baseScore;
|
|
76105
|
+
if (anyWordAtStart) {
|
|
76106
|
+
baseScore = SCORE_MULTI_WORD_AT_START;
|
|
76107
|
+
} else if (anyWordAtWordBoundary) {
|
|
76108
|
+
baseScore = SCORE_AT_WORD_BOUNDARY;
|
|
76109
|
+
} else {
|
|
76110
|
+
baseScore = SCORE_MID_WORD;
|
|
76111
|
+
}
|
|
76112
|
+
const matchScore =
|
|
76113
|
+
(baseScore + (allMatchedWordsExact ? SCORE_BONUS_CASE_EXACT : 0)) *
|
|
76114
|
+
wordRatio;
|
|
76115
|
+
return { match: true, matchScore, matchRanges: mergeRanges(matchRanges) };
|
|
76116
|
+
};
|
|
76117
|
+
|
|
76118
|
+
// Returns true when position idx in str is at a word boundary,
|
|
76119
|
+
// meaning it is either the start of the string or the preceding character
|
|
76120
|
+
// is not a Unicode letter or digit.
|
|
76121
|
+
const isWordBoundary = (str, idx) => {
|
|
76122
|
+
if (idx === 0) {
|
|
76123
|
+
return true;
|
|
76124
|
+
}
|
|
76125
|
+
return !/[\p{L}\p{N}]/u.test(str[idx - 1]);
|
|
76126
|
+
};
|
|
76127
|
+
|
|
76128
|
+
// Strip diacritics for accent-insensitive matching.
|
|
76129
|
+
// NFC normalization first ensures precomposed characters (é → single code unit),
|
|
76130
|
+
// so the folded string has the same length as the NFC source — ranges computed
|
|
76131
|
+
// on the folded string map 1:1 to positions in the original string.
|
|
76132
|
+
const foldAccents = (str) => {
|
|
76133
|
+
return str
|
|
76134
|
+
.normalize("NFC")
|
|
76135
|
+
.normalize("NFD")
|
|
76136
|
+
.replace(/\p{Mn}/gu, "");
|
|
76137
|
+
};
|
|
76138
|
+
|
|
76139
|
+
const SCORE_PHRASE_AT_START = 1;
|
|
76140
|
+
const SCORE_MULTI_WORD_AT_START = 0.75;
|
|
76141
|
+
const SCORE_AT_WORD_BOUNDARY = 0.625;
|
|
76142
|
+
const SCORE_MID_WORD = 0.5;
|
|
76143
|
+
const SCORE_ACRONYM = 0.4;
|
|
76144
|
+
const SCORE_BONUS_CASE_EXACT = 0.125;
|
|
76145
|
+
|
|
76146
|
+
// Acronym match: each char of searchText (spaces stripped) must be the first
|
|
76147
|
+
// letter of a word in value, in order (greedy subsequence on word-starts).
|
|
76148
|
+
// e.g. "TC" matches "Total Count" highlighting the T and C.
|
|
76149
|
+
const tryAcronymMatch = (foldedStr, str, searchText) => {
|
|
76150
|
+
const acronymChars = foldAccents(searchText).toLowerCase().replace(/\s/g, "");
|
|
76151
|
+
if (acronymChars.length < 2) {
|
|
76152
|
+
// Single-char acronym is too ambiguous — skip.
|
|
76153
|
+
return { match: false, matchScore: 0, matchRanges: [] };
|
|
76154
|
+
}
|
|
76155
|
+
const wordStarts = [];
|
|
76156
|
+
for (let i = 0; i < foldedStr.length; i++) {
|
|
76157
|
+
if (isWordBoundary(foldedStr, i)) {
|
|
76158
|
+
wordStarts.push(i);
|
|
76159
|
+
}
|
|
76160
|
+
}
|
|
76161
|
+
const matchedPositions = [];
|
|
76162
|
+
let wordIdx = 0;
|
|
76163
|
+
const originalAcronym = searchText.replace(/\s/g, "");
|
|
76164
|
+
for (let si = 0; si < acronymChars.length; si++) {
|
|
76165
|
+
const ch = acronymChars[si];
|
|
76166
|
+
let found = false;
|
|
76167
|
+
while (wordIdx < wordStarts.length) {
|
|
76168
|
+
const pos = wordStarts[wordIdx];
|
|
76169
|
+
wordIdx++;
|
|
76170
|
+
if (foldedStr[pos] === ch) {
|
|
76171
|
+
matchedPositions.push(pos);
|
|
76172
|
+
found = true;
|
|
76173
|
+
break;
|
|
76174
|
+
}
|
|
76175
|
+
}
|
|
76176
|
+
if (!found) {
|
|
76177
|
+
return { match: false, matchScore: 0, matchRanges: [] };
|
|
76178
|
+
}
|
|
76179
|
+
}
|
|
76180
|
+
const atStart = matchedPositions[0] === 0;
|
|
76181
|
+
const caseExact = matchedPositions.every(
|
|
76182
|
+
(p, i) => str[p] === originalAcronym[i],
|
|
76183
|
+
);
|
|
76184
|
+
const baseScore = atStart ? SCORE_ACRONYM + 0.05 : SCORE_ACRONYM;
|
|
76185
|
+
const matchScore = baseScore + (caseExact ? SCORE_BONUS_CASE_EXACT : 0);
|
|
76186
|
+
const matchRanges = matchedPositions.map((p) => [p, p + 1]);
|
|
76187
|
+
return { match: true, matchScore, matchRanges };
|
|
76188
|
+
};
|
|
76189
|
+
|
|
76190
|
+
// LRU cache for pre-computed search info, avoids recomputing foldAccents/toLowerCase
|
|
76191
|
+
// for the same searchText across all items in a list render.
|
|
76192
|
+
const searchCache = new Map();
|
|
76193
|
+
const SEARCH_CACHE_MAX_SIZE = 20;
|
|
76194
|
+
const getSearchInfo = (searchText) => {
|
|
76195
|
+
if (searchCache.has(searchText)) {
|
|
76196
|
+
const cached = searchCache.get(searchText);
|
|
76197
|
+
searchCache.delete(searchText);
|
|
76198
|
+
searchCache.set(searchText, cached);
|
|
76199
|
+
return cached;
|
|
76200
|
+
}
|
|
76201
|
+
const foldedSearch = foldAccents(searchText).toLowerCase();
|
|
76202
|
+
const words = foldedSearch.split(/\s+/).filter(Boolean);
|
|
76203
|
+
const originalWords = searchText.split(/\s+/).filter(Boolean);
|
|
76204
|
+
const info = { foldedSearch, words, originalWords };
|
|
76205
|
+
searchCache.set(searchText, info);
|
|
76206
|
+
if (searchCache.size > SEARCH_CACHE_MAX_SIZE) {
|
|
76207
|
+
searchCache.delete(searchCache.keys().next().value);
|
|
76208
|
+
}
|
|
76209
|
+
return info;
|
|
76210
|
+
};
|
|
76211
|
+
|
|
76212
|
+
// Merge overlapping or adjacent [start, end] ranges (sorted by start).
|
|
76213
|
+
const mergeRanges = (ranges) => {
|
|
76214
|
+
if (ranges.length < 2) {
|
|
76215
|
+
return ranges;
|
|
76216
|
+
}
|
|
76217
|
+
const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
|
|
76218
|
+
const merged = [sorted[0]];
|
|
76219
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
76220
|
+
const last = merged[merged.length - 1];
|
|
76221
|
+
const current = sorted[i];
|
|
76222
|
+
if (current[0] <= last[1]) {
|
|
76223
|
+
if (current[1] > last[1]) {
|
|
76224
|
+
last[1] = current[1];
|
|
76225
|
+
}
|
|
76226
|
+
} else {
|
|
76227
|
+
merged.push(current);
|
|
76228
|
+
}
|
|
76229
|
+
}
|
|
76230
|
+
return merged;
|
|
76231
|
+
};
|
|
76232
|
+
|
|
76233
|
+
/**
|
|
76234
|
+
* createSearch — builds a matchFn compatible with useSearch that searches
|
|
76235
|
+
* across multiple named fields of an item, each with its own DOM selector
|
|
76236
|
+
* and optional priority weight.
|
|
76237
|
+
*
|
|
76238
|
+
* Usage:
|
|
76239
|
+
* ```js
|
|
76240
|
+
* const searchPerson = createSearch({
|
|
76241
|
+
* name: {
|
|
76242
|
+
* getter: (item) => item.name,
|
|
76243
|
+
* domSelector: ".name",
|
|
76244
|
+
* },
|
|
76245
|
+
* address: {
|
|
76246
|
+
* getter: (item) => item.address,
|
|
76247
|
+
* domSelector: ".address",
|
|
76248
|
+
* priority: 1.5,
|
|
76249
|
+
* },
|
|
76250
|
+
* });
|
|
76251
|
+
*
|
|
76252
|
+
* const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
|
|
76253
|
+
* // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
|
|
76254
|
+
* // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
|
|
76255
|
+
* // — ListItem handles the per-selector object format for matchRanges.
|
|
76256
|
+
* ```
|
|
76257
|
+
*
|
|
76258
|
+
* Each field config:
|
|
76259
|
+
* - getter(item): string — extracts the text to search
|
|
76260
|
+
* - domSelector: string — CSS selector used by ListItem to find the target element
|
|
76261
|
+
* - priority?: number — multiplier applied to the field's score (default 1)
|
|
76262
|
+
* - matchFn?: function — custom match function (searchText, fieldValue) => { match, matchScore, matchRanges }
|
|
76263
|
+
* defaults to applySearch
|
|
76264
|
+
*/
|
|
76265
|
+
const createSearch = (fields) => {
|
|
76266
|
+
return (searchText, item) => {
|
|
76267
|
+
if (!searchText) {
|
|
76268
|
+
return { match: true, matchScore: 0, matchRanges: {} };
|
|
76269
|
+
}
|
|
76270
|
+
let totalScore = 0;
|
|
76271
|
+
const matchRanges = {};
|
|
76272
|
+
for (const [
|
|
76273
|
+
,
|
|
76274
|
+
{ getter, domSelector, priority = 1, matchFn = applySearch },
|
|
76275
|
+
] of Object.entries(fields)) {
|
|
76276
|
+
const fieldValue = getter(item);
|
|
76277
|
+
const result = matchFn(searchText, fieldValue);
|
|
76278
|
+
if (result.match && result.matchRanges.length > 0) {
|
|
76279
|
+
totalScore += result.matchScore * priority;
|
|
76280
|
+
matchRanges[domSelector] = result.matchRanges;
|
|
76281
|
+
}
|
|
76282
|
+
}
|
|
76283
|
+
if (totalScore === 0) {
|
|
76284
|
+
return { match: false, matchScore: 0, matchRanges: {} };
|
|
76285
|
+
}
|
|
76286
|
+
return { match: true, matchScore: totalScore, matchRanges };
|
|
76287
|
+
};
|
|
76288
|
+
};
|
|
76289
|
+
|
|
76290
|
+
/**
|
|
76291
|
+
* useSearch — reorders items so matched ones come first (sorted by score desc),
|
|
76292
|
+
* followed by non-matched items in their natural order. No item is hidden.
|
|
76293
|
+
* Returns [orderedItems, getItemMatchInfo].
|
|
76294
|
+
* - orderedItems: all items, reordered
|
|
76295
|
+
* - getItemMatchInfo(item): { match, matchScore, matchRanges } — pass the
|
|
76296
|
+
* whole thing straight to <ListItem matchInfo={getItemMatchInfo(item)} />,
|
|
76297
|
+
* there is no need to destructure the three fields by hand.
|
|
76298
|
+
*
|
|
76299
|
+
* When searchText is empty, natural order is preserved and all items match with score 0.
|
|
76300
|
+
*
|
|
76301
|
+
* To filter (hide non-matching items), pass filtered={!getItemMatchInfo(item).match}
|
|
76302
|
+
* to each ListItem. The list's matchFallback will be shown when all items are hidden.
|
|
76303
|
+
*/
|
|
76304
|
+
const useSearchText = (searchText, items, matchFn = applySearch) => {
|
|
76305
|
+
if (typeof searchText !== "string" && searchText !== undefined) {
|
|
76306
|
+
throw new TypeError(
|
|
76307
|
+
"useSearchText: searchText must be a string or undefined",
|
|
76308
|
+
);
|
|
76309
|
+
}
|
|
76310
|
+
if (items === undefined) {
|
|
76311
|
+
throw new TypeError("useSearch: items is undefined");
|
|
76312
|
+
}
|
|
76313
|
+
const { orderedItems, matchInfoMap } = useMemo(() => {
|
|
76314
|
+
const { scoreEntries, nonMatched, matchInfoMap } = buildMatchInfo(
|
|
76315
|
+
searchText,
|
|
76316
|
+
items,
|
|
76317
|
+
matchFn,
|
|
76318
|
+
);
|
|
76319
|
+
const orderedItems = [];
|
|
76320
|
+
for (const [, bucket] of scoreEntries) {
|
|
76321
|
+
for (const { item } of bucket) {
|
|
76322
|
+
orderedItems.push(item);
|
|
76323
|
+
}
|
|
76324
|
+
}
|
|
76325
|
+
for (const { item } of nonMatched) {
|
|
76326
|
+
orderedItems.push(item);
|
|
76327
|
+
}
|
|
76328
|
+
return { orderedItems, matchInfoMap };
|
|
76329
|
+
}, [items, searchText, matchFn]);
|
|
76330
|
+
|
|
76331
|
+
// The same function for as long as the map is the same: a `renderItem`
|
|
76332
|
+
// reading it is stable only if this is, and a run keeps the rows it drew
|
|
76333
|
+
// only for a stable `renderItem` (see List.Items).
|
|
76334
|
+
const getItemMatchInfo = useCallback(
|
|
76335
|
+
(item) => matchInfoMap.get(item),
|
|
76336
|
+
[matchInfoMap],
|
|
76337
|
+
);
|
|
76338
|
+
|
|
76339
|
+
return [orderedItems, getItemMatchInfo];
|
|
76340
|
+
};
|
|
76341
|
+
|
|
76342
|
+
const buildMatchInfo = (searchText, items, matchFn) => {
|
|
76343
|
+
// scoreEntries: [score, bucket][] kept sorted desc by score.
|
|
76344
|
+
// New distinct score values are inserted via bisect — O(1) in practice
|
|
76345
|
+
// since there are very few distinct scores (today just 0 and 1).
|
|
76346
|
+
const scoreEntries = []; // [score, bucket][]
|
|
76347
|
+
const nonMatched = [];
|
|
76348
|
+
|
|
76349
|
+
for (const item of items) {
|
|
76350
|
+
const result = matchFn(searchText, item);
|
|
76351
|
+
if (!result.match) {
|
|
76352
|
+
nonMatched.push({
|
|
76353
|
+
item,
|
|
76354
|
+
matchScore: result.matchScore,
|
|
76355
|
+
matchRanges: result.matchRanges,
|
|
76356
|
+
});
|
|
76357
|
+
continue;
|
|
76358
|
+
}
|
|
76359
|
+
const score = result.matchScore;
|
|
76360
|
+
// Find existing bucket or insert a new entry in desc order.
|
|
76361
|
+
let lo = 0;
|
|
76362
|
+
let hi = scoreEntries.length;
|
|
76363
|
+
while (lo < hi) {
|
|
76364
|
+
const mid = (lo + hi) >> 1;
|
|
76365
|
+
if (scoreEntries[mid][0] > score) {
|
|
76366
|
+
lo = mid + 1;
|
|
76367
|
+
} else if (scoreEntries[mid][0] < score) {
|
|
76368
|
+
hi = mid;
|
|
76369
|
+
} else {
|
|
76370
|
+
lo = mid;
|
|
76371
|
+
hi = mid; // exact match — found the bucket
|
|
76372
|
+
}
|
|
76373
|
+
}
|
|
76374
|
+
if (lo < scoreEntries.length && scoreEntries[lo][0] === score) {
|
|
76375
|
+
scoreEntries[lo][1].push({ item, matchRanges: result.matchRanges });
|
|
76093
76376
|
} else {
|
|
76094
|
-
|
|
76377
|
+
scoreEntries.splice(lo, 0, [
|
|
76378
|
+
score,
|
|
76379
|
+
[{ item, matchRanges: result.matchRanges }],
|
|
76380
|
+
]);
|
|
76095
76381
|
}
|
|
76096
76382
|
}
|
|
76097
|
-
|
|
76383
|
+
|
|
76384
|
+
const matchInfoMap = new Map();
|
|
76385
|
+
for (const [score, bucket] of scoreEntries) {
|
|
76386
|
+
for (const { item, matchRanges } of bucket) {
|
|
76387
|
+
matchInfoMap.set(item, { match: true, matchScore: score, matchRanges });
|
|
76388
|
+
}
|
|
76389
|
+
}
|
|
76390
|
+
for (const { item, matchScore, matchRanges } of nonMatched) {
|
|
76391
|
+
matchInfoMap.set(item, { match: false, matchScore, matchRanges });
|
|
76392
|
+
}
|
|
76393
|
+
|
|
76394
|
+
return { scoreEntries, nonMatched, matchInfoMap };
|
|
76098
76395
|
};
|
|
76099
76396
|
|
|
76100
|
-
|
|
76101
|
-
*
|
|
76397
|
+
/*
|
|
76398
|
+
* useItemTracker() — hook that creates a stable item tracker for the lifetime
|
|
76399
|
+
* of the host component.
|
|
76102
76400
|
*
|
|
76103
|
-
*
|
|
76104
|
-
*
|
|
76105
|
-
*
|
|
76106
|
-
*
|
|
76401
|
+
* USAGE:
|
|
76402
|
+
* ```jsx
|
|
76403
|
+
* function ListControlled({ items }) {
|
|
76404
|
+
* const tracker = useItemTracker({
|
|
76405
|
+
* onChange: () => console.log("items changed"),
|
|
76406
|
+
* });
|
|
76107
76407
|
*
|
|
76108
|
-
*
|
|
76408
|
+
* return (
|
|
76409
|
+
* <ul>
|
|
76410
|
+
* {items.map((item, i) => (
|
|
76411
|
+
* <Row key={item.id} id={item.id} index={i} hidden={item.hidden} value={item.value} tracker={tracker} />
|
|
76412
|
+
* ))}
|
|
76413
|
+
* <Count tracker={tracker} />
|
|
76414
|
+
* </ul>
|
|
76415
|
+
* );
|
|
76416
|
+
* }
|
|
76109
76417
|
*
|
|
76110
|
-
*
|
|
76111
|
-
*
|
|
76112
|
-
*
|
|
76113
|
-
*
|
|
76114
|
-
*
|
|
76115
|
-
* phrase / words mid-word 0.5
|
|
76116
|
-
* + case-exact bonus +0.125
|
|
76117
|
-
* multi-word partial: score × (matched/total)
|
|
76418
|
+
* function Row({ id, index, hidden, value, tracker }) {
|
|
76419
|
+
* const visibleIndex = tracker.useTrackItem({ id, index, hidden, value });
|
|
76420
|
+
* if (visibleIndex === -1) return null;
|
|
76421
|
+
* return <li>{value}</li>;
|
|
76422
|
+
* }
|
|
76118
76423
|
*
|
|
76119
|
-
*
|
|
76120
|
-
*
|
|
76424
|
+
* function Count({ tracker }) {
|
|
76425
|
+
* const count = tracker.visibleCountSignal.value; // re-renders only when count changes
|
|
76426
|
+
* return <span>{count} items</span>;
|
|
76427
|
+
* }
|
|
76428
|
+
* ```
|
|
76429
|
+
*
|
|
76430
|
+
* INTERNALS:
|
|
76431
|
+
* - registrations: Map key → data, contains only visible items
|
|
76432
|
+
* - idToKey: Map id → key, stable across renders
|
|
76433
|
+
* - orderedKeys: number[] of visible item keys sorted by explicit order
|
|
76434
|
+
* - keyToOrderedIndex: Map key → orderedKeys index, gives O(1) indexOf equivalent
|
|
76435
|
+
* - keyToExplicitOrder: Map key → explicitly passed index, used to maintain sort order
|
|
76436
|
+
* - allItemsSignal: signal(array), all items including hidden, ordered by explicit index
|
|
76437
|
+
* - visibleItemsSignal: signal(array), non-hidden items only
|
|
76438
|
+
* - countSignal: signal(number), count of all items including hidden
|
|
76439
|
+
* - visibleCountSignal: signal(number), updated in microtask batch, only when count changes
|
|
76440
|
+
* - propSignals: Map propName → signal(array), updated in microtask batch with element equality
|
|
76441
|
+
* - onChangeRef: holds the latest onChange callback, called once per microtask batch
|
|
76442
|
+
*
|
|
76443
|
+
* useTrackItem(id, data, index): registers the item with an explicitly provided index
|
|
76444
|
+
* that determines its position among siblings. The caller (e.g. items.map) knows the
|
|
76445
|
+
* correct order and passes it directly — no render-sequence deduction needed.
|
|
76446
|
+
* Returns the visible rank (position among non-hidden items), or -1 when hidden.
|
|
76447
|
+
* Signals and onChange are deferred to a microtask so multiple items updating
|
|
76448
|
+
* in one commit cause only one notification.
|
|
76449
|
+
*
|
|
76450
|
+
* getTrackedItemByIndex(index): synchronous O(1) lookup of a visible item by
|
|
76451
|
+
* its visible rank. Returns undefined when index is out of range.
|
|
76452
|
+
*
|
|
76453
|
+
* peekItems(): the items as they stand right now, without waiting for the
|
|
76454
|
+
* deferred notification — what a sibling rendering after the items must read
|
|
76455
|
+
* to paint them in the same commit.
|
|
76121
76456
|
*/
|
|
76122
|
-
const applySearch = (searchText, value) => {
|
|
76123
|
-
if (!searchText) {
|
|
76124
|
-
return { match: true, matchScore: 0, matchRanges: [] };
|
|
76125
|
-
}
|
|
76126
|
-
if (searchText.length > 100) {
|
|
76127
|
-
searchText = searchText.slice(0, 100);
|
|
76128
|
-
}
|
|
76129
|
-
const str = String(value);
|
|
76130
|
-
const foldedStr = foldAccents(str).toLowerCase();
|
|
76131
|
-
const { foldedSearch, words, originalWords } = getSearchInfo(searchText);
|
|
76132
76457
|
|
|
76133
|
-
|
|
76134
|
-
const
|
|
76135
|
-
|
|
76136
|
-
|
|
76137
|
-
|
|
76138
|
-
|
|
76139
|
-
|
|
76140
|
-
|
|
76141
|
-
|
|
76142
|
-
const atWordBoundary = phraseRanges.some(([start]) =>
|
|
76143
|
-
isWordBoundary(foldedStr, start),
|
|
76144
|
-
);
|
|
76145
|
-
const caseExact = str.includes(searchText);
|
|
76146
|
-
let baseScore;
|
|
76147
|
-
if (atStart) {
|
|
76148
|
-
baseScore = SCORE_PHRASE_AT_START;
|
|
76149
|
-
} else if (atWordBoundary) {
|
|
76150
|
-
baseScore = SCORE_AT_WORD_BOUNDARY;
|
|
76151
|
-
} else {
|
|
76152
|
-
baseScore = SCORE_MID_WORD;
|
|
76153
|
-
}
|
|
76154
|
-
const matchScore = baseScore + (caseExact ? SCORE_BONUS_CASE_EXACT : 0);
|
|
76155
|
-
return { match: true, matchScore, matchRanges: mergeRanges(phraseRanges) };
|
|
76458
|
+
const useItemTracker = ({ onChange } = {}) => {
|
|
76459
|
+
const onChangeRef = useRef(onChange);
|
|
76460
|
+
onChangeRef.current = onChange;
|
|
76461
|
+
const trackerRef = useRef(null);
|
|
76462
|
+
let tracker = trackerRef.current;
|
|
76463
|
+
if (!tracker) {
|
|
76464
|
+
trackerRef.current = tracker = createItemTracker((items) => {
|
|
76465
|
+
onChangeRef.current?.(items);
|
|
76466
|
+
});
|
|
76156
76467
|
}
|
|
76468
|
+
// When code in useLayoutEffect of the caller wants to run the tracker must be in sync
|
|
76469
|
+
// without this layout effect the tracker might not have been synced yet and preact would call layout effect
|
|
76470
|
+
// before we had time to sync
|
|
76471
|
+
useLayoutEffect(() => {
|
|
76472
|
+
tracker._flushSync();
|
|
76473
|
+
});
|
|
76474
|
+
return tracker;
|
|
76475
|
+
};
|
|
76476
|
+
|
|
76477
|
+
const createItemTracker = (onChange) => {
|
|
76478
|
+
const registrations = new Map(); // key → data (visible items only)
|
|
76479
|
+
const idToKey = new Map(); // id → insertion key (stable, auto-incremented)
|
|
76480
|
+
let keyCounter = 0;
|
|
76481
|
+
// orderedKeys: visible item keys sorted by their explicitly provided index.
|
|
76482
|
+
const orderedKeys = []; // number[]
|
|
76483
|
+
// keyToOrderedIndex: O(1) equivalent of orderedKeys.indexOf(key).
|
|
76484
|
+
const keyToOrderedIndex = new Map(); // key → index in orderedKeys
|
|
76485
|
+
const allKeys = new Set(); // all registered keys including hidden
|
|
76486
|
+
const keyToExplicitOrder = new Map(); // key → explicitly passed index
|
|
76487
|
+
|
|
76488
|
+
const allRegistrations = new Map(); // key → data (all items including hidden)
|
|
76489
|
+
const allOrderedKeys = []; // all item keys sorted by explicit order
|
|
76490
|
+
const keyToAllOrderedIndex = new Map(); // key → index in allOrderedKeys
|
|
76491
|
+
|
|
76492
|
+
const itemsSignal = signal([]);
|
|
76493
|
+
const visibleItemsSignal = signal([]);
|
|
76494
|
+
const countSignal = signal(0);
|
|
76495
|
+
const visibleCountSignal = signal(0);
|
|
76496
|
+
const noMatchCountSignal = signal(0);
|
|
76497
|
+
|
|
76498
|
+
let notifyScheduled = false;
|
|
76499
|
+
const runNotify = () => {
|
|
76500
|
+
batch(() => {
|
|
76501
|
+
let someChange = false;
|
|
76502
|
+
|
|
76503
|
+
const newCount = allKeys.size;
|
|
76504
|
+
const countModified = countSignal.peek() !== newCount;
|
|
76505
|
+
if (countModified) {
|
|
76506
|
+
countSignal.value = newCount;
|
|
76507
|
+
someChange = true;
|
|
76508
|
+
}
|
|
76509
|
+
|
|
76510
|
+
// Build allItems and visibleItems in a single pass over allOrderedKeys.
|
|
76511
|
+
// Visible items are those without data.hidden or data.filtered — same
|
|
76512
|
+
// relative order as orderedKeys (syncItem already excludes both from
|
|
76513
|
+
// orderedKeys; this must match or consumers relying on visibleCountSignal
|
|
76514
|
+
// would count filtered-out items as if they still took up space).
|
|
76515
|
+
const prevAllItems = itemsSignal.peek();
|
|
76516
|
+
const prevVisibleItems = visibleItemsSignal.peek();
|
|
76517
|
+
let allItemsChanged = prevAllItems.length !== allOrderedKeys.length;
|
|
76518
|
+
let visibleItemsChanged = false;
|
|
76519
|
+
const allItems = [];
|
|
76520
|
+
const visibleItems = [];
|
|
76521
|
+
let newNoMatchCount = 0;
|
|
76522
|
+
for (let i = 0; i < allOrderedKeys.length; i++) {
|
|
76523
|
+
const key = allOrderedKeys[i];
|
|
76524
|
+
const item = allRegistrations.get(key);
|
|
76525
|
+
allItems.push(item);
|
|
76526
|
+
// Compare by reference: catches any prop change (id, selected, disabled, …)
|
|
76527
|
+
if (!allItemsChanged && item !== prevAllItems[i]) {
|
|
76528
|
+
allItemsChanged = true;
|
|
76529
|
+
}
|
|
76530
|
+
if (item.match === false) {
|
|
76531
|
+
newNoMatchCount++;
|
|
76532
|
+
}
|
|
76533
|
+
if (!item.hidden && !item.filtered) {
|
|
76534
|
+
const visibleIdx = visibleItems.length;
|
|
76535
|
+
visibleItems.push(item);
|
|
76536
|
+
if (!visibleItemsChanged && item !== prevVisibleItems[visibleIdx]) {
|
|
76537
|
+
visibleItemsChanged = true;
|
|
76538
|
+
}
|
|
76539
|
+
}
|
|
76540
|
+
}
|
|
76541
|
+
|
|
76542
|
+
const newVisibleCount = visibleItems.length;
|
|
76543
|
+
const visibleCountModified =
|
|
76544
|
+
visibleCountSignal.peek() !== newVisibleCount;
|
|
76545
|
+
if (visibleCountModified) {
|
|
76546
|
+
visibleCountSignal.value = newVisibleCount;
|
|
76547
|
+
someChange = true;
|
|
76548
|
+
}
|
|
76549
|
+
if (allItemsChanged) {
|
|
76550
|
+
itemsSignal.value = allItems;
|
|
76551
|
+
someChange = true;
|
|
76552
|
+
}
|
|
76553
|
+
if (visibleItemsChanged) {
|
|
76554
|
+
visibleItemsSignal.value = visibleItems;
|
|
76555
|
+
someChange = true;
|
|
76556
|
+
}
|
|
76557
|
+
const noMatchCountModified =
|
|
76558
|
+
noMatchCountSignal.peek() !== newNoMatchCount;
|
|
76559
|
+
if (noMatchCountModified) {
|
|
76560
|
+
noMatchCountSignal.value = newNoMatchCount;
|
|
76561
|
+
someChange = true;
|
|
76562
|
+
}
|
|
76563
|
+
if (someChange) {
|
|
76564
|
+
onChange?.();
|
|
76565
|
+
}
|
|
76566
|
+
});
|
|
76567
|
+
};
|
|
76157
76568
|
|
|
76158
|
-
|
|
76159
|
-
|
|
76160
|
-
|
|
76161
|
-
// foldedSearch.split filters empty strings). This path also handles the case
|
|
76162
|
-
// where searchText has trailing/leading spaces: the phrase match above tries
|
|
76163
|
-
// the literal (e.g. "tc " in "tc adapter"), and if that fails we fall through
|
|
76164
|
-
// here to try each word individually (e.g. "tc" matches "tca").
|
|
76165
|
-
const matchRanges = [];
|
|
76166
|
-
let matchedWordCount = 0;
|
|
76167
|
-
let anyWordAtStart = false;
|
|
76168
|
-
let anyWordAtWordBoundary = false;
|
|
76169
|
-
let allMatchedWordsExact = true;
|
|
76170
|
-
for (let w = 0; w < words.length; w++) {
|
|
76171
|
-
const word = words[w];
|
|
76172
|
-
const originalWord = originalWords[w];
|
|
76173
|
-
let idx = foldedStr.indexOf(word);
|
|
76174
|
-
if (idx === -1) {
|
|
76175
|
-
continue;
|
|
76569
|
+
const notify = () => {
|
|
76570
|
+
if (notifyScheduled) {
|
|
76571
|
+
return;
|
|
76176
76572
|
}
|
|
76177
|
-
|
|
76178
|
-
|
|
76179
|
-
|
|
76180
|
-
|
|
76181
|
-
if (idx === 0) {
|
|
76182
|
-
anyWordAtStart = true;
|
|
76183
|
-
anyWordAtWordBoundary = true;
|
|
76184
|
-
} else if (isWordBoundary(foldedStr, idx)) {
|
|
76185
|
-
anyWordAtWordBoundary = true;
|
|
76573
|
+
notifyScheduled = true;
|
|
76574
|
+
queueMicrotask(() => {
|
|
76575
|
+
if (!notifyScheduled) {
|
|
76576
|
+
return; // was already flushed synchronously
|
|
76186
76577
|
}
|
|
76187
|
-
|
|
76188
|
-
|
|
76578
|
+
notifyScheduled = false;
|
|
76579
|
+
runNotify();
|
|
76580
|
+
});
|
|
76581
|
+
};
|
|
76582
|
+
|
|
76583
|
+
const _flushSync = () => {
|
|
76584
|
+
if (!notifyScheduled) {
|
|
76585
|
+
return;
|
|
76586
|
+
}
|
|
76587
|
+
notifyScheduled = false;
|
|
76588
|
+
runNotify();
|
|
76589
|
+
};
|
|
76590
|
+
|
|
76591
|
+
// Insert key into orderedKeys at the correct position based on explicitOrder.
|
|
76592
|
+
// Uses binary search for O(log n) insertion.
|
|
76593
|
+
const insertKey = (key, explicitOrder) => {
|
|
76594
|
+
let lo = 0;
|
|
76595
|
+
let hi = orderedKeys.length;
|
|
76596
|
+
while (lo < hi) {
|
|
76597
|
+
const mid = (lo + hi) >> 1;
|
|
76598
|
+
if (keyToExplicitOrder.get(orderedKeys[mid]) <= explicitOrder) {
|
|
76599
|
+
lo = mid + 1;
|
|
76600
|
+
} else {
|
|
76601
|
+
hi = mid;
|
|
76189
76602
|
}
|
|
76190
|
-
idx = foldedStr.indexOf(word, idx + 1);
|
|
76191
76603
|
}
|
|
76192
|
-
|
|
76193
|
-
|
|
76604
|
+
orderedKeys.splice(lo, 0, key);
|
|
76605
|
+
for (let i = lo; i < orderedKeys.length; i++) {
|
|
76606
|
+
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
76194
76607
|
}
|
|
76195
|
-
}
|
|
76196
|
-
if (matchedWordCount === 0) {
|
|
76197
|
-
return tryAcronymMatch(foldedStr, str, searchText);
|
|
76198
|
-
}
|
|
76199
|
-
const wordRatio = matchedWordCount / words.length;
|
|
76200
|
-
let baseScore;
|
|
76201
|
-
if (anyWordAtStart) {
|
|
76202
|
-
baseScore = SCORE_MULTI_WORD_AT_START;
|
|
76203
|
-
} else if (anyWordAtWordBoundary) {
|
|
76204
|
-
baseScore = SCORE_AT_WORD_BOUNDARY;
|
|
76205
|
-
} else {
|
|
76206
|
-
baseScore = SCORE_MID_WORD;
|
|
76207
|
-
}
|
|
76208
|
-
const matchScore =
|
|
76209
|
-
(baseScore + (allMatchedWordsExact ? SCORE_BONUS_CASE_EXACT : 0)) *
|
|
76210
|
-
wordRatio;
|
|
76211
|
-
return { match: true, matchScore, matchRanges: mergeRanges(matchRanges) };
|
|
76212
|
-
};
|
|
76213
|
-
|
|
76214
|
-
// Returns true when position idx in str is at a word boundary,
|
|
76215
|
-
// meaning it is either the start of the string or the preceding character
|
|
76216
|
-
// is not a Unicode letter or digit.
|
|
76217
|
-
const isWordBoundary = (str, idx) => {
|
|
76218
|
-
if (idx === 0) {
|
|
76219
|
-
return true;
|
|
76220
|
-
}
|
|
76221
|
-
return !/[\p{L}\p{N}]/u.test(str[idx - 1]);
|
|
76222
|
-
};
|
|
76223
|
-
|
|
76224
|
-
// Strip diacritics for accent-insensitive matching.
|
|
76225
|
-
// NFC normalization first ensures precomposed characters (é → single code unit),
|
|
76226
|
-
// so the folded string has the same length as the NFC source — ranges computed
|
|
76227
|
-
// on the folded string map 1:1 to positions in the original string.
|
|
76228
|
-
const foldAccents = (str) => {
|
|
76229
|
-
return str
|
|
76230
|
-
.normalize("NFC")
|
|
76231
|
-
.normalize("NFD")
|
|
76232
|
-
.replace(/\p{Mn}/gu, "");
|
|
76233
|
-
};
|
|
76234
|
-
|
|
76235
|
-
const SCORE_PHRASE_AT_START = 1;
|
|
76236
|
-
const SCORE_MULTI_WORD_AT_START = 0.75;
|
|
76237
|
-
const SCORE_AT_WORD_BOUNDARY = 0.625;
|
|
76238
|
-
const SCORE_MID_WORD = 0.5;
|
|
76239
|
-
const SCORE_ACRONYM = 0.4;
|
|
76240
|
-
const SCORE_BONUS_CASE_EXACT = 0.125;
|
|
76608
|
+
};
|
|
76241
76609
|
|
|
76242
|
-
|
|
76243
|
-
|
|
76244
|
-
|
|
76245
|
-
|
|
76246
|
-
|
|
76247
|
-
|
|
76248
|
-
|
|
76249
|
-
|
|
76250
|
-
|
|
76251
|
-
const wordStarts = [];
|
|
76252
|
-
for (let i = 0; i < foldedStr.length; i++) {
|
|
76253
|
-
if (isWordBoundary(foldedStr, i)) {
|
|
76254
|
-
wordStarts.push(i);
|
|
76255
|
-
}
|
|
76256
|
-
}
|
|
76257
|
-
const matchedPositions = [];
|
|
76258
|
-
let wordIdx = 0;
|
|
76259
|
-
const originalAcronym = searchText.replace(/\s/g, "");
|
|
76260
|
-
for (let si = 0; si < acronymChars.length; si++) {
|
|
76261
|
-
const ch = acronymChars[si];
|
|
76262
|
-
let found = false;
|
|
76263
|
-
while (wordIdx < wordStarts.length) {
|
|
76264
|
-
const pos = wordStarts[wordIdx];
|
|
76265
|
-
wordIdx++;
|
|
76266
|
-
if (foldedStr[pos] === ch) {
|
|
76267
|
-
matchedPositions.push(pos);
|
|
76268
|
-
found = true;
|
|
76269
|
-
break;
|
|
76610
|
+
const insertAllKey = (key, explicitOrder) => {
|
|
76611
|
+
let lo = 0;
|
|
76612
|
+
let hi = allOrderedKeys.length;
|
|
76613
|
+
while (lo < hi) {
|
|
76614
|
+
const mid = (lo + hi) >> 1;
|
|
76615
|
+
if (keyToExplicitOrder.get(allOrderedKeys[mid]) <= explicitOrder) {
|
|
76616
|
+
lo = mid + 1;
|
|
76617
|
+
} else {
|
|
76618
|
+
hi = mid;
|
|
76270
76619
|
}
|
|
76271
76620
|
}
|
|
76272
|
-
|
|
76273
|
-
|
|
76621
|
+
allOrderedKeys.splice(lo, 0, key);
|
|
76622
|
+
for (let i = lo; i < allOrderedKeys.length; i++) {
|
|
76623
|
+
keyToAllOrderedIndex.set(allOrderedKeys[i], i);
|
|
76274
76624
|
}
|
|
76275
|
-
}
|
|
76276
|
-
const atStart = matchedPositions[0] === 0;
|
|
76277
|
-
const caseExact = matchedPositions.every(
|
|
76278
|
-
(p, i) => str[p] === originalAcronym[i],
|
|
76279
|
-
);
|
|
76280
|
-
const baseScore = atStart ? SCORE_ACRONYM + 0.05 : SCORE_ACRONYM;
|
|
76281
|
-
const matchScore = baseScore + (caseExact ? SCORE_BONUS_CASE_EXACT : 0);
|
|
76282
|
-
const matchRanges = matchedPositions.map((p) => [p, p + 1]);
|
|
76283
|
-
return { match: true, matchScore, matchRanges };
|
|
76284
|
-
};
|
|
76625
|
+
};
|
|
76285
76626
|
|
|
76286
|
-
|
|
76287
|
-
|
|
76288
|
-
|
|
76289
|
-
|
|
76290
|
-
|
|
76291
|
-
|
|
76292
|
-
|
|
76293
|
-
|
|
76294
|
-
|
|
76295
|
-
|
|
76296
|
-
}
|
|
76297
|
-
const foldedSearch = foldAccents(searchText).toLowerCase();
|
|
76298
|
-
const words = foldedSearch.split(/\s+/).filter(Boolean);
|
|
76299
|
-
const originalWords = searchText.split(/\s+/).filter(Boolean);
|
|
76300
|
-
const info = { foldedSearch, words, originalWords };
|
|
76301
|
-
searchCache.set(searchText, info);
|
|
76302
|
-
if (searchCache.size > SEARCH_CACHE_MAX_SIZE) {
|
|
76303
|
-
searchCache.delete(searchCache.keys().next().value);
|
|
76304
|
-
}
|
|
76305
|
-
return info;
|
|
76306
|
-
};
|
|
76627
|
+
const removeAllKey = (key) => {
|
|
76628
|
+
const idx = keyToAllOrderedIndex.get(key);
|
|
76629
|
+
if (idx !== undefined) {
|
|
76630
|
+
allOrderedKeys.splice(idx, 1);
|
|
76631
|
+
keyToAllOrderedIndex.delete(key);
|
|
76632
|
+
for (let i = idx; i < allOrderedKeys.length; i++) {
|
|
76633
|
+
keyToAllOrderedIndex.set(allOrderedKeys[i], i);
|
|
76634
|
+
}
|
|
76635
|
+
}
|
|
76636
|
+
};
|
|
76307
76637
|
|
|
76308
|
-
//
|
|
76309
|
-
|
|
76310
|
-
|
|
76311
|
-
|
|
76312
|
-
|
|
76313
|
-
|
|
76314
|
-
|
|
76315
|
-
|
|
76316
|
-
|
|
76317
|
-
|
|
76318
|
-
|
|
76319
|
-
|
|
76320
|
-
|
|
76638
|
+
// Register or update an item. data.hidden controls visibility.
|
|
76639
|
+
// explicitOrder is the caller-provided index that determines sort position.
|
|
76640
|
+
const syncItem = (key, index, data) => {
|
|
76641
|
+
if (data.role === "presentation") {
|
|
76642
|
+
registrations.delete(key);
|
|
76643
|
+
const idx = keyToOrderedIndex.get(key);
|
|
76644
|
+
if (idx !== undefined) {
|
|
76645
|
+
orderedKeys.splice(idx, 1);
|
|
76646
|
+
keyToOrderedIndex.delete(key);
|
|
76647
|
+
for (let i = idx; i < orderedKeys.length; i++) {
|
|
76648
|
+
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
76649
|
+
}
|
|
76650
|
+
}
|
|
76651
|
+
keyToExplicitOrder.delete(key);
|
|
76652
|
+
allRegistrations.delete(key);
|
|
76653
|
+
removeAllKey(key);
|
|
76654
|
+
allKeys.delete(key);
|
|
76655
|
+
return;
|
|
76656
|
+
}
|
|
76657
|
+
|
|
76658
|
+
// Maintain allRegistrations and allOrderedKeys for all non-presentation items.
|
|
76659
|
+
allRegistrations.set(key, data);
|
|
76660
|
+
allKeys.add(key);
|
|
76661
|
+
const currentAllIdx = keyToAllOrderedIndex.get(key);
|
|
76662
|
+
const previousOrder = keyToExplicitOrder.get(key);
|
|
76663
|
+
keyToExplicitOrder.set(key, index);
|
|
76664
|
+
if (currentAllIdx === undefined) {
|
|
76665
|
+
insertAllKey(key, index);
|
|
76666
|
+
} else if (previousOrder !== index) {
|
|
76667
|
+
allOrderedKeys.splice(currentAllIdx, 1);
|
|
76668
|
+
keyToAllOrderedIndex.delete(key);
|
|
76669
|
+
for (let i = currentAllIdx; i < allOrderedKeys.length; i++) {
|
|
76670
|
+
keyToAllOrderedIndex.set(allOrderedKeys[i], i);
|
|
76321
76671
|
}
|
|
76322
|
-
|
|
76323
|
-
merged.push(current);
|
|
76672
|
+
insertAllKey(key, index);
|
|
76324
76673
|
}
|
|
76325
|
-
}
|
|
76326
|
-
return merged;
|
|
76327
|
-
};
|
|
76328
76674
|
|
|
76329
|
-
|
|
76330
|
-
|
|
76331
|
-
|
|
76332
|
-
|
|
76333
|
-
|
|
76334
|
-
|
|
76335
|
-
|
|
76336
|
-
|
|
76337
|
-
|
|
76338
|
-
* getter: (item) => item.name,
|
|
76339
|
-
* domSelector: ".name",
|
|
76340
|
-
* },
|
|
76341
|
-
* address: {
|
|
76342
|
-
* getter: (item) => item.address,
|
|
76343
|
-
* domSelector: ".address",
|
|
76344
|
-
* priority: 1.5,
|
|
76345
|
-
* },
|
|
76346
|
-
* });
|
|
76347
|
-
*
|
|
76348
|
-
* const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
|
|
76349
|
-
* // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
|
|
76350
|
-
* // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
|
|
76351
|
-
* // — ListItem handles the per-selector object format for matchRanges.
|
|
76352
|
-
* ```
|
|
76353
|
-
*
|
|
76354
|
-
* Each field config:
|
|
76355
|
-
* - getter(item): string — extracts the text to search
|
|
76356
|
-
* - domSelector: string — CSS selector used by ListItem to find the target element
|
|
76357
|
-
* - priority?: number — multiplier applied to the field's score (default 1)
|
|
76358
|
-
* - matchFn?: function — custom match function (searchText, fieldValue) => { match, matchScore, matchRanges }
|
|
76359
|
-
* defaults to applySearch
|
|
76360
|
-
*/
|
|
76361
|
-
const createSearch = (fields) => {
|
|
76362
|
-
return (searchText, item) => {
|
|
76363
|
-
if (!searchText) {
|
|
76364
|
-
return { match: true, matchScore: 0, matchRanges: {} };
|
|
76365
|
-
}
|
|
76366
|
-
let totalScore = 0;
|
|
76367
|
-
const matchRanges = {};
|
|
76368
|
-
for (const [
|
|
76369
|
-
,
|
|
76370
|
-
{ getter, domSelector, priority = 1, matchFn = applySearch },
|
|
76371
|
-
] of Object.entries(fields)) {
|
|
76372
|
-
const fieldValue = getter(item);
|
|
76373
|
-
const result = matchFn(searchText, fieldValue);
|
|
76374
|
-
if (result.match && result.matchRanges.length > 0) {
|
|
76375
|
-
totalScore += result.matchScore * priority;
|
|
76376
|
-
matchRanges[domSelector] = result.matchRanges;
|
|
76675
|
+
if (data.filtered || data.hidden) {
|
|
76676
|
+
registrations.delete(key);
|
|
76677
|
+
const idx = keyToOrderedIndex.get(key);
|
|
76678
|
+
if (idx !== undefined) {
|
|
76679
|
+
orderedKeys.splice(idx, 1);
|
|
76680
|
+
keyToOrderedIndex.delete(key);
|
|
76681
|
+
for (let i = idx; i < orderedKeys.length; i++) {
|
|
76682
|
+
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
76683
|
+
}
|
|
76377
76684
|
}
|
|
76685
|
+
return;
|
|
76378
76686
|
}
|
|
76379
|
-
|
|
76380
|
-
|
|
76687
|
+
|
|
76688
|
+
registrations.set(key, data);
|
|
76689
|
+
const currentIdx = keyToOrderedIndex.get(key);
|
|
76690
|
+
if (currentIdx === undefined) {
|
|
76691
|
+
insertKey(key, index);
|
|
76692
|
+
return;
|
|
76381
76693
|
}
|
|
76382
|
-
|
|
76694
|
+
if (previousOrder === index) {
|
|
76695
|
+
return;
|
|
76696
|
+
}
|
|
76697
|
+
orderedKeys.splice(currentIdx, 1);
|
|
76698
|
+
keyToOrderedIndex.delete(key);
|
|
76699
|
+
for (let i = currentIdx; i < orderedKeys.length; i++) {
|
|
76700
|
+
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
76701
|
+
}
|
|
76702
|
+
insertKey(key, index);
|
|
76383
76703
|
};
|
|
76384
|
-
};
|
|
76385
76704
|
|
|
76386
|
-
|
|
76387
|
-
|
|
76388
|
-
|
|
76389
|
-
|
|
76390
|
-
|
|
76391
|
-
|
|
76392
|
-
|
|
76393
|
-
|
|
76394
|
-
*
|
|
76395
|
-
* When searchText is empty, natural order is preserved and all items match with score 0.
|
|
76396
|
-
*
|
|
76397
|
-
* To filter (hide non-matching items), pass filtered={!getItemMatchInfo(item).match}
|
|
76398
|
-
* to each ListItem. The list's matchFallback will be shown when all items are hidden.
|
|
76399
|
-
*/
|
|
76400
|
-
const useSearchText = (searchText, items, matchFn = applySearch) => {
|
|
76401
|
-
if (typeof searchText !== "string" && searchText !== undefined) {
|
|
76402
|
-
throw new TypeError(
|
|
76403
|
-
"useSearchText: searchText must be a string or undefined",
|
|
76404
|
-
);
|
|
76405
|
-
}
|
|
76406
|
-
if (items === undefined) {
|
|
76407
|
-
throw new TypeError("useSearch: items is undefined");
|
|
76408
|
-
}
|
|
76409
|
-
const { orderedItems, matchInfoMap } = useMemo(() => {
|
|
76410
|
-
const { scoreEntries, nonMatched, matchInfoMap } = buildMatchInfo(
|
|
76411
|
-
searchText,
|
|
76412
|
-
items,
|
|
76413
|
-
matchFn,
|
|
76414
|
-
);
|
|
76415
|
-
const orderedItems = [];
|
|
76416
|
-
for (const [, bucket] of scoreEntries) {
|
|
76417
|
-
for (const { item } of bucket) {
|
|
76418
|
-
orderedItems.push(item);
|
|
76705
|
+
const unregisterKey = (key) => {
|
|
76706
|
+
registrations.delete(key);
|
|
76707
|
+
const idx = keyToOrderedIndex.get(key);
|
|
76708
|
+
if (idx !== undefined) {
|
|
76709
|
+
orderedKeys.splice(idx, 1);
|
|
76710
|
+
keyToOrderedIndex.delete(key);
|
|
76711
|
+
for (let i = idx; i < orderedKeys.length; i++) {
|
|
76712
|
+
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
76419
76713
|
}
|
|
76420
76714
|
}
|
|
76421
|
-
|
|
76422
|
-
|
|
76715
|
+
keyToExplicitOrder.delete(key);
|
|
76716
|
+
allRegistrations.delete(key);
|
|
76717
|
+
removeAllKey(key);
|
|
76718
|
+
allKeys.delete(key);
|
|
76719
|
+
};
|
|
76720
|
+
|
|
76721
|
+
const keyForId = (id) => {
|
|
76722
|
+
if (!idToKey.has(id)) {
|
|
76723
|
+
idToKey.set(id, keyCounter++);
|
|
76423
76724
|
}
|
|
76424
|
-
return
|
|
76425
|
-
}
|
|
76725
|
+
return idToKey.get(id);
|
|
76726
|
+
};
|
|
76426
76727
|
|
|
76427
|
-
//
|
|
76428
|
-
//
|
|
76429
|
-
//
|
|
76430
|
-
|
|
76431
|
-
|
|
76432
|
-
|
|
76433
|
-
|
|
76728
|
+
// Register an item. data.hidden controls visibility.
|
|
76729
|
+
// explicitOrder is the caller-provided index (e.g. from items.map((item, i) => ...))
|
|
76730
|
+
// that determines this item's position among siblings.
|
|
76731
|
+
// Returns the item's visible rank among non-hidden items, or -1 when hidden.
|
|
76732
|
+
const useTrackItem = (data) => {
|
|
76733
|
+
const { id, index } = data;
|
|
76734
|
+
const key = keyForId(id);
|
|
76434
76735
|
|
|
76435
|
-
|
|
76436
|
-
|
|
76736
|
+
syncItem(key, index, data);
|
|
76737
|
+
notify();
|
|
76437
76738
|
|
|
76438
|
-
|
|
76439
|
-
|
|
76440
|
-
|
|
76441
|
-
|
|
76442
|
-
|
|
76443
|
-
|
|
76739
|
+
useLayoutEffect(() => {
|
|
76740
|
+
return () => {
|
|
76741
|
+
unregisterKey(key);
|
|
76742
|
+
notify();
|
|
76743
|
+
};
|
|
76744
|
+
}, []);
|
|
76444
76745
|
|
|
76445
|
-
|
|
76446
|
-
|
|
76447
|
-
if (!result.match) {
|
|
76448
|
-
nonMatched.push({
|
|
76449
|
-
item,
|
|
76450
|
-
matchScore: result.matchScore,
|
|
76451
|
-
matchRanges: result.matchRanges,
|
|
76452
|
-
});
|
|
76453
|
-
continue;
|
|
76454
|
-
}
|
|
76455
|
-
const score = result.matchScore;
|
|
76456
|
-
// Find existing bucket or insert a new entry in desc order.
|
|
76457
|
-
let lo = 0;
|
|
76458
|
-
let hi = scoreEntries.length;
|
|
76459
|
-
while (lo < hi) {
|
|
76460
|
-
const mid = (lo + hi) >> 1;
|
|
76461
|
-
if (scoreEntries[mid][0] > score) {
|
|
76462
|
-
lo = mid + 1;
|
|
76463
|
-
} else if (scoreEntries[mid][0] < score) {
|
|
76464
|
-
hi = mid;
|
|
76465
|
-
} else {
|
|
76466
|
-
lo = mid;
|
|
76467
|
-
hi = mid; // exact match — found the bucket
|
|
76468
|
-
}
|
|
76746
|
+
if (data.filtered || data.hidden || data.role === "presentation") {
|
|
76747
|
+
return -1;
|
|
76469
76748
|
}
|
|
76470
|
-
|
|
76471
|
-
|
|
76472
|
-
|
|
76473
|
-
|
|
76474
|
-
|
|
76475
|
-
|
|
76476
|
-
|
|
76749
|
+
return keyToOrderedIndex.get(key) ?? -1;
|
|
76750
|
+
};
|
|
76751
|
+
|
|
76752
|
+
const getTrackedItemByIndex = (index) => {
|
|
76753
|
+
const key = orderedKeys[index];
|
|
76754
|
+
if (key === undefined) {
|
|
76755
|
+
return undefined;
|
|
76477
76756
|
}
|
|
76478
|
-
|
|
76757
|
+
return registrations.get(key);
|
|
76758
|
+
};
|
|
76479
76759
|
|
|
76480
|
-
|
|
76481
|
-
|
|
76482
|
-
|
|
76483
|
-
|
|
76760
|
+
// The items as they stand right now, notification pending or not — same
|
|
76761
|
+
// content as itemsSignal, minus the wait.
|
|
76762
|
+
//
|
|
76763
|
+
// Items register during their own render, while the signal is only updated
|
|
76764
|
+
// on a deferred microtask (see notify): a sibling rendering after them would
|
|
76765
|
+
// otherwise paint from an empty list and correct itself a frame later. That
|
|
76766
|
+
// frame is visible whenever the painted size feeds a layout decision — a
|
|
76767
|
+
// dialog sizing itself on its content measures the empty version and shifts
|
|
76768
|
+
// once the real one lands. Reading this instead makes the first paint the
|
|
76769
|
+
// right one. Callers must still subscribe to itemsSignal to re-render on
|
|
76770
|
+
// LATER changes; this is the value to display, not the notification.
|
|
76771
|
+
const peekItems = () => {
|
|
76772
|
+
if (!notifyScheduled) {
|
|
76773
|
+
return itemsSignal.peek();
|
|
76484
76774
|
}
|
|
76485
|
-
|
|
76486
|
-
|
|
76487
|
-
|
|
76488
|
-
|
|
76775
|
+
const items = [];
|
|
76776
|
+
for (const key of allOrderedKeys) {
|
|
76777
|
+
items.push(allRegistrations.get(key));
|
|
76778
|
+
}
|
|
76779
|
+
return items;
|
|
76780
|
+
};
|
|
76489
76781
|
|
|
76490
|
-
return {
|
|
76782
|
+
return {
|
|
76783
|
+
useTrackItem,
|
|
76784
|
+
getTrackedItemByIndex,
|
|
76785
|
+
peekItems,
|
|
76786
|
+
itemsSignal,
|
|
76787
|
+
visibleItemsSignal,
|
|
76788
|
+
countSignal,
|
|
76789
|
+
visibleCountSignal,
|
|
76790
|
+
noMatchCountSignal,
|
|
76791
|
+
_flushSync,
|
|
76792
|
+
};
|
|
76491
76793
|
};
|
|
76492
76794
|
|
|
76493
76795
|
installImportMetaCssBuild(import.meta);
|