@aiquants/spreadsheet 0.8.0 → 0.9.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 +50 -0
- package/README.md +86 -8
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +4159 -3886
- package/dist/styles/spreadsheet.css +1 -1
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/package.json +3 -3
- package/src/Spreadsheet.tsx +198 -22
- package/src/grid/GridBody.tsx +16 -0
- package/src/header/ColumnHeader.tsx +84 -12
- package/src/header/RowHeader.tsx +79 -11
- package/src/header/resizeStrips.ts +86 -1
- package/src/header/useHeaderResize.ts +88 -20
- package/src/header/useHeaderSelection.ts +28 -9
- package/src/keyboard/ensureCellVisible.ts +40 -17
- package/src/keyboard/useGridKeyboard.ts +10 -4
- package/src/model/frozenPartition.ts +183 -59
- package/src/overlay/SelectionOverlay.tsx +599 -116
- package/src/overlay/useFillHandle.ts +65 -11
- package/src/popover/usePopovers.ts +39 -7
- package/src/selection/useAutoScroller.ts +58 -15
- package/src/selection/useSelectionEngine.ts +27 -10
- package/src/styles/spreadsheet.css +32 -0
|
@@ -15,6 +15,17 @@
|
|
|
15
15
|
* bucketed by font key so `ctx.font` is assigned once per distinct font) / row default-height
|
|
16
16
|
* restore / unhide marker restore. While the editor is pointing at a formula reference
|
|
17
17
|
* (v0.8 §4.2.10) a strip press is consumed (focus back to the sink, no gesture, no commit).
|
|
18
|
+
* v0.9 (ADR-26 / ADR-21): the gesture record and the guide carry a REQUIRED
|
|
19
|
+
* `anchor: "leading" | "trailing"` discriminator (classified by attribute presence —
|
|
20
|
+
* `data-aqsp-resize-anchor`, emitted by the trailing header subtrees only); trailing gestures
|
|
21
|
+
* take the INVERTED delta `pendingSize = max(0, round(initialSize − delta))` (pinned-right
|
|
22
|
+
* idiom — dragging left/up grows, the pointer and the moving edge travel together) and BOTH
|
|
23
|
+
* guide writers (the rAF move writer and the press-time write) place the guide at the
|
|
24
|
+
* prospective MOVING edge `offsetOf(index) + initialSize − pendingSize`; the leading path is
|
|
25
|
+
* byte-identical (sign +1, guide `offsetOf + pendingSize`, both writers). Column autofit
|
|
26
|
+
* measures the union `[0, R_eff) ∪ [rowStart, rowEnd] ∪ [rowCount − T_eff, rowCount)` (each
|
|
27
|
+
* filtered by sizeOf > 0 — ADR-21, band counts from a judgment-time `getFrozenSize()` read)
|
|
28
|
+
* and the measured-cell budget gate counts band rows.
|
|
18
29
|
*
|
|
19
30
|
* ヘッダーリサイズ / unhide 機械 (§7 PT-1)。単一フックが両帯を結線 (§7.4 の機械勘定 = 1)。
|
|
20
31
|
* 帯へ wire-once の native capture pointerdown を 1 本 (ストリップはリスナーを持たない —
|
|
@@ -30,6 +41,15 @@
|
|
|
30
41
|
* 代入を異なりフォント毎 1 回にする) / 行は既定行高復元 / unhide マーカーは既定サイズ復元。
|
|
31
42
|
* 参照ポインティング中 (v0.8 §4.2.10) のストリップ押下は consume (シンクへ焦点、ジェスチャも
|
|
32
43
|
* 確定もなし)。
|
|
44
|
+
* v0.9 (ADR-26 / ADR-21): ジェスチャレコードとガイドは必須の
|
|
45
|
+
* `anchor: "leading" | "trailing"` 判別子を持つ (分類は属性保持 — `data-aqsp-resize-anchor`
|
|
46
|
+
* は末尾ヘッダーサブツリーのみ発行)。末尾ジェスチャは反転デルタ
|
|
47
|
+
* `pendingSize = max(0, round(initialSize − delta))` (pinned-right イディオム — 左 / 上
|
|
48
|
+
* ドラッグで拡大、ポインタと動く辺が同行)、ガイドは**両方の書き手** (rAF 移動書き手と押下
|
|
49
|
+
* 即時書込) が予定動く辺 `offsetOf(index) + initialSize − pendingSize` に置く。leading パスは
|
|
50
|
+
* byte 恒等 (符号 +1、ガイド `offsetOf + pendingSize`、両書き手)。列 autofit の測定行集合は
|
|
51
|
+
* 和集合 `[0, R_eff) ∪ [rowStart, rowEnd] ∪ [rowCount − T_eff, rowCount)` (各 sizeOf > 0 —
|
|
52
|
+
* ADR-21。帯件数は判断時 `getFrozenSize()` 読み)、計測セル予算ゲートは帯行を算入する。
|
|
33
53
|
*/
|
|
34
54
|
|
|
35
55
|
import type { VirtualGridHandle } from "@aiquants/virtualscroll"
|
|
@@ -126,11 +146,28 @@ type ResizeGesture = {
|
|
|
126
146
|
axis: "row" | "col"
|
|
127
147
|
kind: "resize" | "unhide"
|
|
128
148
|
index: number
|
|
149
|
+
/** REQUIRED anchor discriminator (ADR-26 — no optional/default): "trailing" inverts the delta sign and the guide formula. / 必須アンカー判別子 (ADR-26 — optional / default なし): "trailing" はデルタ符号とガイド式を反転。 */
|
|
150
|
+
anchor: "leading" | "trailing"
|
|
129
151
|
initialSize: number
|
|
130
152
|
startClientX: number
|
|
131
153
|
startClientY: number
|
|
132
154
|
}
|
|
133
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Prospective guide TREE position for one gesture (ADR-26 — shared by BOTH writer sites: the
|
|
158
|
+
* rAF move writer and the press-time immediate write). Leading: the prospective FAR boundary
|
|
159
|
+
* `offsetOf(index) + pendingSize` (v0.8 byte-identical). Trailing: the prospective MOVING
|
|
160
|
+
* (near) edge `offsetOf(index) + initialSize − pendingSize` — at press pendingSize =
|
|
161
|
+
* initialSize, so the trailing form lands on `offsetOf(index)` (the pressed strip's own near
|
|
162
|
+
* boundary, under the pointer), where the leading form would flash the track's far boundary.
|
|
163
|
+
* ジェスチャ 1 件の予定ガイド木位置 (ADR-26 — rAF 移動書き手と押下即時書込の**両書き手**で共有)。
|
|
164
|
+
* leading は予定遠端境界 `offsetOf(index) + pendingSize` (v0.8 byte 恒等)。trailing は予定
|
|
165
|
+
* **動く辺** (近端) `offsetOf(index) + initialSize − pendingSize` — 押下時 pendingSize =
|
|
166
|
+
* initialSize のため trailing 式は `offsetOf(index)` (押下ストリップ自身の近端 = ポインタ直下)
|
|
167
|
+
* に着地し、leading 式のままだと遠端に点滅する (§14-6 の押下のみピン)。
|
|
168
|
+
*/
|
|
169
|
+
const guidePositionOf = (gesture: ResizeGesture, axisModel: AxisModel, pendingSize: number): number => (gesture.anchor === "trailing" ? axisModel.offsetOf(gesture.index) + gesture.initialSize - pendingSize : axisModel.offsetOf(gesture.index) + pendingSize)
|
|
170
|
+
|
|
134
171
|
/**
|
|
135
172
|
* Wires the resize/unhide machine onto both header bands; returns the §7.4 composite surface.
|
|
136
173
|
* 両ヘッダー帯へリサイズ / unhide 機械を結線し、§7.4 の合成面を返す。
|
|
@@ -267,7 +304,7 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
267
304
|
[collectTargets, applyEntries],
|
|
268
305
|
)
|
|
269
306
|
|
|
270
|
-
/** Column autofit (§7.3 / v0.8 §4.2.10): per-column canvas measurement over the visible window in each cell's painted font, bucketed by font key. / 列 autofit (
|
|
307
|
+
/** Column autofit (§7.3 / v0.8 §4.2.10 / ADR-21): per-column canvas measurement over the union of both frozen bands and the visible window — `[0, R_eff) ∪ [rowStart, rowEnd] ∪ [rowCount − T_eff, rowCount)`, each filtered by sizeOf > 0 — in each cell's painted font, bucketed by font key. / 列 autofit (§7.3 / v0.8 §4.2.10 / ADR-21): 両凍結帯 + 可視ウィンドウの和集合 `[0, R_eff) ∪ [rowStart, rowEnd] ∪ [rowCount − T_eff, rowCount)` (各 sizeOf > 0) をセルの塗りフォントで計測、フォントキーでバケット化。 */
|
|
271
308
|
const autofitColumns = useCallback(
|
|
272
309
|
(band: HTMLElement, pressedIndex: number): void => {
|
|
273
310
|
const p = paramsRef.current
|
|
@@ -275,23 +312,45 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
275
312
|
if (targets === null) {
|
|
276
313
|
return
|
|
277
314
|
}
|
|
278
|
-
//
|
|
315
|
+
// 帯件数は判断時ハンドル読み (ADR-21 / ADR-19 の判断時規律 — getFrozenSize().rows /
|
|
316
|
+
// .trailingRows はエンジンクランプ済み実効値)。装着前 (null) は帯集合なし — 測定集合が
|
|
317
|
+
// 窓のみに縮む v0.8 恒等の能力分岐 (値代替フォールバックではない)
|
|
318
|
+
const handle = p.handleRef.current
|
|
319
|
+
const frozenSize = handle === null ? null : handle.getFrozenSize()
|
|
320
|
+
const leadingRows = frozenSize === null ? 0 : frozenSize.rows
|
|
321
|
+
const trailingRows = frozenSize === null ? 0 : frozenSize.trailingRows
|
|
322
|
+
// 測定行集合 = [0, R_eff) ∪ [rowStart, rowEnd] ∪ [rowCount − T_eff, rowCount) の順序付き
|
|
323
|
+
// 和集合 (ADR-21 — v0.9 §4.14)。幅 0 行は計測しない (sizeOf > 0 フィルタ)、重複は Set で
|
|
324
|
+
// 去重 (R = T = 0 は窓のみ = v0.8 恒等)
|
|
279
325
|
const { rowStart, rowEnd } = p.viewStore.get()
|
|
280
326
|
const last = Math.min(rowEnd, p.rowAxis.count - 1)
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
327
|
+
const measuredRows: number[] = []
|
|
328
|
+
const seen = new Set<number>()
|
|
329
|
+
const pushRow = (row: number): void => {
|
|
330
|
+
if (!seen.has(row) && p.rowAxis.sizeOf(row) > 0) {
|
|
331
|
+
seen.add(row)
|
|
332
|
+
measuredRows.push(row)
|
|
285
333
|
}
|
|
286
334
|
}
|
|
287
|
-
|
|
335
|
+
for (let row = 0; row < leadingRows; row += 1) {
|
|
336
|
+
pushRow(row)
|
|
337
|
+
}
|
|
338
|
+
for (let row = rowStart; row <= last; row += 1) {
|
|
339
|
+
pushRow(row)
|
|
340
|
+
}
|
|
341
|
+
// 末尾帯の開始 = rowCount − T_eff (エンジンクランプ済みのため常に ≥ R_eff ≥ 0 — §1.2)
|
|
342
|
+
for (let row = p.rowAxis.count - trailingRows; row < p.rowAxis.count; row += 1) {
|
|
343
|
+
pushRow(row)
|
|
344
|
+
}
|
|
345
|
+
// 計測対象の可視行がゼロ (全帯 + 窓が非表示) は no-op — Excel も計測対象
|
|
288
346
|
// なしでは幅を変えない (0 計測 → 9px 潰しの破壊を作らない。R1(B) 所見)
|
|
289
|
-
if (
|
|
347
|
+
if (measuredRows.length === 0) {
|
|
290
348
|
return
|
|
291
349
|
}
|
|
292
350
|
// 上限ゲートは計測セル数単位 (§7.3 — トラック数単位では 16k 列 × 可視 40 行が素通り
|
|
293
|
-
// する)。上限は autofit 専用の低い係数 (§13 (e) の設計返却 — 実測根拠は定数 docstring)
|
|
294
|
-
|
|
351
|
+
// する)。上限は autofit 専用の低い係数 (§13 (e) の設計返却 — 実測根拠は定数 docstring)。
|
|
352
|
+
// 帯行も算入する (ADR-21 — measuredRows が帯行を含む)
|
|
353
|
+
const measuredCells = targets.length * measuredRows.length
|
|
295
354
|
const measureLimit = Math.floor(p.maxOperationCells / AUTOFIT_MEASURE_DIVISOR)
|
|
296
355
|
if (measuredCells > measureLimit) {
|
|
297
356
|
p.onOperationRejected?.("resize", measuredCells, "limit")
|
|
@@ -317,7 +376,7 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
317
376
|
const buckets = new Map<string, { col: number; text: string }[]>()
|
|
318
377
|
for (const col of targets) {
|
|
319
378
|
const colIndex = cfRules === null ? null : buildCfIndex(cfRules.filter((rule) => rule.ranges.some((range) => col >= range.startCol && col <= range.endCol)))
|
|
320
|
-
for (const row of
|
|
379
|
+
for (const row of measuredRows) {
|
|
321
380
|
// 計測対象は**画面に描かれる表示文字列** (v0.7 — 値書式適用後)。素の getCellText を測ると
|
|
322
381
|
// 通貨 "¥1,234,567" が素値 "1234567" の幅に潰されて最上位桁ごと左クリップされる (誤読事故)。
|
|
323
382
|
// 書式なしセルは formatCellValue が素通しするため従来と同一。interned フォーマッタにより
|
|
@@ -374,8 +433,8 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
374
433
|
/** Per-axis closest() selector (structural exclusivity of the two hooks). / 軸別 closest() セレクタ。 */
|
|
375
434
|
const selectorFor = (axis: "row" | "col"): string => (axis === "col" ? "[data-aqsp-col-resize], [data-aqsp-unhide]" : "[data-aqsp-row-resize], [data-aqsp-unhide]")
|
|
376
435
|
|
|
377
|
-
/** Resolves a strip hit to a gesture seed. /
|
|
378
|
-
const resolveHit = (axis: "row" | "col", target: EventTarget | null): { kind: "resize" | "unhide"; index: number } | null => {
|
|
436
|
+
/** Resolves a strip hit to a gesture seed (incl. the ADR-26 anchor). / ストリップ命中のジェスチャ種解決 (ADR-26 の anchor 込み)。 */
|
|
437
|
+
const resolveHit = (axis: "row" | "col", target: EventTarget | null): { kind: "resize" | "unhide"; index: number; anchor: "leading" | "trailing" } | null => {
|
|
379
438
|
if (!(target instanceof Element)) {
|
|
380
439
|
return null
|
|
381
440
|
}
|
|
@@ -385,7 +444,10 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
385
444
|
}
|
|
386
445
|
const isUnhide = hit.hasAttribute("data-aqsp-unhide")
|
|
387
446
|
const index = Number(hit.getAttribute(isUnhide ? "data-aqsp-unhide" : axis === "col" ? "data-aqsp-col-resize" : "data-aqsp-row-resize"))
|
|
388
|
-
|
|
447
|
+
// 末尾判別は属性保持の分類分岐 (§2.6 — isUnhide と同型。属性は末尾サブツリーのみ発行、
|
|
448
|
+
// leading DOM は不変なのでフォールバックではない)
|
|
449
|
+
const anchor: "leading" | "trailing" = hit.hasAttribute("data-aqsp-resize-anchor") ? "trailing" : "leading"
|
|
450
|
+
return { kind: isUnhide ? "unhide" : "resize", index, anchor }
|
|
389
451
|
}
|
|
390
452
|
|
|
391
453
|
const applyWindowPointerMove = (event: PointerEvent) => {
|
|
@@ -398,7 +460,9 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
398
460
|
}
|
|
399
461
|
// ズーム境界 (v0.3): ドラッグ差分は視覚 px — 論理 px へ 1 回だけ除算して適用する
|
|
400
462
|
const delta = (gesture.axis === "col" ? event.clientX - gesture.startClientX : event.clientY - gesture.startClientY) / paramsRef.current.zoomRef.current
|
|
401
|
-
|
|
463
|
+
// 末尾アンカーは反転デルタ (ADR-26 — pinned-right: 左 / 上ドラッグで拡大、ポインタと
|
|
464
|
+
// 動く辺が同行)。leading は v0.8 byte 恒等の +delta
|
|
465
|
+
pendingSizeRef.current = Math.max(0, Math.round(gesture.anchor === "trailing" ? gesture.initialSize - delta : gesture.initialSize + delta))
|
|
402
466
|
// ガイドラインは rAF 合流の直書き 1 ノードのみ (§7.2 — セル実サイズは確定まで不変)
|
|
403
467
|
if (guideRafRef.current === null) {
|
|
404
468
|
guideRafRef.current = requestAnimationFrame(() => {
|
|
@@ -409,7 +473,8 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
409
473
|
}
|
|
410
474
|
const p = paramsRef.current
|
|
411
475
|
const axisModel = live.axis === "col" ? p.colAxis : p.rowAxis
|
|
412
|
-
|
|
476
|
+
// rAF 移動書き手もアンカー分岐 (ADR-26 — guidePositionOf が両書き手の単一式)
|
|
477
|
+
p.writeResizeGuide({ axis: live.axis, position: guidePositionOf(live, axisModel, pendingSizeRef.current), anchor: live.anchor })
|
|
413
478
|
})
|
|
414
479
|
}
|
|
415
480
|
}
|
|
@@ -473,8 +538,9 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
473
538
|
}
|
|
474
539
|
p.focusSink()
|
|
475
540
|
const axisModel = axis === "col" ? p.colAxis : p.rowAxis
|
|
476
|
-
|
|
477
|
-
|
|
541
|
+
const gesture: ResizeGesture = { axis, kind: hit.kind, index: hit.index, anchor: hit.anchor, initialSize: axisModel.sizeOf(hit.index), startClientX: event.clientX, startClientY: event.clientY }
|
|
542
|
+
gestureRef.current = gesture
|
|
543
|
+
pendingSizeRef.current = gesture.initialSize
|
|
478
544
|
pressButtonsRef.current = event.buttons
|
|
479
545
|
draggingRef.current = true
|
|
480
546
|
// 自機フラグセット後の義務 press repaint (§8.2 の repaint エッジ — fill ハンドル消灯等)
|
|
@@ -492,8 +558,10 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
492
558
|
windowListenersRef.current = { move, up }
|
|
493
559
|
window.addEventListener("pointermove", move)
|
|
494
560
|
window.addEventListener("pointerup", up)
|
|
495
|
-
// 押下位置の境界にガイドラインを即時表示
|
|
496
|
-
|
|
561
|
+
// 押下位置の境界にガイドラインを即時表示 — 押下書き手もアンカー分岐 (ADR-26 §14-6:
|
|
562
|
+
// 押下時 pendingSize = initialSize のため trailing 式は押下ストリップ自身の近端
|
|
563
|
+
// offsetOf(index) に着地する。leading 式のままだと最初の pointermove まで遠端に点滅)
|
|
564
|
+
p.writeResizeGuide({ axis, position: guidePositionOf(gesture, axisModel, pendingSizeRef.current), anchor: gesture.anchor })
|
|
497
565
|
}
|
|
498
566
|
|
|
499
567
|
/** Band dblclick: autofit / unhide-restore (mouse via lastPointerTypeRef — dblclick has no pointerType). / 帯 dblclick (autofit / 復元)。 */
|
|
@@ -101,7 +101,18 @@ export const useHeaderSelection = (params: UseHeaderSelectionParams): SelectionE
|
|
|
101
101
|
/** Reserved press index ((a) subtract target / (b) conversion anchor seed). / 予約押下 index ((a) の減算対象 / (b) 変換時の anchor 種)。 */
|
|
102
102
|
const reservedIndexRef = useRef(0)
|
|
103
103
|
|
|
104
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Band coordinate to main-axis index via the three-way inverse map (columns add hx, rows add
|
|
106
|
+
* vy; leading > trailing > middle — v0.9 plan §4.3, census #2). Each axis branch takes its
|
|
107
|
+
* OWN handle snapshot and builds the AxisInverseCtx inline (`trailingSize` = the TREE field,
|
|
108
|
+
* `extent` = a judgment-time `getViewportSize()` read — plan §2.1 rules 1/2; extent 0 keeps
|
|
109
|
+
* the trailing branch inert, byte-matching v0.8). The [0, count − 1] clamp is unchanged.
|
|
110
|
+
* 帯座標 → 主軸 index の 3 分岐逆写像 (列は hx、行は vy を加算。先頭 > 末尾 > 中央 — v0.9
|
|
111
|
+
* 計画 §4.3、census #2)。列 / 行の各分岐が自前ハンドルスナップショットを取り AxisInverseCtx を
|
|
112
|
+
* インライン構築 (`trailingSize` は TREE フィールド、`extent` は判断時 `getViewportSize()`
|
|
113
|
+
* 読み — 計画 §2.1 規則 1/2。extent 0 は末尾分岐を不活性に保ち v0.8 と byte 一致)。クランプ
|
|
114
|
+
* [0, count − 1] は不変。
|
|
115
|
+
*/
|
|
105
116
|
const hitIndex = useCallback((clientX: number, clientY: number): number | null => {
|
|
106
117
|
const { axis, mainAxis, handleRef, zoomRef } = paramsRef.current
|
|
107
118
|
const rect = headerRectRef.current
|
|
@@ -112,22 +123,30 @@ export const useHeaderSelection = (params: UseHeaderSelectionParams): SelectionE
|
|
|
112
123
|
// 位置は両軸ともハンドルの同期読み (§7-4 — ドラッグ中オートスクロールでも最新の hx で
|
|
113
124
|
// 着地列が決まる。旧 rAF 写像読みは列ヘッダー帯ドラッグの鮮度非対称の現存証拠だった)
|
|
114
125
|
const z = zoomRef.current
|
|
126
|
+
const handle = handleRef.current
|
|
127
|
+
if (handle === null) {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
115
130
|
if (axis === "col") {
|
|
116
|
-
const hx =
|
|
131
|
+
const hx = handle.getScrollPosition().x
|
|
117
132
|
if (hx < 0) {
|
|
118
133
|
return null
|
|
119
134
|
}
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
// 凍結パーティション 3 分岐逆写像 (v0.5 計画 §2、末尾は v0.9 §2.1) — ctx は列分岐の
|
|
136
|
+
// 自前スナップショットからこの場でインライン構築 (規則 1)。trailingSize は TREE
|
|
137
|
+
// フィールド (規則 2)、extent は判断時の getViewportSize() 読み
|
|
138
|
+
const frozen = handle.getFrozenSize()
|
|
139
|
+
const viewport = handle.getViewportSize()
|
|
140
|
+
return Math.min(Math.max(viewportXToCol(mainAxis, (clientX - rect.left) / z, { scroll: hx, frozenSize: frozen.width, trailingSize: frozen.trailingWidth, extent: viewport.width }), 0), mainAxis.count - 1)
|
|
123
141
|
}
|
|
124
|
-
const vy =
|
|
142
|
+
const vy = handle.getScrollPosition().y
|
|
125
143
|
if (vy < 0) {
|
|
126
144
|
return null
|
|
127
145
|
}
|
|
128
|
-
//
|
|
129
|
-
const
|
|
130
|
-
|
|
146
|
+
// 行ヘッダーも 3 分岐逆写像 (v0.6 計画 §2、末尾は v0.9 §2.1) — 行分岐の自前スナップショット
|
|
147
|
+
const frozen = handle.getFrozenSize()
|
|
148
|
+
const viewport = handle.getViewportSize()
|
|
149
|
+
return Math.min(Math.max(viewportYToRow(mainAxis, (clientY - rect.top) / z, { scroll: vy, frozenSize: frozen.height, trailingSize: frozen.trailingHeight, extent: viewport.height }), 0), mainAxis.count - 1)
|
|
131
150
|
}, [])
|
|
132
151
|
|
|
133
152
|
/** Index to axis-full span corner cells. / index → 片軸全域スパンの両端セル。 */
|
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
* "no-op when visible" align (start/end/center only, and it pins anchors), so arrow-key
|
|
4
4
|
* navigation must use edge-delta scrollBy on BOTH axes to avoid per-keystroke snapping
|
|
5
5
|
* (§7-7 of the VirtualGrid design plan — scrollTo is the floor-and-pin jump API and is
|
|
6
|
-
* deliberately not used here).
|
|
6
|
+
* deliberately not used here). v0.9 (plan §4.7, census #9): trailing frozen tracks are
|
|
7
|
+
* always visible and become per-axis no-ops (`row >= rowCount − trailingRows` blocks the
|
|
8
|
+
* vertical drive, `col >= colCount − trailingCols` the horizontal one), and the far
|
|
9
|
+
* visibility limit of SCROLL tracks shrinks by the trailing band's visible size
|
|
10
|
+
* (`viewport − trailingVisible*` — the Handsontable #5947 under-band landing class); all
|
|
11
|
+
* trailing fields are judgment-time `getFrozenSize()` reads, and T = 0 leaves every
|
|
12
|
+
* expression byte-identical to the v0.8 arithmetic.
|
|
7
13
|
*
|
|
8
14
|
* 自前 nearest 可視化スクロール (§6.6.3)。scrollToCell に「可視なら no-op」は存在しない
|
|
9
15
|
* (start/end/center のみ、かつアンカーを張る) ため、矢印ナビは両軸ともはみ出し分だけの
|
|
10
16
|
* scrollBy で自作する (設計計画 §7-7 — scrollTo は floor + アンカー張りのジャンプ API で不使用)。
|
|
17
|
+
* v0.9 (計画 §4.7、census #9): 末尾凍結トラックは常時可視のため軸別 no-op (縦は
|
|
18
|
+
* `row >= rowCount − trailingRows`、横は `col >= colCount − trailingCols` で遮断)、SCROLL
|
|
19
|
+
* トラックの遠可視限界は末尾帯の可視寸ぶん縮小 (`viewport − trailingVisible*` — Handsontable
|
|
20
|
+
* #5947 類の帯下敷き着地の回避)。trailing 系は全て判断時 `getFrozenSize()` 読みで、T = 0 は
|
|
21
|
+
* 全式が v0.8 算術と byte 一致。
|
|
11
22
|
*/
|
|
12
23
|
|
|
13
24
|
import type { VirtualGridHandle } from "@aiquants/virtualscroll"
|
|
@@ -25,9 +36,14 @@ type EnsureCellVisibleDeps = {
|
|
|
25
36
|
/**
|
|
26
37
|
* Builds the ensureCellVisible helper (constructed once in Spreadsheet, injected into
|
|
27
38
|
* keyboard / editor / Shift+F10 — §6.9-7 ownership). Both axes read the grid handle
|
|
28
|
-
* synchronously (the truth — §7-4) and land via edge-delta scrollBy.
|
|
39
|
+
* synchronously (the truth — §7-4) and land via edge-delta scrollBy. v0.9 (plan §4.7):
|
|
40
|
+
* trailing frozen tracks are always-visible per-axis no-ops and the far limit of scroll
|
|
41
|
+
* tracks shrinks by the trailing band's viewport-clipped visible size (the vis fields —
|
|
42
|
+
* judgment-time handle reads, never render-surface rederivations).
|
|
29
43
|
* ensureCellVisible の構築 (Spreadsheet で 1 回構築し keyboard / editor / Shift+F10 へ配る)。
|
|
30
|
-
* 両軸ともグリッドハンドルの同期読み (§7-4 の真実) + 端差分 scrollBy で着地する。
|
|
44
|
+
* 両軸ともグリッドハンドルの同期読み (§7-4 の真実) + 端差分 scrollBy で着地する。v0.9 (計画
|
|
45
|
+
* §4.7): 末尾凍結トラックは常時可視の軸別 no-op、スクロールトラックの遠限界は末尾帯の
|
|
46
|
+
* viewport クリップ済み可視寸 (vis フィールド — 判断時ハンドル読み、レンダ面再導出禁止) ぶん縮小。
|
|
31
47
|
*/
|
|
32
48
|
export const createEnsureCellVisible = ({ handleRef, rowAxis, colAxis }: EnsureCellVisibleDeps) => {
|
|
33
49
|
return (cell: CellCoord): void => {
|
|
@@ -43,34 +59,41 @@ export const createEnsureCellVisible = ({ handleRef, rowAxis, colAxis }: EnsureC
|
|
|
43
59
|
if (vy < 0 || hx < 0) {
|
|
44
60
|
return
|
|
45
61
|
}
|
|
62
|
+
// 帯幾何は判断毎の同期スナップショット 1 回 (leading / trailing とも同じ鮮度で読む)
|
|
63
|
+
const frozen = handle.getFrozenSize()
|
|
46
64
|
// 縦: 論理座標で可視域と比較し、はみ出しぶんだけ scrollBy (端差分 — §7-7)。
|
|
47
|
-
// 凍結行 (v0.6 計画 §3 — 横の凍結列 no-op と同文)
|
|
48
|
-
// (横調整は下ブロックで続行 —
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
// 凍結行 (v0.6 計画 §3 — 横の凍結列 no-op と同文) と末尾行 (v0.9 §4.7 — row >=
|
|
66
|
+
// rowCount − trailingRows は常時可視) は縦成分のみ no-op (横調整は下ブロックで続行 —
|
|
67
|
+
// 帯行 × スクロール列のセルは横スクロールが要る)。スクロール行の上可視限界は木座標
|
|
68
|
+
// H_F + vy、下可視限界は末尾帯の可視高ぶん縮小した vy + viewportH − H_T_vis (v0.9 §4.7
|
|
69
|
+
// の縮小遠限界 — 素の viewportH のままだと着地行が末尾帯の下敷きになる)
|
|
70
|
+
if (viewportH > 0 && cell.row >= frozen.rows && cell.row < rowAxis.count - frozen.trailingRows) {
|
|
52
71
|
const top = rowAxis.offsetOf(cell.row)
|
|
53
72
|
const bottom = top + rowAxis.sizeOf(cell.row)
|
|
54
|
-
if (top <
|
|
55
|
-
handle.scrollBy({ y: top - (
|
|
56
|
-
} else if (bottom > vy + viewportH) {
|
|
57
|
-
handle.scrollBy({ y: bottom - (vy + viewportH) })
|
|
73
|
+
if (top < frozen.height + vy) {
|
|
74
|
+
handle.scrollBy({ y: top - (frozen.height + vy) })
|
|
75
|
+
} else if (bottom > vy + viewportH - frozen.trailingVisibleHeight) {
|
|
76
|
+
handle.scrollBy({ y: bottom - (vy + viewportH - frozen.trailingVisibleHeight) })
|
|
58
77
|
}
|
|
59
78
|
}
|
|
60
79
|
// 横: 同型の nearest 計算 → 端差分 scrollBy (クランプはハンドルが担う)。
|
|
61
|
-
// 凍結列 (v0.5 計画 §3-4)
|
|
62
|
-
//
|
|
80
|
+
// 凍結列 (v0.5 計画 §3-4) と末尾列 (v0.9 §4.7 — col >= colCount − trailingCols) は
|
|
81
|
+
// 常時可視のため no-op (横ブロックは末尾処理なので早期 return で足りる)。スクロール列の
|
|
82
|
+
// 左可視限界は木座標 W_F + hx (viewport 左端 x = W_F がスクロール帯の始まり — エンジン
|
|
83
|
+
// §8-a)、右可視限界は末尾帯の可視幅ぶん縮小した hx + viewportW − W_T_vis (v0.9 §4.7)
|
|
63
84
|
if (viewportW > 0) {
|
|
64
|
-
const frozen = handle.getFrozenSize()
|
|
65
85
|
if (cell.col < frozen.cols) {
|
|
66
86
|
return
|
|
67
87
|
}
|
|
88
|
+
if (cell.col >= colAxis.count - frozen.trailingCols) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
68
91
|
const left = colAxis.offsetOf(cell.col)
|
|
69
92
|
const right = left + colAxis.sizeOf(cell.col)
|
|
70
93
|
if (left < frozen.width + hx) {
|
|
71
94
|
handle.scrollBy({ x: left - (frozen.width + hx) })
|
|
72
|
-
} else if (right > hx + viewportW) {
|
|
73
|
-
handle.scrollBy({ x: right - (hx + viewportW) })
|
|
95
|
+
} else if (right > hx + viewportW - frozen.trailingVisibleWidth) {
|
|
96
|
+
handle.scrollBy({ x: right - (hx + viewportW - frozen.trailingVisibleWidth) })
|
|
74
97
|
}
|
|
75
98
|
}
|
|
76
99
|
}
|
|
@@ -773,19 +773,25 @@ export const useGridKeyboard = (params: UseGridKeyboardParams): void => {
|
|
|
773
773
|
// 両軸ともハンドルの同期読み (§7-4): ensureCellVisible 直後の cellRect は
|
|
774
774
|
// 最新位置で計算しないと横画面外からのメニュー表示が旧位置へ誤配置される
|
|
775
775
|
const handle = handleRef.current
|
|
776
|
-
const position = handle
|
|
776
|
+
const position = handle === null ? { x: 0, y: 0 } : handle.getScrollPosition()
|
|
777
777
|
// 両軸とも読点で番兵クランプ (対称 — detached の {-1,-1} を座標へ流さない)
|
|
778
778
|
const vy = Math.max(0, position.y)
|
|
779
779
|
const hx = Math.max(0, position.x)
|
|
780
|
+
// 帯件数と extent も同じ判断で読む (v0.9 計画 §4.8、census #10 — AxisForwardCtx は
|
|
781
|
+
// この場でインライン構築、§2.1 規則 1)。detached ハンドルは件数 0 / extent 0 の縮退
|
|
782
|
+
// (末尾分岐不活性 = v0.8 恒等)
|
|
783
|
+
const frozenMenu = handle === null ? { rows: 0, cols: 0, trailingRows: 0, trailingCols: 0 } : handle.getFrozenSize()
|
|
784
|
+
const viewportMenu = handle === null ? { width: 0, height: 0 } : handle.getViewportSize()
|
|
780
785
|
// ズーム境界 (v0.3): cellRect は視覚 px (論理 × z) — consumer は胴体ペインの
|
|
781
786
|
// gBCR (視覚 px) と直接加算して fixed 配置できる (デモ /render が参照実装)
|
|
782
787
|
const zMenu = p.zoomRef.current
|
|
783
788
|
p.onCellContextMenu?.(active, {
|
|
784
789
|
kind: "keyboard",
|
|
785
790
|
cellRect: {
|
|
786
|
-
// 行 /
|
|
787
|
-
|
|
788
|
-
|
|
791
|
+
// 行 / 列とも凍結パーティション 3 分岐順写像 (v0.6 / v0.5 計画 §2、末尾は
|
|
792
|
+
// v0.9 §2.1 — 凍結軸は静的座標、末尾軸は extent アンカー)。×zoom は写像後 (契約不変)
|
|
793
|
+
top: rowToViewportY(p.rowAxis, active.row, { scroll: vy, frozenCount: frozenMenu.rows, trailingCount: frozenMenu.trailingRows, extent: viewportMenu.height }) * zMenu,
|
|
794
|
+
left: colToViewportX(p.colAxis, active.col, { scroll: hx, frozenCount: frozenMenu.cols, trailingCount: frozenMenu.trailingCols, extent: viewportMenu.width }) * zMenu,
|
|
789
795
|
width: p.colAxis.sizeOf(active.col) * zMenu,
|
|
790
796
|
height: p.rowAxis.sizeOf(active.row) * zMenu,
|
|
791
797
|
},
|