@aiquants/spreadsheet 0.16.0 → 0.17.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 +84 -1
- package/README.md +55 -17
- package/dist/index.cjs +8 -8
- package/dist/index.d.cts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +4249 -4172
- package/dist/styles/spreadsheet.css +1 -1
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/dist/tsv.cjs +6 -6
- package/dist/tsv.d.cts +16 -0
- package/dist/tsv.d.ts +16 -0
- package/dist/tsv.js +48 -41
- package/package.json +3 -3
- package/src/Spreadsheet.tsx +280 -92
- package/src/clipboard/tsv.ts +97 -45
- package/src/header/useHeaderResize.ts +137 -68
- package/src/index.ts +2 -2
- package/src/styles/spreadsheet.css +20 -0
- package/src/tsv.ts +5 -3
package/src/Spreadsheet.tsx
CHANGED
|
@@ -53,7 +53,7 @@ import { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle
|
|
|
53
53
|
import type { ClipChannel } from "./clipboard/clipPayload.ts"
|
|
54
54
|
import type { PasteOptions } from "./clipboard/pasteSpecial.ts"
|
|
55
55
|
import { type ClipInfo, type PasteOutcome, type PasteSheet, useClipboard } from "./clipboard/useClipboard.ts"
|
|
56
|
-
import { type BorderPreset, type CommandRunner, useCommands } from "./commands/useCommands.ts"
|
|
56
|
+
import { type BorderPreset, type CommandRunner, type KeyboardCellRect, useCommands } from "./commands/useCommands.ts"
|
|
57
57
|
import { type SinkSnapshot, useCellEditor } from "./editor/useCellEditor.ts"
|
|
58
58
|
import type { RenderCellContext } from "./grid/CellView.tsx"
|
|
59
59
|
import { GridBody } from "./grid/GridBody.tsx"
|
|
@@ -85,7 +85,7 @@ import { clampSelectionToGrid, reduceActiveCycle, reduceSelectionExtend, reduceS
|
|
|
85
85
|
import { useAutoScroller } from "./selection/useAutoScroller.ts"
|
|
86
86
|
import { type PointDragOwner, usePointDrag } from "./selection/usePointDrag.ts"
|
|
87
87
|
import { useSelectionEngine } from "./selection/useSelectionEngine.ts"
|
|
88
|
-
import { clampSpreadsheetZoom, SPREADSHEET_DEFAULT_COLUMN_WIDTH_PX, SPREADSHEET_MAX_OPERATION_CELLS, stepSpreadsheetZoom } from "./view.ts"
|
|
88
|
+
import { clampSpreadsheetZoom, SPREADSHEET_DEFAULT_COLUMN_WIDTH_PX, SPREADSHEET_MAX_OPERATION_CELLS, SPREADSHEET_SCROLLBAR_SIZE_PX, stepSpreadsheetZoom } from "./view.ts"
|
|
89
89
|
|
|
90
90
|
export type { SelectionState } from "./selection/selectionState.ts"
|
|
91
91
|
|
|
@@ -103,12 +103,42 @@ export type { SelectionState } from "./selection/selectionState.ts"
|
|
|
103
103
|
*/
|
|
104
104
|
export type ScrollToCellOptions = { alignY?: "top" | "bottom" | "center"; alignX?: "start" | "end" | "center"; offsetX?: number; offsetY?: number }
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The size of a grid's content in CSS px of the page (v0.17): the column-letter and row-number bands (0 when `headings`
|
|
108
|
+
* is false), every column and row at its current size, and the two scroll bars the grid always reserves, all scaled by
|
|
109
|
+
* the zoom — the box a host gives the grid to show every column and row without scrolling.
|
|
110
|
+
* グリッドの中身の大きさ (ページの CSS px。v0.17)。列の記号と行の番号の帯 (`headings` が false なら 0)、今の大きさの全部の列と行、
|
|
111
|
+
* グリッドがいつも取る 2 本のスクロールバーを、どれも倍率で伸び縮みさせたもの。ホストがスクロールなしで全部の列と行を見せる箱の大きさ。
|
|
112
|
+
*/
|
|
113
|
+
export type SpreadsheetContentSize = {
|
|
114
|
+
/** The width in CSS px. 幅 (CSS px)。 */
|
|
115
|
+
readonly width: number
|
|
116
|
+
/** The height in CSS px. 高さ (CSS px)。 */
|
|
117
|
+
readonly height: number
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
/** Imperative surface (§6.9-8). / 命令 API (§6.9-8)。 */
|
|
107
121
|
export type SpreadsheetHandle = {
|
|
108
122
|
/** Row-height pair seam (§6.9-8). / 行高変更の対シーム。 */
|
|
109
123
|
resizeRow: (row: number, heightPx: number) => void
|
|
110
124
|
/** Column-width resize (§6.9-8). / 列幅変更。 */
|
|
111
125
|
resizeColumn: (col: number, widthPx: number) => void
|
|
126
|
+
/**
|
|
127
|
+
* Fits each listed column to the widest text of ALL its rows (header rows included; v0.17), as Alt+Shift+= fits the
|
|
128
|
+
* active column over the rows in view: the same painted fonts, the 9 px padding and the 10,000 px bound, measured with
|
|
129
|
+
* the cells' base font. A hidden column keeps its width and so does an empty one. Refused through
|
|
130
|
+
* onOperationRejected("resize", cells, "limit") when the cells to measure exceed `maxOperationCells`. Works whatever
|
|
131
|
+
* `enableResizeUI` and `headings` say (an imperative path, like resizeColumn).
|
|
132
|
+
* 名指した列を、その列の**全部の行** (見出しの行も) で最も広い文字に合わせる (v0.17)。Alt+Shift+= が表示中の行で active 列を
|
|
133
|
+
* 合わせるのと同じ塗りの書体・余白 9 px・上限 10,000 px で、セルの基本の書体で測る。隠した列と空の列は幅を保つ。測るセルが
|
|
134
|
+
* `maxOperationCells` を超えると onOperationRejected("resize", セル, "limit") で拒む。`enableResizeUI` と `headings` に依らず
|
|
135
|
+
* 働く (resizeColumn と同じ命令の経路)。
|
|
136
|
+
*
|
|
137
|
+
* @param columns - The columns to fit (repeats count once). 合わせる列 (重複は 1 回)。
|
|
138
|
+
* @returns True when the fit ran (a hidden or empty column changes nothing); false when refused or when the grid cannot measure (`renderCell` is set, or the grid is not mounted). 走ったら true (隠した列・空の列は何も変えない)。拒まれたとき・測れないとき (`renderCell` がある、または未装着) は false。
|
|
139
|
+
* @throws {RangeError} When a column is not an integer between 0 and the column count − 1. 列が 0〜列数 − 1 の整数でない場合。
|
|
140
|
+
*/
|
|
141
|
+
autoFitColumns: (columns: readonly number[]) => boolean
|
|
112
142
|
/**
|
|
113
143
|
* Programmatic viewport jump to a cell (delegates to the engine's scrollToCell — the
|
|
114
144
|
* floor-and-pin jump API). Selection is unchanged; the deep grid stays reachable at any
|
|
@@ -448,6 +478,29 @@ export type SpreadsheetProps = {
|
|
|
448
478
|
* 復元手段を失う (命令 API のみ)。
|
|
449
479
|
*/
|
|
450
480
|
enableResizeUI?: boolean
|
|
481
|
+
/**
|
|
482
|
+
* v0.17 — Shows the column letters, the row numbers and the corner (Excel's "headings"; not `headerRows`), true by
|
|
483
|
+
* default. False draws none of the three and collapses their grid tracks to 0, so the cells start at the grid's
|
|
484
|
+
* top-left corner; the gestures that live only on them go with them (header presses, the corner's select-all, the
|
|
485
|
+
* resize strips and unhide markers), and their keyboard twins stay (Ctrl+Space, Shift+Space, Ctrl+A, Alt+Shift+Arrow,
|
|
486
|
+
* Alt+Shift+=). The bands are `aria-hidden`, so assistive technology meets the same grid. The root carries
|
|
487
|
+
* `data-aqsp-headings-hidden` while they are hidden. LIVE prop.
|
|
488
|
+
* v0.17 — 列の記号・行の番号・角 (Excel の「見出し」。`headerRows` とは別) を見せるか (既定 true)。false は 3 つを描かず、
|
|
489
|
+
* その格子の帯を 0 にしてセルを根の左上から始める。それらの上にだけある操作 (見出しの押下・角の全選択・境の帯と再表示の印) は
|
|
490
|
+
* 無くなり、キーの双子 (Ctrl+Space・Shift+Space・Ctrl+A・Alt+Shift+矢印・Alt+Shift+=) は残る。帯は `aria-hidden` なので、
|
|
491
|
+
* 支援技術から見えるグリッドは変わらない。隠している間、根は `data-aqsp-headings-hidden` を持つ。
|
|
492
|
+
*/
|
|
493
|
+
headings?: boolean
|
|
494
|
+
/**
|
|
495
|
+
* v0.17 — Receives the size of the grid's content (`SpreadsheetContentSize`) after the first layout and again after
|
|
496
|
+
* every change of it: a track's size (a resize, an autofit, a hide or unhide, the imperative resizeRow / resizeColumn),
|
|
497
|
+
* the row or column count, the default sizes, the zoom or `headings`. An unchanged size is not repeated. A host uses it
|
|
498
|
+
* to size the grid's box to its content.
|
|
499
|
+
* v0.17 — グリッドの中身の大きさ (`SpreadsheetContentSize`) を、最初の配置の後と、それが変わるたびに受け取る (トラックの
|
|
500
|
+
* 大きさ — 境の操作・autofit・隠す・再表示・命令の resizeRow / resizeColumn —、行や列の数、既定の大きさ、倍率、`headings`)。
|
|
501
|
+
* 同じ大きさは繰り返さない。ホストはこれでグリッドの箱を中身に合わせる。
|
|
502
|
+
*/
|
|
503
|
+
onContentSizeChange?: (size: SpreadsheetContentSize) => void
|
|
451
504
|
/**
|
|
452
505
|
* AT announcement text for the active cell; omit for silence (no built-in strings). `text`
|
|
453
506
|
* is the RAW canonical getCellText; `displayText` is the value-format display string (the
|
|
@@ -837,6 +890,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
837
890
|
maxOperationCells = SPREADSHEET_MAX_OPERATION_CELLS,
|
|
838
891
|
enableFillHandle = true,
|
|
839
892
|
enableResizeUI = true,
|
|
893
|
+
headings = true,
|
|
894
|
+
onContentSizeChange,
|
|
840
895
|
formatCellAnnouncement,
|
|
841
896
|
formatSelectionAnnouncement,
|
|
842
897
|
formatCommandAnnouncement,
|
|
@@ -1064,6 +1119,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1064
1119
|
}
|
|
1065
1120
|
}, [requestZoom, zoomGestureEnabled])
|
|
1066
1121
|
const bodyRef = useRef<HTMLDivElement | null>(null)
|
|
1122
|
+
const bodyStackRef = useRef<HTMLDivElement | null>(null)
|
|
1123
|
+
const revealRef = useRef<HTMLDivElement | null>(null)
|
|
1067
1124
|
const handleRef = useRef<VirtualGridHandle | null>(null)
|
|
1068
1125
|
const sinkRef = useRef<HTMLTextAreaElement | null>(null)
|
|
1069
1126
|
const rowTrackRef = useRef<HTMLDivElement | null>(null)
|
|
@@ -1347,9 +1404,133 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1347
1404
|
ensureDepsRef.current = { rowAxis, colAxis }
|
|
1348
1405
|
ensureCellVisibleRef.current = createEnsureCellVisible({ handleRef, rowAxis, colAxis })
|
|
1349
1406
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1407
|
+
/**
|
|
1408
|
+
* A cell's rect in visual px relative to the body pane, read now through the three-way forward map (v0.9 §4.9 census
|
|
1409
|
+
* #11 — the keyboard-origin cellRect recipe, the onCellContextMenu twin): the one judgment site of this file, shared by
|
|
1410
|
+
* the keyboard cellRect and the page reveal.
|
|
1411
|
+
* セルの本体ペイン基準の視覚 px 矩形 (3 分岐順写像の判断時読み — v0.9 §4.9 census #11 の keyboard 起点 cellRect のレシピ、
|
|
1412
|
+
* onCellContextMenu と同式)。このファイル唯一の判断点で、keyboard の cellRect とページの追従が共有する。
|
|
1413
|
+
*
|
|
1414
|
+
* @param cell - The cell. セル。
|
|
1415
|
+
* @returns The rect in visual px. 視覚 px の矩形。
|
|
1416
|
+
*/
|
|
1417
|
+
const cellRectOf = useCallback(
|
|
1418
|
+
(cell: CellCoord): KeyboardCellRect => {
|
|
1419
|
+
const handle = handleRef.current
|
|
1420
|
+
const position = handle === null ? { x: 0, y: 0 } : handle.getScrollPosition()
|
|
1421
|
+
// 帯件数と extent も同じ判断で読む (v0.9 計画 §4.9、census #11 — AxisForwardCtx はこの場で
|
|
1422
|
+
// インライン構築、§2.1 規則 1)。detached ハンドルは件数 0 / extent 0 の縮退 (末尾分岐不活性 = v0.8 恒等)
|
|
1423
|
+
const frozen = handle === null ? { rows: 0, cols: 0, trailingRows: 0, trailingCols: 0 } : handle.getFrozenSize()
|
|
1424
|
+
const viewport = handle === null ? { width: 0, height: 0 } : handle.getViewportSize()
|
|
1425
|
+
// 両軸とも番兵クランプ (detached の {-1, -1} を座標へ流さない)、ズーム境界で視覚 px へ
|
|
1426
|
+
const z = zoomRef.current
|
|
1427
|
+
return {
|
|
1428
|
+
// 行 / 列とも凍結パーティション 3 分岐順写像 (末尾は extent アンカー — v0.9 §2.1)。×zoom は写像後 (契約不変)
|
|
1429
|
+
top: rowToViewportY(rowAxis, cell.row, { scroll: Math.max(0, position.y), frozenCount: frozen.rows, trailingCount: frozen.trailingRows, extent: viewport.height }) * z,
|
|
1430
|
+
left: colToViewportX(colAxis, cell.col, { scroll: Math.max(0, position.x), frozenCount: frozen.cols, trailingCount: frozen.trailingCols, extent: viewport.width }) * z,
|
|
1431
|
+
width: colAxis.sizeOf(cell.col) * z,
|
|
1432
|
+
height: rowAxis.sizeOf(cell.row) * z,
|
|
1433
|
+
}
|
|
1434
|
+
},
|
|
1435
|
+
[rowAxis, colAxis],
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Brings a cell the grid has just landed in its own viewport into view in the scroll containers outside the grid (a
|
|
1440
|
+
* host's box, the page), by the least scroll (block and inline "nearest", honouring their scroll-padding): a grid
|
|
1441
|
+
* taller or wider than its scroll container would otherwise keep the active cell off-screen, since the grid sees it
|
|
1442
|
+
* inside its own viewport (WCAG 2.4.11 / 2.4.12). The marker sits under the root, outside every container inside the
|
|
1443
|
+
* grid, and is clipped to the root's box, so only containers outside the grid move.
|
|
1444
|
+
* グリッドが自分の表示域の中へ着地させたセルを、グリッドの外のスクロールの容器 (ホストの箱・ページ) の中でも最小の量で見せる
|
|
1445
|
+
* 処理 (block・inline とも nearest、容器の scroll-padding に従う)。スクロールの容器より高い・広いグリッドでは、グリッドは自分の
|
|
1446
|
+
* 表示域の中にセルを見ているため、選んだセルが画面の外に残る (WCAG 2.4.11・2.4.12)。印は根の直下でグリッドの中のどの容器の
|
|
1447
|
+
* 外にも置き、根の箱に切り詰めるので、動くのはグリッドの外の容器だけ。
|
|
1448
|
+
*
|
|
1449
|
+
* @param cell - The cell, already in the grid's viewport. グリッドの表示域に着地済みのセル。
|
|
1450
|
+
*/
|
|
1451
|
+
const revealCellInPage = useCallback(
|
|
1452
|
+
(cell: CellCoord): void => {
|
|
1453
|
+
const marker = revealRef.current
|
|
1454
|
+
const root = rootRef.current
|
|
1455
|
+
const stack = bodyStackRef.current
|
|
1456
|
+
if (marker === null || root === null || stack === null || handleRef.current === null) {
|
|
1457
|
+
return
|
|
1458
|
+
}
|
|
1459
|
+
const z = zoomRef.current
|
|
1460
|
+
const rect = cellRectOf(cell)
|
|
1461
|
+
// 本体ペインの原点は帯の分だけ根の左上から離れる (論理 px の offset を倍率で視覚 px へ)
|
|
1462
|
+
const top = stack.offsetTop * z + rect.top
|
|
1463
|
+
const left = stack.offsetLeft * z + rect.left
|
|
1464
|
+
// 根は overflow: hidden のスクロールの容器。印が根の箱からはみ出すと scrollIntoView が根自身を動かすので、根の箱に切り詰める
|
|
1465
|
+
const clippedTop = Math.max(0, top)
|
|
1466
|
+
const clippedLeft = Math.max(0, left)
|
|
1467
|
+
const clippedBottom = Math.min(root.clientHeight, top + rect.height)
|
|
1468
|
+
const clippedRight = Math.min(root.clientWidth, left + rect.width)
|
|
1469
|
+
if (clippedBottom <= clippedTop || clippedRight <= clippedLeft) {
|
|
1470
|
+
return
|
|
1471
|
+
}
|
|
1472
|
+
marker.style.top = `${clippedTop}px`
|
|
1473
|
+
marker.style.left = `${clippedLeft}px`
|
|
1474
|
+
marker.style.width = `${clippedRight - clippedLeft}px`
|
|
1475
|
+
marker.style.height = `${clippedBottom - clippedTop}px`
|
|
1476
|
+
// 配置された箱を持たない印 (display: none の中のグリッド) には見せる位置が無い
|
|
1477
|
+
const box = marker.getBoundingClientRect()
|
|
1478
|
+
if (box.width === 0 && box.height === 0) {
|
|
1479
|
+
return
|
|
1480
|
+
}
|
|
1481
|
+
marker.scrollIntoView({ block: "nearest", inline: "nearest" })
|
|
1482
|
+
},
|
|
1483
|
+
[cellRectOf],
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
const ensureCellVisible = useCallback(
|
|
1487
|
+
(cell: CellCoord) => {
|
|
1488
|
+
ensureCellVisibleRef.current?.(cell)
|
|
1489
|
+
revealCellInPage(cell)
|
|
1490
|
+
},
|
|
1491
|
+
[revealCellInPage],
|
|
1492
|
+
)
|
|
1493
|
+
|
|
1494
|
+
// ---- 中身の大きさの知らせ (v0.17) ----
|
|
1495
|
+
const onContentSizeChangeRef = useRef(onContentSizeChange)
|
|
1496
|
+
onContentSizeChangeRef.current = onContentSizeChange
|
|
1497
|
+
const reportedContentSizeRef = useRef<SpreadsheetContentSize | null>(null)
|
|
1498
|
+
/**
|
|
1499
|
+
* Reports the content size to `onContentSizeChange` when it differs from the last one reported: the bands' tracks (the
|
|
1500
|
+
* body pane's offset in the zoom wrapper, so a host's band-size variables and `headings` count as laid out), every
|
|
1501
|
+
* track, and the two scroll bars the grid always reserves, all inside the zoom wrapper and so scaled by the zoom.
|
|
1502
|
+
* 中身の大きさを、前に知らせたものと違うときだけ `onContentSizeChange` へ知らせる処理。帯の格子の帯 (ズームの包みの中の本体
|
|
1503
|
+
* ペインの位置なので、ホストの帯の寸法の変数と `headings` が配置どおりに入る)、全部のトラック、グリッドがいつも取る 2 本の
|
|
1504
|
+
* スクロールバーで、どれもズームの包みの中にあるので倍率で伸び縮みする。
|
|
1505
|
+
*/
|
|
1506
|
+
const reportContentSize = useCallback((): void => {
|
|
1507
|
+
const listener = onContentSizeChangeRef.current
|
|
1508
|
+
const stack = bodyStackRef.current
|
|
1509
|
+
if (listener === undefined || stack === null) {
|
|
1510
|
+
return
|
|
1511
|
+
}
|
|
1512
|
+
const z = zoomRef.current
|
|
1513
|
+
const size: SpreadsheetContentSize = {
|
|
1514
|
+
width: (stack.offsetLeft + colAxis.totalSize() + SPREADSHEET_SCROLLBAR_SIZE_PX) * z,
|
|
1515
|
+
height: (stack.offsetTop + rowAxis.totalSize() + SPREADSHEET_SCROLLBAR_SIZE_PX) * z,
|
|
1516
|
+
}
|
|
1517
|
+
const last = reportedContentSizeRef.current
|
|
1518
|
+
if (last !== null && last.width === size.width && last.height === size.height) {
|
|
1519
|
+
return
|
|
1520
|
+
}
|
|
1521
|
+
reportedContentSizeRef.current = size
|
|
1522
|
+
listener(size)
|
|
1523
|
+
}, [rowAxis, colAxis])
|
|
1524
|
+
// 描画のたびに測る (倍率・headings・行と列の数・既定の大きさの変化はどれも再描画を伴う)。知らせる先が外れたら次に付いた先へ
|
|
1525
|
+
// 今の大きさを改めて知らせ、同じ関数でない新しい知らせ先 (描画ごとに作るアロー関数) には値が変わるまで繰り返さない (同じ値の
|
|
1526
|
+
// 再送が知らせ先の状態更新と再描画を無限に回すため)
|
|
1527
|
+
useLayoutEffect(() => {
|
|
1528
|
+
if (onContentSizeChange === undefined) {
|
|
1529
|
+
reportedContentSizeRef.current = null
|
|
1530
|
+
return
|
|
1531
|
+
}
|
|
1532
|
+
reportContentSize()
|
|
1533
|
+
})
|
|
1353
1534
|
|
|
1354
1535
|
// ---- 読み上げ (live region — §6.9-9) ----
|
|
1355
1536
|
const announceTimerRef = useRef<number | null>(null)
|
|
@@ -1755,6 +1936,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1755
1936
|
// 同期 repaint はエンジン再レンダー前の stale getFrozenSize — 分割線可視は
|
|
1756
1937
|
// post-commit heal が commit 済み値で整合させる (§2.7 — スクロール介在なし)
|
|
1757
1938
|
scheduleDividerHeal()
|
|
1939
|
+
// 寸法の変更は再描画を伴わない命令の経路なので、描画の後の知らせを待たずにここで知らせる
|
|
1940
|
+
reportContentSize()
|
|
1758
1941
|
return
|
|
1759
1942
|
}
|
|
1760
1943
|
for (const entry of entries) {
|
|
@@ -1768,8 +1951,9 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1768
1951
|
repaintOverlay()
|
|
1769
1952
|
// 行側も同文 (H_F / H_T の分割線可視は commit 済み値で heal — §2.7)
|
|
1770
1953
|
scheduleDividerHeal()
|
|
1954
|
+
reportContentSize()
|
|
1771
1955
|
},
|
|
1772
|
-
[rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay, scheduleDividerHeal],
|
|
1956
|
+
[rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay, scheduleDividerHeal, reportContentSize],
|
|
1773
1957
|
)
|
|
1774
1958
|
|
|
1775
1959
|
/** Pointer contextmenu relay to onCellContextMenu (§6.4.3-4 sole channel). / pointer 起点の横流し。 */
|
|
@@ -1864,9 +2048,10 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1864
2048
|
})
|
|
1865
2049
|
|
|
1866
2050
|
// ---- ヘッダー選択 (§6.8 — 列 / 行の転置共通機械) ----
|
|
2051
|
+
// 帯の上の機械は帯があるときだけ結線する (headings を切り替えると依存が変わり、新しい帯の要素へ結線し直す)
|
|
1867
2052
|
const colHeaderMachine = useHeaderSelection({
|
|
1868
2053
|
zoomRef,
|
|
1869
|
-
enabled: !isEmpty,
|
|
2054
|
+
enabled: !isEmpty && headings,
|
|
1870
2055
|
axis: "col",
|
|
1871
2056
|
headerRef: colHeaderRef,
|
|
1872
2057
|
mainAxis: colAxis,
|
|
@@ -1885,7 +2070,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1885
2070
|
})
|
|
1886
2071
|
const rowHeaderMachine = useHeaderSelection({
|
|
1887
2072
|
zoomRef,
|
|
1888
|
-
enabled: !isEmpty,
|
|
2073
|
+
enabled: !isEmpty && headings,
|
|
1889
2074
|
axis: "row",
|
|
1890
2075
|
headerRef: rowHeaderRef,
|
|
1891
2076
|
mainAxis: rowAxis,
|
|
@@ -1906,9 +2091,10 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1906
2091
|
// ---- リサイズ / unhide 機械 (§7 PT-1) + fill 機械 (§8 PT-2) ----
|
|
1907
2092
|
const headerResizeMachine = useHeaderResize({
|
|
1908
2093
|
zoomRef,
|
|
1909
|
-
enabled: !isEmpty && enableResizeUI,
|
|
2094
|
+
enabled: !isEmpty && enableResizeUI && headings,
|
|
1910
2095
|
colBandRef: colHeaderRef,
|
|
1911
2096
|
rowBandRef: rowHeaderRef,
|
|
2097
|
+
fontSourceRef: bodyRef,
|
|
1912
2098
|
colAxis,
|
|
1913
2099
|
rowAxis,
|
|
1914
2100
|
viewStore,
|
|
@@ -2144,26 +2330,12 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2144
2330
|
|
|
2145
2331
|
// ---- コマンド実行 (v0.8 §4.1.13 / ADR-17) ----
|
|
2146
2332
|
/** The active cell's viewport rect in visual px via the three-way forward map (v0.9 §4.9 census #11 — the keyboard-origin cellRect recipe, onCellContextMenu twin) for onPasteSpecialRequest. / active セルの視覚 px 矩形 — 3 分岐順写像 (v0.9 §4.9 census #11。keyboard 起点 cellRect のレシピ、onCellContextMenu と同式)。 */
|
|
2147
|
-
const keyboardCellRect = useCallback(() => {
|
|
2333
|
+
const keyboardCellRect = useCallback((): KeyboardCellRect => {
|
|
2148
2334
|
const active = selectionRef.current.active
|
|
2149
2335
|
// 画面外座標へメニューを出さない (§6.6.1) — 可視化してからハンドルの同期読み (§7-4)
|
|
2150
2336
|
ensureCellVisible(active)
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
// 帯件数と extent も同じ判断で読む (v0.9 計画 §4.9、census #11 — AxisForwardCtx はこの場で
|
|
2154
|
-
// インライン構築、§2.1 規則 1)。detached ハンドルは件数 0 / extent 0 の縮退 (末尾分岐不活性 = v0.8 恒等)
|
|
2155
|
-
const frozen = handle === null ? { rows: 0, cols: 0, trailingRows: 0, trailingCols: 0 } : handle.getFrozenSize()
|
|
2156
|
-
const viewport = handle === null ? { width: 0, height: 0 } : handle.getViewportSize()
|
|
2157
|
-
// 両軸とも番兵クランプ (detached の {-1, -1} を座標へ流さない)、ズーム境界で視覚 px へ
|
|
2158
|
-
const z = zoomRef.current
|
|
2159
|
-
return {
|
|
2160
|
-
// 行 / 列とも凍結パーティション 3 分岐順写像 (末尾は extent アンカー — v0.9 §2.1)。×zoom は写像後 (契約不変)
|
|
2161
|
-
top: rowToViewportY(rowAxis, active.row, { scroll: Math.max(0, position.y), frozenCount: frozen.rows, trailingCount: frozen.trailingRows, extent: viewport.height }) * z,
|
|
2162
|
-
left: colToViewportX(colAxis, active.col, { scroll: Math.max(0, position.x), frozenCount: frozen.cols, trailingCount: frozen.trailingCols, extent: viewport.width }) * z,
|
|
2163
|
-
width: colAxis.sizeOf(active.col) * z,
|
|
2164
|
-
height: rowAxis.sizeOf(active.row) * z,
|
|
2165
|
-
}
|
|
2166
|
-
}, [rowAxis, colAxis, ensureCellVisible])
|
|
2337
|
+
return cellRectOf(active)
|
|
2338
|
+
}, [ensureCellVisible, cellRectOf])
|
|
2167
2339
|
const commands = useCommands({
|
|
2168
2340
|
adapter,
|
|
2169
2341
|
// キーボードの寸法の変更は帯のリサイズ UI と同じ可否 (enableResizeUI が false なら利用者は寸法を変えない)
|
|
@@ -2725,6 +2897,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2725
2897
|
repaintOverlay()
|
|
2726
2898
|
// 分割線可視は commit 済み getFrozenSize の post-commit heal で整合 (§2.7)
|
|
2727
2899
|
scheduleDividerHeal()
|
|
2900
|
+
reportContentSize()
|
|
2728
2901
|
},
|
|
2729
2902
|
resizeColumn: (col: number, widthPx: number) => {
|
|
2730
2903
|
// 対呼び出し (§3.8): colAxis (SSOT) を先に更新し、グリッドの幅木へ通知する。
|
|
@@ -2739,6 +2912,15 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2739
2912
|
repaintOverlay()
|
|
2740
2913
|
// 分割線可視は commit 済み getFrozenSize の post-commit heal で整合 (§2.7)
|
|
2741
2914
|
scheduleDividerHeal()
|
|
2915
|
+
reportContentSize()
|
|
2916
|
+
},
|
|
2917
|
+
autoFitColumns: (columns: readonly number[]) => {
|
|
2918
|
+
for (const col of columns) {
|
|
2919
|
+
if (!Number.isInteger(col) || col < 0 || col >= colAxis.count) {
|
|
2920
|
+
throw new RangeError(`[spreadsheet] autoFitColumns: each column must be a whole number from 0 to ${colAxis.count - 1} (got ${String(col)})`)
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
return headerResizeMachine.autofitColumnsInAllRows(columns)
|
|
2742
2924
|
},
|
|
2743
2925
|
scrollToCell: (cell: CellCoord, options?: ScrollToCellOptions) => {
|
|
2744
2926
|
// エンジンのセル狙いジャンプへ素通し (§7-4 の真実。選択は変えない — 純 viewport 移動)。
|
|
@@ -2773,7 +2955,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2773
2955
|
openPopoverRef.current(kind)
|
|
2774
2956
|
},
|
|
2775
2957
|
}),
|
|
2776
|
-
[rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay, scheduleDividerHeal, focusSink, isEditing, rearmSink, adapter, paintedFormatOf, startEdit, requireCommands],
|
|
2958
|
+
[rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay, scheduleDividerHeal, reportContentSize, headerResizeMachine, focusSink, isEditing, rearmSink, adapter, paintedFormatOf, startEdit, requireCommands],
|
|
2777
2959
|
)
|
|
2778
2960
|
|
|
2779
2961
|
// ---- 空グリッド分岐 (§6.9-1: フックの後・結線の前) ----
|
|
@@ -2791,75 +2973,79 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2791
2973
|
return (
|
|
2792
2974
|
// biome-ignore lint/a11y/useSemanticElements: fieldset はフォーム意味論であり不適。role="group" は Escape 脱出の着地点に現在地名を与えるための選択 (§6.9-7 / §10-4)
|
|
2793
2975
|
// biome-ignore lint/a11y/noNoninteractiveTabindex: tabIndex=0 は WCAG 2.1.2 の脱出着地点 + グリッド再進入口 (§10-4) — 意図的な tab stop
|
|
2794
|
-
<div ref={rootRef} className={className ? `aqsp-root ${className}` : "aqsp-root"} role="group" {...rootName} tabIndex={0} onFocus={onRootFocus} onBlur={onRootBlur} data-aqsp-root="" data-testid={testId}>
|
|
2976
|
+
<div ref={rootRef} className={className ? `aqsp-root ${className}` : "aqsp-root"} role="group" {...rootName} tabIndex={0} onFocus={onRootFocus} onBlur={onRootBlur} data-aqsp-root="" data-aqsp-headings-hidden={headings ? undefined : ""} data-testid={testId}>
|
|
2795
2977
|
{/* ズームラッパー (v0.3): 視覚は transform スケール、内側レイアウトは 100%/z の論理 px —
|
|
2796
2978
|
virtualscroll の実測 (ResizeObserver / clientHeight) は論理値のまま。z=1 は無スタイル
|
|
2797
2979
|
(v0.2 と byte 同一の DOM 幾何 — 既存全ゲートの不変性) */}
|
|
2798
2980
|
<div className="aqsp-zoom" data-aqsp-zoom={zoomValue} onPointerDown={onZoomPointerDown} style={zoomValue === 1 ? undefined : { width: `${100 / zoomValue}%`, height: `${100 / zoomValue}%`, transform: `scale(${zoomValue})` }}>
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2981
|
+
{headings ? (
|
|
2982
|
+
<>
|
|
2983
|
+
<div
|
|
2984
|
+
className="aqsp-corner"
|
|
2985
|
+
aria-hidden="true"
|
|
2986
|
+
data-aqsp-corner=""
|
|
2987
|
+
onPointerDown={(event) => {
|
|
2988
|
+
// mouse 限定 (§6.4.1 の表ヘッダー「mouse のみ」— エンジン / ヘッダー機械と同一ゲート)
|
|
2989
|
+
if (event.pointerType !== "mouse") {
|
|
2990
|
+
return
|
|
2991
|
+
}
|
|
2992
|
+
// 非主ボタンも既定のフォーカス移動だけは止める (全押下面共通 — R2 是正)
|
|
2993
|
+
event.preventDefault()
|
|
2994
|
+
if (event.button !== 0) {
|
|
2995
|
+
return
|
|
2996
|
+
}
|
|
2997
|
+
// 参照ポインティング中 (v0.8 §4.2.13): コーナー押下は consume + シンクへ焦点 (全選択も確定もしない)
|
|
2998
|
+
if (isPointing()) {
|
|
2999
|
+
focusSink()
|
|
3000
|
+
return
|
|
3001
|
+
}
|
|
3002
|
+
// 編集中の requestCommit ゲート (§6.8-3 — コーナーもフォーカス不動で focusout 不発)
|
|
3003
|
+
if (isEditing() && !requestCommit()) {
|
|
3004
|
+
focusSink()
|
|
3005
|
+
return
|
|
3006
|
+
}
|
|
3007
|
+
focusSink()
|
|
3008
|
+
// コーナーの全セル選択は単一構築 state の setSelection 1 回 (過渡状態なし)。active = A1
|
|
3009
|
+
setSelection({
|
|
3010
|
+
ranges: [makeRange({ row: 0, col: 0 }, { row: adapter.rowCount - 1, col: adapter.colCount - 1 })],
|
|
3011
|
+
anchor: { row: 0, col: 0 },
|
|
3012
|
+
active: { row: 0, col: 0 },
|
|
3013
|
+
})
|
|
3014
|
+
}}
|
|
3015
|
+
/>
|
|
3016
|
+
<ColumnHeader
|
|
3017
|
+
viewStore={viewStore}
|
|
3018
|
+
colAxis={colAxis}
|
|
3019
|
+
frozenCols={frozenColumns}
|
|
3020
|
+
frozenTrailingCols={frozenTrailingColumns}
|
|
3021
|
+
handleRef={handleRef}
|
|
3022
|
+
bandRef={colHeaderRef}
|
|
3023
|
+
colTrackRef={colTrackRef}
|
|
3024
|
+
windowOriginXRef={windowOriginXRef}
|
|
3025
|
+
repaintColHeaders={repaintColHeaders}
|
|
3026
|
+
selectionRanges={selection.ranges}
|
|
3027
|
+
enableResizeUI={enableResizeUI}
|
|
3028
|
+
dividerLeadingRef={dividerXLeadingRef}
|
|
3029
|
+
dividerTrailingRef={dividerXTrailingRef}
|
|
3030
|
+
/>
|
|
3031
|
+
<RowHeader
|
|
3032
|
+
viewStore={viewStore}
|
|
3033
|
+
rowAxis={rowAxis}
|
|
3034
|
+
handleRef={handleRef}
|
|
3035
|
+
bandRef={rowHeaderRef}
|
|
3036
|
+
selectionRanges={selection.ranges}
|
|
3037
|
+
rowTrackRef={rowTrackRef}
|
|
3038
|
+
windowOriginYRef={windowOriginYRef}
|
|
3039
|
+
repaintHeaders={repaintHeaders}
|
|
3040
|
+
enableResizeUI={enableResizeUI}
|
|
3041
|
+
frozenRows={frozenRows}
|
|
3042
|
+
frozenTrailingRows={frozenTrailingRows}
|
|
3043
|
+
dividerLeadingRef={dividerYLeadingRef}
|
|
3044
|
+
dividerTrailingRef={dividerYTrailingRef}
|
|
3045
|
+
/>
|
|
3046
|
+
</>
|
|
3047
|
+
) : null}
|
|
3048
|
+
<div ref={bodyStackRef} className="aqsp-body-stack" data-aqsp-body-stack="">
|
|
2863
3049
|
<GridBody
|
|
2864
3050
|
adapter={adapter}
|
|
2865
3051
|
rowAxis={rowAxis}
|
|
@@ -2934,6 +3120,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2934
3120
|
</div>
|
|
2935
3121
|
{hasAnnouncement ? <div ref={liveRegionRef} className="aqsp-live-region" aria-live="polite" data-aqsp-live-region="" /> : null}
|
|
2936
3122
|
</div>
|
|
3123
|
+
{/* キーの移動で選んだセルをページの中で見せる印 (位置は revealCellInPage が書く)。ズームの包みの外の根の直下に置き、グリッドの中の容器を動かさない */}
|
|
3124
|
+
<div ref={revealRef} className="aqsp-reveal" aria-hidden="true" data-aqsp-reveal="" />
|
|
2937
3125
|
</div>
|
|
2938
3126
|
)
|
|
2939
3127
|
})
|