@aiquants/virtualscroll 3.11.0 → 3.11.1

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.
@@ -1,9 +1,9 @@
1
- import React, { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react"
1
+ import React, { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react"
2
2
  import { type DevicePixelSnapEdge, snapToDevicePixelGrid, usePaintingDevicePixelRatio } from "./devicePixelGrid.ts"
3
3
  import { resolveVirtualScrollLabels, type VirtualScrollLabelOverrides, type VirtualScrollLocale } from "./labels.ts"
4
4
  import { Logger } from "./logger.ts"
5
5
  import { ScrollPane, type ScrollPaneContentInsets, type ScrollPaneHandle, type ScrollPaneProps } from "./ScrollPane.tsx"
6
- import { useFenwickMapTree } from "./useFenwickMapTree.ts"
6
+ import { FENWICK_LOOKUP_ONLY, useFenwickMapTree, useFenwickTreeRevision } from "./useFenwickMapTree.ts"
7
7
  import { getAxisScale, keepFocusOnPress, minmax } from "./utils.ts"
8
8
 
9
9
  /**
@@ -906,17 +906,6 @@ const ItemsWrapper = ({ translateY, snapEdge, children, boundaryRef }: ItemsWrap
906
906
  */
907
907
  const DEFAULT_HORIZONTAL_KEY_STEP = 40
908
908
 
909
- /**
910
- * Advances VirtualScroll's tree revision by one (the reducer of the revision that the memos reading the row-height tree
911
- * depend on).
912
- *
913
- * VirtualScroll の木の版数を 1 つ進める処理 (行の高さの木を読む memo が依存する版数の reducer)。
914
- *
915
- * @param revision - The current revision / 今の版数
916
- * @returns The next revision / 次の版数
917
- */
918
- const nextTreeRevision = (revision: number): number => revision + 1
919
-
920
909
  /**
921
910
  * Converts a numeric size into a non-negative bigint for large collection handling.
922
911
  *
@@ -947,9 +936,6 @@ export type VisibleStartRow = {
947
936
  */
948
937
  type VisibleStartRowTree = Pick<ReturnType<typeof useFenwickMapTree>, "findIndexAtOrAfter" | "prefixSum">
949
938
 
950
- /** Tree reads that only look the rows up, never materialising them / 行を引くだけで具現化しない木の読み方 */
951
- const LOOKUP_ONLY = { materializeOption: { materialize: false } } as const
952
-
953
939
  /**
954
940
  * Resolves the first visible row at a logical scroll position by the visible-start boundary rule — the one rule that
955
941
  * `scrollTo` pins, `getScrollAnchor` reports, `updateItemSize` compensates above and `computeRenderingRanges` renders from
@@ -981,9 +967,9 @@ const LOOKUP_ONLY = { materializeOption: { materialize: false } } as const
981
967
  */
982
968
  export const resolveVisibleStartRow = (fenwickTree: VisibleStartRowTree, position: number, itemCount: number): VisibleStartRow => {
983
969
  const lastIndex = itemCount - 1
984
- const found = fenwickTree.findIndexAtOrAfter(position, LOOKUP_ONLY)
970
+ const found = fenwickTree.findIndexAtOrAfter(position, FENWICK_LOOKUP_ONLY)
985
971
  if (found.index === -1 || found.index > lastIndex || found.cumulative === undefined || found.currentValue === undefined) {
986
- const last = fenwickTree.prefixSum(lastIndex, LOOKUP_ONLY)
972
+ const last = fenwickTree.prefixSum(lastIndex, FENWICK_LOOKUP_ONLY)
987
973
  return { index: lastIndex, top: last.cumulative - last.currentValue }
988
974
  }
989
975
  if (found.cumulative === position && found.index < lastIndex) {
@@ -1059,11 +1045,11 @@ export const computeRenderingRangesHuge = (effectiveScrollPosition: number, view
1059
1045
  // number へ安全に変換できない領域では木のクエリが不正確になるため打ち切る
1060
1046
  return null
1061
1047
  }
1062
- const { cumulative: runBottom } = fenwickTree.prefixSum(lastNumber, LOOKUP_ONLY)
1048
+ const { cumulative: runBottom } = fenwickTree.prefixSum(lastNumber, FENWICK_LOOKUP_ONLY)
1063
1049
  if (!Number.isFinite(runBottom)) {
1064
1050
  return null
1065
1051
  }
1066
- const { index: jumpIndex } = fenwickTree.findIndexAtOrAfter(runBottom + 0.5, LOOKUP_ONLY)
1052
+ const { index: jumpIndex } = fenwickTree.findIndexAtOrAfter(runBottom + 0.5, FENWICK_LOOKUP_ONLY)
1067
1053
  if (jumpIndex === -1) {
1068
1054
  return null
1069
1055
  }
@@ -1138,11 +1124,11 @@ export const computeRenderingRangesHuge = (effectiveScrollPosition: number, view
1138
1124
  if (!Number.isSafeInteger(indexNumber)) {
1139
1125
  break
1140
1126
  }
1141
- const { cumulative: runBottom } = fenwickTree.prefixSum(indexNumber, LOOKUP_ONLY)
1127
+ const { cumulative: runBottom } = fenwickTree.prefixSum(indexNumber, FENWICK_LOOKUP_ONLY)
1142
1128
  if (!Number.isFinite(runBottom)) {
1143
1129
  break
1144
1130
  }
1145
- const { index: jumpIndex } = fenwickTree.findIndexAtOrAfter(runBottom - 0.5, LOOKUP_ONLY)
1131
+ const { index: jumpIndex } = fenwickTree.findIndexAtOrAfter(runBottom - 0.5, FENWICK_LOOKUP_ONLY)
1146
1132
  const jumpBig = jumpIndex === -1 ? -1n : toSafeBigInt(jumpIndex)
1147
1133
  if (jumpBig >= 0n && jumpBig < backwardStart) {
1148
1134
  // 0 行の連続を飛び越えて直前の非 0 行から走査を続行する
@@ -1219,8 +1205,8 @@ export const computeRenderingRanges = (scrollPosition: number, viewportSize: num
1219
1205
  if (currentHeight === 0) {
1220
1206
  zeroRun++
1221
1207
  if (zeroRun > ZERO_HEIGHT_RUN_LIMIT) {
1222
- const { cumulative: runBottom } = fenwickTree.prefixSum(forwardCursor - 1, { materializeOption: { materialize: false } })
1223
- const { index: jumpIndex } = Number.isFinite(runBottom) ? fenwickTree.findIndexAtOrAfter(runBottom + 0.5, { materializeOption: { materialize: false } }) : { index: -1 }
1208
+ const { cumulative: runBottom } = fenwickTree.prefixSum(forwardCursor - 1, FENWICK_LOOKUP_ONLY)
1209
+ const { index: jumpIndex } = Number.isFinite(runBottom) ? fenwickTree.findIndexAtOrAfter(runBottom + 0.5, FENWICK_LOOKUP_ONLY) : { index: -1 }
1224
1210
  if (jumpIndex > forwardCursor) {
1225
1211
  // 0 行の連続を飛び越えて次の非 0 行から走査を続行する
1226
1212
  forwardCursor = jumpIndex
@@ -1259,8 +1245,8 @@ export const computeRenderingRanges = (scrollPosition: number, viewportSize: num
1259
1245
  backwardZeroRun++
1260
1246
  if (backwardZeroRun > ZERO_HEIGHT_RUN_LIMIT) {
1261
1247
  // 連続 0 行の直前にある非 0 行へ後方ジャンプ (木上の累積位置 -0.5 で探索)
1262
- const { cumulative: runBottom } = fenwickTree.prefixSum(backwardCursor, { materializeOption: { materialize: false } })
1263
- const { index: jumpIndex } = Number.isFinite(runBottom) ? fenwickTree.findIndexAtOrAfter(runBottom - 0.5, { materializeOption: { materialize: false } }) : { index: -1 }
1248
+ const { cumulative: runBottom } = fenwickTree.prefixSum(backwardCursor, FENWICK_LOOKUP_ONLY)
1249
+ const { index: jumpIndex } = Number.isFinite(runBottom) ? fenwickTree.findIndexAtOrAfter(runBottom - 0.5, FENWICK_LOOKUP_ONLY) : { index: -1 }
1264
1250
  if (jumpIndex !== -1 && jumpIndex < backwardCursor) {
1265
1251
  // 0 行の連続を飛び越えて直前の非 0 行から走査を続行する
1266
1252
  backwardCursor = jumpIndex
@@ -1572,34 +1558,10 @@ const VirtualScrollInner = <T,>(
1572
1558
  const fenwickTree = useFenwickMapTree(itemCount, getItemHeight, fenwickTreeOptions)
1573
1559
 
1574
1560
  // ❗ 木は同一性を保ったまま描画の外で書き換わる (updateItemSize・高さの照合・scrollToIndex の具現化)。接頭辞和の変化は総和が
1575
- // 変わらなくても (差の和が 0 の測り直しの組) 描いた行の上端と高さ、描画範囲を変えるので、木を読むどの memo もこの版数に
1576
- // 依存させる。総和 (中身の寸法) は相殺で変わらず、同じ値の書き込みは React が捨てるため、変化の合図にならない
1577
- const [treeRevision, advanceTreeRevision] = useReducer(nextTreeRevision, 0)
1578
-
1579
- /**
1580
- * Runs one change of the row-height tree made outside render (`updateItemSize`, the height reconciliation, the
1581
- * materialisation of `scrollToIndex`) and, when the tree's revision moved (`FenwickMapTree.revision`), advances the tree
1582
- * revision, so the next commit re-places the rendered rows and re-resolves the rendering range whether or not the total
1583
- * changed.
1584
- *
1585
- * 描画の外で行う行の高さの木の変更を 1 回実行し (`updateItemSize`・高さの照合・`scrollToIndex` の具現化)、木の版
1586
- * (`FenwickMapTree.revision`) が動いたら木の版数を進める処理。総和が変わったかどうかによらず、次の確定が描いた行を置き直し、
1587
- * 描画範囲を解決し直す。
1588
- *
1589
- * @param change - The change of the tree / 木の変更
1590
- * @returns What `change` returned / `change` の戻り値
1591
- */
1592
- const changeTree = useCallback(
1593
- <Result,>(change: () => Result): Result => {
1594
- const revisionBefore = fenwickTree.revision
1595
- const result = change()
1596
- if (fenwickTree.revision !== revisionBefore) {
1597
- advanceTreeRevision()
1598
- }
1599
- return result
1600
- },
1601
- [fenwickTree],
1602
- )
1561
+ // 変わらなくても (差の和が 0 の測り直しの組) 描いた行の上端と高さ、描画範囲を変えるので、描画の外の変更はどれも changeTree を
1562
+ // 通し、木を読むどの memo も treeRevision に依存させる。総和 (中身の寸法) は相殺で変わらず、同じ値の書き込みは React が捨てるため、
1563
+ // 変化の合図にならない
1564
+ const { revision: treeRevision, change: changeTree } = useFenwickTreeRevision(fenwickTree)
1603
1565
 
1604
1566
  const [initialValues] = useState(() => {
1605
1567
  let position = 0
@@ -2163,7 +2125,7 @@ const VirtualScrollInner = <T,>(
2163
2125
  if (pendingVisibleStartIndexRef.current !== null) {
2164
2126
  const { index, align, offset } = pendingVisibleStartIndexRef.current
2165
2127
  const safeIndex = sanitizeIndex(index, itemCount)
2166
- const { cumulative: itemBottom, currentValue: itemHeight } = fenwickTree.prefixSum(safeIndex, { materializeOption: { materialize: false } })
2128
+ const { cumulative: itemBottom, currentValue: itemHeight } = fenwickTree.prefixSum(safeIndex, FENWICK_LOOKUP_ONLY)
2167
2129
 
2168
2130
  if (itemBottom !== undefined && itemHeight !== undefined) {
2169
2131
  const itemTop = Math.max(itemBottom - itemHeight, 0)
@@ -2349,7 +2311,7 @@ const VirtualScrollInner = <T,>(
2349
2311
  throw new RangeError(`[VirtualScroll] scrollToIndex: align "nearest" takes no offset (it lands the row at the edge of the band a visible scroll-to-edge pill leaves); received an offset of type ${typeof offset}.`)
2350
2312
  }
2351
2313
  // 見せるかどうかは今の木で決める。描画の窓とその周りの行は照合で実際の高さを持ち、窓から遠い行は表示域の中にない
2352
- const row = fenwickTree.prefixSum(safeIndex, LOOKUP_ONLY)
2314
+ const row = fenwickTree.prefixSum(safeIndex, FENWICK_LOOKUP_ONLY)
2353
2315
  const reveal = resolveRevealAlignment({ top: row.cumulative - row.currentValue, bottom: row.cumulative }, toLogicalPositionWithInset(latestScrollPositionRef.current, resolvedInsets.top), viewportSize, readObscuredInsets())
2354
2316
  if (reveal === null) {
2355
2317
  return
@@ -2978,7 +2940,7 @@ const VirtualScrollInner = <T,>(
2978
2940
 
2979
2941
  const safeRenderingStartIndex = sanitizeIndex(renderingStartIndex, itemCount)
2980
2942
  const safeRenderingEndIndex = sanitizeIndex(renderingEndIndex, itemCount)
2981
- const { cumulative, currentValue: oldHeight } = fenwickTree.prefixSum(safeRenderingStartIndex, { materializeOption: { materialize: false } })
2943
+ const { cumulative, currentValue: oldHeight } = fenwickTree.prefixSum(safeRenderingStartIndex, FENWICK_LOOKUP_ONLY)
2982
2944
  const startPosition = cumulative - oldHeight
2983
2945
 
2984
2946
  // 量子化アンカーの再基準化: 描画ウィンドウ先頭が現アンカーから一定距離を超えて離れたときだけ
@@ -11,7 +11,7 @@
11
11
  * それを管理するための React フック `useFenwickMapTree` の提供。
12
12
  * 動的なアイテムサイズを持つ仮想スクロールのシナリオに最適化されている。
13
13
  */
14
- import { useRef } from "react"
14
+ import { useCallback, useReducer, useRef } from "react"
15
15
  import { Logger } from "./logger.ts"
16
16
  import { minmax } from "./utils.ts"
17
17
 
@@ -32,6 +32,17 @@ type MaterializeConfig = { materializeOption?: MaterializeOption }
32
32
  type DeltaUpdate = { index: number; change: number }
33
33
  type ValueUpdate = { index: number; value: number }
34
34
 
35
+ /**
36
+ * The options of a tree read that only looks rows up and never materialises them (`prefixSum`, `get`, `getTotal`,
37
+ * `findIndexAtOrAfter`, `findIndexAtOrBefore`): the read leaves the stored values, the size and `revision` as they are. One
38
+ * frozen value that every lookup passes, so no caller spells the option out. Internal to the package (not in the barrel).
39
+ *
40
+ * 行を引くだけで具現化しない木の読み取り (`prefixSum`・`get`・`getTotal`・`findIndexAtOrAfter`・`findIndexAtOrBefore`) の選択肢。
41
+ * 読み取りは保持する値・要素数・`revision` をそのまま残す。どの引き当ても渡す凍結した 1 つの値で、呼び出し側は選択肢を書き下さない。
42
+ * パッケージの内部用 (バレル非公開)。
43
+ */
44
+ export const FENWICK_LOOKUP_ONLY = Object.freeze({ materializeOption: Object.freeze({ materialize: false }) })
45
+
35
46
  /**
36
47
  * Validates that `valueFn` returned a finite number, throwing otherwise. A single NaN/Infinity
37
48
  * propagated into the tree poisons `tree`/`total` irrecoverably, so the materialization and
@@ -1432,3 +1443,73 @@ export const useFenwickMapTree = (size: number, valueOrFn: number | ((index: num
1432
1443
 
1433
1444
  return tree
1434
1445
  }
1446
+
1447
+ /**
1448
+ * What `useFenwickTreeRevision` returns: the revision the memos that read one tree depend on, and the one way to change that
1449
+ * tree outside render so that they see the change.
1450
+ *
1451
+ * `useFenwickTreeRevision` の戻り値。1 本の木を読む memo が依存する版数と、memo が変化に気付くように描画の外でその木を変える
1452
+ * ただ 1 つの方法。
1453
+ */
1454
+ export type FenwickTreeRevision = {
1455
+ /** Advances by one per change made through `change` that moved the tree's revision / `change` を通した変更が木の版を動かすたびに 1 つ進む数 */
1456
+ readonly revision: number
1457
+ /** Runs one change of the tree made outside render and returns its result; advances `revision` only when the tree's revision moved / 描画の外で行う木の変更を 1 回実行してその結果を返し、木の版が動いたときだけ `revision` を進める処理 */
1458
+ readonly change: <Result>(mutate: () => Result) => Result
1459
+ }
1460
+
1461
+ /**
1462
+ * Advances a component's tree revision by one (the reducer behind `FenwickTreeRevision["revision"]`).
1463
+ *
1464
+ * コンポーネントの木の版数を 1 つ進める処理 (`FenwickTreeRevision["revision"]` の reducer)。
1465
+ *
1466
+ * @param revision - The current revision / 今の版数
1467
+ * @returns The next revision / 次の版数
1468
+ */
1469
+ const nextTreeRevision = (revision: number): number => revision + 1
1470
+
1471
+ /**
1472
+ * Ties the memos of a component to the contents of a Fenwick tree. The tree keeps its identity while its contents change
1473
+ * (`useFenwickMapTree`), so a memo that derives anything from its prefix sums (row tops, a rendering range, the placed
1474
+ * columns) cannot see a change through the tree itself, and the total is no signal either: a batch of changes that cancel out
1475
+ * leaves it as it was. Every change made outside render goes through `change`, which advances `revision` exactly when the
1476
+ * tree's own revision (`FenwickMapTree.revision`) moved, so the next commit recomputes the memos that depend on `revision`,
1477
+ * and a change that stores the same values (an update to the value a row already has, a materialisation that finds the stored
1478
+ * value) adds no commit. Changes made during render (a reset or a resize by `useFenwickMapTree`) come with new props, which the
1479
+ * memos depend on already. Internal to the package (not in the barrel).
1480
+ *
1481
+ * コンポーネントの memo を Fenwick 木の中身に結び付けるフック。木は中身が変わっても同一性を保つ (`useFenwickMapTree`) ので、
1482
+ * 接頭辞和から何か (行の上端・描画範囲・配置した列) を導く memo は木そのものからは変化に気付けず、総和も合図にならない (打ち消し合う
1483
+ * 変更の組は総和を変えない)。描画の外の変更はすべて `change` を通し、`change` は木自身の版 (`FenwickMapTree.revision`) が動いたとき
1484
+ * ちょうどに `revision` を進める。だから次の確定は `revision` に依存する memo を計算し直し、同じ値を保つ変更 (行が既に持つ値への更新・
1485
+ * 保持する値と同じ値を見つけた具現化) は確定を足さない。描画中の変更 (`useFenwickMapTree` のリセットと大きさの変更) は新しい props と
1486
+ * 一緒に来るので、memo は既にそれに依存している。パッケージの内部用 (バレル非公開)。
1487
+ *
1488
+ * @param tree - The tree whose changes the memos must see / memo が変化に気付くべき木
1489
+ * @returns The revision and the change runner / 版数と変更の実行関数
1490
+ */
1491
+ export const useFenwickTreeRevision = (tree: FenwickMapTree): FenwickTreeRevision => {
1492
+ const [revision, advanceRevision] = useReducer(nextTreeRevision, 0)
1493
+
1494
+ /**
1495
+ * Runs one change of the tree and advances the revision when the tree's revision moved.
1496
+ *
1497
+ * 木の変更を 1 回実行し、木の版が動いたら版数を進める処理。
1498
+ *
1499
+ * @param mutate - The change of the tree / 木の変更
1500
+ * @returns What `mutate` returned / `mutate` の戻り値
1501
+ */
1502
+ const change = useCallback(
1503
+ <Result>(mutate: () => Result): Result => {
1504
+ const revisionBefore = tree.revision
1505
+ const result = mutate()
1506
+ if (tree.revision !== revisionBefore) {
1507
+ advanceRevision()
1508
+ }
1509
+ return result
1510
+ },
1511
+ [tree],
1512
+ )
1513
+
1514
+ return { revision, change }
1515
+ }
@@ -91,10 +91,10 @@ class DoublyLinkedList<K, V> {
91
91
  }
92
92
 
93
93
  /**
94
- * @method peekHead
95
- * @description Returns the head of the list (the least recently used item) without removing it.
96
- * @description リストの先頭 (最も最近使用されていないアイテム) を外さずに返します。
97
- * @returns {DoublyLinkedListNode<K, V> | null} The head node, or null if the list is empty.
94
+ * Returns the head of the list (the least recently used item) without removing it.
95
+ * リストの先頭 (最も最近使用されていないアイテム) を外さずに返す処理。
96
+ *
97
+ * @returns {DoublyLinkedListNode<K, V> | null} The head node, or null if the list is empty / 先頭のノード (空なら null)
98
98
  */
99
99
  peekHead(): DoublyLinkedListNode<K, V> | null {
100
100
  return this.head