@aiquants/virtualscroll 3.9.2 → 3.11.0
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/CHANGELOG.md +149 -0
- package/README.md +135 -40
- package/dist/ScrollBar.d.cts +45 -38
- package/dist/ScrollBar.d.ts +45 -38
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.cts +12 -14
- package/dist/ScrollPane.d.ts +12 -14
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/VirtualGrid.d.cts +6 -6
- package/dist/VirtualGrid.d.ts +6 -6
- package/dist/VirtualGrid.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +184 -30
- package/dist/VirtualScroll.d.ts +184 -30
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/cli.js +26 -47
- package/dist/index.cjs +1 -1
- package/dist/index.js +2385 -2413
- package/dist/labels.d.cts +38 -7
- package/dist/labels.d.ts +38 -7
- package/dist/labels.d.ts.map +1 -1
- package/dist/logger.d.cts +1 -16
- package/dist/logger.d.ts +1 -16
- package/dist/logger.d.ts.map +1 -1
- package/dist/styles/virtualscroll.css +1 -1
- package/dist/styles/virtualscroll.standalone.css +1 -1
- package/dist/useFenwickMapTree.d.cts +21 -1
- package/dist/useFenwickMapTree.d.ts +21 -1
- package/dist/useFenwickMapTree.d.ts.map +1 -1
- package/dist/useLruCache.d.ts.map +1 -1
- package/dist/utils.d.cts +14 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/ScrollBar.tsx +115 -55
- package/src/ScrollPane.tsx +12 -14
- package/src/VirtualGrid.tsx +27 -25
- package/src/VirtualScroll.tsx +486 -234
- package/src/labels.ts +58 -7
- package/src/logger.ts +1 -25
- package/src/styles/virtualscroll.css +51 -3
- package/src/useFenwickMapTree.ts +60 -98
- package/src/useLruCache.ts +14 -8
- package/src/utils.ts +15 -0
package/src/labels.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module labels
|
|
3
|
-
* @description Built-in UI chrome label catalog of the package. Covers exactly the
|
|
4
|
-
* the components render on their own: the ScrollBar
|
|
3
|
+
* @description Built-in UI chrome label catalog of the package. Covers exactly the eleven strings
|
|
4
|
+
* the components render on their own: the ScrollBar accessible names (per orientation, the two
|
|
5
|
+
* arrows, the `role="scrollbar"` bar and its `role="slider"` thumb), the VirtualScroll
|
|
5
6
|
* scroll-to-edge pill texts and the empty-state text. Live-region wording stays consumer-owned
|
|
6
7
|
* (`liveRegion.format` / `liveRegion.buildMessage`) and is not part of this catalog. The default
|
|
7
8
|
* locale is `"en"`; `"ja"` is the second locale. No language negotiation happens here: hosts map
|
|
8
9
|
* `navigator.language` (or anything else) to a supported locale themselves.
|
|
9
10
|
*
|
|
10
|
-
* @description パッケージ内蔵の UI クローム文言カタログ。コンポーネント自身が描画する
|
|
11
|
-
* (ScrollBar
|
|
11
|
+
* @description パッケージ内蔵の UI クローム文言カタログ。コンポーネント自身が描画する 11 文言
|
|
12
|
+
* (向きごとの ScrollBar のアクセシブルネーム — 矢印 2 個・`role="scrollbar"` のバー・その `role="slider"` の
|
|
13
|
+
* つまみ —、VirtualScroll の端スクロールピル文言、空状態文言) だけを対象とし、
|
|
12
14
|
* ライブリージョンの文言は利用側の所有 (`liveRegion.format` / `liveRegion.buildMessage`) で
|
|
13
15
|
* 本カタログの対象外。既定ロケールは `"en"`、第 2 ロケールは `"ja"`。言語ネゴシエーションは行わず、
|
|
14
16
|
* `navigator.language` 等から対応ロケールへの対応付けはホスト側の責務。
|
|
@@ -53,6 +55,30 @@ export type VirtualScrollLabels = {
|
|
|
53
55
|
* 横 ScrollBar 終端 (右) 矢印ボタンの aria-label。
|
|
54
56
|
*/
|
|
55
57
|
readonly scrollRight: string
|
|
58
|
+
/**
|
|
59
|
+
* aria-label of the vertical ScrollBar root (`role="scrollbar"`). Assistive technology appends
|
|
60
|
+
* the role ("scroll bar") itself, so the name carries only what tells the two bars apart.
|
|
61
|
+
* 縦 ScrollBar のルート (`role="scrollbar"`) の aria-label。ロール名 (スクロールバー) は支援技術が
|
|
62
|
+
* 付け足すため、名前は 2 本のバーを区別する情報のみ。
|
|
63
|
+
*/
|
|
64
|
+
readonly verticalScrollBar: string
|
|
65
|
+
/**
|
|
66
|
+
* aria-label of the horizontal ScrollBar root (`role="scrollbar"`).
|
|
67
|
+
* 横 ScrollBar のルート (`role="scrollbar"`) の aria-label。
|
|
68
|
+
*/
|
|
69
|
+
readonly horizontalScrollBar: string
|
|
70
|
+
/**
|
|
71
|
+
* aria-label of the vertical ScrollBar thumb (`role="slider"`), whose value is the scroll
|
|
72
|
+
* position.
|
|
73
|
+
* 縦 ScrollBar のつまみ (`role="slider"`) の aria-label。値はスクロール位置。
|
|
74
|
+
*/
|
|
75
|
+
readonly verticalScrollThumb: string
|
|
76
|
+
/**
|
|
77
|
+
* aria-label of the horizontal ScrollBar thumb (`role="slider"`), whose value is the scroll
|
|
78
|
+
* position.
|
|
79
|
+
* 横 ScrollBar のつまみ (`role="slider"`) の aria-label。値はスクロール位置。
|
|
80
|
+
*/
|
|
81
|
+
readonly horizontalScrollThumb: string
|
|
56
82
|
/**
|
|
57
83
|
* Text of the VirtualScroll scroll-to-top pill, shown when
|
|
58
84
|
* `scrollBarOptions.enableScrollToTopBottomButtons` is on.
|
|
@@ -87,15 +113,32 @@ export type VirtualScrollLabelOverrides = { readonly [K in keyof VirtualScrollLa
|
|
|
87
113
|
* overrides are validated against.
|
|
88
114
|
* {@link VirtualScrollLabels} の全キー (カタログ順)。上書き検証に用いる閉じたキー集合。
|
|
89
115
|
*/
|
|
90
|
-
export const VIRTUAL_SCROLL_LABEL_KEYS = [
|
|
116
|
+
export const VIRTUAL_SCROLL_LABEL_KEYS = [
|
|
117
|
+
"scrollUp",
|
|
118
|
+
"scrollDown",
|
|
119
|
+
"scrollLeft",
|
|
120
|
+
"scrollRight",
|
|
121
|
+
"verticalScrollBar",
|
|
122
|
+
"horizontalScrollBar",
|
|
123
|
+
"verticalScrollThumb",
|
|
124
|
+
"horizontalScrollThumb",
|
|
125
|
+
"scrollToTop",
|
|
126
|
+
"scrollToBottom",
|
|
127
|
+
"noItems",
|
|
128
|
+
] as const satisfies readonly (keyof VirtualScrollLabels)[]
|
|
91
129
|
|
|
92
130
|
// satisfies は部分集合しか保証しないため、キー追加時の登録漏れを型で検出する
|
|
93
131
|
const keysCoverEveryLabel: [Exclude<keyof VirtualScrollLabels, (typeof VIRTUAL_SCROLL_LABEL_KEYS)[number]>] extends [never] ? true : never = true
|
|
94
132
|
void keysCoverEveryLabel
|
|
95
133
|
|
|
96
134
|
/**
|
|
97
|
-
* Frozen built-in catalogs per locale. `en`
|
|
98
|
-
*
|
|
135
|
+
* Frozen built-in catalogs per locale. The arrow, pill and empty-state values of `en` are the
|
|
136
|
+
* package's historical English wording. The bar names state only the orientation, because
|
|
137
|
+
* assistive technology already reads the role as "scroll bar"; the thumb names state what the
|
|
138
|
+
* value measures.
|
|
139
|
+
* ロケールごとの凍結済み内蔵カタログ。`en` の矢印・ピル・空状態の値はパッケージ従来の英語文言。
|
|
140
|
+
* バーの名前は向きのみ (支援技術がロールを既に「スクロールバー」と読むため)、つまみの名前は値が
|
|
141
|
+
* 表すもの。
|
|
99
142
|
*/
|
|
100
143
|
export const VIRTUAL_SCROLL_LABEL_CATALOGS: Readonly<Record<VirtualScrollLocale, VirtualScrollLabels>> = Object.freeze({
|
|
101
144
|
en: Object.freeze({
|
|
@@ -103,6 +146,10 @@ export const VIRTUAL_SCROLL_LABEL_CATALOGS: Readonly<Record<VirtualScrollLocale,
|
|
|
103
146
|
scrollDown: "Scroll down",
|
|
104
147
|
scrollLeft: "Scroll left",
|
|
105
148
|
scrollRight: "Scroll right",
|
|
149
|
+
verticalScrollBar: "Vertical",
|
|
150
|
+
horizontalScrollBar: "Horizontal",
|
|
151
|
+
verticalScrollThumb: "Vertical scroll position",
|
|
152
|
+
horizontalScrollThumb: "Horizontal scroll position",
|
|
106
153
|
scrollToTop: "Top",
|
|
107
154
|
scrollToBottom: "Bottom",
|
|
108
155
|
noItems: "No items",
|
|
@@ -112,6 +159,10 @@ export const VIRTUAL_SCROLL_LABEL_CATALOGS: Readonly<Record<VirtualScrollLocale,
|
|
|
112
159
|
scrollDown: "下へスクロール",
|
|
113
160
|
scrollLeft: "左へスクロール",
|
|
114
161
|
scrollRight: "右へスクロール",
|
|
162
|
+
verticalScrollBar: "縦方向",
|
|
163
|
+
horizontalScrollBar: "横方向",
|
|
164
|
+
verticalScrollThumb: "縦スクロール位置",
|
|
165
|
+
horizontalScrollThumb: "横スクロール位置",
|
|
115
166
|
scrollToTop: "先頭へ",
|
|
116
167
|
scrollToBottom: "末尾へ",
|
|
117
168
|
noItems: "項目がありません",
|
package/src/logger.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface ILogger {
|
|
|
47
47
|
*/
|
|
48
48
|
export class Logger implements ILogger {
|
|
49
49
|
private level: LogLevel
|
|
50
|
-
private prefix: string
|
|
50
|
+
private readonly prefix: string
|
|
51
51
|
private impl: ILogger
|
|
52
52
|
|
|
53
53
|
/**
|
|
@@ -104,26 +104,6 @@ export class Logger implements ILogger {
|
|
|
104
104
|
this.impl = impl
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/**
|
|
108
|
-
* @method setPrefix
|
|
109
|
-
* @description Updates the log prefix for the static instance.
|
|
110
|
-
* @description 静的インスタンスのログのプレフィックスを更新します。
|
|
111
|
-
* @param {string} prefix - The new prefix.
|
|
112
|
-
*/
|
|
113
|
-
static setPrefix(prefix: string): void {
|
|
114
|
-
Logger.instance.setPrefix(prefix)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* @method setPrefix
|
|
119
|
-
* @description Updates the log prefix.
|
|
120
|
-
* @description ログのプレフィックスを更新します。
|
|
121
|
-
* @param {string} prefix - The new prefix.
|
|
122
|
-
*/
|
|
123
|
-
setPrefix(prefix: string): void {
|
|
124
|
-
this.prefix = prefix
|
|
125
|
-
}
|
|
126
|
-
|
|
127
107
|
/**
|
|
128
108
|
* @method isEnabled
|
|
129
109
|
* @description Returns whether the given level would be emitted at the current level (static instance).
|
|
@@ -197,10 +177,6 @@ export class Logger implements ILogger {
|
|
|
197
177
|
}
|
|
198
178
|
}
|
|
199
179
|
|
|
200
|
-
static info(message?: unknown, ...optionalParams: unknown[]): void {
|
|
201
|
-
Logger.instance.info(message, ...optionalParams)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
180
|
info(message?: unknown, ...optionalParams: unknown[]): void {
|
|
205
181
|
if (this.level <= LogLevel.INFO) {
|
|
206
182
|
this.impl.info(...this.formatMessage(Logger.resolveLazy(message)), ...optionalParams.map(Logger.resolveLazy))
|
|
@@ -329,6 +329,22 @@
|
|
|
329
329
|
width: 100%;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
/* 行ラッパーの包含ブロックで、配置の境界 (Chromium の relayout boundary)。窓をずらす 1 段が行を足し引きしても、配置はこの箱から
|
|
333
|
+
* 始まって箱の中で終わり、文書の根まで遡らない。境界になるには flex の子でも grid の子でもないことが要る (ペインの中身は flex の子
|
|
334
|
+
* なので、ラッパーの包含ブロックにしても境界にならない)。この箱は位置指定と、大きさと配置の封じ込めで境界になる。
|
|
335
|
+
* 箱はペインの中身のパディングの辺に付いて幅を合わせ、高さは 0 に固定する: 行は箱の外へはみ出して描かれ (描画は封じ込めないので、
|
|
336
|
+
* 切り取りはペインの中身の overflow: hidden のまま)、箱は当たり判定で背景 (background) の前に立たない。行のはみ出しは封じ込めで
|
|
337
|
+
* インクのはみ出しになるので、ペインの中身のスクロールできる範囲にも数えない。そのため、ブラウザが自分で行を見せるスクロール
|
|
338
|
+
* (オーバースキャンの行の中への Tab・ページ内検索) はペインの中身を動かせず、行の箱が外側のスクローラーの外にあればそちらを動かす */
|
|
339
|
+
.aqvs-items-boundary {
|
|
340
|
+
position: absolute;
|
|
341
|
+
top: 0;
|
|
342
|
+
right: 0;
|
|
343
|
+
left: 0;
|
|
344
|
+
height: 0;
|
|
345
|
+
contain: size layout style;
|
|
346
|
+
}
|
|
347
|
+
|
|
332
348
|
.aqvs-items-wrapper {
|
|
333
349
|
position: absolute;
|
|
334
350
|
width: 100%;
|
|
@@ -547,15 +563,47 @@
|
|
|
547
563
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
548
564
|
}
|
|
549
565
|
|
|
550
|
-
/*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
566
|
+
/* 強制配色 (forced-colors: active) では、ブラウザがシステム色でない地色をどれも Canvas に置き換える (透明度は残す)。地色だけで
|
|
567
|
+
* 描く部品 (文字色も枠線も輪郭も持たない部品) は周りと区別できなくなるので、どれもここでシステム色の描き方を持つ
|
|
568
|
+
* (forcedColors.spec.tsx がこのファイルから地色だけで描く部品を導いて照合する)。
|
|
569
|
+
* - タップスクロールサークル: グラデーションの視覚は平坦になるので、輪郭で形を残す。
|
|
570
|
+
* - バーとトラック: 縁を GrayText の輪郭で描く (バーは矢印とトラックの下地、トラックはつまみが動く範囲)。
|
|
571
|
+
* - つまみ: ネイティブのスクロールバーと同じくシステム色で塗る。状態の規則 (ホバー・ドラッグ・無効) は同じ選択子でここに
|
|
572
|
+
* 言い直す: 詳細度が同じなので、後に置いたこちらが勝つ。forced-color-adjust: none はホストが塗ったシステム色でない地色を
|
|
573
|
+
* Canvas へ消させない (つまみが溝と同じ色になって消えない)。
|
|
574
|
+
* - サンプルのビジュアルの光と棒: 引いた向きと量を示すので CanvasText で塗る。 */
|
|
553
575
|
@media (forced-colors: active) {
|
|
554
576
|
.aqvs-tap-scroll-circle {
|
|
555
577
|
outline: 1px solid CanvasText;
|
|
556
578
|
outline-offset: -1px;
|
|
557
579
|
border-radius: 50%;
|
|
558
580
|
}
|
|
581
|
+
|
|
582
|
+
.aqvs-scrollbar,
|
|
583
|
+
.aqvs-scrollbar-track {
|
|
584
|
+
outline: 1px solid GrayText;
|
|
585
|
+
outline-offset: -1px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.aqvs-scrollbar-thumb {
|
|
589
|
+
forced-color-adjust: none;
|
|
590
|
+
background-color: CanvasText;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.aqvs-scrollbar-thumb[data-thumb-state="hover"],
|
|
594
|
+
.aqvs-scrollbar-thumb[data-thumb-state="dragging"] {
|
|
595
|
+
background-color: Highlight;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.aqvs-scrollbar-thumb[data-thumb-state="disabled"] {
|
|
599
|
+
background-color: GrayText;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.aqvs-sample-visual-highlight,
|
|
603
|
+
.aqvs-sample-visual-rod {
|
|
604
|
+
forced-color-adjust: none;
|
|
605
|
+
background-color: CanvasText;
|
|
606
|
+
}
|
|
559
607
|
}
|
|
560
608
|
|
|
561
609
|
/* 動きを減らす設定 (prefers-reduced-motion: reduce) では、パッケージが動かす部品はどれも遷移もアニメーションも持たず、
|
package/src/useFenwickMapTree.ts
CHANGED
|
@@ -32,22 +32,6 @@ type MaterializeConfig = { materializeOption?: MaterializeOption }
|
|
|
32
32
|
type DeltaUpdate = { index: number; change: number }
|
|
33
33
|
type ValueUpdate = { index: number; value: number }
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Converts a numeric size into a non-negative bigint, safeguarding against fractional input.
|
|
37
|
-
*
|
|
38
|
-
* 分数入力を安全に丸めてから非負の bigint へ変換。
|
|
39
|
-
*/
|
|
40
|
-
const toSafeBigInt = (value: number): bigint => {
|
|
41
|
-
if (!Number.isFinite(value)) {
|
|
42
|
-
return 0n
|
|
43
|
-
}
|
|
44
|
-
const truncated = Math.trunc(value)
|
|
45
|
-
if (truncated <= 0) {
|
|
46
|
-
return 0n
|
|
47
|
-
}
|
|
48
|
-
return BigInt(truncated)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
35
|
/**
|
|
52
36
|
* Validates that `valueFn` returned a finite number, throwing otherwise. A single NaN/Infinity
|
|
53
37
|
* propagated into the tree poisons `tree`/`total` irrecoverably, so the materialization and
|
|
@@ -75,14 +59,17 @@ const requireFiniteValue = (value: number, index: number): number => {
|
|
|
75
59
|
const NEGATIVE_EFFECTIVE_VALUE_EPSILON = 1e-9
|
|
76
60
|
|
|
77
61
|
/**
|
|
78
|
-
* Derives the lowest set bit without relying on 32-bit bitwise operators.
|
|
62
|
+
* Derives the lowest set bit of a 1-based tree index (a positive integer) without relying on 32-bit bitwise operators. Every
|
|
63
|
+
* caller walks the tree from such an index, so the result is at least 1 and each walk ends: a walk up adds it until the index
|
|
64
|
+
* passes the size, a walk down subtracts it until the index reaches 0.
|
|
65
|
+
*
|
|
66
|
+
* 1 始まりの木の添字 (正の整数) の最下位ビットを、32 ビット演算に依存せずに求める処理。呼び出し元はどれもそのような添字から木を
|
|
67
|
+
* 辿るので、結果は 1 以上で、どの辿り方も終わる (上へ辿ると足して添字が要素数を越え、下へ辿ると引いて添字が 0 に着く)。
|
|
79
68
|
*
|
|
80
|
-
*
|
|
69
|
+
* @param value - A 1-based tree index (a positive integer) / 1 始まりの木の添字 (正の整数)
|
|
70
|
+
* @returns The lowest set bit (1 or more) / 最下位ビット (1 以上)
|
|
81
71
|
*/
|
|
82
72
|
const getLowestSetBit = (value: number): number => {
|
|
83
|
-
if (value <= 0 || !Number.isFinite(value)) {
|
|
84
|
-
return 0
|
|
85
|
-
}
|
|
86
73
|
const integer = Math.trunc(value)
|
|
87
74
|
// 32 ビット符号付き整数の範囲内なら通常のビット演算で最下位ビットを求める (BigInt 割り当てを回避)。
|
|
88
75
|
// ホットパス (prefixSum / _updateTree など) の GC 圧を大幅に削減する。
|
|
@@ -154,6 +141,13 @@ export class FenwickMapTree {
|
|
|
154
141
|
private valueFn?: (index: number) => number
|
|
155
142
|
private total?: number
|
|
156
143
|
|
|
144
|
+
/**
|
|
145
|
+
* @private
|
|
146
|
+
* @property {number} revisionCount - The counter behind `revision`: raised by every change of a stored value or of the size.
|
|
147
|
+
* @property {number} revisionCount - `revision` の元の数。保持する値か要素数が変わるたびに上がる。
|
|
148
|
+
*/
|
|
149
|
+
private revisionCount = 0
|
|
150
|
+
|
|
157
151
|
/**
|
|
158
152
|
* @constructor
|
|
159
153
|
* @description Initializes the Fenwick Tree.
|
|
@@ -193,6 +187,7 @@ export class FenwickMapTree {
|
|
|
193
187
|
this.valueFn = undefined
|
|
194
188
|
this.baseValue = valueOrFn
|
|
195
189
|
this.total = this.baseValue * this.size
|
|
190
|
+
this.revisionCount += 1
|
|
196
191
|
return
|
|
197
192
|
}
|
|
198
193
|
|
|
@@ -276,11 +271,9 @@ export class FenwickMapTree {
|
|
|
276
271
|
this.valueFn = valueOrFn
|
|
277
272
|
this.baseValue = baseValue
|
|
278
273
|
if (seed) {
|
|
274
|
+
// 種は [from, to] (to < size) の値なので、添字は要素数を越えない
|
|
279
275
|
for (let i = 0; i < seed.values.length; i++) {
|
|
280
276
|
const index = seed.from + i
|
|
281
|
-
if (index >= this.size) {
|
|
282
|
-
break
|
|
283
|
-
}
|
|
284
277
|
// _materialize を呼ぶ代わりに、計算フェーズの検証済みの値を使って更新する (二重評価を回避)
|
|
285
278
|
const change = seed.values[i] - this.baseValue
|
|
286
279
|
// baseValue と一致する行は delta を持たせない (Map の無駄な肥大を回避)
|
|
@@ -292,6 +285,7 @@ export class FenwickMapTree {
|
|
|
292
285
|
}
|
|
293
286
|
// 具現化が完了した後に total を計算する
|
|
294
287
|
this.total = this.getTotal()
|
|
288
|
+
this.revisionCount += 1
|
|
295
289
|
}
|
|
296
290
|
|
|
297
291
|
/**
|
|
@@ -318,6 +312,7 @@ export class FenwickMapTree {
|
|
|
318
312
|
// baseValue を差し替えたら total を再計算しないと prefixSum と乖離する。
|
|
319
313
|
// (deltas は温存されるため sum(deltas) + baseValue * size に一致させる)
|
|
320
314
|
this.total = this._computeTreeTotal()
|
|
315
|
+
this.revisionCount += 1
|
|
321
316
|
}
|
|
322
317
|
}
|
|
323
318
|
|
|
@@ -348,10 +343,6 @@ export class FenwickMapTree {
|
|
|
348
343
|
* @returns {number} The representative base value.
|
|
349
344
|
*/
|
|
350
345
|
private static _modeOrMedian(values: number[]): number {
|
|
351
|
-
if (values.length === 0) {
|
|
352
|
-
return 0
|
|
353
|
-
}
|
|
354
|
-
|
|
355
346
|
// 中央値を計算してデフォルトの最頻値として設定
|
|
356
347
|
values.sort((a, b) => a - b)
|
|
357
348
|
const mid = Math.floor(values.length / 2)
|
|
@@ -402,10 +393,6 @@ export class FenwickMapTree {
|
|
|
402
393
|
* @returns {number} The estimated base value.
|
|
403
394
|
*/
|
|
404
395
|
private static _sampleBaseValueStrided(valueFn: (index: number) => number, size: number, sampleCount: number): number {
|
|
405
|
-
if (size <= 0) {
|
|
406
|
-
return 0
|
|
407
|
-
}
|
|
408
|
-
|
|
409
396
|
// stride = max(1, ceil(size / sampleCount)) を基準に、偶数なら +1 して奇数化する。
|
|
410
397
|
// ceil によりサンプル数を多少犠牲にしても必ずリスト全域を跨ぐ (_modeOrMedian の
|
|
411
398
|
// 20% しきい値は割合ベースのためサンプル数減の影響は受けない)。
|
|
@@ -526,15 +513,12 @@ export class FenwickMapTree {
|
|
|
526
513
|
if (change === 0) {
|
|
527
514
|
return
|
|
528
515
|
}
|
|
516
|
+
this.revisionCount += 1
|
|
529
517
|
// tree Map を更新
|
|
530
518
|
let treeIndex = index + 1 // Fenwick Tree のアルゴリズムは 1 始まりで設計される
|
|
531
519
|
while (treeIndex <= this.size) {
|
|
532
520
|
this.tree.set(treeIndex, (this.tree.get(treeIndex) ?? 0) + change)
|
|
533
|
-
|
|
534
|
-
if (step === 0) {
|
|
535
|
-
break
|
|
536
|
-
}
|
|
537
|
-
treeIndex += step
|
|
521
|
+
treeIndex += getLowestSetBit(treeIndex)
|
|
538
522
|
}
|
|
539
523
|
|
|
540
524
|
// 合計値を更新 (totalが計算済みの場合のみ)
|
|
@@ -575,7 +559,8 @@ export class FenwickMapTree {
|
|
|
575
559
|
}
|
|
576
560
|
|
|
577
561
|
// 現在の delta はバッチ内で既に更新済みならその値を、なければ Map の値を使う
|
|
578
|
-
const
|
|
562
|
+
const pendingDelta = pendingDeltas.get(index)
|
|
563
|
+
const currentDelta = pendingDelta === undefined ? (this.deltas.get(index) ?? 0) : pendingDelta
|
|
579
564
|
const oldValue = currentDelta + this.baseValue
|
|
580
565
|
const change = value - oldValue
|
|
581
566
|
if (change !== 0) {
|
|
@@ -603,11 +588,7 @@ export class FenwickMapTree {
|
|
|
603
588
|
let treeIndex = this.size
|
|
604
589
|
while (treeIndex > 0) {
|
|
605
590
|
sum += this.tree.get(treeIndex) ?? 0
|
|
606
|
-
|
|
607
|
-
if (step === 0) {
|
|
608
|
-
break
|
|
609
|
-
}
|
|
610
|
-
treeIndex -= step
|
|
591
|
+
treeIndex -= getLowestSetBit(treeIndex)
|
|
611
592
|
}
|
|
612
593
|
|
|
613
594
|
return sum + this.baseValue * this.size
|
|
@@ -717,7 +698,7 @@ export class FenwickMapTree {
|
|
|
717
698
|
return this._findIndexLarge(target, options, chooseLowerBound)
|
|
718
699
|
}
|
|
719
700
|
if (this.size === 0) {
|
|
720
|
-
return { index: -1, total: this.
|
|
701
|
+
return { index: -1, total: this.getTotal(), cumulative: undefined, currentValue: undefined, safeIndex: undefined }
|
|
721
702
|
}
|
|
722
703
|
|
|
723
704
|
// 探索前に対象範囲を具現化しておく。具現化後のツリー状態で降下を行うことで、
|
|
@@ -737,7 +718,7 @@ export class FenwickMapTree {
|
|
|
737
718
|
resultIdx = this._descend(target, chooseLowerBound)
|
|
738
719
|
|
|
739
720
|
if (resultIdx < 0 || resultIdx >= this.size) {
|
|
740
|
-
return { index: -1, total: this.
|
|
721
|
+
return { index: -1, total: this.getTotal(), cumulative: undefined, currentValue: undefined, safeIndex: undefined }
|
|
741
722
|
}
|
|
742
723
|
|
|
743
724
|
if (!materialize) {
|
|
@@ -759,7 +740,7 @@ export class FenwickMapTree {
|
|
|
759
740
|
|
|
760
741
|
return {
|
|
761
742
|
index: resultIdx,
|
|
762
|
-
total: this.
|
|
743
|
+
total: this.getTotal(),
|
|
763
744
|
cumulative: result.cumulative,
|
|
764
745
|
currentValue: result.currentValue,
|
|
765
746
|
safeIndex: result.safeIndex,
|
|
@@ -812,14 +793,8 @@ export class FenwickMapTree {
|
|
|
812
793
|
* 非常に大きなサイズに対して bigint 演算を用いた二分探索を実施。
|
|
813
794
|
*/
|
|
814
795
|
private _findIndexLarge(target: number, options: MaterializeConfig, chooseLowerBound: boolean): { index: number; total: number | undefined; cumulative: number | undefined; currentValue: number | undefined; safeIndex: number | undefined } {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const sizeBig = toSafeBigInt(this.size)
|
|
820
|
-
if (sizeBig === 0n) {
|
|
821
|
-
return { index: -1, total: this.total ?? 0, cumulative: undefined, currentValue: undefined, safeIndex: undefined }
|
|
822
|
-
}
|
|
796
|
+
// 呼び出し元 (_findIndex) は要素数 2^30 以上のときだけここへ来るので、要素数は正の整数
|
|
797
|
+
const sizeBig = BigInt(this.size)
|
|
823
798
|
|
|
824
799
|
// 小サイズパス (_findIndex) と同一契約: 探索前に対象範囲を具現化し、探索中は木を
|
|
825
800
|
// 変異させない (prefixSum に options を渡さない)。materialize:true の場合は
|
|
@@ -904,7 +879,7 @@ export class FenwickMapTree {
|
|
|
904
879
|
// 空の木 (size=0) は正当な状態として中立値を返す (throw しない)。
|
|
905
880
|
// minmax(index, 0, -1) が負値へ丸められ get(-1) が throw するのを回避する。
|
|
906
881
|
if (this.size === 0) {
|
|
907
|
-
return { cumulative: 0, total: this.
|
|
882
|
+
return { cumulative: 0, total: this.getTotal(), currentValue: 0, safeIndex: 0 }
|
|
908
883
|
}
|
|
909
884
|
// 小数 index は具現化時に非整数キーのノードを作り整数走査と乖離するため切り捨てる
|
|
910
885
|
// (Infinity は trunc を素通りした後 minmax で size-1 にクランプされる)。
|
|
@@ -918,11 +893,7 @@ export class FenwickMapTree {
|
|
|
918
893
|
while (treeIndex > 0) {
|
|
919
894
|
const treeNodeValue = this.tree.get(treeIndex) ?? 0
|
|
920
895
|
sum += treeNodeValue
|
|
921
|
-
|
|
922
|
-
if (step === 0) {
|
|
923
|
-
break
|
|
924
|
-
}
|
|
925
|
-
treeIndex -= step
|
|
896
|
+
treeIndex -= getLowestSetBit(treeIndex)
|
|
926
897
|
}
|
|
927
898
|
|
|
928
899
|
const currentValue = materializeOption?.materialize ? this.get(safeIndex) : (this.deltas.get(safeIndex) || 0) + this.baseValue
|
|
@@ -972,10 +943,6 @@ export class FenwickMapTree {
|
|
|
972
943
|
this.total = 0
|
|
973
944
|
} else {
|
|
974
945
|
this.total = this._computeTreeTotal()
|
|
975
|
-
const lastPrefix = this.prefixSum(this.getSize() - 1)
|
|
976
|
-
if (lastPrefix.cumulative !== lastPrefix.total) {
|
|
977
|
-
Logger.error("Inconsistent Fenwick Tree state")
|
|
978
|
-
}
|
|
979
946
|
}
|
|
980
947
|
}
|
|
981
948
|
|
|
@@ -987,11 +954,11 @@ export class FenwickMapTree {
|
|
|
987
954
|
* @description Rebuilds the Fenwick Tree from the existing `baseValue` and `deltas`. This corrects any discrepancies in the tree's internal state, such as those caused by floating-point errors, by recalculating the tree structure and the total sum from the source `deltas`. This method does not re-materialize values from `valueFn`.
|
|
988
955
|
* @description 既存の `baseValue` と `deltas` から Fenwick Tree を再構築します。これにより、`deltas` からツリー構造と合計値を再計算することで、浮動小数点誤差などによって生じた内部状態の不一致を修正します。このメソッドは `valueFn` から値を再具現化しません。
|
|
989
956
|
* @param {object} [options] - Optional settings for rebuilding.
|
|
990
|
-
* @param {boolean} [options.materialize=false] - If true and `valueFn` is
|
|
957
|
+
* @param {boolean} [options.materialize=false] - If true and `valueFn` is set, rebuilds from `valueFn` instead: a reset with materialization (`reset(size, valueFn, { materialize: true })`) that re-samples `baseValue`, materializes the initial window (rows 0 to `SAMPLE_COUNT - 1`) and discards every other measured delta. A tree with a uniform value (no `valueFn`) rebuilds from its deltas either way. / true かつ `valueFn` があれば、代わりに `valueFn` から作り直す。具現化付きのリセット (`reset(size, valueFn, { materialize: true })`) で、`baseValue` を標本で決め直し、初期の窓 (行 0〜`SAMPLE_COUNT - 1`) を具現化し、ほかの測った差分はすべて捨てる。一様な値の木 (`valueFn` 無し) はどちらでも差分から作り直す。
|
|
991
958
|
*/
|
|
992
959
|
rebuildTree(options?: { materialize?: boolean }) {
|
|
993
960
|
if (options?.materialize && this.valueFn) {
|
|
994
|
-
//
|
|
961
|
+
// 全行の具現化は巨大な木で O(n) の valueFn 呼び出しになるので、リセットと同じく初期の窓だけを具現化する
|
|
995
962
|
const valueFn = this.valueFn
|
|
996
963
|
this.reset(this.size, (i) => valueFn(i), { materialize: true })
|
|
997
964
|
return
|
|
@@ -1001,25 +968,21 @@ export class FenwickMapTree {
|
|
|
1001
968
|
// let newTotal = this.baseValue * this.size
|
|
1002
969
|
|
|
1003
970
|
// 既存の deltas を使って新しいツリーを構築し、合計値も同時に再計算する
|
|
971
|
+
// 0 の delta は保持しない (zero-delta 枝刈り) ので、どの delta も木へ伝える
|
|
1004
972
|
for (const [index, delta] of this.deltas.entries()) {
|
|
1005
|
-
if (delta === 0) {
|
|
1006
|
-
continue
|
|
1007
|
-
}
|
|
1008
973
|
// Fenwick Tree のアルゴリズムは 1 始まりで設計されるため、インデックスを 1 加算する
|
|
1009
974
|
let treeIndex = index + 1
|
|
1010
975
|
while (treeIndex <= this.size) {
|
|
1011
976
|
newTree.set(treeIndex, (newTree.get(treeIndex) ?? 0) + delta)
|
|
1012
|
-
|
|
1013
|
-
if (step === 0) {
|
|
1014
|
-
break
|
|
1015
|
-
}
|
|
1016
|
-
treeIndex += step
|
|
977
|
+
treeIndex += getLowestSetBit(treeIndex)
|
|
1017
978
|
}
|
|
1018
979
|
}
|
|
1019
980
|
|
|
1020
981
|
// 最後に状態をアトミックに更新
|
|
1021
982
|
this.tree = newTree
|
|
1022
983
|
this.total = this._computeTreeTotal()
|
|
984
|
+
// 値は変えないが、浮動小数点の誤差を直した接頭辞和は変わり得る
|
|
985
|
+
this.revisionCount += 1
|
|
1023
986
|
}
|
|
1024
987
|
|
|
1025
988
|
/**
|
|
@@ -1034,11 +997,6 @@ export class FenwickMapTree {
|
|
|
1034
997
|
* @returns {number} The difference between the cached total and the theoretical total.
|
|
1035
998
|
*/
|
|
1036
999
|
calculateAccumulatedError(): number {
|
|
1037
|
-
if (this.total === undefined) {
|
|
1038
|
-
// total がまだ計算されていない場合は、誤差は 0 とする
|
|
1039
|
-
return 0
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
1000
|
// 理論上の合計値を計算する。範囲外へ退避している delta は木に載っていないので数えない
|
|
1043
1001
|
let theoreticalTotal = this.baseValue * this.size
|
|
1044
1002
|
for (const [index, delta] of this.deltas) {
|
|
@@ -1048,7 +1006,7 @@ export class FenwickMapTree {
|
|
|
1048
1006
|
}
|
|
1049
1007
|
|
|
1050
1008
|
// キャッシュされている合計値との差を返す
|
|
1051
|
-
return this.
|
|
1009
|
+
return this.getTotal() - theoreticalTotal
|
|
1052
1010
|
}
|
|
1053
1011
|
|
|
1054
1012
|
/**
|
|
@@ -1140,11 +1098,7 @@ export class FenwickMapTree {
|
|
|
1140
1098
|
this.size = targetSize
|
|
1141
1099
|
this.rebuildTree()
|
|
1142
1100
|
}
|
|
1143
|
-
|
|
1144
|
-
const lastPrefix = this.prefixSum(this.getSize() - 1)
|
|
1145
|
-
if (lastPrefix.cumulative !== lastPrefix.total) {
|
|
1146
|
-
Logger.error("Inconsistent Fenwick Tree state")
|
|
1147
|
-
}
|
|
1101
|
+
this.revisionCount += 1
|
|
1148
1102
|
}
|
|
1149
1103
|
|
|
1150
1104
|
/**
|
|
@@ -1190,20 +1144,12 @@ export class FenwickMapTree {
|
|
|
1190
1144
|
// 既存範囲 (<= oldSize) のノードは更新済みなので、
|
|
1191
1145
|
// 親チェーンを辿って oldSize を超える最初のノードまで進める。
|
|
1192
1146
|
while (treeIndex <= oldSize) {
|
|
1193
|
-
|
|
1194
|
-
if (step === 0) {
|
|
1195
|
-
break
|
|
1196
|
-
}
|
|
1197
|
-
treeIndex += step
|
|
1147
|
+
treeIndex += getLowestSetBit(treeIndex)
|
|
1198
1148
|
}
|
|
1199
1149
|
// 新たに有効化されたノード (oldSize, newSize] にのみ差分を伝播する。
|
|
1200
1150
|
while (treeIndex <= newSize) {
|
|
1201
1151
|
this.tree.set(treeIndex, (this.tree.get(treeIndex) ?? 0) + delta)
|
|
1202
|
-
|
|
1203
|
-
if (step === 0) {
|
|
1204
|
-
break
|
|
1205
|
-
}
|
|
1206
|
-
treeIndex += step
|
|
1152
|
+
treeIndex += getLowestSetBit(treeIndex)
|
|
1207
1153
|
}
|
|
1208
1154
|
}
|
|
1209
1155
|
|
|
@@ -1246,9 +1192,8 @@ export class FenwickMapTree {
|
|
|
1246
1192
|
|
|
1247
1193
|
// total は増分 (+= baseValue * Δ) ではなく prefixSum と同一走査を再現する
|
|
1248
1194
|
// _computeTreeTotal で確定する。増分加算は浮動小数点の分配則不成立により
|
|
1249
|
-
// prefixSum
|
|
1250
|
-
//
|
|
1251
|
-
// 増分更新の速度メリットは保たれる。
|
|
1195
|
+
// prefixSum(size - 1) の走査結果と厳密には一致せず、末尾の位置で total と
|
|
1196
|
+
// 最後の行の下端が食い違うため。O(log n) なので増分更新の速度メリットは保たれる。
|
|
1252
1197
|
this.total = this._computeTreeTotal()
|
|
1253
1198
|
}
|
|
1254
1199
|
|
|
@@ -1291,6 +1236,23 @@ export class FenwickMapTree {
|
|
|
1291
1236
|
return this.baseValue
|
|
1292
1237
|
}
|
|
1293
1238
|
|
|
1239
|
+
/**
|
|
1240
|
+
* @description Gets the tree's revision: a counter that rises whenever a value the tree stores or its size changes — an
|
|
1241
|
+
* update, a materialization that changes a row, a reset, a resize, a new uniform value or a rebuild — and stays the same
|
|
1242
|
+
* otherwise (reads, a materialization that finds the stored value, an update to the same value, a resize to the same size).
|
|
1243
|
+
* The tree keeps its identity while its contents change, so a caller that derives anything from the prefix sums (row tops,
|
|
1244
|
+
* a rendering range) compares two reads: equal reads mean nothing it derived became stale in between. The counter only
|
|
1245
|
+
* rises; how far it rises per change is unspecified.
|
|
1246
|
+
* @description 木の版を取得。木が保持する値か要素数が変わるたび (更新・行を変える具現化・リセット・要素数の変更・一様な値の
|
|
1247
|
+
* 差し替え・再構築) に上がり、それ以外 (読み取り・保持する値と同じ値を見つけた具現化・同じ値への更新・同じ要素数への変更) では
|
|
1248
|
+
* 変わらない数。木は中身が変わっても同一性を保つため、接頭辞和から何か (行の上端・描画範囲) を導く呼び出し側は 2 回の読み取りを
|
|
1249
|
+
* 比べ、等しければその間に導いたものは古くなっていない。数は上がるだけで、1 回の変化でいくつ上がるかは決めない。
|
|
1250
|
+
* @returns {number} The revision. 版。
|
|
1251
|
+
*/
|
|
1252
|
+
get revision(): number {
|
|
1253
|
+
return this.revisionCount
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1294
1256
|
/**
|
|
1295
1257
|
* @method findIndexAtOrAfter
|
|
1296
1258
|
* @description Finds the first index where the cumulative sum is greater than or equal to a target value.
|
package/src/useLruCache.ts
CHANGED
|
@@ -90,6 +90,16 @@ class DoublyLinkedList<K, V> {
|
|
|
90
90
|
node.next = null
|
|
91
91
|
}
|
|
92
92
|
|
|
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.
|
|
98
|
+
*/
|
|
99
|
+
peekHead(): DoublyLinkedListNode<K, V> | null {
|
|
100
|
+
return this.head
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
/**
|
|
94
104
|
* @method removeHead
|
|
95
105
|
* @description Removes and returns the head of the list, which is the least recently used item.
|
|
@@ -176,14 +186,10 @@ export function useLruCache<K, V>(capacity: number) {
|
|
|
176
186
|
if (!Number.isFinite(normalizedCapacity)) {
|
|
177
187
|
return
|
|
178
188
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
} else {
|
|
184
|
-
// This should not happen if cache.current.size > 0
|
|
185
|
-
break
|
|
186
|
-
}
|
|
189
|
+
// 使用順の一覧と Map は同じ項目を持つので、Map が容量を超えている間は一覧の先頭が必ずある
|
|
190
|
+
for (let lruNode = list.current.peekHead(); lruNode !== null && cache.current.size > normalizedCapacity; lruNode = list.current.peekHead()) {
|
|
191
|
+
list.current.remove(lruNode)
|
|
192
|
+
cache.current.delete(lruNode.key)
|
|
187
193
|
}
|
|
188
194
|
}, [capacity, normalizedCapacity])
|
|
189
195
|
|
package/src/utils.ts
CHANGED
|
@@ -56,3 +56,18 @@ export const getAxisScale = (element: HTMLElement, axis: "x" | "y"): number => {
|
|
|
56
56
|
}
|
|
57
57
|
return visualSize / layoutSize
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pointer-down handler of pointer-only scroll chrome (the scrollbar and the scroll-to-edge pills of a host that scrolls by
|
|
62
|
+
* keyboard itself): cancels the press's default action, so the press never moves focus — neither onto a part of the chrome
|
|
63
|
+
* nor away from where the host put it — while the press itself, its pointer events and its click still reach the part.
|
|
64
|
+
*
|
|
65
|
+
* ポインタ専用のスクロールの部品 (キーボードのスクロールを自分で持つホストのスクロールバーと端へ戻るピル) の pointerdown の
|
|
66
|
+
* 処理。押下の既定動作を取り消すので、押下はフォーカスを動かさない — 部品の上へも、ホストが置いた場所の外へも — が、押下
|
|
67
|
+
* そのもの (ポインタのイベントと click) は部品へ届く。
|
|
68
|
+
*
|
|
69
|
+
* @param event - The pointer-down event (only its default action is read) / pointerdown のイベント (既定動作だけを扱う)
|
|
70
|
+
*/
|
|
71
|
+
export const keepFocusOnPress = (event: { readonly preventDefault: () => void }): void => {
|
|
72
|
+
event.preventDefault()
|
|
73
|
+
}
|