@aiquants/spreadsheet 0.15.0 → 0.16.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.
- package/CHANGELOG.md +105 -0
- package/README.md +209 -23
- package/dist/index.cjs +8 -8
- package/dist/index.d.cts +123 -5
- package/dist/index.d.ts +123 -5
- package/dist/index.js +4315 -4166
- package/dist/styles/spreadsheet.css +1 -1
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/dist/tsv.cjs +8 -1
- package/dist/tsv.d.cts +79 -4
- package/dist/tsv.d.ts +79 -4
- package/dist/tsv.js +85 -3
- package/dist/view-BkwCUzTl.cjs +1 -0
- package/dist/view-DycJZi_t.js +41 -0
- package/dist/view.cjs +1 -1
- package/dist/view.d.cts +31 -7
- package/dist/view.d.ts +31 -7
- package/dist/view.js +12 -12
- package/package.json +3 -3
- package/src/Spreadsheet.tsx +110 -14
- package/src/clipboard/tsv.ts +236 -89
- package/src/clipboard/useClipboard.ts +3 -2
- package/src/commands/useCommands.ts +43 -1
- package/src/grid/GridBody.tsx +51 -6
- package/src/header/resizeGates.ts +34 -0
- package/src/header/useHeaderResize.ts +83 -16
- package/src/index.ts +1 -1
- package/src/keyboard/shortcuts.ts +67 -0
- package/src/model/dataAdapter.ts +74 -0
- package/src/overlay/SelectionOverlay.tsx +22 -2
- package/src/styles/spreadsheet.css +193 -123
- package/src/tsv.ts +17 -10
- package/src/view.ts +67 -14
- package/dist/tsv-B6TDExlk.js +0 -91
- package/dist/tsv-BHXbWK5W.cjs +0 -8
package/src/grid/GridBody.tsx
CHANGED
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
* helper (ADR-31 — no package CSS; capability absent ⇒ O(1) false ⇒ keys absent ⇒ the DOM
|
|
29
29
|
* stays byte-identical).
|
|
30
30
|
*
|
|
31
|
+
* Accessibility structure (v0.16): the body wrapper is the ONE `role="grid"` element (row / column counts, its name
|
|
32
|
+
* from `labelledBy` or the built-in root label, `aria-readonly` from `readOnly`; no `aria-multiselectable`, since no
|
|
33
|
+
* cell carries `aria-selected` — the live region announces the selection), so the frozen leading,
|
|
34
|
+
* scrolling and frozen trailing panes VirtualGrid draws inside it are all owned by it; the scrolling pane's content
|
|
35
|
+
* element is the body `rowgroup`, the cells of the first `headerRows` rows are `columnheader` (`data-aqsp-header-row`),
|
|
36
|
+
* and both scroll bars are declared pointer-only (`enableArrowButtonTabStops: false` — the grid owns keyboard
|
|
37
|
+
* scrolling), so no `scrollbar` / `button` becomes a child of the grid.
|
|
38
|
+
*
|
|
31
39
|
* UI chrome locale: the resolved `locale` and the value-stable engine subset `scrollLabels` are
|
|
32
40
|
* forwarded to VirtualGrid, which localizes the embedded vertical bar, the horizontal bar and the
|
|
33
41
|
* degenerate-band "No items" — no lang attribute, no new data-* hook.
|
|
@@ -55,6 +63,13 @@
|
|
|
55
63
|
* `aria-readonly="true"` + `data-aqsp-readonly=""` を `isReadOnlyCell` ヘルパ経由で条件スタンプ
|
|
56
64
|
* する (ADR-31 — パッケージ CSS なし。能力未宣言 = O(1) false = キー不在 = DOM バイト同一)。
|
|
57
65
|
*
|
|
66
|
+
* アクセシビリティの構造 (v0.16): 本体の包みが唯一の `role="grid"` (行数 / 列数・`labelledBy` か組み込みのルートの名前による
|
|
67
|
+
* 名前・`readOnly` からの `aria-readonly`。セルが `aria-selected` を持たないので `aria-multiselectable` は付けず、選択は
|
|
68
|
+
* live region が読み上げる) で、VirtualGrid がその中に描く先頭の凍結・スクロール・末尾の凍結の
|
|
69
|
+
* ペインを全て持つ。スクロールのペインの中身の要素は本体の `rowgroup`、先頭 `headerRows` 行のセルは `columnheader`
|
|
70
|
+
* (`data-aqsp-header-row`)、両方のスクロールバーはポインタ専用 (`enableArrowButtonTabStops: false` — キーボードのスクロールは
|
|
71
|
+
* グリッドが持つ) と宣言し、`scrollbar` / `button` をグリッドの子にしない。
|
|
72
|
+
*
|
|
58
73
|
* UI クロームロケール: 解決済み `locale` と値安定なエンジン部分集合 `scrollLabels` を VirtualGrid へ
|
|
59
74
|
* 素通しし、埋込み縦バー・横バー・退化帯の "No items" を現地化する (lang 属性・新規 data-* フックなし)。
|
|
60
75
|
*/
|
|
@@ -83,6 +98,15 @@ const ALIGN_ONLY_STYLES: Readonly<Record<CellAlign, CellStyle>> = Object.freeze(
|
|
|
83
98
|
right: Object.freeze({ justifyContent: alignToJustify("right") }),
|
|
84
99
|
})
|
|
85
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The scrolling pane's content element is the row group of the body rows; the `role="grid"` element is the body wrapper,
|
|
103
|
+
* which contains the frozen leading pane, the scrolling pane and the frozen trailing pane, so every row has the grid as
|
|
104
|
+
* an ancestor.
|
|
105
|
+
* スクロールするペインの中身の要素は本体の行の行グループ。`role="grid"` は本体の包みが持ち、先頭の凍結のペイン・スクロールの
|
|
106
|
+
* ペイン・末尾の凍結のペインを全て含むので、どの行もグリッドを祖先に持つ。
|
|
107
|
+
*/
|
|
108
|
+
const BODY_ROWGROUP_PROPS = Object.freeze({ role: "rowgroup" }) as { readonly role: "rowgroup" }
|
|
109
|
+
|
|
86
110
|
/** Whole-Map clearing threshold of the canonical-JSON style intern (the CF memo's bound). / 正準 JSON style intern の全消去しきい値 (CF memo と同じ上限)。 */
|
|
87
111
|
export const STYLE_INTERN_LIMIT = 4096
|
|
88
112
|
|
|
@@ -287,6 +311,14 @@ type GridBodyProps = {
|
|
|
287
311
|
frozenTrailingColumns: number
|
|
288
312
|
/** Trailing frozen rows (v0.9 — VirtualGrid frozenTrailingRows engine-validated pass-through; the row twin). / 末尾凍結行数 (v0.9 — frozenTrailingRows のエンジン検証素通し。行双子)。 */
|
|
289
313
|
frozenTrailingRows: number
|
|
314
|
+
/** Leading rows whose cells are column headers (validated by the root against frozenRows). / セルが列の見出しになる先頭の行数 (root が frozenRows と照らして検証済み)。 */
|
|
315
|
+
headerRows: number
|
|
316
|
+
/** The id naming the grid (`aria-labelledby` of the `role="grid"` element), or undefined. / グリッドに名前を与える id (`role="grid"` の要素の `aria-labelledby`)。無ければ undefined。 */
|
|
317
|
+
labelledBy: string | undefined
|
|
318
|
+
/** The grid's own name (`aria-label`) when no `labelledBy` names it, else undefined. / `labelledBy` が無いときのグリッド自身の名前 (`aria-label`)。あれば undefined。 */
|
|
319
|
+
gridLabel: string | undefined
|
|
320
|
+
/** Whole-grid read-only (`aria-readonly` of the `role="grid"` element). / グリッド全体の読み取り専用 (`role="grid"` の要素の `aria-readonly`)。 */
|
|
321
|
+
readOnly: boolean
|
|
290
322
|
renderCell?: (ctx: RenderCellContext) => ReactNode
|
|
291
323
|
/** 2D scroll notification from the grid handle (the truth — §7-4). / グリッドハンドル発の 2 軸スクロール通知 (§7-4 の真実側)。 */
|
|
292
324
|
onScroll: (position: { x: number; y: number }) => void
|
|
@@ -382,6 +414,10 @@ const GridBodyInner = ({
|
|
|
382
414
|
frozenRows,
|
|
383
415
|
frozenTrailingColumns,
|
|
384
416
|
frozenTrailingRows,
|
|
417
|
+
headerRows,
|
|
418
|
+
labelledBy,
|
|
419
|
+
gridLabel,
|
|
420
|
+
readOnly,
|
|
385
421
|
renderCell,
|
|
386
422
|
onScroll,
|
|
387
423
|
onRangeChange,
|
|
@@ -424,14 +460,14 @@ const GridBodyInner = ({
|
|
|
424
460
|
const getRowHeight = useCallback((row: number) => rowAxis.sizeOf(row), [rowAxis])
|
|
425
461
|
const getColWidth = useCallback((col: number) => colAxis.sizeOf(col), [colAxis])
|
|
426
462
|
const getCell = useCallback((row: number, col: number) => adapter.getCellText(row, col), [adapter])
|
|
427
|
-
// ARIA コンテナ属性 (identity は行列数にのみ追随)。型は行側と同型の狭い contentProps
|
|
428
|
-
const contentProps = useMemo(() => ({ role: "grid", "aria-rowcount": adapter.rowCount, "aria-colcount": adapter.colCount, "aria-multiselectable": true }) as const, [adapter.rowCount, adapter.colCount])
|
|
429
463
|
// 挙動オプション: 行木は identity 変化で再構築 (軸再構築追随)、列幅木も対称 + defaultColWidth
|
|
430
464
|
// (§7-8 — サンプリング省略の Fenwick baseValue)。pointerDragInputs はペン / タッチのみ
|
|
431
465
|
// (マウスドラッグは選択エンジンの所有)
|
|
432
466
|
const behaviorOptions = useMemo(() => ({ enableKeyboardNavigation: false, pointerDragInputs: ["pen", "touch"] as const, resetOnGetItemHeightChange: true, resetOnGetColWidthChange: true, defaultColWidth: defaultColumnWidth }), [defaultColumnWidth])
|
|
433
467
|
// タップサークル素通し (v0.10 §2.1): 未指定は undefined キー同乗 = エンジン宣言の不在信号
|
|
434
|
-
|
|
468
|
+
// 矢印の Tab の止まり先は false: キーボードのスクロールはグリッド自身 (シンク) が持つので、バーはネイティブのスクロールバーと同じ
|
|
469
|
+
// ポインタ専用の部品になり (aria-hidden)、role="grid" の中に許されない子 (scrollbar・button) を作らない
|
|
470
|
+
const scrollBarOptions = useMemo(() => ({ width: SPREADSHEET_SCROLLBAR_SIZE_PX, tapScrollCircleOptions, enableArrowButtonTabStops: false }), [tapScrollCircleOptions])
|
|
435
471
|
// 表示値書式の掛け金 (getCellProps が書き、同じセルの children が直後に消費 — 番兵は届かない座標)
|
|
436
472
|
const displayLatchRef = useRef<DisplayLatch>({ row: -1, col: -1, value: undefined })
|
|
437
473
|
/**
|
|
@@ -446,9 +482,10 @@ const GridBodyInner = ({
|
|
|
446
482
|
*/
|
|
447
483
|
const getCellProps = useCallback(
|
|
448
484
|
(row: number, col: number) => {
|
|
485
|
+
const isHeader = row < headerRows
|
|
449
486
|
const base = {
|
|
450
487
|
// aria-rowindex は行ラッパーと重複出力 (合法な上位集合) — セル単独が AT に読まれた際の行文脈補助 (§10-1)
|
|
451
|
-
role: "gridcell",
|
|
488
|
+
role: isHeader ? "columnheader" : "gridcell",
|
|
452
489
|
"aria-rowindex": row + 1,
|
|
453
490
|
"aria-colindex": col + 1,
|
|
454
491
|
"data-aqsp-cell": "",
|
|
@@ -460,6 +497,7 @@ const GridBodyInner = ({
|
|
|
460
497
|
// 単一居住 — layer-2 census)。未宣言 = O(1) false = キー不在 = v0.9 と DOM バイト同一、
|
|
461
498
|
// 宣言ホストは塗りセル毎 1 述語読み (ADR-31 の登記コスト)
|
|
462
499
|
...(isReadOnlyCell(adapter, row, col) ? { "aria-readonly": "true", "data-aqsp-readonly": "" } : {}),
|
|
500
|
+
...(isHeader ? { "data-aqsp-header-row": "" } : {}),
|
|
463
501
|
}
|
|
464
502
|
// v0.12 ホスト属性の継ぎ目: resolveCellAttributes が予約キー (data-aqsp* / data-row /
|
|
465
503
|
// data-col) と非 data-* を fail-fast 済みなのでパッケージ属性と衝突しない。未宣言は
|
|
@@ -477,7 +515,7 @@ const GridBodyInner = ({
|
|
|
477
515
|
}
|
|
478
516
|
return (rich.style === null ? { ...stamped, ...rich.attrs } : { ...stamped, ...rich.attrs, style: rich.style }) as HTMLAttributes<HTMLDivElement>
|
|
479
517
|
},
|
|
480
|
-
[adapter, rowAxis, colAxis, cf],
|
|
518
|
+
[adapter, rowAxis, colAxis, cf, headerRows],
|
|
481
519
|
)
|
|
482
520
|
/** Row attribute surface — carries the engine event gate (data-aqsp-cells §6.4.3-1). / 行属性面 — エンジンのイベントゲート (data-aqsp-cells) を運ぶ。 */
|
|
483
521
|
const getRowProps = useCallback(
|
|
@@ -518,11 +556,18 @@ const GridBodyInner = ({
|
|
|
518
556
|
)
|
|
519
557
|
|
|
520
558
|
return (
|
|
559
|
+
// biome-ignore lint/a11y/useSemanticElements: <table> を描けない — 行は VirtualGrid が 3 つのペインに仮想化して描き、表の構造要素の入れ子を作れないため、ARIA の grid で全ての行を包む
|
|
521
560
|
<div
|
|
522
561
|
ref={bodyRef}
|
|
523
562
|
className="aqsp-body"
|
|
524
563
|
data-aqsp-body=""
|
|
525
564
|
data-testid={testId ? `${testId}-body` : undefined}
|
|
565
|
+
role="grid"
|
|
566
|
+
aria-rowcount={adapter.rowCount}
|
|
567
|
+
aria-colcount={adapter.colCount}
|
|
568
|
+
aria-label={gridLabel}
|
|
569
|
+
aria-labelledby={labelledBy}
|
|
570
|
+
aria-readonly={readOnly ? "true" : undefined}
|
|
526
571
|
onPointerDownCapture={(event) => {
|
|
527
572
|
// バー帯の統合ゲート (§6.8-3 — v0.4 でスクロールバー 2 本ともグリッド内在化):
|
|
528
573
|
// セル領域 (data-aqsp-cells) 由来は選択エンジンの所有 — 素通し。それ以外 (縦バー /
|
|
@@ -574,7 +619,7 @@ const GridBodyInner = ({
|
|
|
574
619
|
getCell={getCell}
|
|
575
620
|
getCellProps={getCellProps}
|
|
576
621
|
getRowProps={getRowProps}
|
|
577
|
-
contentProps={
|
|
622
|
+
contentProps={BODY_ROWGROUP_PROPS}
|
|
578
623
|
behaviorOptions={behaviorOptions}
|
|
579
624
|
scrollBarOptions={scrollBarOptions}
|
|
580
625
|
overscanRows={overscanRows}
|
|
@@ -55,6 +55,40 @@ export const MAX_TRACK_SIZE_PX = 10_000
|
|
|
55
55
|
*/
|
|
56
56
|
export const ROW_FANOUT_DIVISOR = 10
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The ratio of one keyboard resize step: the zoom ladder's 2^(1/4), so four presses double a track and four halve it,
|
|
60
|
+
* at every size.
|
|
61
|
+
* キーボードの寸法の 1 段の比。拡大率の段と同じ 2^(1/4) で、どの寸法でも 4 回で 2 倍、4 回で半分になる。
|
|
62
|
+
*/
|
|
63
|
+
const KEYBOARD_RESIZE_RATIO = 2 ** (1 / 4)
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The size one keyboard step gives a track (increaseColumnWidth / decreaseColumnWidth / increaseRowHeight /
|
|
67
|
+
* decreaseRowHeight): `size × 2^(1/4)` up and `size / 2^(1/4)` down, rounded to whole px, moving at least 1 px, never
|
|
68
|
+
* below 1 px (a step never hides a track) and never above MAX_TRACK_SIZE_PX. A track already at a bound keeps its size:
|
|
69
|
+
* a hidden track (0 px) stays hidden in both directions (only unhide restores it), and growing never shrinks a track a
|
|
70
|
+
* host made wider than MAX_TRACK_SIZE_PX.
|
|
71
|
+
* キーボードの 1 段がトラックに与える寸法。上は `size × 2^(1/4)`、下は `size / 2^(1/4)` を整数 px に丸め、少なくとも 1 px 動き、
|
|
72
|
+
* 1 px を下回らず (1 段でトラックを隠さない)、MAX_TRACK_SIZE_PX を超えない。端にあるトラックは寸法を保つ: 隠れたトラック (0 px)
|
|
73
|
+
* はどちらの向きでも隠れたまま (戻すのは再表示だけ)、ホストが MAX_TRACK_SIZE_PX より広くしたトラックを大きくする向きで縮めない。
|
|
74
|
+
*
|
|
75
|
+
* @param size - The track's current size in px. トラックの今の寸法 (px)。
|
|
76
|
+
* @param direction - 1 to grow, -1 to shrink. 大きくするなら 1、小さくするなら -1。
|
|
77
|
+
* @returns The new size in px. 新しい寸法 (px)。
|
|
78
|
+
*/
|
|
79
|
+
export const keyboardStepSize = (size: number, direction: 1 | -1): number => {
|
|
80
|
+
if (size <= 0) {
|
|
81
|
+
return size
|
|
82
|
+
}
|
|
83
|
+
if (direction > 0) {
|
|
84
|
+
if (size >= MAX_TRACK_SIZE_PX) {
|
|
85
|
+
return size
|
|
86
|
+
}
|
|
87
|
+
return Math.min(MAX_TRACK_SIZE_PX, Math.max(size + 1, Math.round(size * KEYBOARD_RESIZE_RATIO)))
|
|
88
|
+
}
|
|
89
|
+
return Math.max(1, Math.min(size - 1, Math.round(size / KEYBOARD_RESIZE_RATIO)))
|
|
90
|
+
}
|
|
91
|
+
|
|
58
92
|
/** One track-size entry of a resize batch (size in logical px, 0 = hidden). / リサイズバッチの 1 entry (論理 px、0 = 非表示)。 */
|
|
59
93
|
export type ResizeEntry = { index: number; size: number }
|
|
60
94
|
|
|
@@ -63,7 +63,7 @@ import type { ViewStore } from "../model/viewStore.ts"
|
|
|
63
63
|
import type { ResizeGuide } from "../overlay/SelectionOverlay.tsx"
|
|
64
64
|
import type { SelectionState } from "../selection/selectionState.ts"
|
|
65
65
|
import type { SelectionEngine } from "../selection/useSelectionEngine.ts"
|
|
66
|
-
import { gateResizeEntries, type ResizeEntry, ROW_FANOUT_DIVISOR, ROW_HIDDEN_LIMIT } from "./resizeGates.ts"
|
|
66
|
+
import { gateResizeEntries, keyboardStepSize, type ResizeEntry, ROW_FANOUT_DIVISOR, ROW_HIDDEN_LIMIT } from "./resizeGates.ts"
|
|
67
67
|
|
|
68
68
|
/** Autofit horizontal padding: cell padding 4+4 + right border 1 (px). / autofit の水平パディング (セル padding 4+4 + 右罫 1)。 */
|
|
69
69
|
const AUTOFIT_PADDING = 9
|
|
@@ -97,6 +97,8 @@ type UseHeaderResizeParams = {
|
|
|
97
97
|
adapter: SpreadsheetDataAdapter
|
|
98
98
|
/** Autofit adoption condition: renderCell unspecified (custom DOM defeats measureText — §10 deviation). / autofit 採用条件 (renderCell 未指定)。 */
|
|
99
99
|
autofitEnabled: boolean
|
|
100
|
+
/** Leading header rows (`headerRows`): autofit measures their cells in the stylesheet's header-row weight. / 先頭の見出しの行数 (`headerRows`)。autofit はそのセルをスタイルシートの見出し行の太さで測る。 */
|
|
101
|
+
headerRows: number
|
|
100
102
|
defaultRowHeight: number
|
|
101
103
|
defaultColumnWidth: number
|
|
102
104
|
maxOperationCells: number
|
|
@@ -125,7 +127,7 @@ type UseHeaderResizeParams = {
|
|
|
125
127
|
* cellPaint と同式) / フォント族を重ねる)。書式 object 毎に intern (塗り intern が書式毎に 1 object を
|
|
126
128
|
* 保つので、異なりフォント毎に 1 文字列)。
|
|
127
129
|
*/
|
|
128
|
-
const fontKeyOf = (painted: CellFormat, fontKeys: WeakMap<CellFormat, string>, bandStyle: CSSStyleDeclaration): string => {
|
|
130
|
+
const fontKeyOf = (painted: CellFormat, fontKeys: WeakMap<CellFormat, string>, bandStyle: Pick<CSSStyleDeclaration, "fontStyle" | "fontWeight" | "fontSize" | "fontFamily">): string => {
|
|
129
131
|
const cached = fontKeys.get(painted)
|
|
130
132
|
if (cached !== undefined) {
|
|
131
133
|
return cached
|
|
@@ -169,10 +171,26 @@ type ResizeGesture = {
|
|
|
169
171
|
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
172
|
|
|
171
173
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
+
* The resize machine's surface: the §7.4 drag surface plus the keyboard twins of the strips, which resize through the
|
|
175
|
+
* same fan-out, gates and batch seam as a strip release. Both return whether the command ran: false only after a
|
|
176
|
+
* refusal through onOperationRejected (and for autofit while renderCell is set); a track already at a bound, an empty
|
|
177
|
+
* column or nothing to measure is a run that changes nothing (true).
|
|
178
|
+
* リサイズ機械の面。§7.4 のドラッグ面に、ストリップのキーボードの双子を加えたもの (ストリップの放しと同じファンアウト・
|
|
179
|
+
* ゲート・バッチの継ぎ目で寸法を変える)。どちらもコマンドが走ったかを返す: false は onOperationRejected で拒まれたとき
|
|
180
|
+
* (と renderCell がある間の autofit) だけで、端にあるトラック・全空列・測るものが無いときは何も変えずに走った (true)。
|
|
174
181
|
*/
|
|
175
|
-
export
|
|
182
|
+
export type HeaderResizeMachine = SelectionEngine & {
|
|
183
|
+
/** Grows (1) or shrinks (-1) track `index` by one keyboard step (keyboardStepSize), with the strip's fan-out. / トラック `index` を 1 段大きく (1) / 小さく (-1) する (keyboardStepSize。ストリップと同じファンアウト)。 */
|
|
184
|
+
stepTrack: (axis: "row" | "col", index: number, direction: 1 | -1) => boolean
|
|
185
|
+
/** Fits column `index` to its content, as the strip double-click does (false when renderCell is set). / 列 `index` を中身に合わせる (ストリップのダブルクリックと同じ。renderCell があれば false)。 */
|
|
186
|
+
autofitColumn: (index: number) => boolean
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Wires the resize/unhide machine onto both header bands; returns the §7.4 composite surface with the keyboard twins.
|
|
191
|
+
* 両ヘッダー帯へリサイズ / unhide 機械を結線し、§7.4 の合成面とキーボードの双子を返す。
|
|
192
|
+
*/
|
|
193
|
+
export const useHeaderResize = (params: UseHeaderResizeParams): HeaderResizeMachine => {
|
|
176
194
|
// paramsRef パターン: リスナーは結線 1 回、最新パラメータはレンダー毎にここへ写す
|
|
177
195
|
const paramsRef = useRef(params)
|
|
178
196
|
paramsRef.current = params
|
|
@@ -271,16 +289,17 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
271
289
|
* 純関数ゲート (resizeGates.ts: 件数ゲート・丸め・§7.2 の押下トラック 0 化ガード・§5.4(b) の行軸
|
|
272
290
|
* 非表示総数ゲート) を通してから resizeTracks を 1 回呼ぶ。拒否は onOperationRejected("resize")。
|
|
273
291
|
*/
|
|
274
|
-
const applyEntries = useCallback((axis: "row" | "col", rawEntries: readonly ResizeEntry[], pressedIndex: number | null):
|
|
292
|
+
const applyEntries = useCallback((axis: "row" | "col", rawEntries: readonly ResizeEntry[], pressedIndex: number | null): boolean => {
|
|
275
293
|
const p = paramsRef.current
|
|
276
294
|
const axisModel = axis === "col" ? p.colAxis : p.rowAxis
|
|
277
295
|
// ゲートは純関数 (§4.2.10 の委譲) — ヘッダー由来のサイズは常に有限 ≥ 0 なので RangeError 枝は到達しない
|
|
278
296
|
const result = gateResizeEntries(axisModel, rawEntries, pressedIndex, { maxOperationCells: p.maxOperationCells, rowHiddenLimit: ROW_HIDDEN_LIMIT, axisKind: axis })
|
|
279
297
|
if (!result.ok) {
|
|
280
298
|
p.onOperationRejected?.("resize", result.count, result.reason)
|
|
281
|
-
return
|
|
299
|
+
return false
|
|
282
300
|
}
|
|
283
301
|
p.resizeTracks(axis, result.entries)
|
|
302
|
+
return true
|
|
284
303
|
}, [])
|
|
285
304
|
|
|
286
305
|
/** The up commit: same-value skip → fan-out targets → batch apply. / up 確定 (同値スキップ → ファンアウト → バッチ適用)。 */
|
|
@@ -306,11 +325,11 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
306
325
|
|
|
307
326
|
/** 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) をセルの塗りフォントで計測、フォントキーでバケット化。 */
|
|
308
327
|
const autofitColumns = useCallback(
|
|
309
|
-
(band: HTMLElement, pressedIndex: number):
|
|
328
|
+
(band: HTMLElement, pressedIndex: number): boolean => {
|
|
310
329
|
const p = paramsRef.current
|
|
311
330
|
const targets = collectTargets("col", pressedIndex)
|
|
312
331
|
if (targets === null) {
|
|
313
|
-
return
|
|
332
|
+
return false
|
|
314
333
|
}
|
|
315
334
|
// 帯件数は判断時ハンドル読み (ADR-21 / ADR-19 の判断時規律 — getFrozenSize().rows /
|
|
316
335
|
// .trailingRows はエンジンクランプ済み実効値)。装着前 (null) は帯集合なし — 測定集合が
|
|
@@ -344,8 +363,9 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
344
363
|
}
|
|
345
364
|
// 計測対象の可視行がゼロ (全帯 + 窓が非表示) は no-op — Excel も計測対象
|
|
346
365
|
// なしでは幅を変えない (0 計測 → 9px 潰しの破壊を作らない。R1(B) 所見)
|
|
366
|
+
// 測る行が無い / 測る書体が無い / 全空列は「当てるものが無い」実行で、拒否ではない (true)
|
|
347
367
|
if (measuredRows.length === 0) {
|
|
348
|
-
return
|
|
368
|
+
return true
|
|
349
369
|
}
|
|
350
370
|
// 上限ゲートは計測セル数単位 (§7.3 — トラック数単位では 16k 列 × 可視 40 行が素通り
|
|
351
371
|
// する)。上限は autofit 専用の低い係数 (§13 (e) の設計返却 — 実測根拠は定数 docstring)。
|
|
@@ -354,24 +374,30 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
354
374
|
const measureLimit = Math.floor(p.maxOperationCells / AUTOFIT_MEASURE_DIVISOR)
|
|
355
375
|
if (measuredCells > measureLimit) {
|
|
356
376
|
p.onOperationRejected?.("resize", measuredCells, "limit")
|
|
357
|
-
return
|
|
377
|
+
return false
|
|
358
378
|
}
|
|
359
379
|
// canvas 生成 + computed font 取得はジェスチャ 1 回につき 1 度 (§7.3)。font は
|
|
360
380
|
// ロングハンド合成の単一経路 (Firefox は font ショートハンドの computed 値を空で
|
|
361
381
|
// 返すため、合成だけが全ブラウザで決定的)
|
|
362
382
|
const context = document.createElement("canvas").getContext("2d")
|
|
363
383
|
if (context === null) {
|
|
364
|
-
return // 実ブラウザでは常に成立 (jsdom は 2d 未実装 — 計測面は E2E がピン)
|
|
384
|
+
return true // 実ブラウザでは常に成立 (jsdom は 2d 未実装 — 計測面は E2E がピン)
|
|
365
385
|
}
|
|
366
386
|
const bandStyle = getComputedStyle(band)
|
|
367
387
|
// 既定フォント (書式なしセル) — 帯の computed style のロングハンド合成
|
|
368
388
|
const baseFont = `${bandStyle.fontStyle} ${bandStyle.fontWeight} ${bandStyle.fontSize} ${bandStyle.fontFamily}`
|
|
389
|
+
// 見出しの行は書式でなくスタイルシートが太くする — 測る書体もセルと同じ太さにする。スタイルシートが読まれていなければ
|
|
390
|
+
// 見出しの行も帯と同じ太さで描かれるので、帯の太さがそのまま実際の描画の値
|
|
391
|
+
const headerWeight = bandStyle.getPropertyValue("--_aqsp-header-row-font-weight").trim()
|
|
392
|
+
const headerStyle = { fontStyle: bandStyle.fontStyle, fontWeight: headerWeight === "" ? bandStyle.fontWeight : headerWeight, fontSize: bandStyle.fontSize, fontFamily: bandStyle.fontFamily }
|
|
393
|
+
const headerFont = `${headerStyle.fontStyle} ${headerStyle.fontWeight} ${headerStyle.fontSize} ${headerStyle.fontFamily}`
|
|
369
394
|
// CF は幅に効く規則 (bold / italic 設定) と stopIfTrue の障壁だけを 1 ジェスチャ 1 回抽出し列毎に
|
|
370
395
|
// スライスする (実測 155 → 56 ms)。障壁を残すため走査は塗りと同じ位置で止まり bold / italic が
|
|
371
396
|
// 厳密一致する。getConditionalFormats は getCellFormat を要する (root がマウント時に保証)
|
|
372
397
|
const cfRules = p.adapter.getConditionalFormats === undefined ? null : widthRelevantRules(p.adapter.getConditionalFormats())
|
|
373
398
|
const memo = createCfMemo()
|
|
374
399
|
const fontKeys = new WeakMap<CellFormat, string>()
|
|
400
|
+
const headerFontKeys = new WeakMap<CellFormat, string>()
|
|
375
401
|
// フォントキー毎のバケット (列 index + 表示文字列): ctx.font の代入は異なりフォント毎に 1 回
|
|
376
402
|
const buckets = new Map<string, { col: number; text: string }[]>()
|
|
377
403
|
for (const col of targets) {
|
|
@@ -384,7 +410,8 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
384
410
|
const raw = p.adapter.getCellText(row, col)
|
|
385
411
|
// 塗り書式 (base ⊕ 幅に効く CF) — raw は既読なので thunk は再読しない
|
|
386
412
|
const painted = p.adapter.getCellFormat === undefined ? undefined : resolvePaintedFormat(p.adapter, colIndex, memo, row, col, () => raw)
|
|
387
|
-
const
|
|
413
|
+
const isHeaderRow = row < p.headerRows
|
|
414
|
+
const font = painted === undefined ? (isHeaderRow ? headerFont : baseFont) : isHeaderRow ? fontKeyOf(painted, headerFontKeys, headerStyle) : fontKeyOf(painted, fontKeys, bandStyle)
|
|
388
415
|
const text = formatCellValue(raw, painted === undefined ? undefined : painted.value)
|
|
389
416
|
const bucket = buckets.get(font)
|
|
390
417
|
if (bucket === undefined) {
|
|
@@ -415,13 +442,51 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
415
442
|
}
|
|
416
443
|
}
|
|
417
444
|
if (entries.length === 0) {
|
|
418
|
-
return
|
|
445
|
+
return true
|
|
446
|
+
}
|
|
447
|
+
return applyEntries("col", entries, pressedIndex)
|
|
448
|
+
},
|
|
449
|
+
[collectTargets, applyEntries],
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
const stepTrack = useCallback(
|
|
453
|
+
(axis: "row" | "col", index: number, direction: 1 | -1): boolean => {
|
|
454
|
+
const axisModel = axis === "col" ? paramsRef.current.colAxis : paramsRef.current.rowAxis
|
|
455
|
+
const size = keyboardStepSize(axisModel.sizeOf(index), direction)
|
|
456
|
+
// 端にあるトラックは寸法を保つ (当てるものが無い — 拒否ではないので onOperationRejected は鳴らさない)
|
|
457
|
+
if (size === axisModel.sizeOf(index)) {
|
|
458
|
+
return true
|
|
459
|
+
}
|
|
460
|
+
const targets = collectTargets(axis, index)
|
|
461
|
+
if (targets === null) {
|
|
462
|
+
return false
|
|
419
463
|
}
|
|
420
|
-
applyEntries(
|
|
464
|
+
return applyEntries(
|
|
465
|
+
axis,
|
|
466
|
+
targets.map((target) => ({ index: target, size })),
|
|
467
|
+
index,
|
|
468
|
+
)
|
|
421
469
|
},
|
|
422
470
|
[collectTargets, applyEntries],
|
|
423
471
|
)
|
|
424
472
|
|
|
473
|
+
const autofitColumn = useCallback(
|
|
474
|
+
(index: number): boolean => {
|
|
475
|
+
const p = paramsRef.current
|
|
476
|
+
const band = p.colBandRef.current
|
|
477
|
+
// renderCell のカスタム DOM は measureText で測れない (帯のダブルクリックと同じ逸脱) — 帯の未装着は測る書体が無い
|
|
478
|
+
if (!p.autofitEnabled || band === null) {
|
|
479
|
+
return false
|
|
480
|
+
}
|
|
481
|
+
// 隠れた列は隠れたまま (戻すのは再表示だけ) — 走ったが何も変えない
|
|
482
|
+
if (p.colAxis.sizeOf(index) === 0) {
|
|
483
|
+
return true
|
|
484
|
+
}
|
|
485
|
+
return autofitColumns(band, index)
|
|
486
|
+
},
|
|
487
|
+
[autofitColumns],
|
|
488
|
+
)
|
|
489
|
+
|
|
425
490
|
const { enabled, colBandRef, rowBandRef } = params
|
|
426
491
|
useEffect(() => {
|
|
427
492
|
const colBand = colBandRef.current
|
|
@@ -650,6 +715,8 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
650
715
|
|
|
651
716
|
return useMemo(
|
|
652
717
|
() => ({
|
|
718
|
+
stepTrack,
|
|
719
|
+
autofitColumn,
|
|
653
720
|
isDragging: () => draggingRef.current,
|
|
654
721
|
// リサイズ / unhide は選択矩形を配送しない — 配送モードは常に加算
|
|
655
722
|
getDragMode: (): "add" | "subtract" | "point" => "add",
|
|
@@ -661,6 +728,6 @@ export const useHeaderResize = (params: UseHeaderResizeParams): SelectionEngine
|
|
|
661
728
|
},
|
|
662
729
|
cancelDrag: cleanupDrag,
|
|
663
730
|
}),
|
|
664
|
-
[cleanupDrag],
|
|
731
|
+
[cleanupDrag, stepTrack, autofitColumn],
|
|
665
732
|
)
|
|
666
733
|
}
|
package/src/index.ts
CHANGED
|
@@ -46,7 +46,7 @@ export { type HtmlTableKind, type HtmlTableResult, parseHtmlTable, serializeHtml
|
|
|
46
46
|
export type { NumberFormatDecodeOptions } from "./clipboard/numberFormatCodec.ts"
|
|
47
47
|
export type { PasteMode, PasteOperation, PasteOptions } from "./clipboard/pasteSpecial.ts"
|
|
48
48
|
export { estimatePasteCellCount, expandPaste } from "./clipboard/tiling.ts"
|
|
49
|
-
export { parseTsv, serializeTsv } from "./clipboard/tsv.ts"
|
|
49
|
+
export { iterateTsvPrefixRows, iterateTsvRows, measureTsv, parseTsv, serializeClipboardTsv, serializeTsv } from "./clipboard/tsv.ts"
|
|
50
50
|
export type { ClipInfo, PasteOutcome } from "./clipboard/useClipboard.ts"
|
|
51
51
|
export type { BorderPreset } from "./commands/useCommands.ts"
|
|
52
52
|
// renderCell の ctx 型 — prop が公開面にある以上、その引数型も名指しできなければならない
|
|
@@ -67,6 +67,12 @@ export type SpreadsheetCommand =
|
|
|
67
67
|
| "hideColumns"
|
|
68
68
|
| "unhideRows"
|
|
69
69
|
| "unhideColumns"
|
|
70
|
+
// 寸法のキーボード操作 (WCAG 2.1.1 — 帯の境界のドラッグと同じ処理)
|
|
71
|
+
| "increaseColumnWidth"
|
|
72
|
+
| "decreaseColumnWidth"
|
|
73
|
+
| "increaseRowHeight"
|
|
74
|
+
| "decreaseRowHeight"
|
|
75
|
+
| "autoFitColumnWidth"
|
|
70
76
|
// 拡張コマンド (既定 [] = 未束縛、再バインド可)
|
|
71
77
|
| "formatGeneral"
|
|
72
78
|
| "bordersAll"
|
|
@@ -147,6 +153,11 @@ export const SPREADSHEET_COMMANDS: readonly SpreadsheetCommand[] = [
|
|
|
147
153
|
"hideColumns",
|
|
148
154
|
"unhideRows",
|
|
149
155
|
"unhideColumns",
|
|
156
|
+
"increaseColumnWidth",
|
|
157
|
+
"decreaseColumnWidth",
|
|
158
|
+
"increaseRowHeight",
|
|
159
|
+
"decreaseRowHeight",
|
|
160
|
+
"autoFitColumnWidth",
|
|
150
161
|
"formatGeneral",
|
|
151
162
|
"bordersAll",
|
|
152
163
|
"pasteFormats",
|
|
@@ -498,8 +509,64 @@ export const BUILTIN_ROWS: readonly BuiltinRow[] = [
|
|
|
498
509
|
command("hideRows", "Ctrl+Alt+Digit9", "idle", "hide the selected rows"),
|
|
499
510
|
command("unhideRows", "Ctrl+Shift+Digit9", "idle", "unhide rows"),
|
|
500
511
|
command("unhideColumns", "Ctrl+Shift+Digit0", "idle", "unhide columns"),
|
|
512
|
+
command("increaseColumnWidth", "Alt+Shift+ArrowRight", "idle", "widen the active column by one step (x 2^(1/4))"),
|
|
513
|
+
command("decreaseColumnWidth", "Alt+Shift+ArrowLeft", "idle", "narrow the active column by one step (/ 2^(1/4), never below 1 px)"),
|
|
514
|
+
command("increaseRowHeight", "Alt+Shift+ArrowDown", "idle", "make the active row taller by one step (x 2^(1/4))"),
|
|
515
|
+
command("decreaseRowHeight", "Alt+Shift+ArrowUp", "idle", "make the active row shorter by one step (/ 2^(1/4), never below 1 px)"),
|
|
516
|
+
command("autoFitColumnWidth", "Alt+Shift+Equal", "idle", "fit the active column to its content (the strip double-click)"),
|
|
501
517
|
]
|
|
502
518
|
|
|
519
|
+
/**
|
|
520
|
+
* The commands that change the size of the active track (WCAG 2.1.1 — the keyboard twins of the header strips), in the
|
|
521
|
+
* order the hidden input describes them through `aria-keyshortcuts`.
|
|
522
|
+
* アクティブなトラックの寸法を変えるコマンド (WCAG 2.1.1 — 帯の境界のキーボードの双子)。隠しの入力が `aria-keyshortcuts` で
|
|
523
|
+
* 示す順。
|
|
524
|
+
*/
|
|
525
|
+
export const RESIZE_COMMANDS: readonly SpreadsheetCommand[] = Object.freeze(["increaseColumnWidth", "decreaseColumnWidth", "increaseRowHeight", "decreaseRowHeight", "autoFitColumnWidth"])
|
|
526
|
+
|
|
527
|
+
/** ARIA key names of the code tokens whose key differs from the token (US layout, unshifted). / トークンと名前が違うコードの ARIA のキー名 (US 配列・Shift なし)。 */
|
|
528
|
+
const ARIA_KEY_OF_CODE: Readonly<Record<string, string>> = Object.freeze({
|
|
529
|
+
Equal: "=",
|
|
530
|
+
Minus: "-",
|
|
531
|
+
Semicolon: ";",
|
|
532
|
+
Quote: "'",
|
|
533
|
+
Comma: ",",
|
|
534
|
+
Period: ".",
|
|
535
|
+
Slash: "/",
|
|
536
|
+
Backslash: "\\",
|
|
537
|
+
BracketLeft: "[",
|
|
538
|
+
BracketRight: "]",
|
|
539
|
+
Backquote: "`",
|
|
540
|
+
NumpadAdd: "+",
|
|
541
|
+
NumpadSubtract: "-",
|
|
542
|
+
NumpadMultiply: "*",
|
|
543
|
+
NumpadDivide: "/",
|
|
544
|
+
NumpadDecimal: ".",
|
|
545
|
+
NumpadEnter: "Enter",
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* One canonical chord in the `aria-keyshortcuts` syntax: `Ctrl` becomes `Control`, digits and punctuation become the
|
|
550
|
+
* key they produce on a US layout without Shift (`Equal` → `=`, `Digit5` → `5`), and every other token is already an
|
|
551
|
+
* ARIA key name. The layout-specific keys `IntlYen`, `IntlRo` and `IntlBackslash` have no fixed character, so a chord
|
|
552
|
+
* using one has no ARIA form (null).
|
|
553
|
+
* 正準のチョード 1 つの `aria-keyshortcuts` の書き方。`Ctrl` は `Control`、数字と記号は US 配列で Shift なしに生む文字
|
|
554
|
+
* (`Equal` → `=`、`Digit5` → `5`) にし、ほかのトークンはそのまま ARIA のキー名。配列ごとのキー `IntlYen` / `IntlRo` /
|
|
555
|
+
* `IntlBackslash` は文字が決まらないので、それを使うチョードは ARIA の形を持たない (null)。
|
|
556
|
+
*
|
|
557
|
+
* @param chord - A canonical chord (`Ctrl+Alt+Shift+KEY`). 正準のチョード。
|
|
558
|
+
* @returns The ARIA form, or null. ARIA の形、または null。
|
|
559
|
+
*/
|
|
560
|
+
export const toAriaKeyShortcut = (chord: string): string | null => {
|
|
561
|
+
const parsed = parseChord(chord)
|
|
562
|
+
if (parsed.key === "IntlYen" || parsed.key === "IntlRo" || parsed.key === "IntlBackslash") {
|
|
563
|
+
return null
|
|
564
|
+
}
|
|
565
|
+
const digit = /^(?:Digit|Numpad)(\d)$/u.exec(parsed.key)
|
|
566
|
+
const key = digit !== null ? (digit[1] as string) : parsed.key in ARIA_KEY_OF_CODE ? (ARIA_KEY_OF_CODE[parsed.key] as string) : parsed.key
|
|
567
|
+
return `${parsed.ctrl ? "Control+" : ""}${parsed.alt ? "Alt+" : ""}${parsed.shift ? "Shift+" : ""}${key}`
|
|
568
|
+
}
|
|
569
|
+
|
|
503
570
|
/** True when a row is consulted in the given column (`both` covers idle and editing). / 行が指定列で参照されるなら true (`both` は idle と編集の両方)。 */
|
|
504
571
|
const rowInColumn = (row: BuiltinRow, column: "idle" | "editing"): boolean => row.column === "both" || row.column === column
|
|
505
572
|
|
package/src/model/dataAdapter.ts
CHANGED
|
@@ -581,6 +581,80 @@ export const readOnlyDeclared = (adapter: SpreadsheetDataAdapter): boolean => ad
|
|
|
581
581
|
*/
|
|
582
582
|
export const isReadOnlyCell = (adapter: SpreadsheetDataAdapter, row: number, col: number): boolean => adapter.isCellReadOnly?.(row, col) === true
|
|
583
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Every member of SpreadsheetDataAdapter, so a view can forward each one by name; the compile-time check below fails when
|
|
586
|
+
* the adapter type gains a member this list lacks.
|
|
587
|
+
* SpreadsheetDataAdapter の全メンバー。ビューが名前ごとに転送するための一覧で、型にメンバーが増えて一覧に無いときは下の
|
|
588
|
+
* コンパイル時の検査が落ちる。
|
|
589
|
+
*/
|
|
590
|
+
const ADAPTER_MEMBERS = [
|
|
591
|
+
"rowCount",
|
|
592
|
+
"colCount",
|
|
593
|
+
"getCellText",
|
|
594
|
+
"applyEdits",
|
|
595
|
+
"findDataEdge",
|
|
596
|
+
"getCellFormat",
|
|
597
|
+
"getCellFormula",
|
|
598
|
+
"getCellNote",
|
|
599
|
+
"getCellComments",
|
|
600
|
+
"getCellHyperlink",
|
|
601
|
+
"getCellValidation",
|
|
602
|
+
"getConditionalFormats",
|
|
603
|
+
"isCellReadOnly",
|
|
604
|
+
"getCellAttributes",
|
|
605
|
+
"getRangeExtrema",
|
|
606
|
+
"evaluateFormula",
|
|
607
|
+
] as const satisfies readonly (keyof SpreadsheetDataAdapter)[]
|
|
608
|
+
|
|
609
|
+
/** True when ADAPTER_MEMBERS names every member of the adapter type. / ADAPTER_MEMBERS が型の全メンバーを挙げているとき true。 */
|
|
610
|
+
const ADAPTER_MEMBERS_COMPLETE: [Exclude<keyof SpreadsheetDataAdapter, (typeof ADAPTER_MEMBERS)[number]>] extends [never] ? true : never = true
|
|
611
|
+
void ADAPTER_MEMBERS_COMPLETE
|
|
612
|
+
|
|
613
|
+
/** The read-only answer of a whole read-only grid: every cell. / グリッド全体を読み取り専用にしたときの答え (どのセルも読み取り専用)。 */
|
|
614
|
+
const EVERY_CELL_READ_ONLY = (): boolean => true
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* A view of `adapter` whose every cell is read-only (the `readOnly` prop): `isCellReadOnly` answers true for every cell
|
|
618
|
+
* whatever the adapter declares, so every read-only gate refuses every write and routes it to `onReadOnlyRejected`, and
|
|
619
|
+
* every other member reads the adapter live (getters run on the adapter, methods are bound to it once per function
|
|
620
|
+
* identity), so a class-based adapter with private fields or accessors keeps working through the view.
|
|
621
|
+
* 全てのセルを読み取り専用にした `adapter` のビュー (`readOnly` prop)。`isCellReadOnly` はアダプタの宣言に関わらずどのセルにも
|
|
622
|
+
* true を返すので、読み取り専用のゲートは全ての書き込みを拒み `onReadOnlyRejected` へ送る。ほかのメンバーはアダプタを毎回読む
|
|
623
|
+
* (アクセサはアダプタの上で動き、メソッドは関数の同一性ごとに 1 度だけアダプタへ束ねる) ので、private なフィールドやアクセサを
|
|
624
|
+
* 持つクラスのアダプタもビュー越しに動く。
|
|
625
|
+
*
|
|
626
|
+
* @param adapter - The host's adapter. ホストのアダプタ。
|
|
627
|
+
* @returns The read-only view. 読み取り専用のビュー。
|
|
628
|
+
*/
|
|
629
|
+
export const readOnlyAdapterView = (adapter: SpreadsheetDataAdapter): SpreadsheetDataAdapter => {
|
|
630
|
+
const bound = new WeakMap<object, unknown>()
|
|
631
|
+
const view = {} as SpreadsheetDataAdapter
|
|
632
|
+
for (const member of ADAPTER_MEMBERS) {
|
|
633
|
+
if (member === "isCellReadOnly") {
|
|
634
|
+
continue
|
|
635
|
+
}
|
|
636
|
+
Object.defineProperty(view, member, {
|
|
637
|
+
enumerable: true,
|
|
638
|
+
get: () => {
|
|
639
|
+
const value: unknown = Reflect.get(adapter, member, adapter)
|
|
640
|
+
if (typeof value !== "function") {
|
|
641
|
+
return value
|
|
642
|
+
}
|
|
643
|
+
// メソッドの同一性を保つ (毎回 bind すると依存配列の比較や能力の読みのたびに別の関数になる)
|
|
644
|
+
const cached = bound.get(value)
|
|
645
|
+
if (cached !== undefined) {
|
|
646
|
+
return cached
|
|
647
|
+
}
|
|
648
|
+
const fresh = value.bind(adapter)
|
|
649
|
+
bound.set(value, fresh)
|
|
650
|
+
return fresh
|
|
651
|
+
},
|
|
652
|
+
})
|
|
653
|
+
}
|
|
654
|
+
Object.defineProperty(view, "isCellReadOnly", { enumerable: true, value: EVERY_CELL_READ_ONLY })
|
|
655
|
+
return view
|
|
656
|
+
}
|
|
657
|
+
|
|
584
658
|
/** Host cell-attribute key grammar (v0.12): lowercase `data-*` only. / ホストセル属性キーの文法 (v0.12): 小文字 `data-*` のみ。 */
|
|
585
659
|
const CELL_ATTRIBUTE_KEY = /^data-[a-z0-9-]+$/
|
|
586
660
|
|
|
@@ -371,7 +371,7 @@ export const placeBandSegments = (nodes: BandNodes, rect: BandRect, extents: Ban
|
|
|
371
371
|
// 埋め色は族の CSS 変数を参照するインライン background (パレット交換に追随)、交点表示へ戻るとき解除
|
|
372
372
|
if (crossTint !== last.crossTint) {
|
|
373
373
|
last.crossTint = crossTint
|
|
374
|
-
nodes.cross.style.background = crossTint === "intersection" ? "" : `var(--
|
|
374
|
+
nodes.cross.style.background = crossTint === "intersection" ? "" : `var(--_aqsp-band-${crossTint})`
|
|
375
375
|
}
|
|
376
376
|
if (rect.width !== last.crossWidth || rect.height !== last.crossHeight) {
|
|
377
377
|
last.crossWidth = rect.width
|
|
@@ -490,6 +490,10 @@ type SelectionOverlayProps = {
|
|
|
490
490
|
/** v0.9 capability: trailing frozen rows (the vertical twin of trailingCols). / v0.9 能力: 末尾凍結行数 (trailingCols の縦双子)。 */
|
|
491
491
|
trailingRows?: number
|
|
492
492
|
sinkAriaLabel: string
|
|
493
|
+
/** Whole-grid read-only (the `readOnly` prop): the hidden input carries `aria-readonly="true"`. / グリッド全体の読み取り専用 (`readOnly` prop)。隠しの入力が `aria-readonly="true"` を持つ。 */
|
|
494
|
+
sinkReadOnly: boolean
|
|
495
|
+
/** The keyboard resize chords in `aria-keyshortcuts` syntax, or undefined. / キーボードの寸法の操作のチョード (`aria-keyshortcuts` の書き方)。無ければ undefined。 */
|
|
496
|
+
sinkKeyShortcuts: string | undefined
|
|
493
497
|
onSinkMouseDown: (event: MouseEvent) => void
|
|
494
498
|
/**
|
|
495
499
|
* v0.8 capability: the editing sink's paint (line box + vertical alignment) owned by the
|
|
@@ -683,6 +687,8 @@ export const SelectionOverlay = ({
|
|
|
683
687
|
trailingCols,
|
|
684
688
|
trailingRows,
|
|
685
689
|
sinkAriaLabel,
|
|
690
|
+
sinkReadOnly,
|
|
691
|
+
sinkKeyShortcuts,
|
|
686
692
|
onSinkMouseDown,
|
|
687
693
|
editPaintRef,
|
|
688
694
|
highlightRef,
|
|
@@ -1899,7 +1905,21 @@ export const SelectionOverlay = ({
|
|
|
1899
1905
|
一度も変化しない — 変換中の属性変更 (Windows TSF が変換を強制終了する) を構造的に排除する。
|
|
1900
1906
|
OSK の実際の表示は UA 依存: WebKit はフォーカス済み要素の変更を再評価する、Chromium はフォーカス取得時に
|
|
1901
1907
|
しか評価しない。ハードウェアキーボードとデスクトップ IME は無影響 */}
|
|
1902
|
-
<textarea
|
|
1908
|
+
<textarea
|
|
1909
|
+
ref={mergedSinkRef}
|
|
1910
|
+
className="aqsp-sink"
|
|
1911
|
+
aria-label={sinkAriaLabel}
|
|
1912
|
+
aria-readonly={sinkReadOnly ? "true" : undefined}
|
|
1913
|
+
aria-keyshortcuts={sinkKeyShortcuts}
|
|
1914
|
+
tabIndex={-1}
|
|
1915
|
+
defaultValue=" "
|
|
1916
|
+
inputMode={virtualKeyboard ? "text" : "none"}
|
|
1917
|
+
spellCheck={false}
|
|
1918
|
+
autoCorrect="off"
|
|
1919
|
+
autoCapitalize="off"
|
|
1920
|
+
autoComplete="off"
|
|
1921
|
+
data-aqsp-sink=""
|
|
1922
|
+
/>
|
|
1903
1923
|
{/* pointer-events: auto の例外第 2 号 (§8.1) — シンクより後置 = 最上層。enableFillHandle=false は
|
|
1904
1924
|
DOM ごと抑止 (機械も不活性) */}
|
|
1905
1925
|
{enableFillHandle ? <div ref={fillHandleNodeRef} className="aqsp-autofill-handle" data-aqsp-autofill-handle="" style={{ display: "none" }} /> : null}
|