@jsenv/navi 0.29.427 → 0.29.429

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.
@@ -26867,10 +26867,11 @@ const createRangeReader = (
26867
26867
  });
26868
26868
  return { byIndex, count: composition.count };
26869
26869
  };
26870
- // `replace` is a revalidation: the ranks that are not in what just came back
26871
- // stood for a composition that has moved on. Otherwise the ranks are merged,
26872
- // so two lists reading the same collection through their own windows add up
26873
- // to one composition instead of taking turns erasing each other.
26870
+ // `replace` says the list dropped ranks it held (an item the collection moved
26871
+ // elsewhere, ranks past its end): kept here, they would come back at the next
26872
+ // mount. Otherwise the ranks are merged, so two lists reading the same
26873
+ // collection through their own windows add up to one composition instead of
26874
+ // taking turns erasing each other.
26874
26875
  readRange.writeComposition = ({ byIndex, count, replace }) => {
26875
26876
  const params = currentParams();
26876
26877
  let composition = findComposition(params);
@@ -39813,6 +39814,16 @@ const resolveAfterSend = (target, requester) => {
39813
39814
  }
39814
39815
  return undefined;
39815
39816
  };
39817
+ // What follows a send is caused by the send, not by the gesture: the send
39818
+ // answered the gesture and may have prevented it (an Enter must not also click
39819
+ // the submit button, nor activate the trigger of the popup it closed), and a
39820
+ // follow-up judged on that event is refused as a gesture someone cancelled.
39821
+ // Chained, so the gesture stays readable behind it.
39822
+ const createSentEvent = (event) => {
39823
+ const sentEvent = new CustomEvent("sent", { detail: {} });
39824
+ chainEvent(sentEvent, event);
39825
+ return sentEvent;
39826
+ };
39816
39827
 
39817
39828
  registerNaviCommand("--navi-send", (source, event, { requester }) => {
39818
39829
  const expandable = resolveExpandableAround(source);
@@ -39845,7 +39856,9 @@ registerNaviCommand("--navi-send", (source, event, { requester }) => {
39845
39856
  source.getAttribute("data-after-send") ||
39846
39857
  target.getAttribute("data-after-send");
39847
39858
  if (afterSend) {
39848
- triggerNaviCommand(target, afterSend, event, { optional: true });
39859
+ triggerNaviCommand(target, afterSend, createSentEvent(event), {
39860
+ optional: true,
39861
+ });
39849
39862
  }
39850
39863
  return result;
39851
39864
  },
@@ -39889,7 +39902,9 @@ registerNaviCommand("--navi-send", (source, event, { requester }) => {
39889
39902
  if (!afterSend) {
39890
39903
  return;
39891
39904
  }
39892
- triggerNaviCommand(source, afterSend, event, { optional: true });
39905
+ triggerNaviCommand(source, afterSend, createSentEvent(event), {
39906
+ optional: true,
39907
+ });
39893
39908
  };
39894
39909
  const {
39895
39910
  result: sent,
@@ -74110,9 +74125,12 @@ const useListScrollSync = ({
74110
74125
  };
74111
74126
  const [renderWindow, setRenderWindow] = useState(() => {
74112
74127
  // Opening somewhere else than the beginning starts by framing there: the
74113
- // items the list will draw are the items it will ask for.
74128
+ // items the list will draw are the items it will ask for. A named item is
74129
+ // framed where it stood when it was written down, which is where holdWindow
74130
+ // aims before anyone can say where it is.
74114
74131
  const openAt = scrolled ?? defaultScrolled;
74115
- const start = typeof openAt === "number" ? openAt - itemsAboveOpening(openAt, firstWindowItemCount) : 0;
74132
+ const openIndex = typeof openAt === "number" ? openAt : openAt && typeof openAt === "object" && typeof openAt.index === "number" ? openAt.index : null;
74133
+ const start = openIndex === null ? 0 : openIndex - itemsAboveOpening(openAt, firstWindowItemCount);
74116
74134
  const startClamped = start < 0 ? 0 : start;
74117
74135
  return {
74118
74136
  start: startClamped,
@@ -74239,14 +74257,11 @@ const useListScrollSync = ({
74239
74257
  if (wantedStart < 0) {
74240
74258
  wantedStart = 0;
74241
74259
  }
74242
- if (wantedStart === start) {
74243
- listItems.holdPending = false;
74244
- return;
74245
- }
74246
- renderWindowRef.current = {
74247
- start: wantedStart,
74248
- end: wantedStart + windowSize
74249
- };
74260
+ // The window the hold frames goes into the state, not into this render
74261
+ // alone: the next render starts from the state, and once the item stands
74262
+ // inside the window the hold stops framing it — a window left where the
74263
+ // state had it would draw around another place.
74264
+ updateRenderWindow(wantedStart, wantedStart + windowSize, `held on item ${heldItem}`);
74250
74265
  listItems.holdPending = false;
74251
74266
  };
74252
74267
  const pendingScrollRef = useRef();
@@ -74395,6 +74410,13 @@ const useListScrollSync = ({
74395
74410
  return;
74396
74411
  }
74397
74412
  hasBeenDisplayedRef.current = true;
74413
+ const place = startPlaceRef.current;
74414
+ if (!place.userTookOver && place.wanted !== "start" && place.wanted !== undefined) {
74415
+ // Held somewhere: where the list opens is the hold's to say (see
74416
+ // placeWhereHeld). Scrolled here too, the list would go to the top or
74417
+ // the selected item for as long as the hold takes to put it back.
74418
+ return;
74419
+ }
74398
74420
  const items = listItems.itemsSignal.peek();
74399
74421
  const firstSelected = items.find(i => {
74400
74422
  if (i.selected) {
@@ -75097,12 +75119,14 @@ const useListScrollSync = ({
75097
75119
  const spare = budget > screen ? budget - screen : 0;
75098
75120
  const behindSize = countsItems ? Math.floor(spare / 4) : spare / 4;
75099
75121
  const aheadSize = spare - behindSize;
75100
- if (stage === "steady" && !budgetWarnedRef.current && bandEnd > bandStart) {
75101
- const twoLines = (countsItems ? 2 : 2 * (bandTo - bandFrom) / (bandEnd - bandStart)) * itemsPerLineNow;
75102
- if (spare < twoLines) {
75103
- budgetWarnedRef.current = true;
75104
- console.warn(`List: renderBudget=${renderBudget.value}${renderBudget.unit} leaves less than two ${itemsPerLineNow > 1 ? "lines of items" : "items"} beyond what the screen shows (${bandEnd - bandStart} items): items will go blank as it scrolls. Give it room for a screen ahead or more — "300%" of the box that scrolls it, say.`);
75105
- }
75122
+ // Said only when the budget holds nothing past the screen: the blank is
75123
+ // then certain. Past the screen, whether a fling outruns the window is
75124
+ // what the device and the rest of the main thread's work decide, and a
75125
+ // number of items cannot tell it — two one-line items are 76 px, two
75126
+ // cards a whole screen. That is measured on the list, not guessed here.
75127
+ if (stage === "steady" && !budgetWarnedRef.current && bandEnd > bandStart && spare === 0) {
75128
+ budgetWarnedRef.current = true;
75129
+ console.warn(`List: renderBudget=${renderBudget.value}${renderBudget.unit} holds no more than what the screen shows (${bandEnd - bandStart} items on it): the screen goes blank past it as soon as the list scrolls. Give it more than a screen — "150%" of the box that scrolls it, say.`);
75106
75130
  }
75107
75131
  if (!force) {
75108
75132
  const roomAround = readRoomAround({
@@ -77673,19 +77697,57 @@ const useRunItems = (store, {
77673
77697
  // rebuilt as the window slides — a group holds the items of its day that are
77674
77698
  // currently drawn, which is exactly the span its label has to survive.
77675
77699
  let group = null;
77700
+ // Two groups can still share a key (a failure band standing mid-group, data
77701
+ // not sorted by group): the later ones are told apart by their rank among
77702
+ // them, so the first keeps its element. A group keyed by where it starts
77703
+ // would lose its element each time the window slides past its first item.
77704
+ const groupCountByKey = new Map();
77676
77705
  const closeGroup = () => {
77677
77706
  if (!group) {
77678
77707
  return;
77679
77708
  }
77709
+ const groupCount = (groupCountByKey.get(group.key) || 0) + 1;
77710
+ groupCountByKey.set(group.key, groupCount);
77680
77711
  nodes.push(jsx(ListItemGroup, {
77681
77712
  label: group.label,
77682
77713
  labelProps: group.labelProps,
77683
77714
  children: group.children
77684
- }, `${ownerId}_group_${group.key}`));
77715
+ }, groupCount === 1 ? `${ownerId}_group_${group.key}` : `${ownerId}_group_${group.key}_${groupCount}`));
77685
77716
  group = null;
77686
77717
  };
77718
+ // An item not held has no group of its own. Between two items of the open
77719
+ // group it is a line of that group whose content has not arrived: drawn
77720
+ // outside, it would cut the group in two and show its label twice. A hole at
77721
+ // a group's edge stays outside — nothing says which side it belongs to.
77722
+ let holeTo = -1;
77723
+ let holeGroupKey;
77724
+ const groupKeyOfHole = itemIndex => {
77725
+ if (itemIndex < holeTo) {
77726
+ return holeGroupKey;
77727
+ }
77728
+ holeTo = itemIndex + 1;
77729
+ while (holeTo < windowTo && holeTo !== failureFrom && getItemAt(holeTo) === undefined) {
77730
+ holeTo++;
77731
+ }
77732
+ holeGroupKey = undefined;
77733
+ if (group && holeTo < windowTo && holeTo !== failureFrom) {
77734
+ const keyAfterHole = groupBy(getItemAt(holeTo), holeTo);
77735
+ if (keyAfterHole === group.key) {
77736
+ holeGroupKey = keyAfterHole;
77737
+ }
77738
+ }
77739
+ return holeGroupKey;
77740
+ };
77687
77741
  // Which group an item belongs to, or undefined when it belongs to none.
77688
- const groupKeyOf = (item, itemIndex) => groupBy && item !== undefined ? groupBy(item, itemIndex) : undefined;
77742
+ const groupKeyOf = (item, itemIndex) => {
77743
+ if (!groupBy) {
77744
+ return undefined;
77745
+ }
77746
+ if (item === undefined) {
77747
+ return groupKeyOfHole(itemIndex);
77748
+ }
77749
+ return groupBy(item, itemIndex);
77750
+ };
77689
77751
  const pushItem = (itemNode, item, itemIndex, groupKey) => {
77690
77752
  if (groupKey === undefined) {
77691
77753
  closeGroup();
@@ -78022,10 +78084,15 @@ const useItemStore = ({
78022
78084
  }
78023
78085
  const pages = pagesRef.current;
78024
78086
  const [, setPageVersion] = useState(0);
78025
- // The items held are out of date and the run has not asked for the new ones
78026
- // yet. They stay on screen until the answer comes: what is drawn is from
78027
- // before, which is not the same thing as nothing to draw.
78028
- const staleRef = useRef(restored);
78087
+ // The ranks whose item is from before: the run came back to the screen, or
78088
+ // the source said the collection moved. They stay drawn until a page confirms
78089
+ // or replaces them, rank by rank. A page covers what the window framed when it
78090
+ // was asked for, and the window keeps growing meanwhile: dropping what the
78091
+ // page did not cover empties items on screen.
78092
+ const staleRanksRef = useRef(null);
78093
+ if (staleRanksRef.current === null) {
78094
+ staleRanksRef.current = restored ? new Set(pages.byIndex.keys()) : new Set();
78095
+ }
78029
78096
  const [refreshing, setRefreshing] = useState(false);
78030
78097
  // A source that says when what it reads has moved (a resource range reader
78031
78098
  // does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
@@ -78036,7 +78103,7 @@ const useItemStore = ({
78036
78103
  const invalidationRef = useRef(invalidation);
78037
78104
  if (invalidationRef.current !== invalidation) {
78038
78105
  invalidationRef.current = invalidation;
78039
- staleRef.current = true;
78106
+ staleRanksRef.current = new Set(pages.byIndex.keys());
78040
78107
  }
78041
78108
  // The one request in flight, with the means to call it off: a page asked for
78042
78109
  // a window the list has left is work the server and the browser are doing for
@@ -78069,11 +78136,6 @@ const useItemStore = ({
78069
78136
  // It stands for a page of them: a list that is about to be filled looks
78070
78137
  // like items on their way, not like an empty list.
78071
78138
  const itemCount = pages.count ?? count ?? listItems.pageSize;
78072
- // A run that never received anything has nothing to keep on screen: asking
78073
- // again is its first ask, not a refresh.
78074
- if (staleRef.current && pages.count === undefined) {
78075
- staleRef.current = false;
78076
- }
78077
78139
  useLayoutEffect(() => {
78078
78140
  if (!refreshing) {
78079
78141
  return null;
@@ -78146,9 +78208,11 @@ const useItemStore = ({
78146
78208
  const keepFrom = windowFrom - ITEM_STORE_KEEP_AROUND;
78147
78209
  const keepTo = windowTo + ITEM_STORE_KEEP_AROUND;
78148
78210
  if (pages.byIndex.size > budget) {
78211
+ const staleRanks = staleRanksRef.current;
78149
78212
  for (const index of pages.byIndex.keys()) {
78150
78213
  if (index < keepFrom || index > keepTo) {
78151
78214
  pages.byIndex.delete(index);
78215
+ staleRanks.delete(index);
78152
78216
  }
78153
78217
  }
78154
78218
  }
@@ -78183,10 +78247,24 @@ const useItemStore = ({
78183
78247
  let start = missingStart;
78184
78248
  let end = missingEnd;
78185
78249
  let around;
78186
- // Items that are all there but out of date: the ask is the window itself,
78187
- // anchored on the item at its top — a source paginating by cursor gets an
78188
- // item to count from, and the reading position is what must survive.
78189
- const revalidating = staleRef.current;
78250
+ // The window draws items from before: they are asked for again, and stay
78251
+ // drawn meanwhile.
78252
+ const staleRanks = staleRanksRef.current;
78253
+ let staleFrom = -1;
78254
+ let staleTo = -1;
78255
+ if (staleRanks.size > 0) {
78256
+ let rank = windowFrom;
78257
+ while (rank < windowTo) {
78258
+ if (staleRanks.has(rank)) {
78259
+ if (staleFrom === -1) {
78260
+ staleFrom = rank;
78261
+ }
78262
+ staleTo = rank;
78263
+ }
78264
+ rank++;
78265
+ }
78266
+ }
78267
+ const revalidating = staleFrom !== -1;
78190
78268
  // The list is held on an item nothing on screen leads to: the items it holds
78191
78269
  // do not contain it, so no window it could draw will ever bring it. Only
78192
78270
  // asking for it by name does.
@@ -78203,18 +78281,36 @@ const useItemStore = ({
78203
78281
  start = from < 0 ? 0 : from;
78204
78282
  end = start + budget - 1;
78205
78283
  } else if (revalidating) {
78206
- start = windowFrom;
78207
- end = windowTo - 1;
78208
- if (end < start) {
78209
- // Nothing of this run is on screen (the window frames another one, or
78210
- // the list is scrolled past it): its own first items are what it will
78211
- // draw next.
78212
- start = 0;
78213
- end = budget - 1;
78214
- }
78215
- const firstHeld = pages.byIndex.get(windowFrom);
78216
- if (firstHeld && firstHeld.id !== undefined) {
78217
- around = firstHeld.id;
78284
+ // The items from before that the window draws, and the missing ones
78285
+ // with them: one ask.
78286
+ if (start === -1 || staleFrom < start) {
78287
+ start = staleFrom;
78288
+ }
78289
+ if (staleTo > end) {
78290
+ end = staleTo;
78291
+ }
78292
+ // Grown over the items from before on either side, up to a page: the
78293
+ // window grows past its first picture right after it is painted, and
78294
+ // what it grows onto would otherwise be asked for once more.
78295
+ while (end - start + 1 < budget) {
78296
+ const staleBefore = staleRanks.has(start - 1);
78297
+ const staleAfter = staleRanks.has(end + 1);
78298
+ if (!staleBefore && !staleAfter) {
78299
+ break;
78300
+ }
78301
+ if (staleBefore) {
78302
+ start--;
78303
+ }
78304
+ if (staleAfter && end - start + 1 < budget) {
78305
+ end++;
78306
+ }
78307
+ }
78308
+ // Anchored on the first item from before on screen: a source
78309
+ // paginating by cursor gets an item to count from, and the reading
78310
+ // position is what must survive.
78311
+ const firstStale = pages.byIndex.get(staleFrom);
78312
+ if (firstStale && firstStale.id !== undefined) {
78313
+ around = firstStale.id;
78218
78314
  }
78219
78315
  } else if (pages.count === undefined) {
78220
78316
  const scrolled = listItems.scrolled;
@@ -78268,7 +78364,8 @@ const useItemStore = ({
78268
78364
  debugAsk("already revalidating");
78269
78365
  return;
78270
78366
  }
78271
- // A page for a window that is about to be replaced wholesale.
78367
+ // A page asked for before the items went stale answers for the
78368
+ // collection as it was.
78272
78369
  request.controller?.abort();
78273
78370
  request.busy = false;
78274
78371
  }
@@ -78289,11 +78386,11 @@ const useItemStore = ({
78289
78386
  request.controller?.abort();
78290
78387
  request.busy = false;
78291
78388
  }
78292
- const held = pages.byIndex.size;
78293
78389
  // Asking again for a range that was already asked for, having received
78294
- // nothing since, can only produce the same answer. A revalidation is
78295
- // exactly the case where it produces another one.
78296
- if (!revalidating && request.start === start && request.end === end && request.held === held) {
78390
+ // nothing since, can only produce the same answer — unless the
78391
+ // collection moved since, which is what an invalidation says.
78392
+ const held = `${pages.byIndex.size}/${staleRanks.size}/${invalidation}`;
78393
+ if (request.start === start && request.end === end && request.held === held) {
78297
78394
  debugAsk("this range was asked for already");
78298
78395
  return;
78299
78396
  }
@@ -78329,7 +78426,6 @@ const useItemStore = ({
78329
78426
  request.revalidating = false;
78330
78427
  setFailure(null);
78331
78428
  if (revalidating) {
78332
- staleRef.current = false;
78333
78429
  setRefreshing(false);
78334
78430
  }
78335
78431
  publishRequestState();
@@ -78348,15 +78444,35 @@ const useItemStore = ({
78348
78444
  // Before the items land: what is on screen has to stay where it is,
78349
78445
  // and the DOM still shows the state to hold onto.
78350
78446
  listItems.captureAnchor();
78351
- if (revalidating) {
78352
- // The items held stood for a composition that has moved on; the
78353
- // ones outside the window are forgotten and asked for again if the
78354
- // user goes back to them.
78355
- pages.byIndex = new Map();
78447
+ // An item from before is gone from where it stood when the page puts
78448
+ // it at another rank (it would be drawn twice), or when its rank is
78449
+ // past the end of the collection.
78450
+ const staleRanksNow = staleRanksRef.current;
78451
+ const pageEnd = pageStart + pageItems.length;
78452
+ let dropped = false;
78453
+ if (staleRanksNow.size > 0) {
78454
+ const pageIds = new Set();
78455
+ for (const pageItem of pageItems) {
78456
+ if (pageItem && pageItem.id !== undefined) {
78457
+ pageIds.add(pageItem.id);
78458
+ }
78459
+ }
78460
+ for (const rank of staleRanksNow) {
78461
+ if (rank >= pageStart && rank < pageEnd) {
78462
+ continue;
78463
+ }
78464
+ const staleItem = pages.byIndex.get(rank);
78465
+ if (rank >= pageCount || staleItem && pageIds.has(staleItem.id)) {
78466
+ staleRanksNow.delete(rank);
78467
+ pages.byIndex.delete(rank);
78468
+ dropped = true;
78469
+ }
78470
+ }
78356
78471
  }
78357
78472
  let i = 0;
78358
78473
  while (i < pageItems.length) {
78359
78474
  pages.byIndex.set(pageStart + i, pageItems[i]);
78475
+ staleRanksNow.delete(pageStart + i);
78360
78476
  i++;
78361
78477
  }
78362
78478
  pages.count = pageCount;
@@ -78366,7 +78482,7 @@ const useItemStore = ({
78366
78482
  itemsAction.writeComposition({
78367
78483
  byIndex: pages.byIndex,
78368
78484
  count: pageCount,
78369
- replace: revalidating
78485
+ replace: dropped
78370
78486
  });
78371
78487
  }
78372
78488
  listItems.pagesSignal.value = listItems.pagesSignal.peek() + 1;
@@ -78381,9 +78497,14 @@ const useItemStore = ({
78381
78497
  request.revalidating = false;
78382
78498
  publishRequestState();
78383
78499
  if (revalidating) {
78384
- // The items from before stay: a revalidation that failed has
78500
+ // The items from before stay, and are not asked for again until the
78501
+ // source says the collection moved: a revalidation that failed has
78385
78502
  // nothing better to put in their place.
78386
- staleRef.current = false;
78503
+ let rank = start;
78504
+ while (rank <= end) {
78505
+ staleRanksRef.current.delete(rank);
78506
+ rank++;
78507
+ }
78387
78508
  setRefreshing(false);
78388
78509
  return;
78389
78510
  }
@@ -78700,7 +78821,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
78700
78821
  * "Many rows"). The window keeps three quarters of what the screen leaves of
78701
78822
  * it ahead of the scroll, and moves once that falls under half a screen; it
78702
78823
  * has to hold more than the screen shows, with room for that lookahead — the
78703
- * list warns when it leaves less than two items beyond the screen.
78824
+ * list warns when it holds no more than the screen. How much more is
78825
+ * measured: fling the list on the slowest device it runs on.
78704
78826
  *
78705
78827
  * `{ initial, after }` for a list whose first picture is taken as it is
78706
78828
  * built — drawn in the click that opens a popup, or in the update callback