@jsenv/navi 0.29.344 → 0.29.346
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 +1596 -1378
- package/dist/dev/jsenv_navi.js.map +6 -4
- package/dist/jsenv_navi.js +1583 -1378
- package/dist/jsenv_navi.js.map +6 -4
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -66030,452 +66030,666 @@ const cssVars = vars => {
|
|
|
66030
66030
|
};
|
|
66031
66031
|
const lengthValue = value => typeof value === "number" ? `${value}px` : value;
|
|
66032
66032
|
|
|
66033
|
-
|
|
66034
|
-
|
|
66035
|
-
|
|
66036
|
-
|
|
66037
|
-
* USAGE:
|
|
66038
|
-
* ```jsx
|
|
66039
|
-
* function ListControlled({ items }) {
|
|
66040
|
-
* const tracker = useItemTracker({
|
|
66041
|
-
* onChange: () => console.log("items changed"),
|
|
66042
|
-
* });
|
|
66043
|
-
*
|
|
66044
|
-
* return (
|
|
66045
|
-
* <ul>
|
|
66046
|
-
* {items.map((item, i) => (
|
|
66047
|
-
* <Row key={item.id} id={item.id} index={i} hidden={item.hidden} value={item.value} tracker={tracker} />
|
|
66048
|
-
* ))}
|
|
66049
|
-
* <Count tracker={tracker} />
|
|
66050
|
-
* </ul>
|
|
66051
|
-
* );
|
|
66052
|
-
* }
|
|
66053
|
-
*
|
|
66054
|
-
* function Row({ id, index, hidden, value, tracker }) {
|
|
66055
|
-
* const visibleIndex = tracker.useTrackItem({ id, index, hidden, value });
|
|
66056
|
-
* if (visibleIndex === -1) return null;
|
|
66057
|
-
* return <li>{value}</li>;
|
|
66058
|
-
* }
|
|
66059
|
-
*
|
|
66060
|
-
* function Count({ tracker }) {
|
|
66061
|
-
* const count = tracker.visibleCountSignal.value; // re-renders only when count changes
|
|
66062
|
-
* return <span>{count} items</span>;
|
|
66063
|
-
* }
|
|
66064
|
-
* ```
|
|
66065
|
-
*
|
|
66066
|
-
* INTERNALS:
|
|
66067
|
-
* - registrations: Map key → data, contains only visible items
|
|
66068
|
-
* - idToKey: Map id → key, stable across renders
|
|
66069
|
-
* - orderedKeys: number[] of visible item keys sorted by explicit order
|
|
66070
|
-
* - keyToOrderedIndex: Map key → orderedKeys index, gives O(1) indexOf equivalent
|
|
66071
|
-
* - keyToExplicitOrder: Map key → explicitly passed index, used to maintain sort order
|
|
66072
|
-
* - allItemsSignal: signal(array), all items including hidden, ordered by explicit index
|
|
66073
|
-
* - visibleItemsSignal: signal(array), non-hidden items only
|
|
66074
|
-
* - countSignal: signal(number), count of all items including hidden
|
|
66075
|
-
* - visibleCountSignal: signal(number), updated in microtask batch, only when count changes
|
|
66076
|
-
* - propSignals: Map propName → signal(array), updated in microtask batch with element equality
|
|
66077
|
-
* - onChangeRef: holds the latest onChange callback, called once per microtask batch
|
|
66078
|
-
*
|
|
66079
|
-
* useTrackItem(id, data, index): registers the item with an explicitly provided index
|
|
66080
|
-
* that determines its position among siblings. The caller (e.g. items.map) knows the
|
|
66081
|
-
* correct order and passes it directly — no render-sequence deduction needed.
|
|
66082
|
-
* Returns the visible rank (position among non-hidden items), or -1 when hidden.
|
|
66083
|
-
* Signals and onChange are deferred to a microtask so multiple items updating
|
|
66084
|
-
* in one commit cause only one notification.
|
|
66085
|
-
*
|
|
66086
|
-
* getTrackedItemByIndex(index): synchronous O(1) lookup of a visible item by
|
|
66087
|
-
* its visible rank. Returns undefined when index is out of range.
|
|
66088
|
-
*
|
|
66089
|
-
* peekItems(): the items as they stand right now, without waiting for the
|
|
66090
|
-
* deferred notification — what a sibling rendering after the items must read
|
|
66091
|
-
* to paint them in the same commit.
|
|
66092
|
-
*/
|
|
66093
|
-
|
|
66094
|
-
const useItemTracker = ({ onChange } = {}) => {
|
|
66095
|
-
const onChangeRef = useRef(onChange);
|
|
66096
|
-
onChangeRef.current = onChange;
|
|
66097
|
-
const trackerRef = useRef(null);
|
|
66098
|
-
let tracker = trackerRef.current;
|
|
66099
|
-
if (!tracker) {
|
|
66100
|
-
trackerRef.current = tracker = createItemTracker((items) => {
|
|
66101
|
-
onChangeRef.current?.(items);
|
|
66102
|
-
});
|
|
66033
|
+
const ListItemHeaderOrFooterResolver = props => {
|
|
66034
|
+
const Next = useNextResolver();
|
|
66035
|
+
if (props.header) {
|
|
66036
|
+
return renderResolver(ListItemHeader, props);
|
|
66103
66037
|
}
|
|
66104
|
-
|
|
66105
|
-
|
|
66106
|
-
|
|
66107
|
-
|
|
66108
|
-
|
|
66038
|
+
if (props.footer) {
|
|
66039
|
+
return renderResolver(ListItemFooter, props);
|
|
66040
|
+
}
|
|
66041
|
+
return jsx(Next, {
|
|
66042
|
+
...props
|
|
66043
|
+
});
|
|
66044
|
+
};
|
|
66045
|
+
const ListItemHeader = props => {
|
|
66046
|
+
const Next = useNextResolver();
|
|
66047
|
+
const {
|
|
66048
|
+
ref
|
|
66049
|
+
} = props;
|
|
66050
|
+
useDisplayedLayoutEffect(ref, headerEl => {
|
|
66051
|
+
const listContainerEl = headerEl.closest(".navi_list_container");
|
|
66052
|
+
const rect = headerEl.getBoundingClientRect();
|
|
66053
|
+
listContainerEl.style.setProperty("--list-header-height", `${rect.height}px`);
|
|
66054
|
+
listContainerEl.style.setProperty("--list-header-width", `${rect.width}px`);
|
|
66055
|
+
}, []);
|
|
66056
|
+
return jsx(Next, {
|
|
66057
|
+
...props,
|
|
66058
|
+
header: undefined,
|
|
66059
|
+
role: "presentation",
|
|
66060
|
+
baseClassName: "navi_list_item_header"
|
|
66061
|
+
});
|
|
66062
|
+
};
|
|
66063
|
+
const ListItemFooter = props => {
|
|
66064
|
+
const Next = useNextResolver();
|
|
66065
|
+
const {
|
|
66066
|
+
ref
|
|
66067
|
+
} = props;
|
|
66068
|
+
useDisplayedLayoutEffect(ref, footerEl => {
|
|
66069
|
+
const listContainerEl = footerEl.closest(".navi_list_container");
|
|
66070
|
+
const rect = footerEl.getBoundingClientRect();
|
|
66071
|
+
listContainerEl.style.setProperty("--list-footer-height", `${rect.height}px`);
|
|
66072
|
+
listContainerEl.style.setProperty("--list-footer-width", `${rect.width}px`);
|
|
66073
|
+
}, []);
|
|
66074
|
+
return jsx(Next, {
|
|
66075
|
+
...props,
|
|
66076
|
+
footer: undefined,
|
|
66077
|
+
role: "presentation",
|
|
66078
|
+
baseClassName: "navi_list_item_footer"
|
|
66109
66079
|
});
|
|
66110
|
-
return tracker;
|
|
66111
66080
|
};
|
|
66112
66081
|
|
|
66113
|
-
|
|
66114
|
-
|
|
66115
|
-
|
|
66116
|
-
|
|
66117
|
-
|
|
66118
|
-
|
|
66119
|
-
|
|
66120
|
-
|
|
66121
|
-
|
|
66122
|
-
|
|
66123
|
-
|
|
66124
|
-
|
|
66125
|
-
|
|
66126
|
-
|
|
66127
|
-
|
|
66128
|
-
|
|
66129
|
-
|
|
66130
|
-
|
|
66131
|
-
|
|
66132
|
-
|
|
66133
|
-
|
|
66134
|
-
|
|
66135
|
-
|
|
66136
|
-
|
|
66137
|
-
|
|
66082
|
+
// Everything the list knows about its rows, in one place: how many the
|
|
66083
|
+
// collection has and where each child's rows start (the places), which rows
|
|
66084
|
+
// are drawn and which of those mount and show (the rows), and what each says
|
|
66085
|
+
// about itself (the items). Two clocks. Places and "is anything standing
|
|
66086
|
+
// above me" answer synchronously, in the middle of a render pass — a row asks
|
|
66087
|
+
// about the rows before it, and those have rendered already. The items and
|
|
66088
|
+
// the counts settle once per frame (a microtask): many rows change in one
|
|
66089
|
+
// commit, and what reads them wants one notification.
|
|
66090
|
+
//
|
|
66091
|
+
// Why places are read off the walk and not off the renders: a child knows how
|
|
66092
|
+
// many rows it stands for but not what was declared before it, and it cannot
|
|
66093
|
+
// deduce that from when it renders — a render is free to skip it. A child that
|
|
66094
|
+
// draws from signals and whose props are all referentially === the previous
|
|
66095
|
+
// ones does not render again (@preact/signals installs a shouldComponentUpdate
|
|
66096
|
+
// that says so), which is what any child nobody rebuilt this frame is — and
|
|
66097
|
+
// children numbered as they render would then slide up into the place of the
|
|
66098
|
+
// one that was skipped. So the list names a slot for each of its children and
|
|
66099
|
+
// declares them here, in order, before any of them renders (see
|
|
66100
|
+
// ListDeclaredChildren). A child then takes its place BY SLOT, and the place
|
|
66101
|
+
// is a signal: it moves when what stands before it changes — a row filtered
|
|
66102
|
+
// out, a run taking in rows, a slot added or moved — and the child follows,
|
|
66103
|
+
// rendered again for it whether or not anything else would have rendered it.
|
|
66104
|
+
//
|
|
66105
|
+
// Why "first" is a signal too: only the row itself knows, once it renders,
|
|
66106
|
+
// that it renders nothing (filtered out by a search), and the rows after it
|
|
66107
|
+
// may have been handed back unchanged. The first mounted row of a scope is
|
|
66108
|
+
// kept as a signal each of them reads; when it leaves, they are rendered again.
|
|
66138
66109
|
|
|
66139
|
-
|
|
66140
|
-
const countModified = countSignal.peek() !== newCount;
|
|
66141
|
-
if (countModified) {
|
|
66142
|
-
countSignal.value = newCount;
|
|
66143
|
-
someChange = true;
|
|
66144
|
-
}
|
|
66110
|
+
const UNGROUPED = Symbol("ungrouped");
|
|
66145
66111
|
|
|
66146
|
-
|
|
66147
|
-
|
|
66148
|
-
|
|
66149
|
-
|
|
66150
|
-
|
|
66151
|
-
|
|
66152
|
-
|
|
66153
|
-
|
|
66154
|
-
|
|
66155
|
-
|
|
66156
|
-
|
|
66157
|
-
|
|
66158
|
-
|
|
66159
|
-
|
|
66160
|
-
|
|
66161
|
-
|
|
66162
|
-
|
|
66163
|
-
|
|
66164
|
-
|
|
66165
|
-
|
|
66166
|
-
|
|
66167
|
-
|
|
66168
|
-
|
|
66169
|
-
|
|
66170
|
-
|
|
66171
|
-
|
|
66172
|
-
|
|
66173
|
-
|
|
66174
|
-
|
|
66175
|
-
|
|
66176
|
-
|
|
66177
|
-
|
|
66112
|
+
const createListRows = () => {
|
|
66113
|
+
const totalSignal = signal(0);
|
|
66114
|
+
// Bumped whenever a run takes in rows. The list itself has to hear about it:
|
|
66115
|
+
// rows arriving outside the render window change nothing it can see (nothing
|
|
66116
|
+
// registers, nothing is drawn), and yet they are what it may have been
|
|
66117
|
+
// waiting for — the row it was told to open on, for one.
|
|
66118
|
+
const pagesSignal = signal(0);
|
|
66119
|
+
// How many runs are re-reading rows they already show. The list wears it as
|
|
66120
|
+
// an attribute: what is drawn is from before, and the app may want to say so
|
|
66121
|
+
// without taking anything away.
|
|
66122
|
+
const refreshingSignal = signal(0);
|
|
66123
|
+
// The slots each walk declared, in order, by the slot the walk stands in
|
|
66124
|
+
// (null for the list's own children). Together they are a tree: a group's
|
|
66125
|
+
// rows live inside the group's slot.
|
|
66126
|
+
const slotIdsByParent = new Map();
|
|
66127
|
+
// Who took a place — a row, or a run of rows — and how many rows of the
|
|
66128
|
+
// collection it stands for. The place itself is a signal, see take.
|
|
66129
|
+
const ownerById = new Map();
|
|
66130
|
+
// The owners standing in each slot, in the order they took their place.
|
|
66131
|
+
// One, as a rule; a child that renders several rows keeps them in the order
|
|
66132
|
+
// they first rendered, which is all it can be told.
|
|
66133
|
+
const ownerIdsBySlot = new Map();
|
|
66134
|
+
const locatorByOwner = new Map();
|
|
66135
|
+
// The slots as the tree reads, first to last, and where each stands in it.
|
|
66136
|
+
// Rebuilt once a walk has changed the tree, read to place the owners.
|
|
66137
|
+
const slotWalk = [];
|
|
66138
|
+
const rankBySlot = new Map();
|
|
66139
|
+
let rowTotal = 0;
|
|
66140
|
+
// Owners have left and the others have not been moved up yet. Done on the
|
|
66141
|
+
// next ask rather than on the spot: rows leave many at a time (a search, a
|
|
66142
|
+
// list unmounting), and moving the others up once is enough.
|
|
66143
|
+
let placesStale = false;
|
|
66144
|
+
// Where the last slot holding an owner stands: an owner arriving at or after
|
|
66145
|
+
// it is placed at the end without going over the others — a whole first
|
|
66146
|
+
// render, rows arriving in order, costs each row nothing but itself.
|
|
66147
|
+
let rankOwnedLast = -1;
|
|
66178
66148
|
|
|
66179
|
-
|
|
66180
|
-
|
|
66181
|
-
|
|
66182
|
-
|
|
66183
|
-
|
|
66184
|
-
|
|
66185
|
-
|
|
66186
|
-
if (allItemsChanged) {
|
|
66187
|
-
itemsSignal.value = allItems;
|
|
66188
|
-
someChange = true;
|
|
66189
|
-
}
|
|
66190
|
-
if (visibleItemsChanged) {
|
|
66191
|
-
visibleItemsSignal.value = visibleItems;
|
|
66192
|
-
someChange = true;
|
|
66193
|
-
}
|
|
66194
|
-
const noMatchCountModified =
|
|
66195
|
-
noMatchCountSignal.peek() !== newNoMatchCount;
|
|
66196
|
-
if (noMatchCountModified) {
|
|
66197
|
-
noMatchCountSignal.value = newNoMatchCount;
|
|
66198
|
-
someChange = true;
|
|
66149
|
+
const rebuildWalk = () => {
|
|
66150
|
+
slotWalk.length = 0;
|
|
66151
|
+
rankBySlot.clear();
|
|
66152
|
+
const visit = (parentSlotId) => {
|
|
66153
|
+
const slotIds = slotIdsByParent.get(parentSlotId);
|
|
66154
|
+
if (!slotIds) {
|
|
66155
|
+
return;
|
|
66199
66156
|
}
|
|
66200
|
-
|
|
66201
|
-
|
|
66157
|
+
for (const slotId of slotIds) {
|
|
66158
|
+
rankBySlot.set(slotId, slotWalk.length);
|
|
66159
|
+
slotWalk.push(slotId);
|
|
66160
|
+
visit(slotId);
|
|
66202
66161
|
}
|
|
66203
|
-
}
|
|
66162
|
+
};
|
|
66163
|
+
visit(null);
|
|
66204
66164
|
};
|
|
66205
|
-
|
|
66206
|
-
|
|
66207
|
-
|
|
66208
|
-
|
|
66209
|
-
|
|
66210
|
-
|
|
66211
|
-
|
|
66212
|
-
|
|
66213
|
-
|
|
66165
|
+
// Every place, in one go: a place is the sum of what stands before it, so
|
|
66166
|
+
// there is nothing to hand out one at a time. Writing a place that did not
|
|
66167
|
+
// change wakes nobody — a signal ignores a value equal to its own.
|
|
66168
|
+
const refreshPlaces = () => {
|
|
66169
|
+
placesStale = false;
|
|
66170
|
+
let index = 0;
|
|
66171
|
+
let rank = 0;
|
|
66172
|
+
rankOwnedLast = -1;
|
|
66173
|
+
while (rank < slotWalk.length) {
|
|
66174
|
+
const ownerIds = ownerIdsBySlot.get(slotWalk[rank]);
|
|
66175
|
+
if (ownerIds) {
|
|
66176
|
+
for (const ownerId of ownerIds) {
|
|
66177
|
+
const owner = ownerById.get(ownerId);
|
|
66178
|
+
owner.placeSignal.value = index;
|
|
66179
|
+
index += owner.rowCount;
|
|
66180
|
+
}
|
|
66181
|
+
rankOwnedLast = rank;
|
|
66214
66182
|
}
|
|
66215
|
-
|
|
66216
|
-
|
|
66217
|
-
|
|
66183
|
+
rank++;
|
|
66184
|
+
}
|
|
66185
|
+
rowTotal = index;
|
|
66186
|
+
totalSignal.value = index;
|
|
66187
|
+
// A run's edges are places too (see refreshFirst).
|
|
66188
|
+
markStale(UNGROUPED);
|
|
66218
66189
|
};
|
|
66219
|
-
|
|
66220
|
-
|
|
66221
|
-
if (
|
|
66190
|
+
const addToSlot = (slotId, ownerId) => {
|
|
66191
|
+
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
66192
|
+
if (ownerIds) {
|
|
66193
|
+
ownerIds.push(ownerId);
|
|
66194
|
+
} else {
|
|
66195
|
+
ownerIdsBySlot.set(slotId, [ownerId]);
|
|
66196
|
+
}
|
|
66197
|
+
warnIfEveryRowInOneSlot(slotId);
|
|
66198
|
+
};
|
|
66199
|
+
// Rows that all stand in the same slot keep the order they first mounted in:
|
|
66200
|
+
// the walk is over the children the list is given, and a component holding
|
|
66201
|
+
// them is one child however many rows it renders. Everything about a place
|
|
66202
|
+
// then stops following what the caller writes — a search reordering the rows
|
|
66203
|
+
// moves nothing. Said once, and only for the shape that can be nothing else:
|
|
66204
|
+
// the list's whole content is one child, and several rows came out of it.
|
|
66205
|
+
let everyRowInOneSlotWarned = false;
|
|
66206
|
+
const warnIfEveryRowInOneSlot = (slotId) => {
|
|
66207
|
+
if (everyRowInOneSlotWarned) {
|
|
66222
66208
|
return;
|
|
66223
66209
|
}
|
|
66224
|
-
|
|
66225
|
-
|
|
66210
|
+
const rootSlotIds = slotIdsByParent.get(null);
|
|
66211
|
+
if (!rootSlotIds || rootSlotIds.length !== 1 || rootSlotIds[0] !== slotId) {
|
|
66212
|
+
return;
|
|
66213
|
+
}
|
|
66214
|
+
if (ownerIdsBySlot.get(slotId).length < 2) {
|
|
66215
|
+
return;
|
|
66216
|
+
}
|
|
66217
|
+
everyRowInOneSlotWarned = true;
|
|
66218
|
+
console.warn(
|
|
66219
|
+
`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.`,
|
|
66220
|
+
);
|
|
66226
66221
|
};
|
|
66227
|
-
|
|
66228
|
-
|
|
66229
|
-
|
|
66230
|
-
|
|
66231
|
-
let lo = 0;
|
|
66232
|
-
let hi = orderedKeys.length;
|
|
66233
|
-
while (lo < hi) {
|
|
66234
|
-
const mid = (lo + hi) >> 1;
|
|
66235
|
-
if (keyToExplicitOrder.get(orderedKeys[mid]) <= explicitOrder) {
|
|
66236
|
-
lo = mid + 1;
|
|
66237
|
-
} else {
|
|
66238
|
-
hi = mid;
|
|
66239
|
-
}
|
|
66222
|
+
const removeFromSlot = (slotId, ownerId) => {
|
|
66223
|
+
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
66224
|
+
if (!ownerIds) {
|
|
66225
|
+
return;
|
|
66240
66226
|
}
|
|
66241
|
-
|
|
66242
|
-
|
|
66243
|
-
|
|
66227
|
+
const index = ownerIds.indexOf(ownerId);
|
|
66228
|
+
if (index !== -1) {
|
|
66229
|
+
ownerIds.splice(index, 1);
|
|
66230
|
+
}
|
|
66231
|
+
if (ownerIds.length === 0) {
|
|
66232
|
+
ownerIdsBySlot.delete(slotId);
|
|
66244
66233
|
}
|
|
66245
66234
|
};
|
|
66246
|
-
|
|
66247
|
-
|
|
66248
|
-
|
|
66249
|
-
|
|
66250
|
-
|
|
66251
|
-
const
|
|
66252
|
-
|
|
66253
|
-
lo = mid + 1;
|
|
66254
|
-
} else {
|
|
66255
|
-
hi = mid;
|
|
66235
|
+
// A slot the walk no longer names: whatever stood in it is gone, and so is
|
|
66236
|
+
// whatever a walk inside it had declared.
|
|
66237
|
+
const dropSlot = (slotId) => {
|
|
66238
|
+
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
66239
|
+
if (ownerIds) {
|
|
66240
|
+
for (const ownerId of ownerIds) {
|
|
66241
|
+
ownerById.delete(ownerId);
|
|
66256
66242
|
}
|
|
66243
|
+
ownerIdsBySlot.delete(slotId);
|
|
66257
66244
|
}
|
|
66258
|
-
|
|
66259
|
-
|
|
66260
|
-
|
|
66245
|
+
const childSlotIds = slotIdsByParent.get(slotId);
|
|
66246
|
+
if (childSlotIds) {
|
|
66247
|
+
slotIdsByParent.delete(slotId);
|
|
66248
|
+
for (const childSlotId of childSlotIds) {
|
|
66249
|
+
dropSlot(childSlotId);
|
|
66250
|
+
}
|
|
66261
66251
|
}
|
|
66262
66252
|
};
|
|
66263
66253
|
|
|
66264
|
-
|
|
66265
|
-
|
|
66266
|
-
|
|
66267
|
-
|
|
66268
|
-
|
|
66269
|
-
|
|
66270
|
-
|
|
66271
|
-
|
|
66254
|
+
// ---- the rows drawn ----
|
|
66255
|
+
// rowId → { ownerId, place, groupId, data, mounted, visible, item }
|
|
66256
|
+
const rowById = new Map();
|
|
66257
|
+
// groupId → { firstSignal, countSignal, noMatchCountSignal }
|
|
66258
|
+
const groupById = new Map();
|
|
66259
|
+
// ownerId → { from, to }, for the runs (see declareWindow).
|
|
66260
|
+
const windowByOwner = new Map();
|
|
66261
|
+
// The first place something stands at, outside any group: the mounted rows
|
|
66262
|
+
// and the rows a run holds above its window. Per group, the group's first
|
|
66263
|
+
// mounted row.
|
|
66264
|
+
const firstStandingSignal = signal(-1);
|
|
66265
|
+
// The scopes whose first row left: recounted on the next ask, or at the end
|
|
66266
|
+
// of the frame, whichever comes first. A row mounting before the first one
|
|
66267
|
+
// moves it at once, no recount needed — the first can only ever move up.
|
|
66268
|
+
const staleScopes = new Set();
|
|
66269
|
+
const scopeOf = (groupId) => (groupId === undefined ? UNGROUPED : groupId);
|
|
66270
|
+
const groupOf = (groupId) => {
|
|
66271
|
+
let group = groupById.get(groupId);
|
|
66272
|
+
if (!group) {
|
|
66273
|
+
group = {
|
|
66274
|
+
firstSignal: signal(-1),
|
|
66275
|
+
countSignal: signal(0),
|
|
66276
|
+
noMatchCountSignal: signal(0),
|
|
66277
|
+
};
|
|
66278
|
+
groupById.set(groupId, group);
|
|
66272
66279
|
}
|
|
66280
|
+
return group;
|
|
66273
66281
|
};
|
|
66274
|
-
|
|
66275
|
-
|
|
66276
|
-
|
|
66277
|
-
|
|
66278
|
-
|
|
66279
|
-
|
|
66280
|
-
|
|
66281
|
-
|
|
66282
|
-
orderedKeys.splice(idx, 1);
|
|
66283
|
-
keyToOrderedIndex.delete(key);
|
|
66284
|
-
for (let i = idx; i < orderedKeys.length; i++) {
|
|
66285
|
-
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
66286
|
-
}
|
|
66282
|
+
const firstSignalOf = (scope) =>
|
|
66283
|
+
scope === UNGROUPED ? firstStandingSignal : groupOf(scope).firstSignal;
|
|
66284
|
+
const refreshFirst = (scope) => {
|
|
66285
|
+
staleScopes.delete(scope);
|
|
66286
|
+
let first = -1;
|
|
66287
|
+
const consider = (place) => {
|
|
66288
|
+
if (place !== undefined && (first === -1 || place < first)) {
|
|
66289
|
+
first = place;
|
|
66287
66290
|
}
|
|
66288
|
-
|
|
66289
|
-
|
|
66290
|
-
|
|
66291
|
-
|
|
66292
|
-
return;
|
|
66293
|
-
}
|
|
66294
|
-
|
|
66295
|
-
// Maintain allRegistrations and allOrderedKeys for all non-presentation items.
|
|
66296
|
-
allRegistrations.set(key, data);
|
|
66297
|
-
allKeys.add(key);
|
|
66298
|
-
const currentAllIdx = keyToAllOrderedIndex.get(key);
|
|
66299
|
-
const previousOrder = keyToExplicitOrder.get(key);
|
|
66300
|
-
keyToExplicitOrder.set(key, index);
|
|
66301
|
-
if (currentAllIdx === undefined) {
|
|
66302
|
-
insertAllKey(key, index);
|
|
66303
|
-
} else if (previousOrder !== index) {
|
|
66304
|
-
allOrderedKeys.splice(currentAllIdx, 1);
|
|
66305
|
-
keyToAllOrderedIndex.delete(key);
|
|
66306
|
-
for (let i = currentAllIdx; i < allOrderedKeys.length; i++) {
|
|
66307
|
-
keyToAllOrderedIndex.set(allOrderedKeys[i], i);
|
|
66291
|
+
};
|
|
66292
|
+
for (const row of rowById.values()) {
|
|
66293
|
+
if (row.mounted && scopeOf(row.groupId) === scope) {
|
|
66294
|
+
consider(row.place);
|
|
66308
66295
|
}
|
|
66309
|
-
insertAllKey(key, index);
|
|
66310
66296
|
}
|
|
66311
|
-
|
|
66312
|
-
|
|
66313
|
-
|
|
66314
|
-
|
|
66315
|
-
|
|
66316
|
-
|
|
66317
|
-
|
|
66318
|
-
|
|
66319
|
-
|
|
66297
|
+
if (scope === UNGROUPED) {
|
|
66298
|
+
for (const [ownerId, window] of windowByOwner) {
|
|
66299
|
+
const owner = ownerById.get(ownerId);
|
|
66300
|
+
if (!owner) {
|
|
66301
|
+
continue;
|
|
66302
|
+
}
|
|
66303
|
+
const start = owner.placeSignal.peek();
|
|
66304
|
+
if (window.from > start) {
|
|
66305
|
+
consider(start);
|
|
66306
|
+
}
|
|
66307
|
+
if (window.to < start + owner.rowCount) {
|
|
66308
|
+
consider(window.to);
|
|
66320
66309
|
}
|
|
66321
66310
|
}
|
|
66322
|
-
return;
|
|
66323
|
-
}
|
|
66324
|
-
|
|
66325
|
-
registrations.set(key, data);
|
|
66326
|
-
const currentIdx = keyToOrderedIndex.get(key);
|
|
66327
|
-
if (currentIdx === undefined) {
|
|
66328
|
-
insertKey(key, index);
|
|
66329
|
-
return;
|
|
66330
66311
|
}
|
|
66331
|
-
|
|
66312
|
+
firstSignalOf(scope).value = first;
|
|
66313
|
+
};
|
|
66314
|
+
const markStale = (scope) => {
|
|
66315
|
+
if (staleScopes.has(scope)) {
|
|
66332
66316
|
return;
|
|
66333
66317
|
}
|
|
66334
|
-
|
|
66335
|
-
|
|
66336
|
-
|
|
66337
|
-
|
|
66318
|
+
staleScopes.add(scope);
|
|
66319
|
+
queueMicrotask(() => {
|
|
66320
|
+
if (staleScopes.has(scope)) {
|
|
66321
|
+
refreshFirst(scope);
|
|
66322
|
+
}
|
|
66323
|
+
});
|
|
66324
|
+
};
|
|
66325
|
+
const leaveFirst = (row) => {
|
|
66326
|
+
const scope = scopeOf(row.groupId);
|
|
66327
|
+
if (firstSignalOf(scope).peek() === row.place) {
|
|
66328
|
+
markStale(scope);
|
|
66338
66329
|
}
|
|
66339
|
-
insertKey(key, index);
|
|
66340
66330
|
};
|
|
66341
66331
|
|
|
66342
|
-
|
|
66343
|
-
|
|
66344
|
-
|
|
66345
|
-
|
|
66346
|
-
|
|
66347
|
-
|
|
66348
|
-
|
|
66349
|
-
|
|
66332
|
+
// ---- the items, settled once per frame ----
|
|
66333
|
+
const itemsSignal = signal([]);
|
|
66334
|
+
const visibleItemsSignal = signal([]);
|
|
66335
|
+
const countSignal = signal(0);
|
|
66336
|
+
const visibleCountSignal = signal(0);
|
|
66337
|
+
const noMatchCountSignal = signal(0);
|
|
66338
|
+
let notifyScheduled = false;
|
|
66339
|
+
const runNotify = () => {
|
|
66340
|
+
batch(() => {
|
|
66341
|
+
const itemRows = [];
|
|
66342
|
+
for (const row of rowById.values()) {
|
|
66343
|
+
if (row.item) {
|
|
66344
|
+
itemRows.push(row);
|
|
66345
|
+
}
|
|
66350
66346
|
}
|
|
66351
|
-
|
|
66352
|
-
|
|
66353
|
-
|
|
66354
|
-
|
|
66355
|
-
|
|
66347
|
+
itemRows.sort(compareRowPlaces);
|
|
66348
|
+
const items = [];
|
|
66349
|
+
const visibleItems = [];
|
|
66350
|
+
let noMatchCount = 0;
|
|
66351
|
+
const countByGroup = new Map();
|
|
66352
|
+
const noMatchCountByGroup = new Map();
|
|
66353
|
+
const prevItems = itemsSignal.peek();
|
|
66354
|
+
const prevVisibleItems = visibleItemsSignal.peek();
|
|
66355
|
+
let itemsChanged = prevItems.length !== itemRows.length;
|
|
66356
|
+
let visibleItemsChanged = false;
|
|
66357
|
+
for (const row of itemRows) {
|
|
66358
|
+
const item = row.data;
|
|
66359
|
+
// Compared by reference: any prop change (selected, disabled, …) is a
|
|
66360
|
+
// new props object.
|
|
66361
|
+
if (!itemsChanged && item !== prevItems[items.length]) {
|
|
66362
|
+
itemsChanged = true;
|
|
66363
|
+
}
|
|
66364
|
+
items.push(item);
|
|
66365
|
+
const noMatch = item.match === false;
|
|
66366
|
+
if (noMatch) {
|
|
66367
|
+
noMatchCount++;
|
|
66368
|
+
}
|
|
66369
|
+
if (row.groupId !== undefined) {
|
|
66370
|
+
countByGroup.set(
|
|
66371
|
+
row.groupId,
|
|
66372
|
+
(countByGroup.get(row.groupId) || 0) + 1,
|
|
66373
|
+
);
|
|
66374
|
+
if (noMatch) {
|
|
66375
|
+
noMatchCountByGroup.set(
|
|
66376
|
+
row.groupId,
|
|
66377
|
+
(noMatchCountByGroup.get(row.groupId) || 0) + 1,
|
|
66378
|
+
);
|
|
66379
|
+
}
|
|
66380
|
+
}
|
|
66381
|
+
if (row.visible) {
|
|
66382
|
+
if (
|
|
66383
|
+
!visibleItemsChanged &&
|
|
66384
|
+
item !== prevVisibleItems[visibleItems.length]
|
|
66385
|
+
) {
|
|
66386
|
+
visibleItemsChanged = true;
|
|
66387
|
+
}
|
|
66388
|
+
visibleItems.push(item);
|
|
66389
|
+
}
|
|
66390
|
+
}
|
|
66391
|
+
if (visibleItems.length !== prevVisibleItems.length) {
|
|
66392
|
+
visibleItemsChanged = true;
|
|
66393
|
+
}
|
|
66394
|
+
let someChange = false;
|
|
66395
|
+
if (countSignal.peek() !== items.length) {
|
|
66396
|
+
countSignal.value = items.length;
|
|
66397
|
+
someChange = true;
|
|
66398
|
+
}
|
|
66399
|
+
if (visibleCountSignal.peek() !== visibleItems.length) {
|
|
66400
|
+
visibleCountSignal.value = visibleItems.length;
|
|
66401
|
+
someChange = true;
|
|
66402
|
+
}
|
|
66403
|
+
if (itemsChanged) {
|
|
66404
|
+
itemsSignal.value = items;
|
|
66405
|
+
someChange = true;
|
|
66406
|
+
}
|
|
66407
|
+
if (visibleItemsChanged) {
|
|
66408
|
+
visibleItemsSignal.value = visibleItems;
|
|
66409
|
+
someChange = true;
|
|
66410
|
+
}
|
|
66411
|
+
if (noMatchCountSignal.peek() !== noMatchCount) {
|
|
66412
|
+
noMatchCountSignal.value = noMatchCount;
|
|
66413
|
+
someChange = true;
|
|
66414
|
+
}
|
|
66415
|
+
for (const [groupId, group] of groupById) {
|
|
66416
|
+
group.countSignal.value = countByGroup.get(groupId) || 0;
|
|
66417
|
+
group.noMatchCountSignal.value = noMatchCountByGroup.get(groupId) || 0;
|
|
66418
|
+
}
|
|
66419
|
+
if (someChange && listRows.onChange) {
|
|
66420
|
+
listRows.onChange();
|
|
66421
|
+
}
|
|
66422
|
+
});
|
|
66356
66423
|
};
|
|
66357
|
-
|
|
66358
|
-
|
|
66359
|
-
|
|
66360
|
-
idToKey.set(id, keyCounter++);
|
|
66424
|
+
const notify = () => {
|
|
66425
|
+
if (notifyScheduled) {
|
|
66426
|
+
return;
|
|
66361
66427
|
}
|
|
66362
|
-
|
|
66363
|
-
|
|
66364
|
-
|
|
66365
|
-
|
|
66366
|
-
|
|
66367
|
-
|
|
66368
|
-
|
|
66369
|
-
|
|
66370
|
-
const { id, index } = data;
|
|
66371
|
-
const key = keyForId(id);
|
|
66372
|
-
|
|
66373
|
-
syncItem(key, index, data);
|
|
66374
|
-
notify();
|
|
66375
|
-
|
|
66376
|
-
useLayoutEffect(() => {
|
|
66377
|
-
return () => {
|
|
66378
|
-
unregisterKey(key);
|
|
66379
|
-
notify();
|
|
66380
|
-
};
|
|
66381
|
-
}, []);
|
|
66382
|
-
|
|
66383
|
-
if (data.filtered || data.hidden || data.role === "presentation") {
|
|
66384
|
-
return -1;
|
|
66385
|
-
}
|
|
66386
|
-
return keyToOrderedIndex.get(key) ?? -1;
|
|
66387
|
-
};
|
|
66388
|
-
|
|
66389
|
-
const getTrackedItemByIndex = (index) => {
|
|
66390
|
-
const key = orderedKeys[index];
|
|
66391
|
-
if (key === undefined) {
|
|
66392
|
-
return undefined;
|
|
66393
|
-
}
|
|
66394
|
-
return registrations.get(key);
|
|
66395
|
-
};
|
|
66396
|
-
|
|
66397
|
-
// The items as they stand right now, notification pending or not — same
|
|
66398
|
-
// content as itemsSignal, minus the wait.
|
|
66399
|
-
//
|
|
66400
|
-
// Items register during their own render, while the signal is only updated
|
|
66401
|
-
// on a deferred microtask (see notify): a sibling rendering after them would
|
|
66402
|
-
// otherwise paint from an empty list and correct itself a frame later. That
|
|
66403
|
-
// frame is visible whenever the painted size feeds a layout decision — a
|
|
66404
|
-
// dialog sizing itself on its content measures the empty version and shifts
|
|
66405
|
-
// once the real one lands. Reading this instead makes the first paint the
|
|
66406
|
-
// right one. Callers must still subscribe to itemsSignal to re-render on
|
|
66407
|
-
// LATER changes; this is the value to display, not the notification.
|
|
66408
|
-
const peekItems = () => {
|
|
66409
|
-
if (!notifyScheduled) {
|
|
66410
|
-
return itemsSignal.peek();
|
|
66411
|
-
}
|
|
66412
|
-
const items = [];
|
|
66413
|
-
for (const key of allOrderedKeys) {
|
|
66414
|
-
items.push(allRegistrations.get(key));
|
|
66415
|
-
}
|
|
66416
|
-
return items;
|
|
66428
|
+
notifyScheduled = true;
|
|
66429
|
+
queueMicrotask(() => {
|
|
66430
|
+
if (!notifyScheduled) {
|
|
66431
|
+
return;
|
|
66432
|
+
}
|
|
66433
|
+
notifyScheduled = false;
|
|
66434
|
+
runNotify();
|
|
66435
|
+
});
|
|
66417
66436
|
};
|
|
66418
66437
|
|
|
66419
|
-
|
|
66420
|
-
|
|
66421
|
-
|
|
66422
|
-
|
|
66438
|
+
const listRows = {
|
|
66439
|
+
totalSignal,
|
|
66440
|
+
pagesSignal,
|
|
66441
|
+
refreshingSignal,
|
|
66442
|
+
// The rows that are items, in place order — every one drawn, and the ones
|
|
66443
|
+
// that show — and what the search made of them. Written once per frame.
|
|
66423
66444
|
itemsSignal,
|
|
66424
66445
|
visibleItemsSignal,
|
|
66425
66446
|
countSignal,
|
|
66426
66447
|
visibleCountSignal,
|
|
66427
66448
|
noMatchCountSignal,
|
|
66428
|
-
|
|
66449
|
+
// Called once per frame in which the items changed. Set by the list.
|
|
66450
|
+
onChange: null,
|
|
66451
|
+
// What a run needs to know about the list it lives in: how many rows the
|
|
66452
|
+
// list is willing to draw at once, which end it opens on, and how much
|
|
66453
|
+
// room one row is given — a row whose content has not arrived must take
|
|
66454
|
+
// exactly that, or the rows drawn would not reach where the list says they
|
|
66455
|
+
// are.
|
|
66456
|
+
renderBudget: 0,
|
|
66457
|
+
scrolled: "start",
|
|
66458
|
+
// The list is on its way somewhere: what the window frames is not what it
|
|
66459
|
+
// is about to frame, so a run must not fetch for it (see holdWindow).
|
|
66460
|
+
holdPending: false,
|
|
66461
|
+
// Called by a run just before rows land in it: what is on screen must not
|
|
66462
|
+
// move because something arrived above it. Set by the list itself.
|
|
66463
|
+
captureAnchor: () => {},
|
|
66464
|
+
horizontal: false,
|
|
66465
|
+
virtualItemSizeSignal: null,
|
|
66466
|
+
renderSkeleton: undefined,
|
|
66467
|
+
// The children a walk stands over, in order — said in one call, before any
|
|
66468
|
+
// of them renders, so that what a child asks next is answered against the
|
|
66469
|
+
// whole picture and not against the children that happened to render
|
|
66470
|
+
// first. Said again on every render of the walk, and heard only when
|
|
66471
|
+
// something moved.
|
|
66472
|
+
declareSlots: (parentSlotId, slotIds) => {
|
|
66473
|
+
const slotIdsPrevious = slotIdsByParent.get(parentSlotId);
|
|
66474
|
+
if (slotIdsPrevious && sameSlotIds(slotIdsPrevious, slotIds)) {
|
|
66475
|
+
return;
|
|
66476
|
+
}
|
|
66477
|
+
if (slotIdsPrevious) {
|
|
66478
|
+
const slotIdSet = new Set(slotIds);
|
|
66479
|
+
for (const slotId of slotIdsPrevious) {
|
|
66480
|
+
if (!slotIdSet.has(slotId)) {
|
|
66481
|
+
dropSlot(slotId);
|
|
66482
|
+
}
|
|
66483
|
+
}
|
|
66484
|
+
}
|
|
66485
|
+
slotIdsByParent.set(parentSlotId, slotIds);
|
|
66486
|
+
rebuildWalk();
|
|
66487
|
+
refreshPlaces();
|
|
66488
|
+
},
|
|
66489
|
+
// Whether something has taken this slot for its own: what it renders
|
|
66490
|
+
// inside is then its to place (a run draws its groups with their rows
|
|
66491
|
+
// already placed), and no walk inside it has anything to declare.
|
|
66492
|
+
slotHasOwner: (slotId) => ownerIdsBySlot.has(slotId),
|
|
66493
|
+
// Whether any run of rows lives in this list: what makes a render window
|
|
66494
|
+
// mean anything (see List's renderBudget).
|
|
66495
|
+
hasRuns: () => locatorByOwner.size > 0,
|
|
66496
|
+
setRowLocator: (ownerId, locate) => {
|
|
66497
|
+
locatorByOwner.set(ownerId, locate);
|
|
66498
|
+
},
|
|
66499
|
+
dropRowLocator: (ownerId) => {
|
|
66500
|
+
locatorByOwner.delete(ownerId);
|
|
66501
|
+
},
|
|
66502
|
+
// Where the row named by that id sits, asked of whoever holds it.
|
|
66503
|
+
locateRow: (id) => {
|
|
66504
|
+
for (const locate of locatorByOwner.values()) {
|
|
66505
|
+
const index = locate(id);
|
|
66506
|
+
if (index !== null) {
|
|
66507
|
+
return index;
|
|
66508
|
+
}
|
|
66509
|
+
}
|
|
66510
|
+
return null;
|
|
66511
|
+
},
|
|
66512
|
+
// The place the owner's rows start at — read from a signal, so that the
|
|
66513
|
+
// owner is rendered again when it moves (see the top of this file). Asked on
|
|
66514
|
+
// every render, and answered without a second look for as long as the
|
|
66515
|
+
// owner stands in the same slot for the same number of rows.
|
|
66516
|
+
take: (ownerId, rowCount, slotId) => {
|
|
66517
|
+
let owner = ownerById.get(ownerId);
|
|
66518
|
+
if (owner) {
|
|
66519
|
+
if (owner.slotId !== slotId || owner.rowCount !== rowCount) {
|
|
66520
|
+
removeFromSlot(owner.slotId, ownerId);
|
|
66521
|
+
addToSlot(slotId, ownerId);
|
|
66522
|
+
owner.slotId = slotId;
|
|
66523
|
+
owner.rowCount = rowCount;
|
|
66524
|
+
placesStale = true;
|
|
66525
|
+
}
|
|
66526
|
+
if (placesStale) {
|
|
66527
|
+
refreshPlaces();
|
|
66528
|
+
}
|
|
66529
|
+
return owner.placeSignal.value;
|
|
66530
|
+
}
|
|
66531
|
+
if (placesStale) {
|
|
66532
|
+
refreshPlaces();
|
|
66533
|
+
}
|
|
66534
|
+
const rank = rankBySlot.get(slotId);
|
|
66535
|
+
addToSlot(slotId, ownerId);
|
|
66536
|
+
if (rank !== undefined && rank >= rankOwnedLast) {
|
|
66537
|
+
owner = { slotId, rowCount, placeSignal: signal(rowTotal) };
|
|
66538
|
+
ownerById.set(ownerId, owner);
|
|
66539
|
+
rowTotal += rowCount;
|
|
66540
|
+
rankOwnedLast = rank;
|
|
66541
|
+
totalSignal.value = rowTotal;
|
|
66542
|
+
return owner.placeSignal.value;
|
|
66543
|
+
}
|
|
66544
|
+
owner = { slotId, rowCount, placeSignal: signal(0) };
|
|
66545
|
+
ownerById.set(ownerId, owner);
|
|
66546
|
+
refreshPlaces();
|
|
66547
|
+
return owner.placeSignal.value;
|
|
66548
|
+
},
|
|
66549
|
+
// The owner stands for no row of the collection: it was filtered out by a
|
|
66550
|
+
// search, or it is gone.
|
|
66551
|
+
drop: (ownerId) => {
|
|
66552
|
+
const owner = ownerById.get(ownerId);
|
|
66553
|
+
if (!owner) {
|
|
66554
|
+
return;
|
|
66555
|
+
}
|
|
66556
|
+
ownerById.delete(ownerId);
|
|
66557
|
+
removeFromSlot(owner.slotId, ownerId);
|
|
66558
|
+
windowByOwner.delete(ownerId);
|
|
66559
|
+
if (placesStale) {
|
|
66560
|
+
return;
|
|
66561
|
+
}
|
|
66562
|
+
placesStale = true;
|
|
66563
|
+
queueMicrotask(() => {
|
|
66564
|
+
if (placesStale) {
|
|
66565
|
+
refreshPlaces();
|
|
66566
|
+
}
|
|
66567
|
+
});
|
|
66568
|
+
},
|
|
66569
|
+
|
|
66570
|
+
// ---- the rows drawn ----
|
|
66571
|
+
|
|
66572
|
+
// A run says which of its rows it draws. The others stand: rows above the
|
|
66573
|
+
// window are above every row drawn, whether or not any of the drawn ones
|
|
66574
|
+
// mounts (see refreshFirst).
|
|
66575
|
+
declareWindow: (ownerId, from, to) => {
|
|
66576
|
+
const window = windowByOwner.get(ownerId);
|
|
66577
|
+
if (window && window.from === from && window.to === to) {
|
|
66578
|
+
return;
|
|
66579
|
+
}
|
|
66580
|
+
windowByOwner.set(ownerId, { from, to });
|
|
66581
|
+
markStale(UNGROUPED);
|
|
66582
|
+
},
|
|
66583
|
+
// A row says what it is, where it renders: its place, the group it is
|
|
66584
|
+
// in, and its data — from which follows whether it mounts at all
|
|
66585
|
+
// (filtered out), whether it shows (hidden keeps the room, not the
|
|
66586
|
+
// content), and whether it is an item (a group wrapper, a skeleton, are
|
|
66587
|
+
// rows of the list but items of nobody). Said in the name of the
|
|
66588
|
+
// component, not of the item id: two components may stand for one item for
|
|
66589
|
+
// a moment, one leaving as the other arrives.
|
|
66590
|
+
draw: (rowId, { ownerId, place, groupId, data }) => {
|
|
66591
|
+
const mounted = !data.filtered;
|
|
66592
|
+
const visible = mounted && !data.hidden;
|
|
66593
|
+
const item = !data.skeleton && data.role !== "presentation";
|
|
66594
|
+
let row = rowById.get(rowId);
|
|
66595
|
+
if (row) {
|
|
66596
|
+
if (
|
|
66597
|
+
row.mounted &&
|
|
66598
|
+
(row.place !== place || row.groupId !== groupId || !mounted)
|
|
66599
|
+
) {
|
|
66600
|
+
leaveFirst(row);
|
|
66601
|
+
}
|
|
66602
|
+
row.ownerId = ownerId;
|
|
66603
|
+
row.place = place;
|
|
66604
|
+
row.groupId = groupId;
|
|
66605
|
+
row.data = data;
|
|
66606
|
+
row.mounted = mounted;
|
|
66607
|
+
row.visible = visible;
|
|
66608
|
+
row.item = item;
|
|
66609
|
+
} else {
|
|
66610
|
+
row = { ownerId, place, groupId, data, mounted, visible, item };
|
|
66611
|
+
rowById.set(rowId, row);
|
|
66612
|
+
}
|
|
66613
|
+
if (mounted) {
|
|
66614
|
+
const firstSignal = firstSignalOf(scopeOf(groupId));
|
|
66615
|
+
const first = firstSignal.peek();
|
|
66616
|
+
if (first === -1 || place < first) {
|
|
66617
|
+
firstSignal.value = place;
|
|
66618
|
+
}
|
|
66619
|
+
}
|
|
66620
|
+
notify();
|
|
66621
|
+
},
|
|
66622
|
+
// The row is gone. A declared row is its own owner and gives its place
|
|
66623
|
+
// back with it; a run's row leaves the run's places alone.
|
|
66624
|
+
erase: (rowId) => {
|
|
66625
|
+
const row = rowById.get(rowId);
|
|
66626
|
+
if (!row) {
|
|
66627
|
+
return;
|
|
66628
|
+
}
|
|
66629
|
+
rowById.delete(rowId);
|
|
66630
|
+
if (row.mounted) {
|
|
66631
|
+
leaveFirst(row);
|
|
66632
|
+
}
|
|
66633
|
+
if (row.ownerId === rowId) {
|
|
66634
|
+
listRows.drop(rowId);
|
|
66635
|
+
}
|
|
66636
|
+
notify();
|
|
66637
|
+
},
|
|
66638
|
+
// Whether nothing of the list stands above this row: in its group, no
|
|
66639
|
+
// other row of the group mounts before it; outside groups, no row mounts
|
|
66640
|
+
// before it and no run has rows above its window before it. Answered from
|
|
66641
|
+
// a signal, so a row rendered from a kept vnode is rendered again when the
|
|
66642
|
+
// row before it leaves or comes back.
|
|
66643
|
+
isFirst: (rowId) => {
|
|
66644
|
+
const row = rowById.get(rowId);
|
|
66645
|
+
const scope = scopeOf(row.groupId);
|
|
66646
|
+
if (staleScopes.has(scope)) {
|
|
66647
|
+
refreshFirst(scope);
|
|
66648
|
+
}
|
|
66649
|
+
return firstSignalOf(scope).value === row.place;
|
|
66650
|
+
},
|
|
66651
|
+
// What a group knows about its rows: how many, and how many of them the
|
|
66652
|
+
// search left out (see ListItemGroup).
|
|
66653
|
+
group: (groupId) => groupOf(groupId),
|
|
66654
|
+
dropGroup: (groupId) => {
|
|
66655
|
+
groupById.delete(groupId);
|
|
66656
|
+
staleScopes.delete(groupId);
|
|
66657
|
+
},
|
|
66658
|
+
// Written when a frame's rows have settled (see notify); the value to act
|
|
66659
|
+
// on is peeked from wherever the change is heard.
|
|
66660
|
+
flushSync: () => {
|
|
66661
|
+
if (!notifyScheduled) {
|
|
66662
|
+
return;
|
|
66663
|
+
}
|
|
66664
|
+
notifyScheduled = false;
|
|
66665
|
+
runNotify();
|
|
66666
|
+
},
|
|
66429
66667
|
};
|
|
66668
|
+
return listRows;
|
|
66430
66669
|
};
|
|
66431
|
-
|
|
66432
|
-
|
|
66433
|
-
|
|
66434
|
-
if (props.header) {
|
|
66435
|
-
return renderResolver(ListItemHeader, props);
|
|
66670
|
+
const sameSlotIds = (left, right) => {
|
|
66671
|
+
if (left.length !== right.length) {
|
|
66672
|
+
return false;
|
|
66436
66673
|
}
|
|
66437
|
-
|
|
66438
|
-
|
|
66674
|
+
let index = 0;
|
|
66675
|
+
while (index < left.length) {
|
|
66676
|
+
if (left[index] !== right[index]) {
|
|
66677
|
+
return false;
|
|
66678
|
+
}
|
|
66679
|
+
index++;
|
|
66439
66680
|
}
|
|
66440
|
-
return
|
|
66441
|
-
...props
|
|
66442
|
-
});
|
|
66681
|
+
return true;
|
|
66443
66682
|
};
|
|
66444
|
-
|
|
66445
|
-
|
|
66446
|
-
|
|
66447
|
-
|
|
66448
|
-
|
|
66449
|
-
|
|
66450
|
-
|
|
66451
|
-
|
|
66452
|
-
|
|
66453
|
-
|
|
66454
|
-
}, []);
|
|
66455
|
-
return jsx(Next, {
|
|
66456
|
-
...props,
|
|
66457
|
-
header: undefined,
|
|
66458
|
-
role: "presentation",
|
|
66459
|
-
baseClassName: "navi_list_item_header"
|
|
66460
|
-
});
|
|
66461
|
-
};
|
|
66462
|
-
const ListItemFooter = props => {
|
|
66463
|
-
const Next = useNextResolver();
|
|
66464
|
-
const {
|
|
66465
|
-
ref
|
|
66466
|
-
} = props;
|
|
66467
|
-
useDisplayedLayoutEffect(ref, footerEl => {
|
|
66468
|
-
const listContainerEl = footerEl.closest(".navi_list_container");
|
|
66469
|
-
const rect = footerEl.getBoundingClientRect();
|
|
66470
|
-
listContainerEl.style.setProperty("--list-footer-height", `${rect.height}px`);
|
|
66471
|
-
listContainerEl.style.setProperty("--list-footer-width", `${rect.width}px`);
|
|
66472
|
-
}, []);
|
|
66473
|
-
return jsx(Next, {
|
|
66474
|
-
...props,
|
|
66475
|
-
footer: undefined,
|
|
66476
|
-
role: "presentation",
|
|
66477
|
-
baseClassName: "navi_list_item_footer"
|
|
66478
|
-
});
|
|
66683
|
+
|
|
66684
|
+
// Rows in place order; a row with no place (a declared row filtered out) last.
|
|
66685
|
+
const compareRowPlaces = (left, right) => {
|
|
66686
|
+
if (left.place === undefined) {
|
|
66687
|
+
return right.place === undefined ? 0 : 1;
|
|
66688
|
+
}
|
|
66689
|
+
if (right.place === undefined) {
|
|
66690
|
+
return -1;
|
|
66691
|
+
}
|
|
66692
|
+
return left.place - right.place;
|
|
66479
66693
|
};
|
|
66480
66694
|
|
|
66481
66695
|
// What a row may say it is waiting on, all of them about the row as a thing
|
|
@@ -67283,8 +67497,10 @@ const applySearchHighlight = (el, highlight) => {
|
|
|
67283
67497
|
};
|
|
67284
67498
|
|
|
67285
67499
|
installImportMetaCssBuild(import.meta);
|
|
67286
|
-
const
|
|
67287
|
-
|
|
67500
|
+
const ListRowsContext = createContext(null);
|
|
67501
|
+
// The group a row is declared in, by id (see ListItemGroup): what its count
|
|
67502
|
+
// and its separator are scoped to.
|
|
67503
|
+
const ListGroupContext = createContext(undefined);
|
|
67288
67504
|
const PendingScrollRefContext = createContext(null);
|
|
67289
67505
|
// Controls how List.Item behaves when match=false (set via List searchNoMatchMode prop):
|
|
67290
67506
|
// "remove" — remove from DOM (default)
|
|
@@ -67323,21 +67539,15 @@ const SeparatorContext = createContext(null);
|
|
|
67323
67539
|
// Set by <List itemTransition>: each row then gets a view-transition-name of
|
|
67324
67540
|
// its own, so a change wrapped in a view transition animates row by row.
|
|
67325
67541
|
const ItemTransitionContext = createContext(false);
|
|
67326
|
-
// What the list knows about the collection as a whole: how many rows it has,
|
|
67327
|
-
// which of them it actually holds, and where each child's rows start. Filled in
|
|
67328
|
-
// by the children as they render (see createListVirtual), read by everything
|
|
67329
|
-
// that must reserve room for what is not rendered.
|
|
67330
|
-
const ListVirtualContext = createContext(null);
|
|
67331
67542
|
// Set around each row a run of items renders (see ListItems): which row of the
|
|
67332
|
-
// collection it is, where it stands among the rows the list holds, and
|
|
67333
|
-
// run
|
|
67334
|
-
//
|
|
67335
|
-
//
|
|
67336
|
-
// own — instead of a bare <List.Item> — works the same way.
|
|
67543
|
+
// collection it is, where it stands among the rows the list holds, and which
|
|
67544
|
+
// run it belongs to. Carried by context rather than injected into whatever
|
|
67545
|
+
// vnode renderItem returned, so that returning a component of one's own —
|
|
67546
|
+
// instead of a bare <List.Item> — works the same way.
|
|
67337
67547
|
const ListRowContext = createContext(null);
|
|
67338
67548
|
// The slot a child of the list stands in, by id (see ListDeclaredChildren). A
|
|
67339
67549
|
// row takes its place in the collection by slot: the place is then the list's
|
|
67340
|
-
// to move, and the row's to follow — see
|
|
67550
|
+
// to move, and the row's to follow — see list_rows.js.
|
|
67341
67551
|
const ListSlotContext = createContext(null);
|
|
67342
67552
|
const css$x = /* css */`@layer navi {
|
|
67343
67553
|
.navi_list_container {
|
|
@@ -67828,7 +68038,7 @@ const ListUI = props => {
|
|
|
67828
68038
|
overflow,
|
|
67829
68039
|
overflowX,
|
|
67830
68040
|
overflowY,
|
|
67831
|
-
|
|
68041
|
+
listRows,
|
|
67832
68042
|
...rest
|
|
67833
68043
|
} = props;
|
|
67834
68044
|
const scrollBoxPaddingProps = {};
|
|
@@ -67897,16 +68107,19 @@ const ListUI = props => {
|
|
|
67897
68107
|
observer.disconnect();
|
|
67898
68108
|
};
|
|
67899
68109
|
}, [lockSize]);
|
|
67900
|
-
|
|
67901
|
-
|
|
67902
|
-
|
|
67903
|
-
|
|
68110
|
+
listRows.onChange = () => {
|
|
68111
|
+
onListVisibleItemsChange?.(listRows.visibleItemsSignal.peek());
|
|
68112
|
+
};
|
|
68113
|
+
// Code in a layout effect of the list reads the rows as they stand after
|
|
68114
|
+
// the commit; the rows settle on a microtask, which preact does not wait for.
|
|
68115
|
+
useLayoutEffect(() => {
|
|
68116
|
+
listRows.flushSync();
|
|
67904
68117
|
});
|
|
67905
68118
|
// What the runs ask for and stand for: the steady budget, whatever the
|
|
67906
68119
|
// window of the first paint draws — a run asking for the rows of the first
|
|
67907
68120
|
// picture and then for the rest is two round trips for one opening.
|
|
67908
|
-
|
|
67909
|
-
|
|
68121
|
+
listRows.renderBudget = renderBudgetAfterPaint;
|
|
68122
|
+
listRows.scrolled = scrolled ?? defaultScrolled;
|
|
67910
68123
|
const {
|
|
67911
68124
|
virtualItemSizeSignal,
|
|
67912
68125
|
renderWindow,
|
|
@@ -67915,11 +68128,10 @@ const ListUI = props => {
|
|
|
67915
68128
|
captureAnchor
|
|
67916
68129
|
} = useListScrollSync({
|
|
67917
68130
|
ref,
|
|
67918
|
-
|
|
68131
|
+
listRows,
|
|
67919
68132
|
renderBudget,
|
|
67920
68133
|
renderBudgetSteady: renderBudgetAfterPaint,
|
|
67921
68134
|
virtualItemSize,
|
|
67922
|
-
virtual,
|
|
67923
68135
|
scrolled,
|
|
67924
68136
|
defaultScrolled,
|
|
67925
68137
|
onScrolledChange,
|
|
@@ -67938,28 +68150,28 @@ const ListUI = props => {
|
|
|
67938
68150
|
if (props.renderBudget === undefined || renderBudgetWarnedRef.current) {
|
|
67939
68151
|
return;
|
|
67940
68152
|
}
|
|
67941
|
-
if (
|
|
68153
|
+
if (listRows.hasRuns() || listRows.itemsSignal.peek().length === 0) {
|
|
67942
68154
|
return;
|
|
67943
68155
|
}
|
|
67944
68156
|
renderBudgetWarnedRef.current = true;
|
|
67945
68157
|
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.`);
|
|
67946
68158
|
});
|
|
67947
|
-
|
|
67948
|
-
|
|
67949
|
-
|
|
67950
|
-
|
|
68159
|
+
listRows.captureAnchor = captureAnchor;
|
|
68160
|
+
listRows.virtualItemSizeSignal = virtualItemSizeSignal;
|
|
68161
|
+
listRows.horizontal = Boolean(horizontal);
|
|
68162
|
+
listRows.renderSkeleton = renderSkeleton;
|
|
67951
68163
|
|
|
67952
68164
|
// A row is addressed by id from outside (--navi-scroll, --navi-select): the
|
|
67953
|
-
// ones drawn have
|
|
68165
|
+
// ones drawn have said so (see list_rows.js), and the ones a run
|
|
67954
68166
|
// holds without drawing are known only to that run (see List.Items' row
|
|
67955
68167
|
// locator). Both answer here, so a row is reachable whether or not the
|
|
67956
68168
|
// window happens to frame it.
|
|
67957
68169
|
const getItemById = itemId => {
|
|
67958
|
-
const itemDrawn =
|
|
68170
|
+
const itemDrawn = listRows.itemsSignal.peek().find(item => item.id === itemId);
|
|
67959
68171
|
if (itemDrawn) {
|
|
67960
68172
|
return itemDrawn;
|
|
67961
68173
|
}
|
|
67962
|
-
const rowIndex =
|
|
68174
|
+
const rowIndex = listRows.locateRow(itemId);
|
|
67963
68175
|
if (rowIndex === null) {
|
|
67964
68176
|
return undefined;
|
|
67965
68177
|
}
|
|
@@ -67968,13 +68180,13 @@ const ListUI = props => {
|
|
|
67968
68180
|
index: rowIndex
|
|
67969
68181
|
};
|
|
67970
68182
|
};
|
|
67971
|
-
const noMatchCount =
|
|
68183
|
+
const noMatchCount = listRows.noMatchCountSignal.value;
|
|
67972
68184
|
// What the list stands for, which is not always what it holds: a run saying
|
|
67973
68185
|
// it covers 60 rows is not an empty list while it waits for the first of
|
|
67974
68186
|
// them (see List.Items).
|
|
67975
68187
|
// eslint-disable-next-line no-unused-expressions
|
|
67976
|
-
|
|
67977
|
-
const itemCount =
|
|
68188
|
+
listRows.pagesSignal.value;
|
|
68189
|
+
const itemCount = listRows.countSignal.value || listRows.totalSignal.value;
|
|
67978
68190
|
const allNoMatch = noMatchCount > 0 && noMatchCount === itemCount;
|
|
67979
68191
|
const searching = Boolean(searchText);
|
|
67980
68192
|
const fallbackDisabled = fallback !== undefined && !fallback;
|
|
@@ -68072,7 +68284,7 @@ const ListUI = props => {
|
|
|
68072
68284
|
expand: expand,
|
|
68073
68285
|
"navi-nothing-to-display": nothingToDisplay ? "" : undefined,
|
|
68074
68286
|
"navi-loading": loading ? "" : undefined,
|
|
68075
|
-
"navi-refreshing":
|
|
68287
|
+
"navi-refreshing": listRows.refreshingSignal.value ? "" : undefined,
|
|
68076
68288
|
"navi-error": error ? "" : undefined,
|
|
68077
68289
|
styleCSSVars: LIST_STYLE_CSS_VARS,
|
|
68078
68290
|
pseudoClasses: LIST_PSEUDO_CLASSES,
|
|
@@ -68108,9 +68320,8 @@ const ListUI = props => {
|
|
|
68108
68320
|
spacing: spacing,
|
|
68109
68321
|
columns: columns,
|
|
68110
68322
|
itemColumns: itemColumns,
|
|
68111
|
-
|
|
68323
|
+
listRows: listRows,
|
|
68112
68324
|
renderWindow: renderWindow,
|
|
68113
|
-
virtual: virtual,
|
|
68114
68325
|
pendingScrollRef: pendingScrollRef,
|
|
68115
68326
|
overflow: overflow,
|
|
68116
68327
|
overflowX: overflowX,
|
|
@@ -68131,11 +68342,11 @@ const ListFirstResolver = props => {
|
|
|
68131
68342
|
props.ref = props.ref || refDefault;
|
|
68132
68343
|
const idDefault = useId();
|
|
68133
68344
|
props.id = props.id || idDefault;
|
|
68134
|
-
const
|
|
68135
|
-
if (!
|
|
68136
|
-
|
|
68345
|
+
const listRowsRef = useRef(null);
|
|
68346
|
+
if (!listRowsRef.current) {
|
|
68347
|
+
listRowsRef.current = createListRows();
|
|
68137
68348
|
}
|
|
68138
|
-
props.
|
|
68349
|
+
props.listRows = listRowsRef.current;
|
|
68139
68350
|
const parallelGuard = useParallelGuard(props.parallelGuard ?? PARALLEL_GUARD_DEFAULT);
|
|
68140
68351
|
return jsx(ParallelGuardContext.Provider, {
|
|
68141
68352
|
value: parallelGuard,
|
|
@@ -68161,9 +68372,8 @@ const ListContent = ({
|
|
|
68161
68372
|
spacing,
|
|
68162
68373
|
columns,
|
|
68163
68374
|
itemColumns,
|
|
68164
|
-
|
|
68375
|
+
listRows,
|
|
68165
68376
|
renderWindow,
|
|
68166
|
-
virtual,
|
|
68167
68377
|
pendingScrollRef,
|
|
68168
68378
|
overflow,
|
|
68169
68379
|
overflowX,
|
|
@@ -68217,9 +68427,8 @@ const ListContent = ({
|
|
|
68217
68427
|
columns: columns,
|
|
68218
68428
|
itemColumns: itemColumns,
|
|
68219
68429
|
...listProps,
|
|
68220
|
-
|
|
68430
|
+
listRows: listRows,
|
|
68221
68431
|
renderWindow: renderWindow,
|
|
68222
|
-
virtual: virtual,
|
|
68223
68432
|
children: children
|
|
68224
68433
|
})
|
|
68225
68434
|
})
|
|
@@ -68243,11 +68452,10 @@ const LIST_STYLE_CSS_VARS = {
|
|
|
68243
68452
|
const LIST_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":focus-within", ":read-only", ":disabled", ":-navi-void", ":-navi-expanded"];
|
|
68244
68453
|
const useListScrollSync = ({
|
|
68245
68454
|
ref,
|
|
68246
|
-
|
|
68455
|
+
listRows,
|
|
68247
68456
|
renderBudget,
|
|
68248
68457
|
renderBudgetSteady,
|
|
68249
68458
|
virtualItemSize,
|
|
68250
|
-
virtual,
|
|
68251
68459
|
scrolled,
|
|
68252
68460
|
defaultScrolled,
|
|
68253
68461
|
onScrolledChange,
|
|
@@ -68257,7 +68465,7 @@ const useListScrollSync = ({
|
|
|
68257
68465
|
}) => {
|
|
68258
68466
|
const debugScroll = useDebugScroll();
|
|
68259
68467
|
const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
|
|
68260
|
-
|
|
68468
|
+
listRows,
|
|
68261
68469
|
renderBudget,
|
|
68262
68470
|
scrolledWanted: scrolled ?? defaultScrolled
|
|
68263
68471
|
});
|
|
@@ -68278,11 +68486,12 @@ const useListScrollSync = ({
|
|
|
68278
68486
|
};
|
|
68279
68487
|
useLayoutEffect(resolveScroller);
|
|
68280
68488
|
useStickyScrollportWarning(ref, scroller);
|
|
68489
|
+
useDuplicateHeaderWarning(ref);
|
|
68281
68490
|
useStuckWindowWarning({
|
|
68282
68491
|
ref,
|
|
68283
68492
|
scrollerElResolved,
|
|
68284
68493
|
renderBudget,
|
|
68285
|
-
totalSignal:
|
|
68494
|
+
totalSignal: listRows.totalSignal,
|
|
68286
68495
|
virtualItemSizeSignal,
|
|
68287
68496
|
horizontal
|
|
68288
68497
|
});
|
|
@@ -68307,7 +68516,7 @@ const useListScrollSync = ({
|
|
|
68307
68516
|
anchorRef.current = captureScrollAnchor({
|
|
68308
68517
|
scrollerEl: getScroller(),
|
|
68309
68518
|
listEl: getListEl(),
|
|
68310
|
-
items:
|
|
68519
|
+
items: listRows.visibleItemsSignal.peek(),
|
|
68311
68520
|
horizontal
|
|
68312
68521
|
});
|
|
68313
68522
|
};
|
|
@@ -68339,7 +68548,7 @@ const useListScrollSync = ({
|
|
|
68339
68548
|
start,
|
|
68340
68549
|
end
|
|
68341
68550
|
} = renderWindowRef.current;
|
|
68342
|
-
const total =
|
|
68551
|
+
const total = listRows.totalSignal.peek();
|
|
68343
68552
|
let framedStart = start;
|
|
68344
68553
|
let framedEnd = start + renderBudget;
|
|
68345
68554
|
if (total > 0 && framedEnd > total) {
|
|
@@ -68383,19 +68592,19 @@ const useListScrollSync = ({
|
|
|
68383
68592
|
// jumped.
|
|
68384
68593
|
const holdWindow = () => {
|
|
68385
68594
|
if (startPlaceRef.current.userTookOver) {
|
|
68386
|
-
|
|
68595
|
+
listRows.holdPending = false;
|
|
68387
68596
|
return;
|
|
68388
68597
|
}
|
|
68389
68598
|
// Held somewhere it has not reached yet: what the window frames right now
|
|
68390
68599
|
// is not what it will frame, so nothing should be fetched for it.
|
|
68391
|
-
|
|
68392
|
-
const total =
|
|
68600
|
+
listRows.holdPending = scrolledWanted !== "start" && scrolledWanted !== undefined;
|
|
68601
|
+
const total = listRows.totalSignal.peek();
|
|
68393
68602
|
if (total <= renderBudget) {
|
|
68394
68603
|
// The whole collection is what the list draws: wherever in it the list is
|
|
68395
68604
|
// held, the window is already its place. Nowhere to move to means nothing
|
|
68396
68605
|
// to wait for — a hold left standing here is a list that never asks for
|
|
68397
68606
|
// anything again.
|
|
68398
|
-
|
|
68607
|
+
listRows.holdPending = false;
|
|
68399
68608
|
return;
|
|
68400
68609
|
}
|
|
68401
68610
|
const half = Math.floor(renderBudget / 2);
|
|
@@ -68405,7 +68614,7 @@ const useListScrollSync = ({
|
|
|
68405
68614
|
} else if (typeof scrolledWanted === "number") {
|
|
68406
68615
|
wantedStart = scrolledWanted - half;
|
|
68407
68616
|
} else if (scrolledWanted && scrolledWanted.id !== undefined) {
|
|
68408
|
-
const rowIndex =
|
|
68617
|
+
const rowIndex = listRows.locateRow(scrolledWanted.id);
|
|
68409
68618
|
if (rowIndex !== null) {
|
|
68410
68619
|
wantedStart = rowIndex - half;
|
|
68411
68620
|
} else if (typeof scrolledWanted.index === "number") {
|
|
@@ -68432,14 +68641,14 @@ const useListScrollSync = ({
|
|
|
68432
68641
|
end
|
|
68433
68642
|
} = renderWindowRef.current;
|
|
68434
68643
|
if (wantedStart === start && end - start === renderBudget) {
|
|
68435
|
-
|
|
68644
|
+
listRows.holdPending = false;
|
|
68436
68645
|
return;
|
|
68437
68646
|
}
|
|
68438
68647
|
renderWindowRef.current = {
|
|
68439
68648
|
start: wantedStart,
|
|
68440
68649
|
end: wantedStart + renderBudget
|
|
68441
68650
|
};
|
|
68442
|
-
|
|
68651
|
+
listRows.holdPending = false;
|
|
68443
68652
|
};
|
|
68444
68653
|
const pendingScrollRef = useRef();
|
|
68445
68654
|
const scrollToItem = (item, {
|
|
@@ -68450,7 +68659,7 @@ const useListScrollSync = ({
|
|
|
68450
68659
|
if (!item) {
|
|
68451
68660
|
return;
|
|
68452
68661
|
}
|
|
68453
|
-
const items =
|
|
68662
|
+
const items = listRows.itemsSignal.peek();
|
|
68454
68663
|
const itemCount = items.length;
|
|
68455
68664
|
if (itemCount === 0) {
|
|
68456
68665
|
return;
|
|
@@ -68579,7 +68788,7 @@ const useListScrollSync = ({
|
|
|
68579
68788
|
return;
|
|
68580
68789
|
}
|
|
68581
68790
|
hasBeenDisplayedRef.current = true;
|
|
68582
|
-
const items =
|
|
68791
|
+
const items = listRows.itemsSignal.peek();
|
|
68583
68792
|
const firstSelected = items.find(i => {
|
|
68584
68793
|
if (i.selected) {
|
|
68585
68794
|
return true;
|
|
@@ -68664,7 +68873,7 @@ const useListScrollSync = ({
|
|
|
68664
68873
|
scrollValues: savedScroll,
|
|
68665
68874
|
scrollerEl: listScrollContainerEl,
|
|
68666
68875
|
listEl: getListEl(),
|
|
68667
|
-
|
|
68876
|
+
listRows,
|
|
68668
68877
|
virtualItemSizeSignal,
|
|
68669
68878
|
renderWindowRef,
|
|
68670
68879
|
horizontal
|
|
@@ -68681,7 +68890,7 @@ const useListScrollSync = ({
|
|
|
68681
68890
|
});
|
|
68682
68891
|
return undefined;
|
|
68683
68892
|
}
|
|
68684
|
-
const visibleItems =
|
|
68893
|
+
const visibleItems = listRows.visibleItemsSignal.peek();
|
|
68685
68894
|
const topItems = visibleItems.slice(0, renderBudget);
|
|
68686
68895
|
const topMatchScoresKey = topItems.map(i => `${i.id}:${i.matchInfo?.matchScore ?? ""}`).join(",");
|
|
68687
68896
|
const currentTopMatchScore = topMatchScoresKeyRef.current;
|
|
@@ -68719,7 +68928,7 @@ const useListScrollSync = ({
|
|
|
68719
68928
|
if (scrolledWanted === "start" || scrolledWanted === undefined || startPlaceRef.current.userTookOver || !ref.current) {
|
|
68720
68929
|
return;
|
|
68721
68930
|
}
|
|
68722
|
-
if (
|
|
68931
|
+
if (listRows.totalSignal.peek() === 0 || virtualItemSizeSignal.peek() === 0) {
|
|
68723
68932
|
return;
|
|
68724
68933
|
}
|
|
68725
68934
|
// Coming back to a named row: it has to be on screen to be put back where
|
|
@@ -68731,13 +68940,13 @@ const useListScrollSync = ({
|
|
|
68731
68940
|
// Only whoever holds the rows can say where that one sits: the list
|
|
68732
68941
|
// itself knows the rows it has drawn, and this one is precisely the one
|
|
68733
68942
|
// it has not drawn yet.
|
|
68734
|
-
const rowIndex =
|
|
68943
|
+
const rowIndex = listRows.locateRow(scrolledWanted.id);
|
|
68735
68944
|
if (rowIndex === null) {
|
|
68736
68945
|
// Not there yet. Where it stood is enough to be roughly right in the
|
|
68737
68946
|
// meantime — the scrollbar lands near its final place instead of at the
|
|
68738
68947
|
// top, and the exact position is taken once the row itself can be
|
|
68739
68948
|
// measured.
|
|
68740
|
-
if (
|
|
68949
|
+
if (listRows.pagesSignal.peek() === 0) {
|
|
68741
68950
|
if (typeof scrolledWanted.index === "number") {
|
|
68742
68951
|
const rowPosition = scrolledWanted.index * virtualItemSizeSignal.peek();
|
|
68743
68952
|
anchorRef.current = null;
|
|
@@ -68854,7 +69063,7 @@ const useListScrollSync = ({
|
|
|
68854
69063
|
const position = captureScrollAnchor({
|
|
68855
69064
|
scrollerEl: getScroller(),
|
|
68856
69065
|
listEl: getListEl(),
|
|
68857
|
-
items:
|
|
69066
|
+
items: listRows.visibleItemsSignal.peek(),
|
|
68858
69067
|
horizontal
|
|
68859
69068
|
});
|
|
68860
69069
|
if (!position) {
|
|
@@ -68942,7 +69151,7 @@ const useListScrollSync = ({
|
|
|
68942
69151
|
anchorRef.current = null;
|
|
68943
69152
|
return;
|
|
68944
69153
|
}
|
|
68945
|
-
const items =
|
|
69154
|
+
const items = listRows.visibleItemsSignal.peek();
|
|
68946
69155
|
const itemNow = items.find(i => i.id === anchor.id);
|
|
68947
69156
|
if (!itemNow) {
|
|
68948
69157
|
anchorRef.current = null;
|
|
@@ -68964,7 +69173,7 @@ const useListScrollSync = ({
|
|
|
68964
69173
|
const windowSize = end - start;
|
|
68965
69174
|
const startShifted = start + indexShift;
|
|
68966
69175
|
let startWanted = startShifted < 0 ? 0 : startShifted;
|
|
68967
|
-
const total =
|
|
69176
|
+
const total = listRows.totalSignal.peek();
|
|
68968
69177
|
// Same normalization as the scroll listener: a window running past the
|
|
68969
69178
|
// last row slides back instead of framing fewer rows than its budget
|
|
68970
69179
|
// allows — every row that fits in it must stay rendered.
|
|
@@ -69022,7 +69231,7 @@ const useListScrollSync = ({
|
|
|
69022
69231
|
const windowSlidRef = useRef(false);
|
|
69023
69232
|
const budgetWarnedRef = useRef(false);
|
|
69024
69233
|
const evaluateWindow = reason => {
|
|
69025
|
-
const total =
|
|
69234
|
+
const total = listRows.totalSignal.peek();
|
|
69026
69235
|
if (total <= renderBudget) {
|
|
69027
69236
|
return;
|
|
69028
69237
|
}
|
|
@@ -69044,7 +69253,7 @@ const useListScrollSync = ({
|
|
|
69044
69253
|
},
|
|
69045
69254
|
scrollerEl,
|
|
69046
69255
|
listEl,
|
|
69047
|
-
|
|
69256
|
+
listRows,
|
|
69048
69257
|
virtualItemSizeSignal,
|
|
69049
69258
|
renderWindowRef,
|
|
69050
69259
|
horizontal
|
|
@@ -69300,6 +69509,34 @@ const useStickyScrollportWarning = (ref, scroller) => {
|
|
|
69300
69509
|
});
|
|
69301
69510
|
});
|
|
69302
69511
|
};
|
|
69512
|
+
// A list has one header: the row that caps it — the column row of a table —
|
|
69513
|
+
// and the box the list measures to keep the others from scrolling under it. A
|
|
69514
|
+
// second one takes that same place, so both sit at the capped edge before
|
|
69515
|
+
// every row and the rows declared between them read as belonging to the last:
|
|
69516
|
+
// a title meant to open a run of rows ends up titling nothing. That title is a
|
|
69517
|
+
// group label, which is why this points at List.Group rather than at the
|
|
69518
|
+
// stacking.
|
|
69519
|
+
const useDuplicateHeaderWarning = ref => {
|
|
69520
|
+
const doneRef = useRef(false);
|
|
69521
|
+
useLayoutEffect(() => {
|
|
69522
|
+
if (doneRef.current) {
|
|
69523
|
+
return;
|
|
69524
|
+
}
|
|
69525
|
+
const listContainerEl = ref.current;
|
|
69526
|
+
if (!listContainerEl) {
|
|
69527
|
+
return;
|
|
69528
|
+
}
|
|
69529
|
+
const headerEls = listContainerEl.querySelectorAll(".navi_list_item_header");
|
|
69530
|
+
if (headerEls.length < 2) {
|
|
69531
|
+
return;
|
|
69532
|
+
}
|
|
69533
|
+
doneRef.current = true;
|
|
69534
|
+
console.warn(`<List> has ${headerEls.length} rows carrying "header", and a list has one: they all stick to the edge it caps, before every row, and the rows declared between them read as belonging to the last one. A title standing over a run of rows is a group: <List.Group label="...">{rows}</List.Group>.`, {
|
|
69535
|
+
list: listContainerEl,
|
|
69536
|
+
headers: [...headerEls]
|
|
69537
|
+
});
|
|
69538
|
+
});
|
|
69539
|
+
};
|
|
69303
69540
|
|
|
69304
69541
|
/**
|
|
69305
69542
|
* "Am I stuck?" — the question a `position: sticky` element cannot ask about
|
|
@@ -69726,12 +69963,12 @@ const getScrollInfo = ({
|
|
|
69726
69963
|
scrollValues,
|
|
69727
69964
|
scrollerEl,
|
|
69728
69965
|
listEl,
|
|
69729
|
-
|
|
69966
|
+
listRows,
|
|
69730
69967
|
virtualItemSizeSignal,
|
|
69731
69968
|
renderWindowRef,
|
|
69732
69969
|
horizontal
|
|
69733
69970
|
}) => {
|
|
69734
|
-
const items =
|
|
69971
|
+
const items = listRows.itemsSignal.peek();
|
|
69735
69972
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
69736
69973
|
const listRect = listEl.getBoundingClientRect();
|
|
69737
69974
|
let hitEl = null;
|
|
@@ -69856,7 +70093,7 @@ const measureItemSize = (listEl, horizontal) => {
|
|
|
69856
70093
|
};
|
|
69857
70094
|
};
|
|
69858
70095
|
const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
69859
|
-
|
|
70096
|
+
listRows,
|
|
69860
70097
|
renderBudget,
|
|
69861
70098
|
scrolledWanted
|
|
69862
70099
|
}) => {
|
|
@@ -69916,7 +70153,7 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
69916
70153
|
// size is for, and a list drawing every row it has would pay a layout on
|
|
69917
70154
|
// each of its renders for a number nothing reads.
|
|
69918
70155
|
const sizeAlreadyKnown = virtualSizeSignal.peek() !== 0;
|
|
69919
|
-
const rowsHeldOffScreen =
|
|
70156
|
+
const rowsHeldOffScreen = listRows.totalSignal.peek() > renderBudget;
|
|
69920
70157
|
if (!virtualItemSizeProp && sizeAlreadyKnown && rowsHeldOffScreen && ref.current) {
|
|
69921
70158
|
const listEl = ref.current.querySelector(".navi_list");
|
|
69922
70159
|
const measure = listEl ? measureItemSize(listEl, horizontal) : null;
|
|
@@ -69932,7 +70169,7 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
69932
70169
|
// screen, and a list held somewhere (placeWhereHeld) before it knows where
|
|
69933
70170
|
// that is. A list drawing every row it has, opening at its start, would
|
|
69934
70171
|
// pay a layout in every commit for a number nobody reads.
|
|
69935
|
-
const sizeRead =
|
|
70172
|
+
const sizeRead = listRows.totalSignal.peek() > renderBudget || scrolledWanted !== undefined && scrolledWanted !== "start";
|
|
69936
70173
|
if (!sizeRead) {
|
|
69937
70174
|
return undefined;
|
|
69938
70175
|
}
|
|
@@ -69984,9 +70221,8 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
69984
70221
|
// item after each commit and writes to the signal, causing only the fillers to
|
|
69985
70222
|
// re-render.
|
|
69986
70223
|
const UnorderedList = ({
|
|
69987
|
-
|
|
70224
|
+
listRows,
|
|
69988
70225
|
renderWindow,
|
|
69989
|
-
virtual,
|
|
69990
70226
|
fallback,
|
|
69991
70227
|
fallbackShown,
|
|
69992
70228
|
searchFallback,
|
|
@@ -70036,17 +70272,14 @@ const UnorderedList = ({
|
|
|
70036
70272
|
value: separator ?? null,
|
|
70037
70273
|
children: jsx(ItemTransitionContext.Provider, {
|
|
70038
70274
|
value: Boolean(itemTransition),
|
|
70039
|
-
children: jsx(
|
|
70040
|
-
value:
|
|
70041
|
-
children: jsx(
|
|
70042
|
-
value:
|
|
70043
|
-
children: jsx(
|
|
70044
|
-
value: null,
|
|
70045
|
-
children: jsx(
|
|
70046
|
-
|
|
70047
|
-
children: jsx(ListDeclaredChildren, {
|
|
70048
|
-
children: children
|
|
70049
|
-
})
|
|
70275
|
+
children: jsx(ListRowsContext.Provider, {
|
|
70276
|
+
value: listRows,
|
|
70277
|
+
children: jsx(ListRowContext.Provider, {
|
|
70278
|
+
value: null,
|
|
70279
|
+
children: jsx(ListItemColumnsContext.Provider, {
|
|
70280
|
+
value: columns ? null : itemColumns || null,
|
|
70281
|
+
children: jsx(ListDeclaredChildren, {
|
|
70282
|
+
children: children
|
|
70050
70283
|
})
|
|
70051
70284
|
})
|
|
70052
70285
|
})
|
|
@@ -70097,8 +70330,8 @@ const VirtualFiller = ({
|
|
|
70097
70330
|
edge,
|
|
70098
70331
|
itemCount
|
|
70099
70332
|
}) => {
|
|
70100
|
-
const
|
|
70101
|
-
const sizeToFill = itemCount *
|
|
70333
|
+
const listRows = useContext(ListRowsContext);
|
|
70334
|
+
const sizeToFill = itemCount * listRows.virtualItemSizeSignal.value;
|
|
70102
70335
|
if (!sizeToFill) {
|
|
70103
70336
|
return null;
|
|
70104
70337
|
}
|
|
@@ -70241,12 +70474,11 @@ const ListItemUI = props => {
|
|
|
70241
70474
|
}
|
|
70242
70475
|
const idDefault = useId();
|
|
70243
70476
|
props.id = props.id || idDefault;
|
|
70244
|
-
const
|
|
70245
|
-
const
|
|
70477
|
+
const listRows = useContext(ListRowsContext);
|
|
70478
|
+
const groupId = useContext(ListGroupContext);
|
|
70246
70479
|
const searchNoMatchMode = useContext(SearchNoMatchModeContext);
|
|
70247
70480
|
// The run this row belongs to, when it comes from one (see ListItems): it
|
|
70248
|
-
//
|
|
70249
|
-
// the row mounts is decided here, and told back to the run.
|
|
70481
|
+
// gave the row its place and decided it is inside the render window.
|
|
70250
70482
|
const row = useContext(ListRowContext);
|
|
70251
70483
|
const slotId = useContext(ListSlotContext);
|
|
70252
70484
|
// There is no standalone match/matchScore/highlight prop — participation
|
|
@@ -70254,7 +70486,7 @@ const ListItemUI = props => {
|
|
|
70254
70486
|
// (e.g. useSearchText's getItemMatchInfo(item): { match, matchScore,
|
|
70255
70487
|
// matchRanges }), so there is exactly one way to wire it up.
|
|
70256
70488
|
const matchInfo = props.matchInfo;
|
|
70257
|
-
// Expose match on the
|
|
70489
|
+
// Expose match on the row: the list counts non-matching rows via
|
|
70258
70490
|
// `item.match === false` (drives noMatchCount → allNoMatch → the searchFallback
|
|
70259
70491
|
// / hide-when-empty behavior). Without this a matchInfo-based search would
|
|
70260
70492
|
// filter items out but never register them as "no match".
|
|
@@ -70278,33 +70510,27 @@ const ListItemUI = props => {
|
|
|
70278
70510
|
// name of this very component (idDefault, not the row's id): two components
|
|
70279
70511
|
// may stand for the same row for a moment, one leaving as the other arrives,
|
|
70280
70512
|
// and the one leaving must give back its own place, not the newcomer's.
|
|
70281
|
-
if (row) {
|
|
70513
|
+
if (!row) {
|
|
70282
70514
|
if (props.filtered) {
|
|
70283
|
-
|
|
70515
|
+
listRows.drop(idDefault);
|
|
70284
70516
|
} else {
|
|
70285
|
-
|
|
70517
|
+
props.index = listRows.take(idDefault, 1, slotId);
|
|
70286
70518
|
}
|
|
70287
|
-
} else if (props.filtered) {
|
|
70288
|
-
virtual.drop(idDefault);
|
|
70289
|
-
} else {
|
|
70290
|
-
props.index = virtual.take(idDefault, 1, slotId);
|
|
70291
70519
|
}
|
|
70520
|
+
// Every row that renders says so, whether it was declared one by one or
|
|
70521
|
+
// drawn by a run: what it is (its value, whether it is selected) and whether
|
|
70522
|
+
// it mounts at all are written where it renders, in one place.
|
|
70523
|
+
listRows.draw(idDefault, {
|
|
70524
|
+
ownerId: row ? row.ownerId : idDefault,
|
|
70525
|
+
place: props.index,
|
|
70526
|
+
groupId,
|
|
70527
|
+
data: props
|
|
70528
|
+
});
|
|
70292
70529
|
useLayoutEffect(() => {
|
|
70293
70530
|
return () => {
|
|
70294
|
-
|
|
70295
|
-
row.run.unmount(idDefault);
|
|
70296
|
-
} else {
|
|
70297
|
-
virtual.drop(idDefault);
|
|
70298
|
-
}
|
|
70531
|
+
listRows.erase(idDefault);
|
|
70299
70532
|
};
|
|
70300
70533
|
}, []);
|
|
70301
|
-
// Every row that is drawn registers itself, whether it was declared one by
|
|
70302
|
-
// one or drawn by a run: what it says about itself (its value, whether it is
|
|
70303
|
-
// selected) is written where it is drawn, in one place.
|
|
70304
|
-
const item = props;
|
|
70305
|
-
tracker.useTrackItem(item);
|
|
70306
|
-
const groupTracker = useContext(GroupItemTrackerContext);
|
|
70307
|
-
const groupVisibleIndex = groupTracker ? groupTracker.useTrackItem(item) : null;
|
|
70308
70534
|
const separator = useContext(SeparatorContext);
|
|
70309
70535
|
if (props.filtered) {
|
|
70310
70536
|
return null;
|
|
@@ -70312,32 +70538,13 @@ const ListItemUI = props => {
|
|
|
70312
70538
|
const listItemVnode = jsx(ListItemReal, {
|
|
70313
70539
|
...props
|
|
70314
70540
|
});
|
|
70315
|
-
|
|
70316
|
-
|
|
70317
|
-
|
|
70318
|
-
// The separator a row wears is the one at the gap above it, so the first row
|
|
70319
|
-
// that mounts wears none. "Am I first?" is answered by whoever handed out
|
|
70320
|
-
// the place — the run for its rows, the list's virtual for a declared one
|
|
70321
|
-
// (virtual.take above) — not by the tracker's visibleIndex: during a reorder
|
|
70322
|
-
// render pass (items resorted by search score) the other items still carry
|
|
70323
|
-
// stale keyToExplicitOrder values, the binary search reads them, no item
|
|
70324
|
-
// comes out at 0 and a spurious separator appears at the top. Inside a
|
|
70325
|
-
// declared group, each group has its own tracker and its items do not
|
|
70326
|
-
// reorder, so groupVisibleIndex is reliable there.
|
|
70327
|
-
let isFirst;
|
|
70328
|
-
if (row) {
|
|
70329
|
-
isFirst = row.run.isFirst(props.index, row.groupKey);
|
|
70330
|
-
} else if (groupVisibleIndex === null || props.hidden) {
|
|
70331
|
-
isFirst = props.index === 0;
|
|
70332
|
-
} else {
|
|
70333
|
-
isFirst = groupVisibleIndex === 0;
|
|
70334
|
-
}
|
|
70335
|
-
if (isFirst) {
|
|
70541
|
+
// The separator a row wears is the one at the gap above it: none when
|
|
70542
|
+
// nothing of the list stands above it (see list_rows.js).
|
|
70543
|
+
if (!separator || listRows.isFirst(idDefault)) {
|
|
70336
70544
|
return listItemVnode;
|
|
70337
70545
|
}
|
|
70338
70546
|
// The gap index, only used as the function-form argument.
|
|
70339
|
-
|
|
70340
|
-
let separatorVnode = resolveSeparatorVnode(separator, gapIndex);
|
|
70547
|
+
let separatorVnode = resolveSeparatorVnode(separator, props.index - 1);
|
|
70341
70548
|
if (props.hidden) {
|
|
70342
70549
|
// A row kept in the DOM but hidden keeps its separator, hidden with it:
|
|
70343
70550
|
// the point of keeping a row that matches nothing is that nothing moves,
|
|
@@ -70666,441 +70873,45 @@ const ListItem = /*#__PURE__*/createComponentResolver([ListItemFirstResolver, Li
|
|
|
70666
70873
|
pure: true
|
|
70667
70874
|
});
|
|
70668
70875
|
|
|
70669
|
-
//
|
|
70670
|
-
//
|
|
70671
|
-
//
|
|
70672
|
-
//
|
|
70673
|
-
// A child knows how many rows it stands for but not what was declared before
|
|
70674
|
-
// it, and it cannot deduce that from when it renders: a render is free to skip
|
|
70675
|
-
// it. A child that draws from signals and whose props are all referentially
|
|
70676
|
-
// === the previous ones does not render again (@preact/signals installs a
|
|
70677
|
-
// shouldComponentUpdate that says so), which is what any child nobody rebuilt
|
|
70678
|
-
// this frame is — and children numbered as they render would then slide up
|
|
70679
|
-
// into the place of the one that was skipped.
|
|
70876
|
+
// The walk that gives the list's children their places: a slot for each of
|
|
70877
|
+
// them, declared to the list's rows all at once before any child renders,
|
|
70878
|
+
// and handed to the child through a provider of its own — which is what lets
|
|
70879
|
+
// the row reach it however deep the caller buried it in components of theirs.
|
|
70680
70880
|
//
|
|
70681
|
-
//
|
|
70682
|
-
//
|
|
70683
|
-
//
|
|
70684
|
-
//
|
|
70685
|
-
//
|
|
70686
|
-
|
|
70687
|
-
|
|
70688
|
-
|
|
70689
|
-
const
|
|
70690
|
-
|
|
70691
|
-
|
|
70692
|
-
|
|
70693
|
-
|
|
70694
|
-
const
|
|
70695
|
-
|
|
70696
|
-
|
|
70697
|
-
|
|
70698
|
-
|
|
70699
|
-
|
|
70700
|
-
|
|
70701
|
-
|
|
70702
|
-
|
|
70703
|
-
|
|
70704
|
-
|
|
70705
|
-
const
|
|
70706
|
-
|
|
70707
|
-
|
|
70708
|
-
|
|
70709
|
-
|
|
70710
|
-
|
|
70711
|
-
|
|
70712
|
-
|
|
70713
|
-
|
|
70714
|
-
|
|
70715
|
-
let rowTotal = 0;
|
|
70716
|
-
// Owners have left and the others have not been moved up yet. Done on the
|
|
70717
|
-
// next ask rather than on the spot: rows leave many at a time (a search, a
|
|
70718
|
-
// list unmounting), and moving the others up once is enough.
|
|
70719
|
-
let placesStale = false;
|
|
70720
|
-
// Where the last slot holding an owner stands: an owner arriving at or after
|
|
70721
|
-
// it is placed at the end without going over the others — a whole first
|
|
70722
|
-
// render, rows arriving in order, costs each row nothing but itself.
|
|
70723
|
-
let rankOwnedLast = -1;
|
|
70724
|
-
const rebuildWalk = () => {
|
|
70725
|
-
slotWalk.length = 0;
|
|
70726
|
-
rankBySlot.clear();
|
|
70727
|
-
const visit = parentSlotId => {
|
|
70728
|
-
const slotIds = slotIdsByParent.get(parentSlotId);
|
|
70729
|
-
if (!slotIds) {
|
|
70730
|
-
return;
|
|
70731
|
-
}
|
|
70732
|
-
for (const slotId of slotIds) {
|
|
70733
|
-
rankBySlot.set(slotId, slotWalk.length);
|
|
70734
|
-
slotWalk.push(slotId);
|
|
70735
|
-
visit(slotId);
|
|
70736
|
-
}
|
|
70737
|
-
};
|
|
70738
|
-
visit(null);
|
|
70739
|
-
};
|
|
70740
|
-
// Every place, in one go: a place is the sum of what stands before it, so
|
|
70741
|
-
// there is nothing to hand out one at a time. Writing a place that did not
|
|
70742
|
-
// change wakes nobody — a signal ignores a value equal to its own.
|
|
70743
|
-
const refreshPlaces = () => {
|
|
70744
|
-
placesStale = false;
|
|
70745
|
-
let index = 0;
|
|
70746
|
-
let rank = 0;
|
|
70747
|
-
rankOwnedLast = -1;
|
|
70748
|
-
while (rank < slotWalk.length) {
|
|
70749
|
-
const ownerIds = ownerIdsBySlot.get(slotWalk[rank]);
|
|
70750
|
-
if (ownerIds) {
|
|
70751
|
-
for (const ownerId of ownerIds) {
|
|
70752
|
-
const owner = ownerById.get(ownerId);
|
|
70753
|
-
owner.placeSignal.value = index;
|
|
70754
|
-
index += owner.rowCount;
|
|
70755
|
-
}
|
|
70756
|
-
rankOwnedLast = rank;
|
|
70757
|
-
}
|
|
70758
|
-
rank++;
|
|
70759
|
-
}
|
|
70760
|
-
rowTotal = index;
|
|
70761
|
-
totalSignal.value = index;
|
|
70762
|
-
};
|
|
70763
|
-
const addToSlot = (slotId, ownerId) => {
|
|
70764
|
-
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
70765
|
-
if (ownerIds) {
|
|
70766
|
-
ownerIds.push(ownerId);
|
|
70767
|
-
} else {
|
|
70768
|
-
ownerIdsBySlot.set(slotId, [ownerId]);
|
|
70769
|
-
}
|
|
70770
|
-
warnIfEveryRowInOneSlot(slotId);
|
|
70771
|
-
};
|
|
70772
|
-
// Rows that all stand in the same slot keep the order they first mounted in:
|
|
70773
|
-
// the walk is over the children the list is given, and a component holding
|
|
70774
|
-
// them is one child however many rows it renders. Everything about a place
|
|
70775
|
-
// then stops following what the caller writes — a search reordering the rows
|
|
70776
|
-
// moves nothing. Said once, and only for the shape that can be nothing else:
|
|
70777
|
-
// the list's whole content is one child, and several rows came out of it.
|
|
70778
|
-
let everyRowInOneSlotWarned = false;
|
|
70779
|
-
const warnIfEveryRowInOneSlot = slotId => {
|
|
70780
|
-
if (everyRowInOneSlotWarned) {
|
|
70781
|
-
return;
|
|
70782
|
-
}
|
|
70783
|
-
const rootSlotIds = slotIdsByParent.get(null);
|
|
70784
|
-
if (!rootSlotIds || rootSlotIds.length !== 1 || rootSlotIds[0] !== slotId) {
|
|
70785
|
-
return;
|
|
70786
|
-
}
|
|
70787
|
-
if (ownerIdsBySlot.get(slotId).length < 2) {
|
|
70788
|
-
return;
|
|
70789
|
-
}
|
|
70790
|
-
everyRowInOneSlotWarned = true;
|
|
70791
|
-
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.`);
|
|
70792
|
-
};
|
|
70793
|
-
const removeFromSlot = (slotId, ownerId) => {
|
|
70794
|
-
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
70795
|
-
if (!ownerIds) {
|
|
70796
|
-
return;
|
|
70797
|
-
}
|
|
70798
|
-
const index = ownerIds.indexOf(ownerId);
|
|
70799
|
-
if (index !== -1) {
|
|
70800
|
-
ownerIds.splice(index, 1);
|
|
70801
|
-
}
|
|
70802
|
-
if (ownerIds.length === 0) {
|
|
70803
|
-
ownerIdsBySlot.delete(slotId);
|
|
70804
|
-
}
|
|
70805
|
-
};
|
|
70806
|
-
// A slot the walk no longer names: whatever stood in it is gone, and so is
|
|
70807
|
-
// whatever a walk inside it had declared.
|
|
70808
|
-
const dropSlot = slotId => {
|
|
70809
|
-
const ownerIds = ownerIdsBySlot.get(slotId);
|
|
70810
|
-
if (ownerIds) {
|
|
70811
|
-
for (const ownerId of ownerIds) {
|
|
70812
|
-
ownerById.delete(ownerId);
|
|
70813
|
-
}
|
|
70814
|
-
ownerIdsBySlot.delete(slotId);
|
|
70815
|
-
}
|
|
70816
|
-
const childSlotIds = slotIdsByParent.get(slotId);
|
|
70817
|
-
if (childSlotIds) {
|
|
70818
|
-
slotIdsByParent.delete(slotId);
|
|
70819
|
-
for (const childSlotId of childSlotIds) {
|
|
70820
|
-
dropSlot(childSlotId);
|
|
70821
|
-
}
|
|
70822
|
-
}
|
|
70823
|
-
};
|
|
70824
|
-
const virtual = {
|
|
70825
|
-
totalSignal,
|
|
70826
|
-
pagesSignal,
|
|
70827
|
-
refreshingSignal,
|
|
70828
|
-
// What a run needs to know about the list it lives in: how many rows the
|
|
70829
|
-
// list is willing to draw at once, which end it opens on, and how much
|
|
70830
|
-
// room one row is given — a row whose content has not arrived must take
|
|
70831
|
-
// exactly that, or the rows drawn would not reach where the list says they
|
|
70832
|
-
// are.
|
|
70833
|
-
renderBudget: 0,
|
|
70834
|
-
scrolled: "start",
|
|
70835
|
-
// The list is on its way somewhere: what the window frames is not what it
|
|
70836
|
-
// is about to frame, so a run must not fetch for it (see holdWindow).
|
|
70837
|
-
holdPending: false,
|
|
70838
|
-
// Called by a run just before rows land in it: what is on screen must not
|
|
70839
|
-
// move because something arrived above it. Set by the list itself.
|
|
70840
|
-
captureAnchor: () => {},
|
|
70841
|
-
horizontal: false,
|
|
70842
|
-
virtualItemSizeSignal: null,
|
|
70843
|
-
renderSkeleton: undefined,
|
|
70844
|
-
// The children a walk stands over, in order — said in one call, before any
|
|
70845
|
-
// of them renders, so that what a child asks next is answered against the
|
|
70846
|
-
// whole picture and not against the children that happened to render
|
|
70847
|
-
// first. Said again on every render of the walk, and heard only when
|
|
70848
|
-
// something moved.
|
|
70849
|
-
declareSlots: (parentSlotId, slotIds) => {
|
|
70850
|
-
const slotIdsPrevious = slotIdsByParent.get(parentSlotId);
|
|
70851
|
-
if (slotIdsPrevious && sameSlotIds(slotIdsPrevious, slotIds)) {
|
|
70852
|
-
return;
|
|
70853
|
-
}
|
|
70854
|
-
if (slotIdsPrevious) {
|
|
70855
|
-
const slotIdSet = new Set(slotIds);
|
|
70856
|
-
for (const slotId of slotIdsPrevious) {
|
|
70857
|
-
if (!slotIdSet.has(slotId)) {
|
|
70858
|
-
dropSlot(slotId);
|
|
70859
|
-
}
|
|
70860
|
-
}
|
|
70861
|
-
}
|
|
70862
|
-
slotIdsByParent.set(parentSlotId, slotIds);
|
|
70863
|
-
rebuildWalk();
|
|
70864
|
-
refreshPlaces();
|
|
70865
|
-
},
|
|
70866
|
-
// Whether something has taken this slot for its own: what it renders
|
|
70867
|
-
// inside is then its to place (a run draws its groups with their rows
|
|
70868
|
-
// already placed), and no walk inside it has anything to declare.
|
|
70869
|
-
slotHasOwner: slotId => ownerIdsBySlot.has(slotId),
|
|
70870
|
-
// Whether any run of rows lives in this list: what makes a render window
|
|
70871
|
-
// mean anything (see List's renderBudget).
|
|
70872
|
-
hasRuns: () => locatorByOwner.size > 0,
|
|
70873
|
-
setRowLocator: (ownerId, locate) => {
|
|
70874
|
-
locatorByOwner.set(ownerId, locate);
|
|
70875
|
-
},
|
|
70876
|
-
dropRowLocator: ownerId => {
|
|
70877
|
-
locatorByOwner.delete(ownerId);
|
|
70878
|
-
},
|
|
70879
|
-
// Where the row named by that id sits, asked of whoever holds it.
|
|
70880
|
-
locateRow: id => {
|
|
70881
|
-
for (const locate of locatorByOwner.values()) {
|
|
70882
|
-
const index = locate(id);
|
|
70883
|
-
if (index !== null) {
|
|
70884
|
-
return index;
|
|
70885
|
-
}
|
|
70886
|
-
}
|
|
70887
|
-
return null;
|
|
70888
|
-
},
|
|
70889
|
-
// The place the owner's rows start at — read from a signal, so that the
|
|
70890
|
-
// owner is rendered again when it moves (see createListVirtual). Asked on
|
|
70891
|
-
// every render, and answered without a second look for as long as the
|
|
70892
|
-
// owner stands in the same slot for the same number of rows.
|
|
70893
|
-
take: (ownerId, rowCount, slotId) => {
|
|
70894
|
-
let owner = ownerById.get(ownerId);
|
|
70895
|
-
if (owner) {
|
|
70896
|
-
if (owner.slotId !== slotId || owner.rowCount !== rowCount) {
|
|
70897
|
-
removeFromSlot(owner.slotId, ownerId);
|
|
70898
|
-
addToSlot(slotId, ownerId);
|
|
70899
|
-
owner.slotId = slotId;
|
|
70900
|
-
owner.rowCount = rowCount;
|
|
70901
|
-
placesStale = true;
|
|
70902
|
-
}
|
|
70903
|
-
if (placesStale) {
|
|
70904
|
-
refreshPlaces();
|
|
70905
|
-
}
|
|
70906
|
-
return owner.placeSignal.value;
|
|
70907
|
-
}
|
|
70908
|
-
if (placesStale) {
|
|
70909
|
-
refreshPlaces();
|
|
70910
|
-
}
|
|
70911
|
-
const rank = rankBySlot.get(slotId);
|
|
70912
|
-
addToSlot(slotId, ownerId);
|
|
70913
|
-
if (rank !== undefined && rank >= rankOwnedLast) {
|
|
70914
|
-
owner = {
|
|
70915
|
-
slotId,
|
|
70916
|
-
rowCount,
|
|
70917
|
-
placeSignal: signal(rowTotal)
|
|
70918
|
-
};
|
|
70919
|
-
ownerById.set(ownerId, owner);
|
|
70920
|
-
rowTotal += rowCount;
|
|
70921
|
-
rankOwnedLast = rank;
|
|
70922
|
-
totalSignal.value = rowTotal;
|
|
70923
|
-
return owner.placeSignal.value;
|
|
70924
|
-
}
|
|
70925
|
-
owner = {
|
|
70926
|
-
slotId,
|
|
70927
|
-
rowCount,
|
|
70928
|
-
placeSignal: signal(0)
|
|
70929
|
-
};
|
|
70930
|
-
ownerById.set(ownerId, owner);
|
|
70931
|
-
refreshPlaces();
|
|
70932
|
-
return owner.placeSignal.value;
|
|
70933
|
-
},
|
|
70934
|
-
// The owner stands for no row of the collection: it was filtered out by a
|
|
70935
|
-
// search, or it is gone.
|
|
70936
|
-
drop: ownerId => {
|
|
70937
|
-
const owner = ownerById.get(ownerId);
|
|
70938
|
-
if (!owner) {
|
|
70939
|
-
return;
|
|
70940
|
-
}
|
|
70941
|
-
ownerById.delete(ownerId);
|
|
70942
|
-
removeFromSlot(owner.slotId, ownerId);
|
|
70943
|
-
if (placesStale) {
|
|
70944
|
-
return;
|
|
70945
|
-
}
|
|
70946
|
-
placesStale = true;
|
|
70947
|
-
queueMicrotask(() => {
|
|
70948
|
-
if (placesStale) {
|
|
70949
|
-
refreshPlaces();
|
|
70950
|
-
}
|
|
70951
|
-
});
|
|
70952
|
-
}
|
|
70953
|
-
};
|
|
70954
|
-
return virtual;
|
|
70955
|
-
};
|
|
70956
|
-
const sameSlotIds = (left, right) => {
|
|
70957
|
-
if (left.length !== right.length) {
|
|
70958
|
-
return false;
|
|
70959
|
-
}
|
|
70960
|
-
let index = 0;
|
|
70961
|
-
while (index < left.length) {
|
|
70962
|
-
if (left[index] !== right[index]) {
|
|
70963
|
-
return false;
|
|
70964
|
-
}
|
|
70965
|
-
index++;
|
|
70966
|
-
}
|
|
70967
|
-
return true;
|
|
70968
|
-
};
|
|
70969
|
-
|
|
70970
|
-
// Which of a run's rows mount, and which of them comes first. A run draws
|
|
70971
|
-
// every row of its window, and only the row itself knows, once it renders,
|
|
70972
|
-
// that it renders nothing (filtered out by a search, see ListItemUI). The
|
|
70973
|
-
// separator a row wears is the one at the gap above it, so the first row that
|
|
70974
|
-
// mounts wears none — and "first" is read off the rows that mount, not off
|
|
70975
|
-
// the collection. Rows say so as they render, in order, and the answer is a
|
|
70976
|
-
// signal: a row rendered from a kept vnode is rendered again when the row
|
|
70977
|
-
// before it leaves or comes back. Grouped rows are counted per group, the gap
|
|
70978
|
-
// above a group's first row being the group wrapper's own.
|
|
70979
|
-
const createRunRows = () => {
|
|
70980
|
-
// rowId → { index, groupKey }
|
|
70981
|
-
const rowById = new Map();
|
|
70982
|
-
// groupKey (undefined outside groups) → the index of the group's first
|
|
70983
|
-
// mounted row, -1 when none.
|
|
70984
|
-
const firstSignalByGroup = new Map();
|
|
70985
|
-
// Where the window starts: a run cut by the window has rows above its first
|
|
70986
|
-
// drawn one, and so does a run standing after declared rows.
|
|
70987
|
-
const windowFromSignal = signal(0);
|
|
70988
|
-
// Groups whose first row left: recounted on the next ask, or at the end of
|
|
70989
|
-
// the frame, whichever comes first.
|
|
70990
|
-
const staleGroupKeys = new Set();
|
|
70991
|
-
const firstSignalOf = groupKey => {
|
|
70992
|
-
let firstSignal = firstSignalByGroup.get(groupKey);
|
|
70993
|
-
if (!firstSignal) {
|
|
70994
|
-
firstSignal = signal(-1);
|
|
70995
|
-
firstSignalByGroup.set(groupKey, firstSignal);
|
|
70996
|
-
}
|
|
70997
|
-
return firstSignal;
|
|
70998
|
-
};
|
|
70999
|
-
const refresh = groupKey => {
|
|
71000
|
-
staleGroupKeys.delete(groupKey);
|
|
71001
|
-
let first = -1;
|
|
71002
|
-
for (const row of rowById.values()) {
|
|
71003
|
-
if (row.groupKey === groupKey && (first === -1 || row.index < first)) {
|
|
71004
|
-
first = row.index;
|
|
71005
|
-
}
|
|
71006
|
-
}
|
|
71007
|
-
firstSignalOf(groupKey).value = first;
|
|
71008
|
-
};
|
|
71009
|
-
const leave = row => {
|
|
71010
|
-
if (firstSignalOf(row.groupKey).peek() !== row.index) {
|
|
71011
|
-
return;
|
|
71012
|
-
}
|
|
71013
|
-
staleGroupKeys.add(row.groupKey);
|
|
71014
|
-
queueMicrotask(() => {
|
|
71015
|
-
if (staleGroupKeys.has(row.groupKey)) {
|
|
71016
|
-
refresh(row.groupKey);
|
|
71017
|
-
}
|
|
71018
|
-
});
|
|
71019
|
-
};
|
|
71020
|
-
return {
|
|
71021
|
-
setWindowFrom: windowFrom => {
|
|
71022
|
-
windowFromSignal.value = windowFrom;
|
|
71023
|
-
},
|
|
71024
|
-
mount: (rowId, index, groupKey) => {
|
|
71025
|
-
const row = rowById.get(rowId);
|
|
71026
|
-
if (row) {
|
|
71027
|
-
if (row.index === index && row.groupKey === groupKey) {
|
|
71028
|
-
return;
|
|
71029
|
-
}
|
|
71030
|
-
leave(row);
|
|
71031
|
-
row.index = index;
|
|
71032
|
-
row.groupKey = groupKey;
|
|
71033
|
-
} else {
|
|
71034
|
-
rowById.set(rowId, {
|
|
71035
|
-
index,
|
|
71036
|
-
groupKey
|
|
71037
|
-
});
|
|
71038
|
-
}
|
|
71039
|
-
const firstSignal = firstSignalOf(groupKey);
|
|
71040
|
-
const first = firstSignal.peek();
|
|
71041
|
-
if (first === -1 || index < first) {
|
|
71042
|
-
firstSignal.value = index;
|
|
71043
|
-
}
|
|
71044
|
-
},
|
|
71045
|
-
unmount: rowId => {
|
|
71046
|
-
const row = rowById.get(rowId);
|
|
71047
|
-
if (!row) {
|
|
71048
|
-
return;
|
|
71049
|
-
}
|
|
71050
|
-
rowById.delete(rowId);
|
|
71051
|
-
leave(row);
|
|
71052
|
-
},
|
|
71053
|
-
isFirst: (index, groupKey) => {
|
|
71054
|
-
if (staleGroupKeys.has(groupKey)) {
|
|
71055
|
-
refresh(groupKey);
|
|
71056
|
-
}
|
|
71057
|
-
if (firstSignalOf(groupKey).value !== index) {
|
|
71058
|
-
return false;
|
|
71059
|
-
}
|
|
71060
|
-
return groupKey !== undefined || windowFromSignal.value === 0;
|
|
71061
|
-
}
|
|
71062
|
-
};
|
|
71063
|
-
};
|
|
71064
|
-
|
|
71065
|
-
// The walk that gives the list's children their places: a slot for each of
|
|
71066
|
-
// them, declared to the list's virtual all at once before any child renders,
|
|
71067
|
-
// and handed to the child through a provider of its own — which is what lets
|
|
71068
|
-
// the row reach it however deep the caller buried it in components of theirs.
|
|
71069
|
-
//
|
|
71070
|
-
// A slot is named the way preact tells the child apart: by key when it has
|
|
71071
|
-
// one, by position otherwise, and inside the array it was given in — a nested
|
|
71072
|
-
// array is one child to preact, so what follows the array keeps its name
|
|
71073
|
-
// however many rows the array holds. A child preact would not render (null,
|
|
71074
|
-
// a boolean) has no slot: it is not there.
|
|
71075
|
-
const ListDeclaredChildren = ({
|
|
71076
|
-
children
|
|
71077
|
-
}) => {
|
|
71078
|
-
const virtual = useContext(ListVirtualContext);
|
|
71079
|
-
const parentSlotId = useContext(ListSlotContext);
|
|
71080
|
-
if (parentSlotId !== null && virtual.slotHasOwner(parentSlotId)) {
|
|
71081
|
-
return children;
|
|
71082
|
-
}
|
|
71083
|
-
const slotIds = [];
|
|
71084
|
-
const declared = [];
|
|
71085
|
-
declareChildren(children, parentSlotId === null ? "" : `${parentSlotId}/`, slotIds, declared);
|
|
71086
|
-
virtual.declareSlots(parentSlotId, slotIds);
|
|
71087
|
-
return jsx(Fragment, {
|
|
71088
|
-
children: declared
|
|
71089
|
-
});
|
|
71090
|
-
};
|
|
71091
|
-
const declareChildren = (children, prefix, slotIds, declared) => {
|
|
71092
|
-
const childArray = Array.isArray(children) ? children : [children];
|
|
71093
|
-
let index = 0;
|
|
71094
|
-
for (const child of childArray) {
|
|
71095
|
-
if (Array.isArray(child)) {
|
|
71096
|
-
declareChildren(child, `${prefix}${index}/`, slotIds, declared);
|
|
71097
|
-
} else if (child !== null && child !== undefined && child !== false && child !== true) {
|
|
71098
|
-
const slotId = child.key === undefined || child.key === null ? `${prefix}i${index}` : `${prefix}k${child.key}`;
|
|
71099
|
-
slotIds.push(slotId);
|
|
71100
|
-
declared.push(jsx(ListSlotContext.Provider, {
|
|
71101
|
-
value: slotId,
|
|
71102
|
-
children: child
|
|
71103
|
-
}, slotId));
|
|
70881
|
+
// A slot is named the way preact tells the child apart: by key when it has
|
|
70882
|
+
// one, by position otherwise, and inside the array it was given in — a nested
|
|
70883
|
+
// array is one child to preact, so what follows the array keeps its name
|
|
70884
|
+
// however many rows the array holds. A child preact would not render (null,
|
|
70885
|
+
// a boolean) has no slot: it is not there.
|
|
70886
|
+
const ListDeclaredChildren = ({
|
|
70887
|
+
children
|
|
70888
|
+
}) => {
|
|
70889
|
+
const listRows = useContext(ListRowsContext);
|
|
70890
|
+
const parentSlotId = useContext(ListSlotContext);
|
|
70891
|
+
if (parentSlotId !== null && listRows.slotHasOwner(parentSlotId)) {
|
|
70892
|
+
return children;
|
|
70893
|
+
}
|
|
70894
|
+
const slotIds = [];
|
|
70895
|
+
const declared = [];
|
|
70896
|
+
declareChildren(children, parentSlotId === null ? "" : `${parentSlotId}/`, slotIds, declared);
|
|
70897
|
+
listRows.declareSlots(parentSlotId, slotIds);
|
|
70898
|
+
return jsx(Fragment, {
|
|
70899
|
+
children: declared
|
|
70900
|
+
});
|
|
70901
|
+
};
|
|
70902
|
+
const declareChildren = (children, prefix, slotIds, declared) => {
|
|
70903
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
70904
|
+
let index = 0;
|
|
70905
|
+
for (const child of childArray) {
|
|
70906
|
+
if (Array.isArray(child)) {
|
|
70907
|
+
declareChildren(child, `${prefix}${index}/`, slotIds, declared);
|
|
70908
|
+
} else if (child !== null && child !== undefined && child !== false && child !== true) {
|
|
70909
|
+
const slotId = child.key === undefined || child.key === null ? `${prefix}i${index}` : `${prefix}k${child.key}`;
|
|
70910
|
+
slotIds.push(slotId);
|
|
70911
|
+
declared.push(jsx(ListSlotContext.Provider, {
|
|
70912
|
+
value: slotId,
|
|
70913
|
+
children: child
|
|
70914
|
+
}, slotId));
|
|
71104
70915
|
}
|
|
71105
70916
|
index++;
|
|
71106
70917
|
}
|
|
@@ -71242,15 +71053,10 @@ const ListItems = ({
|
|
|
71242
71053
|
onRequestStateChange
|
|
71243
71054
|
}) => {
|
|
71244
71055
|
const ownerId = useId();
|
|
71245
|
-
const
|
|
71056
|
+
const listRows = useContext(ListRowsContext);
|
|
71246
71057
|
const slotId = useContext(ListSlotContext);
|
|
71247
71058
|
const renderWindow = useContext(RenderWindowContext);
|
|
71248
71059
|
const separator = useContext(SeparatorContext);
|
|
71249
|
-
const runRowsRef = useRef(null);
|
|
71250
|
-
if (!runRowsRef.current) {
|
|
71251
|
-
runRowsRef.current = createRunRows();
|
|
71252
|
-
}
|
|
71253
|
-
const runRows = runRowsRef.current;
|
|
71254
71060
|
// The vnode drawn for a row, kept by item: a run rendering again (its window
|
|
71255
71061
|
// moving, its first paint's budget giving way to the full one) hands preact
|
|
71256
71062
|
// the same vnode for a row that has not changed, and preact leaves that
|
|
@@ -71273,7 +71079,7 @@ const ListItems = ({
|
|
|
71273
71079
|
memoryBudget,
|
|
71274
71080
|
onRequestStateChange
|
|
71275
71081
|
});
|
|
71276
|
-
const renderRowSkeleton = renderSkeleton === undefined ?
|
|
71082
|
+
const renderRowSkeleton = renderSkeleton === undefined ? listRows.renderSkeleton : renderSkeleton;
|
|
71277
71083
|
// A row on its way takes the room the list reserves for it: anything else
|
|
71278
71084
|
// and the rows drawn stop short of where the scroll says they are. Read
|
|
71279
71085
|
// where a row is actually missing, and not before: the size settles after
|
|
@@ -71285,9 +71091,9 @@ const ListItems = ({
|
|
|
71285
71091
|
return skeletonRow;
|
|
71286
71092
|
}
|
|
71287
71093
|
skeletonRow = {};
|
|
71288
|
-
const virtualItemSize =
|
|
71094
|
+
const virtualItemSize = listRows.virtualItemSizeSignal.value;
|
|
71289
71095
|
if (virtualItemSize) {
|
|
71290
|
-
if (
|
|
71096
|
+
if (listRows.horizontal) {
|
|
71291
71097
|
skeletonRow.rowMinWidth = `${virtualItemSize}px`;
|
|
71292
71098
|
} else {
|
|
71293
71099
|
skeletonRow.rowMinHeight = `${virtualItemSize}px`;
|
|
@@ -71295,7 +71101,7 @@ const ListItems = ({
|
|
|
71295
71101
|
}
|
|
71296
71102
|
return skeletonRow;
|
|
71297
71103
|
};
|
|
71298
|
-
const runStart =
|
|
71104
|
+
const runStart = listRows.take(ownerId, store.rowCount, slotId);
|
|
71299
71105
|
const runEnd = runStart + store.rowCount;
|
|
71300
71106
|
// The two ways to count the same row. The list numbers its rows from its own
|
|
71301
71107
|
// first one, whatever draws it; the store numbers the collection's, straight
|
|
@@ -71310,7 +71116,7 @@ const ListItems = ({
|
|
|
71310
71116
|
const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;
|
|
71311
71117
|
const windowTo = renderWindow.end < runEnd ? renderWindow.end : runEnd;
|
|
71312
71118
|
store.forget(rankOf(windowFrom), rankOf(windowTo));
|
|
71313
|
-
|
|
71119
|
+
listRows.declareWindow(ownerId, windowFrom, windowTo);
|
|
71314
71120
|
|
|
71315
71121
|
// The row answers to its own id when the item carries one — that is what
|
|
71316
71122
|
// addresses it from outside (--navi-select, --navi-scroll, startAt) — and
|
|
@@ -71320,7 +71126,7 @@ const ListItems = ({
|
|
|
71320
71126
|
// Where a row named from outside actually sits. Only the run can answer:
|
|
71321
71127
|
// rows it holds but does not draw are nowhere else — a list only knows the
|
|
71322
71128
|
// rows it has drawn (they register themselves, see ListItemUI).
|
|
71323
|
-
|
|
71129
|
+
listRows.setRowLocator(ownerId, id => {
|
|
71324
71130
|
let found = null;
|
|
71325
71131
|
store.eachHeld((item, rank) => {
|
|
71326
71132
|
const rowIndex = rowOf(rank);
|
|
@@ -71332,8 +71138,8 @@ const ListItems = ({
|
|
|
71332
71138
|
});
|
|
71333
71139
|
useLayoutEffect(() => {
|
|
71334
71140
|
return () => {
|
|
71335
|
-
|
|
71336
|
-
|
|
71141
|
+
listRows.dropRowLocator(ownerId);
|
|
71142
|
+
listRows.drop(ownerId);
|
|
71337
71143
|
};
|
|
71338
71144
|
}, []);
|
|
71339
71145
|
|
|
@@ -71359,7 +71165,7 @@ const ListItems = ({
|
|
|
71359
71165
|
let askStart = missingStart;
|
|
71360
71166
|
let askEnd = missingEnd;
|
|
71361
71167
|
if (missingStart !== -1) {
|
|
71362
|
-
const rowsPerPage = pageSize ||
|
|
71168
|
+
const rowsPerPage = pageSize || listRows.renderBudget;
|
|
71363
71169
|
const holeSize = missingEnd - missingStart + 1;
|
|
71364
71170
|
if (holeSize < rowsPerPage) {
|
|
71365
71171
|
// Which way the page grows: away from the rows already held, which is
|
|
@@ -71474,7 +71280,7 @@ const ListItems = ({
|
|
|
71474
71280
|
rows.push(jsx("li", {
|
|
71475
71281
|
className: "navi_list_failed_rows",
|
|
71476
71282
|
style: {
|
|
71477
|
-
"--size-to-fill": `${failedRowCount *
|
|
71283
|
+
"--size-to-fill": `${failedRowCount * listRows.virtualItemSizeSignal.value}px`
|
|
71478
71284
|
},
|
|
71479
71285
|
children: renderError ? renderError({
|
|
71480
71286
|
error: store.failure.error,
|
|
@@ -71512,13 +71318,12 @@ const ListItems = ({
|
|
|
71512
71318
|
}
|
|
71513
71319
|
if (rowVnode) {
|
|
71514
71320
|
pushRow(jsx(ListRunSkeletonRow, {
|
|
71515
|
-
run: runRows,
|
|
71516
71321
|
row: {
|
|
71517
71322
|
id: key,
|
|
71518
71323
|
index: rowIndex,
|
|
71324
|
+
ownerId,
|
|
71519
71325
|
...getSkeletonRow()
|
|
71520
71326
|
},
|
|
71521
|
-
groupKey: groupKey,
|
|
71522
71327
|
separator: separator,
|
|
71523
71328
|
children: rowVnode
|
|
71524
71329
|
}, key), item, rowIndex, groupKey);
|
|
@@ -71529,7 +71334,7 @@ const ListItems = ({
|
|
|
71529
71334
|
let rowVnode;
|
|
71530
71335
|
let rowContextValue;
|
|
71531
71336
|
const rowVnodeKept = rowVnodesByItem.get(item);
|
|
71532
|
-
if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing
|
|
71337
|
+
if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing) {
|
|
71533
71338
|
rowVnode = rowVnodeKept.vnode;
|
|
71534
71339
|
rowContextValue = rowVnodeKept.rowContextValue;
|
|
71535
71340
|
} else {
|
|
@@ -71542,8 +71347,7 @@ const ListItems = ({
|
|
|
71542
71347
|
id: key,
|
|
71543
71348
|
index: rowIndex,
|
|
71544
71349
|
item,
|
|
71545
|
-
|
|
71546
|
-
groupKey
|
|
71350
|
+
ownerId
|
|
71547
71351
|
};
|
|
71548
71352
|
rowVnodesByItem.set(item, {
|
|
71549
71353
|
vnode: rowVnode,
|
|
@@ -71570,28 +71374,37 @@ const ListItems = ({
|
|
|
71570
71374
|
return rows;
|
|
71571
71375
|
};
|
|
71572
71376
|
|
|
71573
|
-
// A run's row that has not arrived, standing where the real one will
|
|
71574
|
-
//
|
|
71575
|
-
// above it
|
|
71377
|
+
// A run's row that has not arrived, standing where the real one will. It never
|
|
71378
|
+
// reaches ListItemUI (see ListItemSkeletonResolver), so it is drawn among the
|
|
71379
|
+
// rows here, and wears the separator of the gap above it the way a real row
|
|
71380
|
+
// does there.
|
|
71381
|
+
const SKELETON_ROW_DATA = {
|
|
71382
|
+
skeleton: true
|
|
71383
|
+
};
|
|
71576
71384
|
const ListRunSkeletonRow = ({
|
|
71577
|
-
run,
|
|
71578
71385
|
row,
|
|
71579
|
-
groupKey,
|
|
71580
71386
|
separator,
|
|
71581
71387
|
children
|
|
71582
71388
|
}) => {
|
|
71389
|
+
const listRows = useContext(ListRowsContext);
|
|
71390
|
+
const groupId = useContext(ListGroupContext);
|
|
71583
71391
|
const rowId = useId();
|
|
71584
|
-
|
|
71392
|
+
listRows.draw(rowId, {
|
|
71393
|
+
ownerId: row.ownerId,
|
|
71394
|
+
place: row.index,
|
|
71395
|
+
groupId,
|
|
71396
|
+
data: SKELETON_ROW_DATA
|
|
71397
|
+
});
|
|
71585
71398
|
useLayoutEffect(() => {
|
|
71586
71399
|
return () => {
|
|
71587
|
-
|
|
71400
|
+
listRows.erase(rowId);
|
|
71588
71401
|
};
|
|
71589
71402
|
}, []);
|
|
71590
71403
|
const rowVnode = jsx(ListRowContext.Provider, {
|
|
71591
71404
|
value: row,
|
|
71592
71405
|
children: children
|
|
71593
71406
|
});
|
|
71594
|
-
if (!separator ||
|
|
71407
|
+
if (!separator || listRows.isFirst(rowId)) {
|
|
71595
71408
|
return rowVnode;
|
|
71596
71409
|
}
|
|
71597
71410
|
return jsxs(Fragment, {
|
|
@@ -71753,7 +71566,7 @@ const useItemStore = ({
|
|
|
71753
71566
|
// where the hole is, and cleared by a retry — which is what makes the same
|
|
71754
71567
|
// range askable again (see the request memory just above).
|
|
71755
71568
|
const [failure, setFailure] = useState(null);
|
|
71756
|
-
const
|
|
71569
|
+
const listRows = useContext(ListRowsContext);
|
|
71757
71570
|
// The rows are there, which is what the list waits for to place itself on the
|
|
71758
71571
|
// row it is held at (see placeWhereHeld). Said from an effect: a signal read
|
|
71759
71572
|
// during this very render must not be written during it.
|
|
@@ -71762,12 +71575,12 @@ const useItemStore = ({
|
|
|
71762
71575
|
return;
|
|
71763
71576
|
}
|
|
71764
71577
|
itemsHeldRef.current = true;
|
|
71765
|
-
|
|
71578
|
+
listRows.pagesSignal.value = listRows.pagesSignal.peek() + 1;
|
|
71766
71579
|
});
|
|
71767
71580
|
// Before the first answer a run does not know how many rows it stands for.
|
|
71768
71581
|
// It stands for a windowful of them: a list that is about to be filled looks
|
|
71769
71582
|
// like rows on their way, not like an empty list.
|
|
71770
|
-
const rowCount = pages.count ?? count ??
|
|
71583
|
+
const rowCount = pages.count ?? count ?? listRows.renderBudget;
|
|
71771
71584
|
// A run that never received anything has nothing to keep on screen: asking
|
|
71772
71585
|
// again is its first ask, not a refresh.
|
|
71773
71586
|
if (staleRef.current && pages.count === undefined) {
|
|
@@ -71777,9 +71590,9 @@ const useItemStore = ({
|
|
|
71777
71590
|
if (!refreshing) {
|
|
71778
71591
|
return null;
|
|
71779
71592
|
}
|
|
71780
|
-
|
|
71593
|
+
listRows.refreshingSignal.value = listRows.refreshingSignal.peek() + 1;
|
|
71781
71594
|
return () => {
|
|
71782
|
-
|
|
71595
|
+
listRows.refreshingSignal.value = listRows.refreshingSignal.peek() - 1;
|
|
71783
71596
|
};
|
|
71784
71597
|
}, [refreshing]);
|
|
71785
71598
|
|
|
@@ -71878,7 +71691,7 @@ const useItemStore = ({
|
|
|
71878
71691
|
// how many rows there are, so it asks for the rows the list would open
|
|
71879
71692
|
// on — counting back from the end when that is where it opens, the way
|
|
71880
71693
|
// an HTTP range does.
|
|
71881
|
-
const budget =
|
|
71694
|
+
const budget = listRows.renderBudget;
|
|
71882
71695
|
let start = missingStart;
|
|
71883
71696
|
let end = missingEnd;
|
|
71884
71697
|
let around;
|
|
@@ -71889,11 +71702,11 @@ const useItemStore = ({
|
|
|
71889
71702
|
// The list is held on a row nothing on screen leads to: the rows it holds
|
|
71890
71703
|
// do not contain it, so no window it could draw will ever bring it. Only
|
|
71891
71704
|
// asking for it by name does.
|
|
71892
|
-
const wanted =
|
|
71705
|
+
const wanted = listRows.scrolled;
|
|
71893
71706
|
const askingAroundWantedRow = revalidating &&
|
|
71894
71707
|
// Only while the hold stands: once the user has taken the list over,
|
|
71895
71708
|
// the reading position is where they are, not where it opened.
|
|
71896
|
-
|
|
71709
|
+
listRows.holdPending && wanted && typeof wanted === "object" && wanted.id !== undefined && listRows.locateRow(wanted.id) === null;
|
|
71897
71710
|
if (askingAroundWantedRow) {
|
|
71898
71711
|
around = wanted.id;
|
|
71899
71712
|
// Where it stood when it was written down is enough to frame the ask;
|
|
@@ -71916,7 +71729,7 @@ const useItemStore = ({
|
|
|
71916
71729
|
around = firstHeld.id;
|
|
71917
71730
|
}
|
|
71918
71731
|
} else if (pages.count === undefined) {
|
|
71919
|
-
const scrolled =
|
|
71732
|
+
const scrolled = listRows.scrolled;
|
|
71920
71733
|
if (scrolled === "end") {
|
|
71921
71734
|
// Counting back from the end, the way an HTTP range does: a list
|
|
71922
71735
|
// opening on its last rows asks for them before it knows how many
|
|
@@ -71947,7 +71760,7 @@ const useItemStore = ({
|
|
|
71947
71760
|
// way somewhere the window does not frame yet, `count` that it knows
|
|
71948
71761
|
// how many rows it stands for.
|
|
71949
71762
|
const debugAsk = outcome => {
|
|
71950
|
-
debugScroll(`ask ${start}-${end}: ${outcome}`, `(revalidating=${revalidating} holdPending=${
|
|
71763
|
+
debugScroll(`ask ${start}-${end}: ${outcome}`, `(revalidating=${revalidating} holdPending=${listRows.holdPending} count=${pages.count})`);
|
|
71951
71764
|
};
|
|
71952
71765
|
if (start === -1) {
|
|
71953
71766
|
// Nothing missing and nothing to revalidate: the run has what it
|
|
@@ -71955,7 +71768,7 @@ const useItemStore = ({
|
|
|
71955
71768
|
debugAsk("nothing missing");
|
|
71956
71769
|
return;
|
|
71957
71770
|
}
|
|
71958
|
-
if (
|
|
71771
|
+
if (listRows.holdPending && pages.count !== undefined && !askingAroundWantedRow) {
|
|
71959
71772
|
// The one ask a hold lets through: the row the list is held on is
|
|
71960
71773
|
// what would lift the hold, and nothing else is going to bring it.
|
|
71961
71774
|
debugAsk("held on a row not reached yet");
|
|
@@ -72046,7 +71859,7 @@ const useItemStore = ({
|
|
|
72046
71859
|
const pageCount = Array.isArray(page) ? pageItems.length : page.count ?? pageStart + pageItems.length;
|
|
72047
71860
|
// Before the rows land: what is on screen has to stay where it is,
|
|
72048
71861
|
// and the DOM still shows the state to hold onto.
|
|
72049
|
-
|
|
71862
|
+
listRows.captureAnchor();
|
|
72050
71863
|
if (revalidating) {
|
|
72051
71864
|
// The rows held stood for a composition that has moved on; the
|
|
72052
71865
|
// ones outside the window are forgotten and asked for again if the
|
|
@@ -72068,7 +71881,7 @@ const useItemStore = ({
|
|
|
72068
71881
|
replace: revalidating
|
|
72069
71882
|
});
|
|
72070
71883
|
}
|
|
72071
|
-
|
|
71884
|
+
listRows.pagesSignal.value = listRows.pagesSignal.peek() + 1;
|
|
72072
71885
|
setPageVersion(version => version + 1);
|
|
72073
71886
|
};
|
|
72074
71887
|
const failed = error => {
|
|
@@ -72118,7 +71931,7 @@ const useItemStore = ({
|
|
|
72118
71931
|
};
|
|
72119
71932
|
|
|
72120
71933
|
/**
|
|
72121
|
-
*
|
|
71934
|
+
* List.Group — a labeled group of list items.
|
|
72122
71935
|
*
|
|
72123
71936
|
* Renders a <li role="presentation"> wrapper containing a label span
|
|
72124
71937
|
* (accessible via aria-labelledby) and a <ul role="group"> for the items.
|
|
@@ -72136,10 +71949,16 @@ const ListItemGroup = ({
|
|
|
72136
71949
|
...rest
|
|
72137
71950
|
}) => {
|
|
72138
71951
|
const groupId = useId();
|
|
72139
|
-
const
|
|
71952
|
+
const listRows = useContext(ListRowsContext);
|
|
71953
|
+
const group = listRows.group(groupId);
|
|
71954
|
+
useLayoutEffect(() => {
|
|
71955
|
+
return () => {
|
|
71956
|
+
listRows.dropGroup(groupId);
|
|
71957
|
+
};
|
|
71958
|
+
}, []);
|
|
72140
71959
|
const searchNoMatchMode = useContext(SearchNoMatchModeContext);
|
|
72141
|
-
const groupItemCount =
|
|
72142
|
-
const groupNoMatchCount =
|
|
71960
|
+
const groupItemCount = group.countSignal.value;
|
|
71961
|
+
const groupNoMatchCount = group.noMatchCountSignal.value;
|
|
72143
71962
|
// Every row of this group failed the search: the label has nothing left to
|
|
72144
71963
|
// title. "remove" empties the group on its own (and hiddenWhileEmpty takes it
|
|
72145
71964
|
// out of the flow), "muted" keeps the rows readable so the label stays useful
|
|
@@ -72183,8 +72002,8 @@ const ListItemGroup = ({
|
|
|
72183
72002
|
className: "navi_list_item_group_list",
|
|
72184
72003
|
role: "group",
|
|
72185
72004
|
"aria-labelledby": groupId,
|
|
72186
|
-
children: jsx(
|
|
72187
|
-
value:
|
|
72005
|
+
children: jsx(ListGroupContext.Provider, {
|
|
72006
|
+
value: groupId,
|
|
72188
72007
|
children: jsx(ListDeclaredChildren, {
|
|
72189
72008
|
children: children
|
|
72190
72009
|
})
|
|
@@ -72448,7 +72267,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
72448
72267
|
*/
|
|
72449
72268
|
const List = /*#__PURE__*/Object.assign(ListResolved, {
|
|
72450
72269
|
Item: ListItem,
|
|
72451
|
-
Items: ListItems
|
|
72270
|
+
Items: ListItems,
|
|
72271
|
+
Group: ListItemGroup
|
|
72452
72272
|
});
|
|
72453
72273
|
|
|
72454
72274
|
const PickerNaviTime = props => {
|
|
@@ -78146,414 +77966,812 @@ const SplitButton = props => {
|
|
|
78146
77966
|
});
|
|
78147
77967
|
};
|
|
78148
77968
|
|
|
78149
|
-
// What the Picker's popup answers to — Picker's own popup props, named here so
|
|
78150
|
-
// a caller reaches all of them through the split button (see picker.jsx's JSDoc
|
|
78151
|
-
// for what each one says).
|
|
78152
|
-
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"]);
|
|
78153
|
-
const splitPopupProps = props => {
|
|
78154
|
-
const popupProps = {};
|
|
78155
|
-
const boxProps = {};
|
|
78156
|
-
for (const key of Object.keys(props)) {
|
|
78157
|
-
if (POPUP_PROP_SET.has(key)) {
|
|
78158
|
-
popupProps[key] = props[key];
|
|
77969
|
+
// What the Picker's popup answers to — Picker's own popup props, named here so
|
|
77970
|
+
// a caller reaches all of them through the split button (see picker.jsx's JSDoc
|
|
77971
|
+
// for what each one says).
|
|
77972
|
+
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"]);
|
|
77973
|
+
const splitPopupProps = props => {
|
|
77974
|
+
const popupProps = {};
|
|
77975
|
+
const boxProps = {};
|
|
77976
|
+
for (const key of Object.keys(props)) {
|
|
77977
|
+
if (POPUP_PROP_SET.has(key)) {
|
|
77978
|
+
popupProps[key] = props[key];
|
|
77979
|
+
} else {
|
|
77980
|
+
boxProps[key] = props[key];
|
|
77981
|
+
}
|
|
77982
|
+
}
|
|
77983
|
+
return [popupProps, boxProps];
|
|
77984
|
+
};
|
|
77985
|
+
|
|
77986
|
+
/**
|
|
77987
|
+
* applySearch — matches value against searchText.
|
|
77988
|
+
*
|
|
77989
|
+
* Accent-insensitive: "gue" matches "Guérin", "e" matches "é".
|
|
77990
|
+
* Case-insensitive: "bob" matches "Bob", with a score bonus for case-exact matches.
|
|
77991
|
+
* Multi-word: if searchText contains spaces, each word must appear somewhere in
|
|
77992
|
+
* the value for it to match. Ranges for all words are returned.
|
|
77993
|
+
*
|
|
77994
|
+
* Score table:
|
|
77995
|
+
*
|
|
77996
|
+
* Situation Score
|
|
77997
|
+
* ─────────────────────────────────────── ───────────────────────────
|
|
77998
|
+
* phrase at start of value 1
|
|
77999
|
+
* multi-word, one word at start (all match) 0.75
|
|
78000
|
+
* phrase / word at word boundary 0.625
|
|
78001
|
+
* phrase / words mid-word 0.5
|
|
78002
|
+
* + case-exact bonus +0.125
|
|
78003
|
+
* multi-word partial: score × (matched/total)
|
|
78004
|
+
*
|
|
78005
|
+
* matchRanges: [start, end] pairs (exclusive end) for CSS Highlight API.
|
|
78006
|
+
* Intended to be passed to useSearch as the matchFn parameter.
|
|
78007
|
+
*/
|
|
78008
|
+
const applySearch = (searchText, value) => {
|
|
78009
|
+
if (!searchText) {
|
|
78010
|
+
return { match: true, matchScore: 0, matchRanges: [] };
|
|
78011
|
+
}
|
|
78012
|
+
if (searchText.length > 100) {
|
|
78013
|
+
searchText = searchText.slice(0, 100);
|
|
78014
|
+
}
|
|
78015
|
+
const str = String(value);
|
|
78016
|
+
const foldedStr = foldAccents(str).toLowerCase();
|
|
78017
|
+
const { foldedSearch, words, originalWords } = getSearchInfo(searchText);
|
|
78018
|
+
|
|
78019
|
+
// Try exact phrase match first (gives best score).
|
|
78020
|
+
const phraseRanges = [];
|
|
78021
|
+
let phraseIdx = foldedStr.indexOf(foldedSearch);
|
|
78022
|
+
while (phraseIdx !== -1) {
|
|
78023
|
+
phraseRanges.push([phraseIdx, phraseIdx + foldedSearch.length]);
|
|
78024
|
+
phraseIdx = foldedStr.indexOf(foldedSearch, phraseIdx + 1);
|
|
78025
|
+
}
|
|
78026
|
+
if (phraseRanges.length > 0) {
|
|
78027
|
+
const atStart = foldedStr.startsWith(foldedSearch);
|
|
78028
|
+
const atWordBoundary = phraseRanges.some(([start]) =>
|
|
78029
|
+
isWordBoundary(foldedStr, start),
|
|
78030
|
+
);
|
|
78031
|
+
const caseExact = str.includes(searchText);
|
|
78032
|
+
let baseScore;
|
|
78033
|
+
if (atStart) {
|
|
78034
|
+
baseScore = SCORE_PHRASE_AT_START;
|
|
78035
|
+
} else if (atWordBoundary) {
|
|
78036
|
+
baseScore = SCORE_AT_WORD_BOUNDARY;
|
|
78037
|
+
} else {
|
|
78038
|
+
baseScore = SCORE_MID_WORD;
|
|
78039
|
+
}
|
|
78040
|
+
const matchScore = baseScore + (caseExact ? SCORE_BONUS_CASE_EXACT : 0);
|
|
78041
|
+
return { match: true, matchScore, matchRanges: mergeRanges(phraseRanges) };
|
|
78042
|
+
}
|
|
78043
|
+
|
|
78044
|
+
// Multi-word OR: split on whitespace, any word matching contributes to the score.
|
|
78045
|
+
// Items where all words match rank higher than partial matches.
|
|
78046
|
+
// Note: words always has at least 1 element here (searchText is non-empty and
|
|
78047
|
+
// foldedSearch.split filters empty strings). This path also handles the case
|
|
78048
|
+
// where searchText has trailing/leading spaces: the phrase match above tries
|
|
78049
|
+
// the literal (e.g. "tc " in "tc adapter"), and if that fails we fall through
|
|
78050
|
+
// here to try each word individually (e.g. "tc" matches "tca").
|
|
78051
|
+
const matchRanges = [];
|
|
78052
|
+
let matchedWordCount = 0;
|
|
78053
|
+
let anyWordAtStart = false;
|
|
78054
|
+
let anyWordAtWordBoundary = false;
|
|
78055
|
+
let allMatchedWordsExact = true;
|
|
78056
|
+
for (let w = 0; w < words.length; w++) {
|
|
78057
|
+
const word = words[w];
|
|
78058
|
+
const originalWord = originalWords[w];
|
|
78059
|
+
let idx = foldedStr.indexOf(word);
|
|
78060
|
+
if (idx === -1) {
|
|
78061
|
+
continue;
|
|
78062
|
+
}
|
|
78063
|
+
matchedWordCount++;
|
|
78064
|
+
let wordHasExactMatch = false;
|
|
78065
|
+
while (idx !== -1) {
|
|
78066
|
+
matchRanges.push([idx, idx + word.length]);
|
|
78067
|
+
if (idx === 0) {
|
|
78068
|
+
anyWordAtStart = true;
|
|
78069
|
+
anyWordAtWordBoundary = true;
|
|
78070
|
+
} else if (isWordBoundary(foldedStr, idx)) {
|
|
78071
|
+
anyWordAtWordBoundary = true;
|
|
78072
|
+
}
|
|
78073
|
+
if (str.slice(idx, idx + word.length) === originalWord) {
|
|
78074
|
+
wordHasExactMatch = true;
|
|
78075
|
+
}
|
|
78076
|
+
idx = foldedStr.indexOf(word, idx + 1);
|
|
78077
|
+
}
|
|
78078
|
+
if (!wordHasExactMatch) {
|
|
78079
|
+
allMatchedWordsExact = false;
|
|
78080
|
+
}
|
|
78081
|
+
}
|
|
78082
|
+
if (matchedWordCount === 0) {
|
|
78083
|
+
return tryAcronymMatch(foldedStr, str, searchText);
|
|
78084
|
+
}
|
|
78085
|
+
const wordRatio = matchedWordCount / words.length;
|
|
78086
|
+
let baseScore;
|
|
78087
|
+
if (anyWordAtStart) {
|
|
78088
|
+
baseScore = SCORE_MULTI_WORD_AT_START;
|
|
78089
|
+
} else if (anyWordAtWordBoundary) {
|
|
78090
|
+
baseScore = SCORE_AT_WORD_BOUNDARY;
|
|
78091
|
+
} else {
|
|
78092
|
+
baseScore = SCORE_MID_WORD;
|
|
78093
|
+
}
|
|
78094
|
+
const matchScore =
|
|
78095
|
+
(baseScore + (allMatchedWordsExact ? SCORE_BONUS_CASE_EXACT : 0)) *
|
|
78096
|
+
wordRatio;
|
|
78097
|
+
return { match: true, matchScore, matchRanges: mergeRanges(matchRanges) };
|
|
78098
|
+
};
|
|
78099
|
+
|
|
78100
|
+
// Returns true when position idx in str is at a word boundary,
|
|
78101
|
+
// meaning it is either the start of the string or the preceding character
|
|
78102
|
+
// is not a Unicode letter or digit.
|
|
78103
|
+
const isWordBoundary = (str, idx) => {
|
|
78104
|
+
if (idx === 0) {
|
|
78105
|
+
return true;
|
|
78106
|
+
}
|
|
78107
|
+
return !/[\p{L}\p{N}]/u.test(str[idx - 1]);
|
|
78108
|
+
};
|
|
78109
|
+
|
|
78110
|
+
// Strip diacritics for accent-insensitive matching.
|
|
78111
|
+
// NFC normalization first ensures precomposed characters (é → single code unit),
|
|
78112
|
+
// so the folded string has the same length as the NFC source — ranges computed
|
|
78113
|
+
// on the folded string map 1:1 to positions in the original string.
|
|
78114
|
+
const foldAccents = (str) => {
|
|
78115
|
+
return str
|
|
78116
|
+
.normalize("NFC")
|
|
78117
|
+
.normalize("NFD")
|
|
78118
|
+
.replace(/\p{Mn}/gu, "");
|
|
78119
|
+
};
|
|
78120
|
+
|
|
78121
|
+
const SCORE_PHRASE_AT_START = 1;
|
|
78122
|
+
const SCORE_MULTI_WORD_AT_START = 0.75;
|
|
78123
|
+
const SCORE_AT_WORD_BOUNDARY = 0.625;
|
|
78124
|
+
const SCORE_MID_WORD = 0.5;
|
|
78125
|
+
const SCORE_ACRONYM = 0.4;
|
|
78126
|
+
const SCORE_BONUS_CASE_EXACT = 0.125;
|
|
78127
|
+
|
|
78128
|
+
// Acronym match: each char of searchText (spaces stripped) must be the first
|
|
78129
|
+
// letter of a word in value, in order (greedy subsequence on word-starts).
|
|
78130
|
+
// e.g. "TC" matches "Total Count" highlighting the T and C.
|
|
78131
|
+
const tryAcronymMatch = (foldedStr, str, searchText) => {
|
|
78132
|
+
const acronymChars = foldAccents(searchText).toLowerCase().replace(/\s/g, "");
|
|
78133
|
+
if (acronymChars.length < 2) {
|
|
78134
|
+
// Single-char acronym is too ambiguous — skip.
|
|
78135
|
+
return { match: false, matchScore: 0, matchRanges: [] };
|
|
78136
|
+
}
|
|
78137
|
+
const wordStarts = [];
|
|
78138
|
+
for (let i = 0; i < foldedStr.length; i++) {
|
|
78139
|
+
if (isWordBoundary(foldedStr, i)) {
|
|
78140
|
+
wordStarts.push(i);
|
|
78141
|
+
}
|
|
78142
|
+
}
|
|
78143
|
+
const matchedPositions = [];
|
|
78144
|
+
let wordIdx = 0;
|
|
78145
|
+
const originalAcronym = searchText.replace(/\s/g, "");
|
|
78146
|
+
for (let si = 0; si < acronymChars.length; si++) {
|
|
78147
|
+
const ch = acronymChars[si];
|
|
78148
|
+
let found = false;
|
|
78149
|
+
while (wordIdx < wordStarts.length) {
|
|
78150
|
+
const pos = wordStarts[wordIdx];
|
|
78151
|
+
wordIdx++;
|
|
78152
|
+
if (foldedStr[pos] === ch) {
|
|
78153
|
+
matchedPositions.push(pos);
|
|
78154
|
+
found = true;
|
|
78155
|
+
break;
|
|
78156
|
+
}
|
|
78157
|
+
}
|
|
78158
|
+
if (!found) {
|
|
78159
|
+
return { match: false, matchScore: 0, matchRanges: [] };
|
|
78160
|
+
}
|
|
78161
|
+
}
|
|
78162
|
+
const atStart = matchedPositions[0] === 0;
|
|
78163
|
+
const caseExact = matchedPositions.every(
|
|
78164
|
+
(p, i) => str[p] === originalAcronym[i],
|
|
78165
|
+
);
|
|
78166
|
+
const baseScore = atStart ? SCORE_ACRONYM + 0.05 : SCORE_ACRONYM;
|
|
78167
|
+
const matchScore = baseScore + (caseExact ? SCORE_BONUS_CASE_EXACT : 0);
|
|
78168
|
+
const matchRanges = matchedPositions.map((p) => [p, p + 1]);
|
|
78169
|
+
return { match: true, matchScore, matchRanges };
|
|
78170
|
+
};
|
|
78171
|
+
|
|
78172
|
+
// LRU cache for pre-computed search info, avoids recomputing foldAccents/toLowerCase
|
|
78173
|
+
// for the same searchText across all items in a list render.
|
|
78174
|
+
const searchCache = new Map();
|
|
78175
|
+
const SEARCH_CACHE_MAX_SIZE = 20;
|
|
78176
|
+
const getSearchInfo = (searchText) => {
|
|
78177
|
+
if (searchCache.has(searchText)) {
|
|
78178
|
+
const cached = searchCache.get(searchText);
|
|
78179
|
+
searchCache.delete(searchText);
|
|
78180
|
+
searchCache.set(searchText, cached);
|
|
78181
|
+
return cached;
|
|
78182
|
+
}
|
|
78183
|
+
const foldedSearch = foldAccents(searchText).toLowerCase();
|
|
78184
|
+
const words = foldedSearch.split(/\s+/).filter(Boolean);
|
|
78185
|
+
const originalWords = searchText.split(/\s+/).filter(Boolean);
|
|
78186
|
+
const info = { foldedSearch, words, originalWords };
|
|
78187
|
+
searchCache.set(searchText, info);
|
|
78188
|
+
if (searchCache.size > SEARCH_CACHE_MAX_SIZE) {
|
|
78189
|
+
searchCache.delete(searchCache.keys().next().value);
|
|
78190
|
+
}
|
|
78191
|
+
return info;
|
|
78192
|
+
};
|
|
78193
|
+
|
|
78194
|
+
// Merge overlapping or adjacent [start, end] ranges (sorted by start).
|
|
78195
|
+
const mergeRanges = (ranges) => {
|
|
78196
|
+
if (ranges.length < 2) {
|
|
78197
|
+
return ranges;
|
|
78198
|
+
}
|
|
78199
|
+
const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
|
|
78200
|
+
const merged = [sorted[0]];
|
|
78201
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
78202
|
+
const last = merged[merged.length - 1];
|
|
78203
|
+
const current = sorted[i];
|
|
78204
|
+
if (current[0] <= last[1]) {
|
|
78205
|
+
if (current[1] > last[1]) {
|
|
78206
|
+
last[1] = current[1];
|
|
78207
|
+
}
|
|
78208
|
+
} else {
|
|
78209
|
+
merged.push(current);
|
|
78210
|
+
}
|
|
78211
|
+
}
|
|
78212
|
+
return merged;
|
|
78213
|
+
};
|
|
78214
|
+
|
|
78215
|
+
/**
|
|
78216
|
+
* createSearch — builds a matchFn compatible with useSearch that searches
|
|
78217
|
+
* across multiple named fields of an item, each with its own DOM selector
|
|
78218
|
+
* and optional priority weight.
|
|
78219
|
+
*
|
|
78220
|
+
* Usage:
|
|
78221
|
+
* ```js
|
|
78222
|
+
* const searchPerson = createSearch({
|
|
78223
|
+
* name: {
|
|
78224
|
+
* getter: (item) => item.name,
|
|
78225
|
+
* domSelector: ".name",
|
|
78226
|
+
* },
|
|
78227
|
+
* address: {
|
|
78228
|
+
* getter: (item) => item.address,
|
|
78229
|
+
* domSelector: ".address",
|
|
78230
|
+
* priority: 1.5,
|
|
78231
|
+
* },
|
|
78232
|
+
* });
|
|
78233
|
+
*
|
|
78234
|
+
* const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
|
|
78235
|
+
* // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
|
|
78236
|
+
* // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
|
|
78237
|
+
* // — ListItem handles the per-selector object format for matchRanges.
|
|
78238
|
+
* ```
|
|
78239
|
+
*
|
|
78240
|
+
* Each field config:
|
|
78241
|
+
* - getter(item): string — extracts the text to search
|
|
78242
|
+
* - domSelector: string — CSS selector used by ListItem to find the target element
|
|
78243
|
+
* - priority?: number — multiplier applied to the field's score (default 1)
|
|
78244
|
+
* - matchFn?: function — custom match function (searchText, fieldValue) => { match, matchScore, matchRanges }
|
|
78245
|
+
* defaults to applySearch
|
|
78246
|
+
*/
|
|
78247
|
+
const createSearch = (fields) => {
|
|
78248
|
+
return (searchText, item) => {
|
|
78249
|
+
if (!searchText) {
|
|
78250
|
+
return { match: true, matchScore: 0, matchRanges: {} };
|
|
78251
|
+
}
|
|
78252
|
+
let totalScore = 0;
|
|
78253
|
+
const matchRanges = {};
|
|
78254
|
+
for (const [
|
|
78255
|
+
,
|
|
78256
|
+
{ getter, domSelector, priority = 1, matchFn = applySearch },
|
|
78257
|
+
] of Object.entries(fields)) {
|
|
78258
|
+
const fieldValue = getter(item);
|
|
78259
|
+
const result = matchFn(searchText, fieldValue);
|
|
78260
|
+
if (result.match && result.matchRanges.length > 0) {
|
|
78261
|
+
totalScore += result.matchScore * priority;
|
|
78262
|
+
matchRanges[domSelector] = result.matchRanges;
|
|
78263
|
+
}
|
|
78264
|
+
}
|
|
78265
|
+
if (totalScore === 0) {
|
|
78266
|
+
return { match: false, matchScore: 0, matchRanges: {} };
|
|
78267
|
+
}
|
|
78268
|
+
return { match: true, matchScore: totalScore, matchRanges };
|
|
78269
|
+
};
|
|
78270
|
+
};
|
|
78271
|
+
|
|
78272
|
+
/**
|
|
78273
|
+
* useSearch — reorders items so matched ones come first (sorted by score desc),
|
|
78274
|
+
* followed by non-matched items in their natural order. No item is hidden.
|
|
78275
|
+
* Returns [orderedItems, getItemMatchInfo].
|
|
78276
|
+
* - orderedItems: all items, reordered
|
|
78277
|
+
* - getItemMatchInfo(item): { match, matchScore, matchRanges } — pass the
|
|
78278
|
+
* whole thing straight to <ListItem matchInfo={getItemMatchInfo(item)} />,
|
|
78279
|
+
* there is no need to destructure the three fields by hand.
|
|
78280
|
+
*
|
|
78281
|
+
* When searchText is empty, natural order is preserved and all items match with score 0.
|
|
78282
|
+
*
|
|
78283
|
+
* To filter (hide non-matching items), pass filtered={!getItemMatchInfo(item).match}
|
|
78284
|
+
* to each ListItem. The list's matchFallback will be shown when all items are hidden.
|
|
78285
|
+
*/
|
|
78286
|
+
const useSearchText = (searchText, items, matchFn = applySearch) => {
|
|
78287
|
+
if (typeof searchText !== "string" && searchText !== undefined) {
|
|
78288
|
+
throw new TypeError(
|
|
78289
|
+
"useSearchText: searchText must be a string or undefined",
|
|
78290
|
+
);
|
|
78291
|
+
}
|
|
78292
|
+
if (items === undefined) {
|
|
78293
|
+
throw new TypeError("useSearch: items is undefined");
|
|
78294
|
+
}
|
|
78295
|
+
const { orderedItems, matchInfoMap } = useMemo(() => {
|
|
78296
|
+
const { scoreEntries, nonMatched, matchInfoMap } = buildMatchInfo(
|
|
78297
|
+
searchText,
|
|
78298
|
+
items,
|
|
78299
|
+
matchFn,
|
|
78300
|
+
);
|
|
78301
|
+
const orderedItems = [];
|
|
78302
|
+
for (const [, bucket] of scoreEntries) {
|
|
78303
|
+
for (const { item } of bucket) {
|
|
78304
|
+
orderedItems.push(item);
|
|
78305
|
+
}
|
|
78306
|
+
}
|
|
78307
|
+
for (const { item } of nonMatched) {
|
|
78308
|
+
orderedItems.push(item);
|
|
78309
|
+
}
|
|
78310
|
+
return { orderedItems, matchInfoMap };
|
|
78311
|
+
}, [items, searchText, matchFn]);
|
|
78312
|
+
|
|
78313
|
+
// The same function for as long as the map is the same: a `renderItem`
|
|
78314
|
+
// reading it is stable only if this is, and a run keeps the rows it drew
|
|
78315
|
+
// only for a stable `renderItem` (see List.Items).
|
|
78316
|
+
const getItemMatchInfo = useCallback(
|
|
78317
|
+
(item) => matchInfoMap.get(item),
|
|
78318
|
+
[matchInfoMap],
|
|
78319
|
+
);
|
|
78320
|
+
|
|
78321
|
+
return [orderedItems, getItemMatchInfo];
|
|
78322
|
+
};
|
|
78323
|
+
|
|
78324
|
+
const buildMatchInfo = (searchText, items, matchFn) => {
|
|
78325
|
+
// scoreEntries: [score, bucket][] kept sorted desc by score.
|
|
78326
|
+
// New distinct score values are inserted via bisect — O(1) in practice
|
|
78327
|
+
// since there are very few distinct scores (today just 0 and 1).
|
|
78328
|
+
const scoreEntries = []; // [score, bucket][]
|
|
78329
|
+
const nonMatched = [];
|
|
78330
|
+
|
|
78331
|
+
for (const item of items) {
|
|
78332
|
+
const result = matchFn(searchText, item);
|
|
78333
|
+
if (!result.match) {
|
|
78334
|
+
nonMatched.push({
|
|
78335
|
+
item,
|
|
78336
|
+
matchScore: result.matchScore,
|
|
78337
|
+
matchRanges: result.matchRanges,
|
|
78338
|
+
});
|
|
78339
|
+
continue;
|
|
78340
|
+
}
|
|
78341
|
+
const score = result.matchScore;
|
|
78342
|
+
// Find existing bucket or insert a new entry in desc order.
|
|
78343
|
+
let lo = 0;
|
|
78344
|
+
let hi = scoreEntries.length;
|
|
78345
|
+
while (lo < hi) {
|
|
78346
|
+
const mid = (lo + hi) >> 1;
|
|
78347
|
+
if (scoreEntries[mid][0] > score) {
|
|
78348
|
+
lo = mid + 1;
|
|
78349
|
+
} else if (scoreEntries[mid][0] < score) {
|
|
78350
|
+
hi = mid;
|
|
78351
|
+
} else {
|
|
78352
|
+
lo = mid;
|
|
78353
|
+
hi = mid; // exact match — found the bucket
|
|
78354
|
+
}
|
|
78355
|
+
}
|
|
78356
|
+
if (lo < scoreEntries.length && scoreEntries[lo][0] === score) {
|
|
78357
|
+
scoreEntries[lo][1].push({ item, matchRanges: result.matchRanges });
|
|
78159
78358
|
} else {
|
|
78160
|
-
|
|
78359
|
+
scoreEntries.splice(lo, 0, [
|
|
78360
|
+
score,
|
|
78361
|
+
[{ item, matchRanges: result.matchRanges }],
|
|
78362
|
+
]);
|
|
78161
78363
|
}
|
|
78162
78364
|
}
|
|
78163
|
-
|
|
78365
|
+
|
|
78366
|
+
const matchInfoMap = new Map();
|
|
78367
|
+
for (const [score, bucket] of scoreEntries) {
|
|
78368
|
+
for (const { item, matchRanges } of bucket) {
|
|
78369
|
+
matchInfoMap.set(item, { match: true, matchScore: score, matchRanges });
|
|
78370
|
+
}
|
|
78371
|
+
}
|
|
78372
|
+
for (const { item, matchScore, matchRanges } of nonMatched) {
|
|
78373
|
+
matchInfoMap.set(item, { match: false, matchScore, matchRanges });
|
|
78374
|
+
}
|
|
78375
|
+
|
|
78376
|
+
return { scoreEntries, nonMatched, matchInfoMap };
|
|
78164
78377
|
};
|
|
78165
78378
|
|
|
78166
|
-
|
|
78167
|
-
*
|
|
78379
|
+
/*
|
|
78380
|
+
* useItemTracker() — hook that creates a stable item tracker for the lifetime
|
|
78381
|
+
* of the host component.
|
|
78168
78382
|
*
|
|
78169
|
-
*
|
|
78170
|
-
*
|
|
78171
|
-
*
|
|
78172
|
-
*
|
|
78383
|
+
* USAGE:
|
|
78384
|
+
* ```jsx
|
|
78385
|
+
* function ListControlled({ items }) {
|
|
78386
|
+
* const tracker = useItemTracker({
|
|
78387
|
+
* onChange: () => console.log("items changed"),
|
|
78388
|
+
* });
|
|
78173
78389
|
*
|
|
78174
|
-
*
|
|
78390
|
+
* return (
|
|
78391
|
+
* <ul>
|
|
78392
|
+
* {items.map((item, i) => (
|
|
78393
|
+
* <Row key={item.id} id={item.id} index={i} hidden={item.hidden} value={item.value} tracker={tracker} />
|
|
78394
|
+
* ))}
|
|
78395
|
+
* <Count tracker={tracker} />
|
|
78396
|
+
* </ul>
|
|
78397
|
+
* );
|
|
78398
|
+
* }
|
|
78175
78399
|
*
|
|
78176
|
-
*
|
|
78177
|
-
*
|
|
78178
|
-
*
|
|
78179
|
-
*
|
|
78180
|
-
*
|
|
78181
|
-
* phrase / words mid-word 0.5
|
|
78182
|
-
* + case-exact bonus +0.125
|
|
78183
|
-
* multi-word partial: score × (matched/total)
|
|
78400
|
+
* function Row({ id, index, hidden, value, tracker }) {
|
|
78401
|
+
* const visibleIndex = tracker.useTrackItem({ id, index, hidden, value });
|
|
78402
|
+
* if (visibleIndex === -1) return null;
|
|
78403
|
+
* return <li>{value}</li>;
|
|
78404
|
+
* }
|
|
78184
78405
|
*
|
|
78185
|
-
*
|
|
78186
|
-
*
|
|
78406
|
+
* function Count({ tracker }) {
|
|
78407
|
+
* const count = tracker.visibleCountSignal.value; // re-renders only when count changes
|
|
78408
|
+
* return <span>{count} items</span>;
|
|
78409
|
+
* }
|
|
78410
|
+
* ```
|
|
78411
|
+
*
|
|
78412
|
+
* INTERNALS:
|
|
78413
|
+
* - registrations: Map key → data, contains only visible items
|
|
78414
|
+
* - idToKey: Map id → key, stable across renders
|
|
78415
|
+
* - orderedKeys: number[] of visible item keys sorted by explicit order
|
|
78416
|
+
* - keyToOrderedIndex: Map key → orderedKeys index, gives O(1) indexOf equivalent
|
|
78417
|
+
* - keyToExplicitOrder: Map key → explicitly passed index, used to maintain sort order
|
|
78418
|
+
* - allItemsSignal: signal(array), all items including hidden, ordered by explicit index
|
|
78419
|
+
* - visibleItemsSignal: signal(array), non-hidden items only
|
|
78420
|
+
* - countSignal: signal(number), count of all items including hidden
|
|
78421
|
+
* - visibleCountSignal: signal(number), updated in microtask batch, only when count changes
|
|
78422
|
+
* - propSignals: Map propName → signal(array), updated in microtask batch with element equality
|
|
78423
|
+
* - onChangeRef: holds the latest onChange callback, called once per microtask batch
|
|
78424
|
+
*
|
|
78425
|
+
* useTrackItem(id, data, index): registers the item with an explicitly provided index
|
|
78426
|
+
* that determines its position among siblings. The caller (e.g. items.map) knows the
|
|
78427
|
+
* correct order and passes it directly — no render-sequence deduction needed.
|
|
78428
|
+
* Returns the visible rank (position among non-hidden items), or -1 when hidden.
|
|
78429
|
+
* Signals and onChange are deferred to a microtask so multiple items updating
|
|
78430
|
+
* in one commit cause only one notification.
|
|
78431
|
+
*
|
|
78432
|
+
* getTrackedItemByIndex(index): synchronous O(1) lookup of a visible item by
|
|
78433
|
+
* its visible rank. Returns undefined when index is out of range.
|
|
78434
|
+
*
|
|
78435
|
+
* peekItems(): the items as they stand right now, without waiting for the
|
|
78436
|
+
* deferred notification — what a sibling rendering after the items must read
|
|
78437
|
+
* to paint them in the same commit.
|
|
78187
78438
|
*/
|
|
78188
|
-
const applySearch = (searchText, value) => {
|
|
78189
|
-
if (!searchText) {
|
|
78190
|
-
return { match: true, matchScore: 0, matchRanges: [] };
|
|
78191
|
-
}
|
|
78192
|
-
if (searchText.length > 100) {
|
|
78193
|
-
searchText = searchText.slice(0, 100);
|
|
78194
|
-
}
|
|
78195
|
-
const str = String(value);
|
|
78196
|
-
const foldedStr = foldAccents(str).toLowerCase();
|
|
78197
|
-
const { foldedSearch, words, originalWords } = getSearchInfo(searchText);
|
|
78198
78439
|
|
|
78199
|
-
|
|
78200
|
-
const
|
|
78201
|
-
|
|
78202
|
-
|
|
78203
|
-
|
|
78204
|
-
|
|
78205
|
-
|
|
78206
|
-
|
|
78207
|
-
|
|
78208
|
-
const atWordBoundary = phraseRanges.some(([start]) =>
|
|
78209
|
-
isWordBoundary(foldedStr, start),
|
|
78210
|
-
);
|
|
78211
|
-
const caseExact = str.includes(searchText);
|
|
78212
|
-
let baseScore;
|
|
78213
|
-
if (atStart) {
|
|
78214
|
-
baseScore = SCORE_PHRASE_AT_START;
|
|
78215
|
-
} else if (atWordBoundary) {
|
|
78216
|
-
baseScore = SCORE_AT_WORD_BOUNDARY;
|
|
78217
|
-
} else {
|
|
78218
|
-
baseScore = SCORE_MID_WORD;
|
|
78219
|
-
}
|
|
78220
|
-
const matchScore = baseScore + (caseExact ? SCORE_BONUS_CASE_EXACT : 0);
|
|
78221
|
-
return { match: true, matchScore, matchRanges: mergeRanges(phraseRanges) };
|
|
78440
|
+
const useItemTracker = ({ onChange } = {}) => {
|
|
78441
|
+
const onChangeRef = useRef(onChange);
|
|
78442
|
+
onChangeRef.current = onChange;
|
|
78443
|
+
const trackerRef = useRef(null);
|
|
78444
|
+
let tracker = trackerRef.current;
|
|
78445
|
+
if (!tracker) {
|
|
78446
|
+
trackerRef.current = tracker = createItemTracker((items) => {
|
|
78447
|
+
onChangeRef.current?.(items);
|
|
78448
|
+
});
|
|
78222
78449
|
}
|
|
78450
|
+
// When code in useLayoutEffect of the caller wants to run the tracker must be in sync
|
|
78451
|
+
// without this layout effect the tracker might not have been synced yet and preact would call layout effect
|
|
78452
|
+
// before we had time to sync
|
|
78453
|
+
useLayoutEffect(() => {
|
|
78454
|
+
tracker._flushSync();
|
|
78455
|
+
});
|
|
78456
|
+
return tracker;
|
|
78457
|
+
};
|
|
78458
|
+
|
|
78459
|
+
const createItemTracker = (onChange) => {
|
|
78460
|
+
const registrations = new Map(); // key → data (visible items only)
|
|
78461
|
+
const idToKey = new Map(); // id → insertion key (stable, auto-incremented)
|
|
78462
|
+
let keyCounter = 0;
|
|
78463
|
+
// orderedKeys: visible item keys sorted by their explicitly provided index.
|
|
78464
|
+
const orderedKeys = []; // number[]
|
|
78465
|
+
// keyToOrderedIndex: O(1) equivalent of orderedKeys.indexOf(key).
|
|
78466
|
+
const keyToOrderedIndex = new Map(); // key → index in orderedKeys
|
|
78467
|
+
const allKeys = new Set(); // all registered keys including hidden
|
|
78468
|
+
const keyToExplicitOrder = new Map(); // key → explicitly passed index
|
|
78469
|
+
|
|
78470
|
+
const allRegistrations = new Map(); // key → data (all items including hidden)
|
|
78471
|
+
const allOrderedKeys = []; // all item keys sorted by explicit order
|
|
78472
|
+
const keyToAllOrderedIndex = new Map(); // key → index in allOrderedKeys
|
|
78473
|
+
|
|
78474
|
+
const itemsSignal = signal([]);
|
|
78475
|
+
const visibleItemsSignal = signal([]);
|
|
78476
|
+
const countSignal = signal(0);
|
|
78477
|
+
const visibleCountSignal = signal(0);
|
|
78478
|
+
const noMatchCountSignal = signal(0);
|
|
78479
|
+
|
|
78480
|
+
let notifyScheduled = false;
|
|
78481
|
+
const runNotify = () => {
|
|
78482
|
+
batch(() => {
|
|
78483
|
+
let someChange = false;
|
|
78484
|
+
|
|
78485
|
+
const newCount = allKeys.size;
|
|
78486
|
+
const countModified = countSignal.peek() !== newCount;
|
|
78487
|
+
if (countModified) {
|
|
78488
|
+
countSignal.value = newCount;
|
|
78489
|
+
someChange = true;
|
|
78490
|
+
}
|
|
78491
|
+
|
|
78492
|
+
// Build allItems and visibleItems in a single pass over allOrderedKeys.
|
|
78493
|
+
// Visible items are those without data.hidden or data.filtered — same
|
|
78494
|
+
// relative order as orderedKeys (syncItem already excludes both from
|
|
78495
|
+
// orderedKeys; this must match or consumers relying on visibleCountSignal
|
|
78496
|
+
// would count filtered-out items as if they still took up space).
|
|
78497
|
+
const prevAllItems = itemsSignal.peek();
|
|
78498
|
+
const prevVisibleItems = visibleItemsSignal.peek();
|
|
78499
|
+
let allItemsChanged = prevAllItems.length !== allOrderedKeys.length;
|
|
78500
|
+
let visibleItemsChanged = false;
|
|
78501
|
+
const allItems = [];
|
|
78502
|
+
const visibleItems = [];
|
|
78503
|
+
let newNoMatchCount = 0;
|
|
78504
|
+
for (let i = 0; i < allOrderedKeys.length; i++) {
|
|
78505
|
+
const key = allOrderedKeys[i];
|
|
78506
|
+
const item = allRegistrations.get(key);
|
|
78507
|
+
allItems.push(item);
|
|
78508
|
+
// Compare by reference: catches any prop change (id, selected, disabled, …)
|
|
78509
|
+
if (!allItemsChanged && item !== prevAllItems[i]) {
|
|
78510
|
+
allItemsChanged = true;
|
|
78511
|
+
}
|
|
78512
|
+
if (item.match === false) {
|
|
78513
|
+
newNoMatchCount++;
|
|
78514
|
+
}
|
|
78515
|
+
if (!item.hidden && !item.filtered) {
|
|
78516
|
+
const visibleIdx = visibleItems.length;
|
|
78517
|
+
visibleItems.push(item);
|
|
78518
|
+
if (!visibleItemsChanged && item !== prevVisibleItems[visibleIdx]) {
|
|
78519
|
+
visibleItemsChanged = true;
|
|
78520
|
+
}
|
|
78521
|
+
}
|
|
78522
|
+
}
|
|
78523
|
+
|
|
78524
|
+
const newVisibleCount = visibleItems.length;
|
|
78525
|
+
const visibleCountModified =
|
|
78526
|
+
visibleCountSignal.peek() !== newVisibleCount;
|
|
78527
|
+
if (visibleCountModified) {
|
|
78528
|
+
visibleCountSignal.value = newVisibleCount;
|
|
78529
|
+
someChange = true;
|
|
78530
|
+
}
|
|
78531
|
+
if (allItemsChanged) {
|
|
78532
|
+
itemsSignal.value = allItems;
|
|
78533
|
+
someChange = true;
|
|
78534
|
+
}
|
|
78535
|
+
if (visibleItemsChanged) {
|
|
78536
|
+
visibleItemsSignal.value = visibleItems;
|
|
78537
|
+
someChange = true;
|
|
78538
|
+
}
|
|
78539
|
+
const noMatchCountModified =
|
|
78540
|
+
noMatchCountSignal.peek() !== newNoMatchCount;
|
|
78541
|
+
if (noMatchCountModified) {
|
|
78542
|
+
noMatchCountSignal.value = newNoMatchCount;
|
|
78543
|
+
someChange = true;
|
|
78544
|
+
}
|
|
78545
|
+
if (someChange) {
|
|
78546
|
+
onChange?.();
|
|
78547
|
+
}
|
|
78548
|
+
});
|
|
78549
|
+
};
|
|
78223
78550
|
|
|
78224
|
-
|
|
78225
|
-
|
|
78226
|
-
|
|
78227
|
-
// foldedSearch.split filters empty strings). This path also handles the case
|
|
78228
|
-
// where searchText has trailing/leading spaces: the phrase match above tries
|
|
78229
|
-
// the literal (e.g. "tc " in "tc adapter"), and if that fails we fall through
|
|
78230
|
-
// here to try each word individually (e.g. "tc" matches "tca").
|
|
78231
|
-
const matchRanges = [];
|
|
78232
|
-
let matchedWordCount = 0;
|
|
78233
|
-
let anyWordAtStart = false;
|
|
78234
|
-
let anyWordAtWordBoundary = false;
|
|
78235
|
-
let allMatchedWordsExact = true;
|
|
78236
|
-
for (let w = 0; w < words.length; w++) {
|
|
78237
|
-
const word = words[w];
|
|
78238
|
-
const originalWord = originalWords[w];
|
|
78239
|
-
let idx = foldedStr.indexOf(word);
|
|
78240
|
-
if (idx === -1) {
|
|
78241
|
-
continue;
|
|
78551
|
+
const notify = () => {
|
|
78552
|
+
if (notifyScheduled) {
|
|
78553
|
+
return;
|
|
78242
78554
|
}
|
|
78243
|
-
|
|
78244
|
-
|
|
78245
|
-
|
|
78246
|
-
|
|
78247
|
-
if (idx === 0) {
|
|
78248
|
-
anyWordAtStart = true;
|
|
78249
|
-
anyWordAtWordBoundary = true;
|
|
78250
|
-
} else if (isWordBoundary(foldedStr, idx)) {
|
|
78251
|
-
anyWordAtWordBoundary = true;
|
|
78555
|
+
notifyScheduled = true;
|
|
78556
|
+
queueMicrotask(() => {
|
|
78557
|
+
if (!notifyScheduled) {
|
|
78558
|
+
return; // was already flushed synchronously
|
|
78252
78559
|
}
|
|
78253
|
-
|
|
78254
|
-
|
|
78560
|
+
notifyScheduled = false;
|
|
78561
|
+
runNotify();
|
|
78562
|
+
});
|
|
78563
|
+
};
|
|
78564
|
+
|
|
78565
|
+
const _flushSync = () => {
|
|
78566
|
+
if (!notifyScheduled) {
|
|
78567
|
+
return;
|
|
78568
|
+
}
|
|
78569
|
+
notifyScheduled = false;
|
|
78570
|
+
runNotify();
|
|
78571
|
+
};
|
|
78572
|
+
|
|
78573
|
+
// Insert key into orderedKeys at the correct position based on explicitOrder.
|
|
78574
|
+
// Uses binary search for O(log n) insertion.
|
|
78575
|
+
const insertKey = (key, explicitOrder) => {
|
|
78576
|
+
let lo = 0;
|
|
78577
|
+
let hi = orderedKeys.length;
|
|
78578
|
+
while (lo < hi) {
|
|
78579
|
+
const mid = (lo + hi) >> 1;
|
|
78580
|
+
if (keyToExplicitOrder.get(orderedKeys[mid]) <= explicitOrder) {
|
|
78581
|
+
lo = mid + 1;
|
|
78582
|
+
} else {
|
|
78583
|
+
hi = mid;
|
|
78255
78584
|
}
|
|
78256
|
-
idx = foldedStr.indexOf(word, idx + 1);
|
|
78257
78585
|
}
|
|
78258
|
-
|
|
78259
|
-
|
|
78586
|
+
orderedKeys.splice(lo, 0, key);
|
|
78587
|
+
for (let i = lo; i < orderedKeys.length; i++) {
|
|
78588
|
+
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
78260
78589
|
}
|
|
78261
|
-
}
|
|
78262
|
-
if (matchedWordCount === 0) {
|
|
78263
|
-
return tryAcronymMatch(foldedStr, str, searchText);
|
|
78264
|
-
}
|
|
78265
|
-
const wordRatio = matchedWordCount / words.length;
|
|
78266
|
-
let baseScore;
|
|
78267
|
-
if (anyWordAtStart) {
|
|
78268
|
-
baseScore = SCORE_MULTI_WORD_AT_START;
|
|
78269
|
-
} else if (anyWordAtWordBoundary) {
|
|
78270
|
-
baseScore = SCORE_AT_WORD_BOUNDARY;
|
|
78271
|
-
} else {
|
|
78272
|
-
baseScore = SCORE_MID_WORD;
|
|
78273
|
-
}
|
|
78274
|
-
const matchScore =
|
|
78275
|
-
(baseScore + (allMatchedWordsExact ? SCORE_BONUS_CASE_EXACT : 0)) *
|
|
78276
|
-
wordRatio;
|
|
78277
|
-
return { match: true, matchScore, matchRanges: mergeRanges(matchRanges) };
|
|
78278
|
-
};
|
|
78279
|
-
|
|
78280
|
-
// Returns true when position idx in str is at a word boundary,
|
|
78281
|
-
// meaning it is either the start of the string or the preceding character
|
|
78282
|
-
// is not a Unicode letter or digit.
|
|
78283
|
-
const isWordBoundary = (str, idx) => {
|
|
78284
|
-
if (idx === 0) {
|
|
78285
|
-
return true;
|
|
78286
|
-
}
|
|
78287
|
-
return !/[\p{L}\p{N}]/u.test(str[idx - 1]);
|
|
78288
|
-
};
|
|
78289
|
-
|
|
78290
|
-
// Strip diacritics for accent-insensitive matching.
|
|
78291
|
-
// NFC normalization first ensures precomposed characters (é → single code unit),
|
|
78292
|
-
// so the folded string has the same length as the NFC source — ranges computed
|
|
78293
|
-
// on the folded string map 1:1 to positions in the original string.
|
|
78294
|
-
const foldAccents = (str) => {
|
|
78295
|
-
return str
|
|
78296
|
-
.normalize("NFC")
|
|
78297
|
-
.normalize("NFD")
|
|
78298
|
-
.replace(/\p{Mn}/gu, "");
|
|
78299
|
-
};
|
|
78300
|
-
|
|
78301
|
-
const SCORE_PHRASE_AT_START = 1;
|
|
78302
|
-
const SCORE_MULTI_WORD_AT_START = 0.75;
|
|
78303
|
-
const SCORE_AT_WORD_BOUNDARY = 0.625;
|
|
78304
|
-
const SCORE_MID_WORD = 0.5;
|
|
78305
|
-
const SCORE_ACRONYM = 0.4;
|
|
78306
|
-
const SCORE_BONUS_CASE_EXACT = 0.125;
|
|
78590
|
+
};
|
|
78307
78591
|
|
|
78308
|
-
|
|
78309
|
-
|
|
78310
|
-
|
|
78311
|
-
|
|
78312
|
-
|
|
78313
|
-
|
|
78314
|
-
|
|
78315
|
-
|
|
78316
|
-
|
|
78317
|
-
const wordStarts = [];
|
|
78318
|
-
for (let i = 0; i < foldedStr.length; i++) {
|
|
78319
|
-
if (isWordBoundary(foldedStr, i)) {
|
|
78320
|
-
wordStarts.push(i);
|
|
78321
|
-
}
|
|
78322
|
-
}
|
|
78323
|
-
const matchedPositions = [];
|
|
78324
|
-
let wordIdx = 0;
|
|
78325
|
-
const originalAcronym = searchText.replace(/\s/g, "");
|
|
78326
|
-
for (let si = 0; si < acronymChars.length; si++) {
|
|
78327
|
-
const ch = acronymChars[si];
|
|
78328
|
-
let found = false;
|
|
78329
|
-
while (wordIdx < wordStarts.length) {
|
|
78330
|
-
const pos = wordStarts[wordIdx];
|
|
78331
|
-
wordIdx++;
|
|
78332
|
-
if (foldedStr[pos] === ch) {
|
|
78333
|
-
matchedPositions.push(pos);
|
|
78334
|
-
found = true;
|
|
78335
|
-
break;
|
|
78592
|
+
const insertAllKey = (key, explicitOrder) => {
|
|
78593
|
+
let lo = 0;
|
|
78594
|
+
let hi = allOrderedKeys.length;
|
|
78595
|
+
while (lo < hi) {
|
|
78596
|
+
const mid = (lo + hi) >> 1;
|
|
78597
|
+
if (keyToExplicitOrder.get(allOrderedKeys[mid]) <= explicitOrder) {
|
|
78598
|
+
lo = mid + 1;
|
|
78599
|
+
} else {
|
|
78600
|
+
hi = mid;
|
|
78336
78601
|
}
|
|
78337
78602
|
}
|
|
78338
|
-
|
|
78339
|
-
|
|
78603
|
+
allOrderedKeys.splice(lo, 0, key);
|
|
78604
|
+
for (let i = lo; i < allOrderedKeys.length; i++) {
|
|
78605
|
+
keyToAllOrderedIndex.set(allOrderedKeys[i], i);
|
|
78340
78606
|
}
|
|
78341
|
-
}
|
|
78342
|
-
const atStart = matchedPositions[0] === 0;
|
|
78343
|
-
const caseExact = matchedPositions.every(
|
|
78344
|
-
(p, i) => str[p] === originalAcronym[i],
|
|
78345
|
-
);
|
|
78346
|
-
const baseScore = atStart ? SCORE_ACRONYM + 0.05 : SCORE_ACRONYM;
|
|
78347
|
-
const matchScore = baseScore + (caseExact ? SCORE_BONUS_CASE_EXACT : 0);
|
|
78348
|
-
const matchRanges = matchedPositions.map((p) => [p, p + 1]);
|
|
78349
|
-
return { match: true, matchScore, matchRanges };
|
|
78350
|
-
};
|
|
78607
|
+
};
|
|
78351
78608
|
|
|
78352
|
-
|
|
78353
|
-
|
|
78354
|
-
|
|
78355
|
-
|
|
78356
|
-
|
|
78357
|
-
|
|
78358
|
-
|
|
78359
|
-
|
|
78360
|
-
|
|
78361
|
-
|
|
78362
|
-
}
|
|
78363
|
-
const foldedSearch = foldAccents(searchText).toLowerCase();
|
|
78364
|
-
const words = foldedSearch.split(/\s+/).filter(Boolean);
|
|
78365
|
-
const originalWords = searchText.split(/\s+/).filter(Boolean);
|
|
78366
|
-
const info = { foldedSearch, words, originalWords };
|
|
78367
|
-
searchCache.set(searchText, info);
|
|
78368
|
-
if (searchCache.size > SEARCH_CACHE_MAX_SIZE) {
|
|
78369
|
-
searchCache.delete(searchCache.keys().next().value);
|
|
78370
|
-
}
|
|
78371
|
-
return info;
|
|
78372
|
-
};
|
|
78609
|
+
const removeAllKey = (key) => {
|
|
78610
|
+
const idx = keyToAllOrderedIndex.get(key);
|
|
78611
|
+
if (idx !== undefined) {
|
|
78612
|
+
allOrderedKeys.splice(idx, 1);
|
|
78613
|
+
keyToAllOrderedIndex.delete(key);
|
|
78614
|
+
for (let i = idx; i < allOrderedKeys.length; i++) {
|
|
78615
|
+
keyToAllOrderedIndex.set(allOrderedKeys[i], i);
|
|
78616
|
+
}
|
|
78617
|
+
}
|
|
78618
|
+
};
|
|
78373
78619
|
|
|
78374
|
-
//
|
|
78375
|
-
|
|
78376
|
-
|
|
78377
|
-
|
|
78378
|
-
|
|
78379
|
-
|
|
78380
|
-
|
|
78381
|
-
|
|
78382
|
-
|
|
78383
|
-
|
|
78384
|
-
|
|
78385
|
-
|
|
78386
|
-
|
|
78620
|
+
// Register or update an item. data.hidden controls visibility.
|
|
78621
|
+
// explicitOrder is the caller-provided index that determines sort position.
|
|
78622
|
+
const syncItem = (key, index, data) => {
|
|
78623
|
+
if (data.role === "presentation") {
|
|
78624
|
+
registrations.delete(key);
|
|
78625
|
+
const idx = keyToOrderedIndex.get(key);
|
|
78626
|
+
if (idx !== undefined) {
|
|
78627
|
+
orderedKeys.splice(idx, 1);
|
|
78628
|
+
keyToOrderedIndex.delete(key);
|
|
78629
|
+
for (let i = idx; i < orderedKeys.length; i++) {
|
|
78630
|
+
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
78631
|
+
}
|
|
78632
|
+
}
|
|
78633
|
+
keyToExplicitOrder.delete(key);
|
|
78634
|
+
allRegistrations.delete(key);
|
|
78635
|
+
removeAllKey(key);
|
|
78636
|
+
allKeys.delete(key);
|
|
78637
|
+
return;
|
|
78638
|
+
}
|
|
78639
|
+
|
|
78640
|
+
// Maintain allRegistrations and allOrderedKeys for all non-presentation items.
|
|
78641
|
+
allRegistrations.set(key, data);
|
|
78642
|
+
allKeys.add(key);
|
|
78643
|
+
const currentAllIdx = keyToAllOrderedIndex.get(key);
|
|
78644
|
+
const previousOrder = keyToExplicitOrder.get(key);
|
|
78645
|
+
keyToExplicitOrder.set(key, index);
|
|
78646
|
+
if (currentAllIdx === undefined) {
|
|
78647
|
+
insertAllKey(key, index);
|
|
78648
|
+
} else if (previousOrder !== index) {
|
|
78649
|
+
allOrderedKeys.splice(currentAllIdx, 1);
|
|
78650
|
+
keyToAllOrderedIndex.delete(key);
|
|
78651
|
+
for (let i = currentAllIdx; i < allOrderedKeys.length; i++) {
|
|
78652
|
+
keyToAllOrderedIndex.set(allOrderedKeys[i], i);
|
|
78387
78653
|
}
|
|
78388
|
-
|
|
78389
|
-
merged.push(current);
|
|
78654
|
+
insertAllKey(key, index);
|
|
78390
78655
|
}
|
|
78391
|
-
}
|
|
78392
|
-
return merged;
|
|
78393
|
-
};
|
|
78394
78656
|
|
|
78395
|
-
|
|
78396
|
-
|
|
78397
|
-
|
|
78398
|
-
|
|
78399
|
-
|
|
78400
|
-
|
|
78401
|
-
|
|
78402
|
-
|
|
78403
|
-
|
|
78404
|
-
* getter: (item) => item.name,
|
|
78405
|
-
* domSelector: ".name",
|
|
78406
|
-
* },
|
|
78407
|
-
* address: {
|
|
78408
|
-
* getter: (item) => item.address,
|
|
78409
|
-
* domSelector: ".address",
|
|
78410
|
-
* priority: 1.5,
|
|
78411
|
-
* },
|
|
78412
|
-
* });
|
|
78413
|
-
*
|
|
78414
|
-
* const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
|
|
78415
|
-
* // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
|
|
78416
|
-
* // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
|
|
78417
|
-
* // — ListItem handles the per-selector object format for matchRanges.
|
|
78418
|
-
* ```
|
|
78419
|
-
*
|
|
78420
|
-
* Each field config:
|
|
78421
|
-
* - getter(item): string — extracts the text to search
|
|
78422
|
-
* - domSelector: string — CSS selector used by ListItem to find the target element
|
|
78423
|
-
* - priority?: number — multiplier applied to the field's score (default 1)
|
|
78424
|
-
* - matchFn?: function — custom match function (searchText, fieldValue) => { match, matchScore, matchRanges }
|
|
78425
|
-
* defaults to applySearch
|
|
78426
|
-
*/
|
|
78427
|
-
const createSearch = (fields) => {
|
|
78428
|
-
return (searchText, item) => {
|
|
78429
|
-
if (!searchText) {
|
|
78430
|
-
return { match: true, matchScore: 0, matchRanges: {} };
|
|
78431
|
-
}
|
|
78432
|
-
let totalScore = 0;
|
|
78433
|
-
const matchRanges = {};
|
|
78434
|
-
for (const [
|
|
78435
|
-
,
|
|
78436
|
-
{ getter, domSelector, priority = 1, matchFn = applySearch },
|
|
78437
|
-
] of Object.entries(fields)) {
|
|
78438
|
-
const fieldValue = getter(item);
|
|
78439
|
-
const result = matchFn(searchText, fieldValue);
|
|
78440
|
-
if (result.match && result.matchRanges.length > 0) {
|
|
78441
|
-
totalScore += result.matchScore * priority;
|
|
78442
|
-
matchRanges[domSelector] = result.matchRanges;
|
|
78657
|
+
if (data.filtered || data.hidden) {
|
|
78658
|
+
registrations.delete(key);
|
|
78659
|
+
const idx = keyToOrderedIndex.get(key);
|
|
78660
|
+
if (idx !== undefined) {
|
|
78661
|
+
orderedKeys.splice(idx, 1);
|
|
78662
|
+
keyToOrderedIndex.delete(key);
|
|
78663
|
+
for (let i = idx; i < orderedKeys.length; i++) {
|
|
78664
|
+
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
78665
|
+
}
|
|
78443
78666
|
}
|
|
78667
|
+
return;
|
|
78444
78668
|
}
|
|
78445
|
-
|
|
78446
|
-
|
|
78669
|
+
|
|
78670
|
+
registrations.set(key, data);
|
|
78671
|
+
const currentIdx = keyToOrderedIndex.get(key);
|
|
78672
|
+
if (currentIdx === undefined) {
|
|
78673
|
+
insertKey(key, index);
|
|
78674
|
+
return;
|
|
78447
78675
|
}
|
|
78448
|
-
|
|
78676
|
+
if (previousOrder === index) {
|
|
78677
|
+
return;
|
|
78678
|
+
}
|
|
78679
|
+
orderedKeys.splice(currentIdx, 1);
|
|
78680
|
+
keyToOrderedIndex.delete(key);
|
|
78681
|
+
for (let i = currentIdx; i < orderedKeys.length; i++) {
|
|
78682
|
+
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
78683
|
+
}
|
|
78684
|
+
insertKey(key, index);
|
|
78449
78685
|
};
|
|
78450
|
-
};
|
|
78451
78686
|
|
|
78452
|
-
|
|
78453
|
-
|
|
78454
|
-
|
|
78455
|
-
|
|
78456
|
-
|
|
78457
|
-
|
|
78458
|
-
|
|
78459
|
-
|
|
78460
|
-
*
|
|
78461
|
-
* When searchText is empty, natural order is preserved and all items match with score 0.
|
|
78462
|
-
*
|
|
78463
|
-
* To filter (hide non-matching items), pass filtered={!getItemMatchInfo(item).match}
|
|
78464
|
-
* to each ListItem. The list's matchFallback will be shown when all items are hidden.
|
|
78465
|
-
*/
|
|
78466
|
-
const useSearchText = (searchText, items, matchFn = applySearch) => {
|
|
78467
|
-
if (typeof searchText !== "string" && searchText !== undefined) {
|
|
78468
|
-
throw new TypeError(
|
|
78469
|
-
"useSearchText: searchText must be a string or undefined",
|
|
78470
|
-
);
|
|
78471
|
-
}
|
|
78472
|
-
if (items === undefined) {
|
|
78473
|
-
throw new TypeError("useSearch: items is undefined");
|
|
78474
|
-
}
|
|
78475
|
-
const { orderedItems, matchInfoMap } = useMemo(() => {
|
|
78476
|
-
const { scoreEntries, nonMatched, matchInfoMap } = buildMatchInfo(
|
|
78477
|
-
searchText,
|
|
78478
|
-
items,
|
|
78479
|
-
matchFn,
|
|
78480
|
-
);
|
|
78481
|
-
const orderedItems = [];
|
|
78482
|
-
for (const [, bucket] of scoreEntries) {
|
|
78483
|
-
for (const { item } of bucket) {
|
|
78484
|
-
orderedItems.push(item);
|
|
78687
|
+
const unregisterKey = (key) => {
|
|
78688
|
+
registrations.delete(key);
|
|
78689
|
+
const idx = keyToOrderedIndex.get(key);
|
|
78690
|
+
if (idx !== undefined) {
|
|
78691
|
+
orderedKeys.splice(idx, 1);
|
|
78692
|
+
keyToOrderedIndex.delete(key);
|
|
78693
|
+
for (let i = idx; i < orderedKeys.length; i++) {
|
|
78694
|
+
keyToOrderedIndex.set(orderedKeys[i], i);
|
|
78485
78695
|
}
|
|
78486
78696
|
}
|
|
78487
|
-
|
|
78488
|
-
|
|
78697
|
+
keyToExplicitOrder.delete(key);
|
|
78698
|
+
allRegistrations.delete(key);
|
|
78699
|
+
removeAllKey(key);
|
|
78700
|
+
allKeys.delete(key);
|
|
78701
|
+
};
|
|
78702
|
+
|
|
78703
|
+
const keyForId = (id) => {
|
|
78704
|
+
if (!idToKey.has(id)) {
|
|
78705
|
+
idToKey.set(id, keyCounter++);
|
|
78489
78706
|
}
|
|
78490
|
-
return
|
|
78491
|
-
}
|
|
78707
|
+
return idToKey.get(id);
|
|
78708
|
+
};
|
|
78492
78709
|
|
|
78493
|
-
//
|
|
78494
|
-
//
|
|
78495
|
-
//
|
|
78496
|
-
|
|
78497
|
-
|
|
78498
|
-
|
|
78499
|
-
|
|
78710
|
+
// Register an item. data.hidden controls visibility.
|
|
78711
|
+
// explicitOrder is the caller-provided index (e.g. from items.map((item, i) => ...))
|
|
78712
|
+
// that determines this item's position among siblings.
|
|
78713
|
+
// Returns the item's visible rank among non-hidden items, or -1 when hidden.
|
|
78714
|
+
const useTrackItem = (data) => {
|
|
78715
|
+
const { id, index } = data;
|
|
78716
|
+
const key = keyForId(id);
|
|
78500
78717
|
|
|
78501
|
-
|
|
78502
|
-
|
|
78718
|
+
syncItem(key, index, data);
|
|
78719
|
+
notify();
|
|
78503
78720
|
|
|
78504
|
-
|
|
78505
|
-
|
|
78506
|
-
|
|
78507
|
-
|
|
78508
|
-
|
|
78509
|
-
|
|
78721
|
+
useLayoutEffect(() => {
|
|
78722
|
+
return () => {
|
|
78723
|
+
unregisterKey(key);
|
|
78724
|
+
notify();
|
|
78725
|
+
};
|
|
78726
|
+
}, []);
|
|
78510
78727
|
|
|
78511
|
-
|
|
78512
|
-
|
|
78513
|
-
if (!result.match) {
|
|
78514
|
-
nonMatched.push({
|
|
78515
|
-
item,
|
|
78516
|
-
matchScore: result.matchScore,
|
|
78517
|
-
matchRanges: result.matchRanges,
|
|
78518
|
-
});
|
|
78519
|
-
continue;
|
|
78520
|
-
}
|
|
78521
|
-
const score = result.matchScore;
|
|
78522
|
-
// Find existing bucket or insert a new entry in desc order.
|
|
78523
|
-
let lo = 0;
|
|
78524
|
-
let hi = scoreEntries.length;
|
|
78525
|
-
while (lo < hi) {
|
|
78526
|
-
const mid = (lo + hi) >> 1;
|
|
78527
|
-
if (scoreEntries[mid][0] > score) {
|
|
78528
|
-
lo = mid + 1;
|
|
78529
|
-
} else if (scoreEntries[mid][0] < score) {
|
|
78530
|
-
hi = mid;
|
|
78531
|
-
} else {
|
|
78532
|
-
lo = mid;
|
|
78533
|
-
hi = mid; // exact match — found the bucket
|
|
78534
|
-
}
|
|
78728
|
+
if (data.filtered || data.hidden || data.role === "presentation") {
|
|
78729
|
+
return -1;
|
|
78535
78730
|
}
|
|
78536
|
-
|
|
78537
|
-
|
|
78538
|
-
|
|
78539
|
-
|
|
78540
|
-
|
|
78541
|
-
|
|
78542
|
-
|
|
78731
|
+
return keyToOrderedIndex.get(key) ?? -1;
|
|
78732
|
+
};
|
|
78733
|
+
|
|
78734
|
+
const getTrackedItemByIndex = (index) => {
|
|
78735
|
+
const key = orderedKeys[index];
|
|
78736
|
+
if (key === undefined) {
|
|
78737
|
+
return undefined;
|
|
78543
78738
|
}
|
|
78544
|
-
|
|
78739
|
+
return registrations.get(key);
|
|
78740
|
+
};
|
|
78545
78741
|
|
|
78546
|
-
|
|
78547
|
-
|
|
78548
|
-
|
|
78549
|
-
|
|
78742
|
+
// The items as they stand right now, notification pending or not — same
|
|
78743
|
+
// content as itemsSignal, minus the wait.
|
|
78744
|
+
//
|
|
78745
|
+
// Items register during their own render, while the signal is only updated
|
|
78746
|
+
// on a deferred microtask (see notify): a sibling rendering after them would
|
|
78747
|
+
// otherwise paint from an empty list and correct itself a frame later. That
|
|
78748
|
+
// frame is visible whenever the painted size feeds a layout decision — a
|
|
78749
|
+
// dialog sizing itself on its content measures the empty version and shifts
|
|
78750
|
+
// once the real one lands. Reading this instead makes the first paint the
|
|
78751
|
+
// right one. Callers must still subscribe to itemsSignal to re-render on
|
|
78752
|
+
// LATER changes; this is the value to display, not the notification.
|
|
78753
|
+
const peekItems = () => {
|
|
78754
|
+
if (!notifyScheduled) {
|
|
78755
|
+
return itemsSignal.peek();
|
|
78550
78756
|
}
|
|
78551
|
-
|
|
78552
|
-
|
|
78553
|
-
|
|
78554
|
-
|
|
78757
|
+
const items = [];
|
|
78758
|
+
for (const key of allOrderedKeys) {
|
|
78759
|
+
items.push(allRegistrations.get(key));
|
|
78760
|
+
}
|
|
78761
|
+
return items;
|
|
78762
|
+
};
|
|
78555
78763
|
|
|
78556
|
-
return {
|
|
78764
|
+
return {
|
|
78765
|
+
useTrackItem,
|
|
78766
|
+
getTrackedItemByIndex,
|
|
78767
|
+
peekItems,
|
|
78768
|
+
itemsSignal,
|
|
78769
|
+
visibleItemsSignal,
|
|
78770
|
+
countSignal,
|
|
78771
|
+
visibleCountSignal,
|
|
78772
|
+
noMatchCountSignal,
|
|
78773
|
+
_flushSync,
|
|
78774
|
+
};
|
|
78557
78775
|
};
|
|
78558
78776
|
|
|
78559
78777
|
installImportMetaCssBuild(import.meta);
|