@aiquants/spreadsheet 0.8.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 +1103 -0
- package/LICENSE.md +155 -0
- package/README.md +817 -0
- package/dist/index.cjs +21 -0
- package/dist/index.d.cts +1960 -0
- package/dist/index.d.ts +1960 -0
- package/dist/index.js +7711 -0
- package/dist/styles/spreadsheet.css +2 -0
- package/dist/styles/spreadsheet.standalone.css +3 -0
- package/package.json +94 -0
- package/src/Spreadsheet.tsx +2313 -0
- package/src/clipboard/clipPayload.ts +1242 -0
- package/src/clipboard/fixtures/excel.fixture.ts +301 -0
- package/src/clipboard/fixtures/html.fixture.ts +90 -0
- package/src/clipboard/fixtures/sheets.fixture.ts +83 -0
- package/src/clipboard/htmlInterop.ts +1694 -0
- package/src/clipboard/numberFormatCodec.ts +870 -0
- package/src/clipboard/pasteSpecial.ts +608 -0
- package/src/clipboard/tiling.ts +370 -0
- package/src/clipboard/tsv.ts +186 -0
- package/src/clipboard/useClipboard.ts +1154 -0
- package/src/commands/selfCalc.ts +279 -0
- package/src/commands/useCommands.ts +946 -0
- package/src/editor/useCellEditor.ts +708 -0
- package/src/grid/CellView.tsx +51 -0
- package/src/grid/GridBody.tsx +489 -0
- package/src/header/ColumnHeader.tsx +167 -0
- package/src/header/RowHeader.tsx +144 -0
- package/src/header/columnName.ts +31 -0
- package/src/header/resizeGates.ts +146 -0
- package/src/header/resizeStrips.ts +79 -0
- package/src/header/useHeaderResize.ts +598 -0
- package/src/header/useHeaderSelection.ts +454 -0
- package/src/index.ts +122 -0
- package/src/keyboard/compositionGuard.ts +53 -0
- package/src/keyboard/ensureCellVisible.ts +77 -0
- package/src/keyboard/shortcuts.ts +718 -0
- package/src/keyboard/useGridKeyboard.ts +940 -0
- package/src/model/axisModel.ts +186 -0
- package/src/model/cellAnnotations.ts +92 -0
- package/src/model/cellCoord.ts +114 -0
- package/src/model/cellFormat.ts +562 -0
- package/src/model/cellPaint.ts +537 -0
- package/src/model/conditionalFormat.ts +805 -0
- package/src/model/dataAdapter.ts +478 -0
- package/src/model/dataValidation.ts +542 -0
- package/src/model/formulaRefs.ts +984 -0
- package/src/model/frozenPartition.ts +100 -0
- package/src/model/labels.ts +228 -0
- package/src/model/viewStore.ts +124 -0
- package/src/overlay/SelectionOverlay.tsx +1425 -0
- package/src/overlay/useFillHandle.ts +432 -0
- package/src/popover/CommentPopover.tsx +109 -0
- package/src/popover/LinkPopover.tsx +108 -0
- package/src/popover/ListPopover.tsx +161 -0
- package/src/popover/MessagePopover.tsx +36 -0
- package/src/popover/NotePopover.tsx +106 -0
- package/src/popover/NoteTip.tsx +26 -0
- package/src/popover/PopoverLayer.tsx +210 -0
- package/src/popover/placement.ts +138 -0
- package/src/popover/usePopovers.ts +642 -0
- package/src/selection/selectionState.ts +239 -0
- package/src/selection/useAutoScroller.ts +226 -0
- package/src/selection/usePointDrag.ts +273 -0
- package/src/selection/useSelectionEngine.ts +687 -0
- package/src/styles/components.entry.css +10 -0
- package/src/styles/spreadsheet.css +1029 -0
- package/src/styles/standalone.entry.css +9 -0
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The command runner (v0.8 §4.1.13 — ADR-17): `executeCommand` for all 70
|
|
3
|
+
* `SpreadsheetCommand` literals plus the handle's write group (`applyFormat` / `setBorders` /
|
|
4
|
+
* `setHyperlink` / `setNote` / `addComment` / `setValidation`) and the capability gate
|
|
5
|
+
* `isCommandAvailable`. One table (`COMMAND_SPECS`, exhaustive over the union — tsc proves every
|
|
6
|
+
* literal is classified) drives availability and execution. Contract:
|
|
7
|
+
* - Every selection write funnels `gateSelectionWrite` (ADR-12) → `enumerateRangeCells` → ONE
|
|
8
|
+
* `commitEdits` with the operation's source (`format` / `calc` / `fill` / `annotate` / `edit`).
|
|
9
|
+
* - Format commands compute their patch ONCE per gesture from the ACTIVE cell's BASE format
|
|
10
|
+
* (Excel: a mixed selection follows the active cell — design v5 "Misc") and apply it to every
|
|
11
|
+
* selected cell through `mergeFormat`, memoised per base identity (an interned base ⇒ one
|
|
12
|
+
* `format` object identity in the `applyEdits` argument); `mergeFormat(base, patch) === base` ⇒
|
|
13
|
+
* no edit. `bordersAll` / `bordersClear` are constant patches; `bordersOuter` and the four
|
|
14
|
+
* single-side presets are position-dependent (each cell's OWN sides ⊕ its targeted range-edge
|
|
15
|
+
* sides, memoised per base × edge mask — cells off the targeted edge keep their borders and get
|
|
16
|
+
* no edit; Excel / Sheets range-edge semantics, 分冊 08 §11). Fill / font
|
|
17
|
+
* colour toggles compare the active BASE colour (`normalizeColor`) with the target: equal ⇒
|
|
18
|
+
* clear on the whole selection, else set (named presets: pastel fills / pure fonts — design v3 §7).
|
|
19
|
+
* - Editing column (`EDITING_COLUMN_COMMANDS` while `isEditing()`): format toggles / aligns /
|
|
20
|
+
* number formats write the SESSION cell only and repaint the sink (`refreshEditPaint`);
|
|
21
|
+
* `insertDate` / `insertTime` insert at the caret; `fillSelection` runs the root validation gate
|
|
22
|
+
* (`validateCommit`) on EVERY target cell (rules are per cell — the first strict rejection keeps
|
|
23
|
+
* the session open and returns false, the Enter keep path) and then writes the session text to
|
|
24
|
+
* every selected cell in ONE `applyEdits` and closes the session. Every other command commits
|
|
25
|
+
* the session first (`requestCommit`; refusal aborts with `false`).
|
|
26
|
+
* - `insertDate` / `insertTime` idle write the ACTIVE cell only (`YYYY-MM-DD` / `HH:mm:ss` local
|
|
27
|
+
* from the `now` seam), source `edit`, THROUGH the root validation gate (`validateCommit` on the
|
|
28
|
+
* active cell — Excel's Ctrl+; goes through edit mode, so its commit is validated; a strict
|
|
29
|
+
* rejection returns false without writing). Self-calc uses selfCalc.ts (formula cells only when
|
|
30
|
+
* `getCellFormula` is declared; `null` ⇒ the cell is left alone). Fill copies text / formula
|
|
31
|
+
* (shifted) / format / validation / hyperlink / note — comments excluded. Hide / unhide go
|
|
32
|
+
* through `gateResizeEntries` and `resizeTracks` (non-undoable geometry). Paste commands go to
|
|
33
|
+
* the menu path `paste(options)`; `pasteSpecial` prefers `onPasteSpecialRequest`. Popover
|
|
34
|
+
* commands open the kind (the popover hook owns its own commit gate). Highlight toggles flip the
|
|
35
|
+
* root-owned state through `setHighlight`.
|
|
36
|
+
* - `formatCommandAnnouncement` (opt-in) is pushed to `announce` after a successful format
|
|
37
|
+
* command with `state` = the resulting toggle state (bold / italic / underline / strikethrough /
|
|
38
|
+
* colour toggles) or `null` for non-toggles.
|
|
39
|
+
* - Return value: true = committed (or nothing needed writing), false = gate rejection / adapter
|
|
40
|
+
* rejection / command unavailable / commit-gate refusal. Every callback is identity-stable
|
|
41
|
+
* (params are read through `paramsRef`).
|
|
42
|
+
*
|
|
43
|
+
* コマンド実行器 (v0.8 §4.1.13 — ADR-17)。70 の SpreadsheetCommand 全件の executeCommand、handle の
|
|
44
|
+
* 書込群 (applyFormat / setBorders / setHyperlink / setNote / addComment / setValidation)、能力
|
|
45
|
+
* ゲート isCommandAvailable。単一表 COMMAND_SPECS (union 全件の網羅を tsc が証明) が可用性と実行の
|
|
46
|
+
* 両方を駆動する。契約: 全選択書込は gateSelectionWrite (ADR-12) → enumerateRangeCells → 1 回の
|
|
47
|
+
* commitEdits (source は format / calc / fill / annotate / edit)。書式コマンドはパッチを ACTIVE セルの
|
|
48
|
+
* BASE 書式から 1 ジェスチャ 1 回だけ計算し (Excel: 混在選択は active に従う — 設計 v5) mergeFormat を
|
|
49
|
+
* base identity 毎に memo して全セルへ適用 (intern された base ⇒ applyEdits 引数の format identity は
|
|
50
|
+
* 1 つ)、無変化 (=== base) は edit を出さない。bordersAll / bordersClear は定数パッチ、bordersOuter と
|
|
51
|
+
* 単一辺 4 プリセットは位置依存 (各セル自身の辺 ⊕ レンジ外周の該当辺、base × 辺マスクで memo — 対象辺
|
|
52
|
+
* 外のセルは罫線を保ち edit を出さない。Excel / Sheets のレンジ辺意味論 — 分冊 08 §11)。色トグルは
|
|
53
|
+
* active BASE の色 (normalizeColor) と目標を比較し、等しければ
|
|
54
|
+
* 選択全体を消去、違えば設定 (名前つきプリセットは塗り淡色 / 文字原色 — 設計 v3 §7)。編集列 (isEditing 中の EDITING_COLUMN_COMMANDS): 書式トグル / 揃え /
|
|
55
|
+
* 数値書式はセッションセルだけへ書きシンクを再塗り (refreshEditPaint)、insertDate / insertTime は
|
|
56
|
+
* キャレット挿入、fillSelection は対象セル毎に root の検証ゲート validateCommit を通し (規則はセル毎、
|
|
57
|
+
* 最初の strict 拒否はセッション維持のまま false = Enter の keep 経路) セッション文字列を全選択セルへ
|
|
58
|
+
* 1 applyEdits で書きセッションを閉じる。他コマンドは先に requestCommit (拒否で false 中断)。
|
|
59
|
+
* insertDate / insertTime の idle は
|
|
60
|
+
* ACTIVE セルだけ (ローカル YYYY-MM-DD / HH:mm:ss、now シーム)、source edit、かつ root の検証ゲート
|
|
61
|
+
* validateCommit を通す (Excel の Ctrl+; は編集モード経由で確定が検証される — strict 拒否は書込なしで
|
|
62
|
+
* false)。自己計算は selfCalc.ts
|
|
63
|
+
* (数式扱いは getCellFormula 宣言時のみ、null は不変)。fill は text / 数式 (shift) / format /
|
|
64
|
+
* validation / hyperlink / note を複製し comments は除外。hide / unhide は gateResizeEntries →
|
|
65
|
+
* resizeTracks (非 undo 幾何)。paste 系はメニュー経路 paste(options)、pasteSpecial は
|
|
66
|
+
* onPasteSpecialRequest 優先。popover 系は kind を開く (commit ゲートは popover フック自身)。強調
|
|
67
|
+
* トグルは setHighlight。formatCommandAnnouncement (opt-in) は書式コマンド成功後に announce へ
|
|
68
|
+
* (state はトグルの適用後状態、非トグルは null)。戻り値: true = commit 成功 (書くものが無い場合も)、
|
|
69
|
+
* false = ゲート拒否 / アダプタ拒否 / 不可用 / commit ゲート拒否。全コールバックは identity 安定
|
|
70
|
+
* (パラメータは paramsRef 経由で読む)。
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
import { useCallback, useMemo, useRef } from "react"
|
|
74
|
+
import type { PasteOptions } from "../clipboard/pasteSpecial.ts"
|
|
75
|
+
import { fillSourceEdit, readFillSource } from "../clipboard/tiling.ts"
|
|
76
|
+
import { gateResizeEntries, type ResizeEntry, ROW_FANOUT_DIVISOR, ROW_HIDDEN_LIMIT } from "../header/resizeGates.ts"
|
|
77
|
+
import { EDITING_COLUMN_COMMANDS, type SpreadsheetCommand } from "../keyboard/shortcuts.ts"
|
|
78
|
+
import type { AxisModel } from "../model/axisModel.ts"
|
|
79
|
+
import { type CellCommentInput, normalizeAdapterHyperlink, normalizeHyperlink } from "../model/cellAnnotations.ts"
|
|
80
|
+
import { type CellCoord, type CellRange, countRangeCells, enumerateRangeCells, makeRange, type SpreadsheetOperation } from "../model/cellCoord.ts"
|
|
81
|
+
import { BORDER_DEFAULT_COLOR, type CellBorder, type CellBorders, type CellFormat, type CellFormatPatch, type CellValueFormat, mergeFormat } from "../model/cellFormat.ts"
|
|
82
|
+
import { normalizeColor } from "../model/cellPaint.ts"
|
|
83
|
+
import type { CellEdit, EditRejection, SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
84
|
+
import type { DataValidationRule, ValidationVerdict } from "../model/dataValidation.ts"
|
|
85
|
+
import type { PopoverKind, PopoverOverrides } from "../popover/usePopovers.ts"
|
|
86
|
+
import type { SelectionState } from "../selection/selectionState.ts"
|
|
87
|
+
import { reverseSignFormula, reverseSignText, type ScaleExponent, scaleDecimalText, scaleFormula } from "./selfCalc.ts"
|
|
88
|
+
|
|
89
|
+
/** Border preset vocabulary of `setBorders` (v0.8 §1.10). / setBorders のプリセット語彙 (v0.8 §1.10)。 */
|
|
90
|
+
export type BorderPreset = "outer" | "all" | "clear" | "top" | "right" | "bottom" | "left"
|
|
91
|
+
|
|
92
|
+
/** Row / column / intersection highlight state (structurally the root's HighlightState — v0.8 §1.9). / 行 / 列 / 交点の強調状態 (root の HighlightState と構造同一 — v0.8 §1.9)。 */
|
|
93
|
+
export type CommandHighlightState = { row: boolean; column: boolean; intersection: boolean }
|
|
94
|
+
|
|
95
|
+
/** Keyboard-origin cell rect (visual px — the onCellContextMenu shape). / キーボード起点のセル矩形 (視覚 px — onCellContextMenu と同形)。 */
|
|
96
|
+
export type KeyboardCellRect = { top: number; left: number; width: number; height: number }
|
|
97
|
+
|
|
98
|
+
/** The `formats` prop (currency + optional rendering locale). / `formats` prop (通貨 + 任意の描画 locale)。 */
|
|
99
|
+
export type CommandFormats = { currency: string; locale?: string }
|
|
100
|
+
|
|
101
|
+
export type UseCommandsParams = {
|
|
102
|
+
adapter: SpreadsheetDataAdapter
|
|
103
|
+
getSelection: () => SelectionState
|
|
104
|
+
/** The §6.9-2 wrapper: the transaction id on success, null = rejected. / §6.9-2 ラッパー (成功は transaction id、null = 拒否)。 */
|
|
105
|
+
commitEdits: (edits: readonly CellEdit[], rejection: EditRejection) => number | null
|
|
106
|
+
getActive: () => CellCoord
|
|
107
|
+
isEditing: () => boolean
|
|
108
|
+
requestCommit: () => boolean
|
|
109
|
+
/** Closes the session discarding its text (fillSelection wrote it everywhere already). / セッションを値破棄で閉じる (fillSelection が全セルへ書いた後)。 */
|
|
110
|
+
cancelEdit: () => void
|
|
111
|
+
/** The live session text, null when idle. / セッション文字列 (idle は null)。 */
|
|
112
|
+
getSessionText: () => string | null
|
|
113
|
+
/** The root validation gate (分冊 08 §4.2) — the editing-column fillSelection runs it per target cell and the idle insertDate / insertTime run it on the active cell; a strict rejection is reported by the root (onValidationRejected + message popover) and comes back as ok: false. / root の検証ゲート (分冊 08 §4.2) — 編集列 fillSelection が対象セル毎に、idle の insertDate / insertTime が active セルへ呼ぶ。strict 拒否は root が通知 (onValidationRejected + message ポップオーバー) し ok: false で返る。 */
|
|
114
|
+
validateCommit: (cell: CellCoord, text: string) => ValidationVerdict
|
|
115
|
+
/** Inserts text at the sink caret (editing column insertDate / insertTime). / シンクのキャレットへ挿入 (編集列の insertDate / insertTime)。 */
|
|
116
|
+
insertAtCaret: (text: string) => void
|
|
117
|
+
/** Recomputes the sink paint after a format write on the session cell. / セッションセルの書式書込後にシンク塗りを再計算。 */
|
|
118
|
+
refreshEditPaint: () => void
|
|
119
|
+
maxOperationCells: number
|
|
120
|
+
onOperationRejected?: (op: SpreadsheetOperation, cellCount: number, reason: "limit" | "bounds") => void
|
|
121
|
+
/** Custom colour slots (5 each) for fillColorCustomN / fontColorCustomN. / fillColorCustomN / fontColorCustomN の色スロット (各 5)。 */
|
|
122
|
+
colors: { fill: readonly string[]; font: readonly string[] }
|
|
123
|
+
/** The formatCurrency currency / rendering locale; undefined ⇒ formatCurrency is unavailable (passes through). / formatCurrency の通貨 / locale。未宣言なら formatCurrency は行不在 (素通し)。 */
|
|
124
|
+
formats: CommandFormats | undefined
|
|
125
|
+
getHighlight: () => CommandHighlightState
|
|
126
|
+
setHighlight: (next: CommandHighlightState) => void
|
|
127
|
+
/** The `popovers` prop (kind availability gate). / `popovers` prop (kind の可用性ゲート)。 */
|
|
128
|
+
popovers: PopoverOverrides | undefined
|
|
129
|
+
openPopover: (kind: PopoverKind) => void
|
|
130
|
+
onHyperlinkOpen?: (url: string, cell: CellCoord) => void
|
|
131
|
+
onPasteSpecialRequest?: (origin: { kind: "keyboard"; cellRect: KeyboardCellRect }) => void
|
|
132
|
+
/** The clipboard menu path (paste-special commands). / クリップボードのメニュー経路 (paste-special 系)。 */
|
|
133
|
+
paste: (options: PasteOptions) => Promise<"applied" | "empty" | "rejected">
|
|
134
|
+
rowAxis: AxisModel
|
|
135
|
+
colAxis: AxisModel
|
|
136
|
+
/** The batch resize seam (already-gated entries). / バッチリサイズシーム (ゲート済み entries)。 */
|
|
137
|
+
resizeTracks: (axis: "row" | "col", entries: readonly ResizeEntry[]) => void
|
|
138
|
+
defaultRowHeight: number
|
|
139
|
+
defaultColumnWidth: number
|
|
140
|
+
/** Clock seam for insertDate / insertTime. / insertDate / insertTime の時計シーム。 */
|
|
141
|
+
now: () => number
|
|
142
|
+
announce: (text: string) => void
|
|
143
|
+
formatCommandAnnouncement?: (ctx: { command: SpreadsheetCommand; state: boolean | null }) => string
|
|
144
|
+
/** The active cell's keyboard rect (onPasteSpecialRequest origin). / active セルのキーボード矩形 (onPasteSpecialRequest の origin)。 */
|
|
145
|
+
keyboardCellRect: () => KeyboardCellRect
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The handle's write group + the command entry points (v0.8 §1.10 / §4.1.13). / handle の書込群 + コマンド入口 (v0.8 §1.10 / §4.1.13)。 */
|
|
149
|
+
export type CommandRunner = {
|
|
150
|
+
executeCommand: (command: SpreadsheetCommand) => boolean
|
|
151
|
+
applyFormat: (patch: CellFormatPatch) => boolean
|
|
152
|
+
setBorders: (preset: BorderPreset, border?: CellBorder) => boolean
|
|
153
|
+
setHyperlink: (url: string | null) => boolean
|
|
154
|
+
setNote: (text: string | null) => boolean
|
|
155
|
+
addComment: (input: CellCommentInput) => boolean
|
|
156
|
+
setValidation: (rule: DataValidationRule | null) => boolean
|
|
157
|
+
/** Capability gate: false = the keymap row does not exist (the chord passes through). `openHyperlink` with `onHyperlinkOpen` declared is cell-aware: the row exists only while the active cell has a link. / 能力ゲート: false = 行不在 (チョードは素通し)。`onHyperlinkOpen` 宣言時の `openHyperlink` はセル依存 — active にリンクがある間だけ行が存在する。 */
|
|
158
|
+
isCommandAvailable: (command: SpreadsheetCommand) => boolean
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* ADR-12 selection-write gate: count the cells, reject with "limit" over the cap.
|
|
163
|
+
* ADR-12 の選択書込ゲート: セル数を数え、上限超過は "limit" で拒否する。
|
|
164
|
+
*/
|
|
165
|
+
export const gateSelectionWrite = (op: SpreadsheetOperation, ranges: readonly CellRange[], max: number, reject: ((op: SpreadsheetOperation, count: number, reason: "limit") => void) | undefined): boolean => {
|
|
166
|
+
const count = countRangeCells(ranges)
|
|
167
|
+
if (count > max) {
|
|
168
|
+
reject?.(op, count, "limit")
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Toggle keys of the four boolean format commands. / 真偽値書式コマンド 4 種のキー。 */
|
|
175
|
+
type ToggleKey = "bold" | "italic" | "underline" | "strikethrough"
|
|
176
|
+
|
|
177
|
+
/** The six value-format commands. / 値書式コマンド 6 種。 */
|
|
178
|
+
type ValueKind = "number" | "time" | "date" | "currency" | "percent" | "scientific"
|
|
179
|
+
|
|
180
|
+
/** Classification of one command (the single table both gates and dispatch consume). / コマンド 1 件の分類 (ゲートと dispatch が共用する単一表)。 */
|
|
181
|
+
type CommandSpec =
|
|
182
|
+
| { kind: "toggle"; key: ToggleKey }
|
|
183
|
+
| { kind: "patch"; patch: CellFormatPatch }
|
|
184
|
+
| { kind: "clear" }
|
|
185
|
+
| { kind: "value"; value: ValueKind }
|
|
186
|
+
| { kind: "borders"; preset: BorderPreset }
|
|
187
|
+
| { kind: "color"; key: "background" | "color"; preset: string | null; custom: number | null }
|
|
188
|
+
| { kind: "calc"; exponent: ScaleExponent }
|
|
189
|
+
| { kind: "sign" }
|
|
190
|
+
| { kind: "fill"; direction: "down" | "right" | "selection" }
|
|
191
|
+
| { kind: "popover"; popover: "link" | "note" | "comment" | "list" }
|
|
192
|
+
| { kind: "openHyperlink" }
|
|
193
|
+
| { kind: "pasteSpecial" }
|
|
194
|
+
| { kind: "paste"; options: PasteOptions }
|
|
195
|
+
| { kind: "insertNow"; unit: "date" | "time" }
|
|
196
|
+
| { kind: "hide"; axis: "row" | "col"; hide: boolean }
|
|
197
|
+
| { kind: "highlight"; target: "row" | "column" | "both" | "intersection" }
|
|
198
|
+
|
|
199
|
+
/** Fill presets of the four named fill-colour toggles — pastels (design v3 §7 / the reference extension: legible behind the default text colour). / 名前つき塗りトグル 4 種のプリセット — 淡色 (設計 v3 §7 / 参照拡張機能と同値: 既定文字色の背後で可読)。 */
|
|
200
|
+
const FILL_PRESETS = { Yellow: "#FFFF99", Blue: "#CCFFFF", Green: "#CCFFCC", Red: "#FFCCCC" } as const
|
|
201
|
+
|
|
202
|
+
/** Font presets of the four named font-colour toggles — pure primaries (design v3 §7). / 名前つき文字色トグル 4 種のプリセット — 原色 (設計 v3 §7)。 */
|
|
203
|
+
const FONT_PRESETS = { Yellow: "#FFFF00", Blue: "#0000FF", Green: "#00FF00", Red: "#FF0000" } as const
|
|
204
|
+
|
|
205
|
+
/** The exhaustive command table (a missing literal is a tsc error). / 網羅的コマンド表 (リテラル欠落は tsc エラー)。 */
|
|
206
|
+
const COMMAND_SPECS: { readonly [K in SpreadsheetCommand]: CommandSpec } = {
|
|
207
|
+
bold: { kind: "toggle", key: "bold" },
|
|
208
|
+
italic: { kind: "toggle", key: "italic" },
|
|
209
|
+
underline: { kind: "toggle", key: "underline" },
|
|
210
|
+
strikethrough: { kind: "toggle", key: "strikethrough" },
|
|
211
|
+
clearFormatting: { kind: "clear" },
|
|
212
|
+
alignLeft: { kind: "patch", patch: { align: "left" } },
|
|
213
|
+
alignCenter: { kind: "patch", patch: { align: "center" } },
|
|
214
|
+
alignRight: { kind: "patch", patch: { align: "right" } },
|
|
215
|
+
formatNumber: { kind: "value", value: "number" },
|
|
216
|
+
formatTime: { kind: "value", value: "time" },
|
|
217
|
+
formatDate: { kind: "value", value: "date" },
|
|
218
|
+
formatCurrency: { kind: "value", value: "currency" },
|
|
219
|
+
formatPercent: { kind: "value", value: "percent" },
|
|
220
|
+
formatScientific: { kind: "value", value: "scientific" },
|
|
221
|
+
bordersOuter: { kind: "borders", preset: "outer" },
|
|
222
|
+
bordersClear: { kind: "borders", preset: "clear" },
|
|
223
|
+
borderTop: { kind: "borders", preset: "top" },
|
|
224
|
+
borderRight: { kind: "borders", preset: "right" },
|
|
225
|
+
borderBottom: { kind: "borders", preset: "bottom" },
|
|
226
|
+
borderLeft: { kind: "borders", preset: "left" },
|
|
227
|
+
insertLink: { kind: "popover", popover: "link" },
|
|
228
|
+
insertNote: { kind: "popover", popover: "note" },
|
|
229
|
+
insertComment: { kind: "popover", popover: "comment" },
|
|
230
|
+
openHyperlink: { kind: "openHyperlink" },
|
|
231
|
+
openValidationList: { kind: "popover", popover: "list" },
|
|
232
|
+
pasteSpecial: { kind: "pasteSpecial" },
|
|
233
|
+
fillDown: { kind: "fill", direction: "down" },
|
|
234
|
+
fillRight: { kind: "fill", direction: "right" },
|
|
235
|
+
fillSelection: { kind: "fill", direction: "selection" },
|
|
236
|
+
insertDate: { kind: "insertNow", unit: "date" },
|
|
237
|
+
insertTime: { kind: "insertNow", unit: "time" },
|
|
238
|
+
hideRows: { kind: "hide", axis: "row", hide: true },
|
|
239
|
+
hideColumns: { kind: "hide", axis: "col", hide: true },
|
|
240
|
+
unhideRows: { kind: "hide", axis: "row", hide: false },
|
|
241
|
+
unhideColumns: { kind: "hide", axis: "col", hide: false },
|
|
242
|
+
formatGeneral: { kind: "patch", patch: { value: null } },
|
|
243
|
+
bordersAll: { kind: "borders", preset: "all" },
|
|
244
|
+
pasteFormats: { kind: "paste", options: { mode: "formats" } },
|
|
245
|
+
pasteFormulas: { kind: "paste", options: { mode: "formulas" } },
|
|
246
|
+
pasteAdd: { kind: "paste", options: { mode: "all", operation: "add" } },
|
|
247
|
+
pasteSubtract: { kind: "paste", options: { mode: "all", operation: "subtract" } },
|
|
248
|
+
pasteMultiply: { kind: "paste", options: { mode: "all", operation: "multiply" } },
|
|
249
|
+
pasteDivide: { kind: "paste", options: { mode: "all", operation: "divide" } },
|
|
250
|
+
multiplyBy100: { kind: "calc", exponent: 2 },
|
|
251
|
+
multiplyBy1000: { kind: "calc", exponent: 3 },
|
|
252
|
+
divideBy100: { kind: "calc", exponent: -2 },
|
|
253
|
+
divideBy1000: { kind: "calc", exponent: -3 },
|
|
254
|
+
signReversal: { kind: "sign" },
|
|
255
|
+
fillColorYellow: { kind: "color", key: "background", preset: FILL_PRESETS.Yellow, custom: null },
|
|
256
|
+
fillColorBlue: { kind: "color", key: "background", preset: FILL_PRESETS.Blue, custom: null },
|
|
257
|
+
fillColorGreen: { kind: "color", key: "background", preset: FILL_PRESETS.Green, custom: null },
|
|
258
|
+
fillColorRed: { kind: "color", key: "background", preset: FILL_PRESETS.Red, custom: null },
|
|
259
|
+
fillColorCustom1: { kind: "color", key: "background", preset: null, custom: 0 },
|
|
260
|
+
fillColorCustom2: { kind: "color", key: "background", preset: null, custom: 1 },
|
|
261
|
+
fillColorCustom3: { kind: "color", key: "background", preset: null, custom: 2 },
|
|
262
|
+
fillColorCustom4: { kind: "color", key: "background", preset: null, custom: 3 },
|
|
263
|
+
fillColorCustom5: { kind: "color", key: "background", preset: null, custom: 4 },
|
|
264
|
+
fontColorYellow: { kind: "color", key: "color", preset: FONT_PRESETS.Yellow, custom: null },
|
|
265
|
+
fontColorBlue: { kind: "color", key: "color", preset: FONT_PRESETS.Blue, custom: null },
|
|
266
|
+
fontColorGreen: { kind: "color", key: "color", preset: FONT_PRESETS.Green, custom: null },
|
|
267
|
+
fontColorRed: { kind: "color", key: "color", preset: FONT_PRESETS.Red, custom: null },
|
|
268
|
+
fontColorCustom1: { kind: "color", key: "color", preset: null, custom: 0 },
|
|
269
|
+
fontColorCustom2: { kind: "color", key: "color", preset: null, custom: 1 },
|
|
270
|
+
fontColorCustom3: { kind: "color", key: "color", preset: null, custom: 2 },
|
|
271
|
+
fontColorCustom4: { kind: "color", key: "color", preset: null, custom: 3 },
|
|
272
|
+
fontColorCustom5: { kind: "color", key: "color", preset: null, custom: 4 },
|
|
273
|
+
toggleRowHighlight: { kind: "highlight", target: "row" },
|
|
274
|
+
toggleColumnHighlight: { kind: "highlight", target: "column" },
|
|
275
|
+
toggleBothHighlight: { kind: "highlight", target: "both" },
|
|
276
|
+
toggleIntersectionHighlight: { kind: "highlight", target: "intersection" },
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** `clearFormatting` = every CellFormat key cleared (`-?` makes a newly added key a tsc error here). / clearFormatting = CellFormat 全キーの消去 (`-?` により新キーの追加漏れは tsc エラー)。 */
|
|
280
|
+
const CLEAR_PATCH: { readonly [K in keyof CellFormat]-?: null } = { value: null, align: null, verticalAlign: null, wrap: null, background: null, color: null, bold: null, italic: null, underline: null, strikethrough: null, fontFamily: null, fontSize: null, borders: null }
|
|
281
|
+
|
|
282
|
+
/** Two-digit zero padding. / 2 桁ゼロ埋め。 */
|
|
283
|
+
const pad2 = (n: number): string => (n < 10 ? `0${n}` : String(n))
|
|
284
|
+
|
|
285
|
+
/** Local `YYYY-MM-DD` of a timestamp. / タイムスタンプのローカル YYYY-MM-DD。 */
|
|
286
|
+
const formatLocalDate = (timestamp: number): string => {
|
|
287
|
+
const d = new Date(timestamp)
|
|
288
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Local `HH:mm:ss` of a timestamp. / タイムスタンプのローカル HH:mm:ss。 */
|
|
292
|
+
const formatLocalTime = (timestamp: number): string => {
|
|
293
|
+
const d = new Date(timestamp)
|
|
294
|
+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Edge bit mask of a cell inside its range (top 1 / right 2 / bottom 4 / left 8). / レンジ内セルの外周辺ビット (top 1 / right 2 / bottom 4 / left 8)。 */
|
|
298
|
+
const edgeMaskOf = (cell: CellCoord, range: CellRange): number => (cell.row === range.startRow ? 1 : 0) | (cell.col === range.endCol ? 2 : 0) | (cell.row === range.endRow ? 4 : 0) | (cell.col === range.startCol ? 8 : 0)
|
|
299
|
+
|
|
300
|
+
/** The sides a mask selects, each set to `border`. / マスクが選ぶ辺 (各辺を border に)。 */
|
|
301
|
+
const sidesOfMask = (mask: number, border: CellBorder): CellBorders => ({
|
|
302
|
+
...((mask & 1) !== 0 ? { top: border } : {}),
|
|
303
|
+
...((mask & 2) !== 0 ? { right: border } : {}),
|
|
304
|
+
...((mask & 4) !== 0 ? { bottom: border } : {}),
|
|
305
|
+
...((mask & 8) !== 0 ? { left: border } : {}),
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
/** The sides a base format carries (empty when none). / base 書式の辺集合 (無ければ空)。 */
|
|
309
|
+
const sidesOf = (base: CellFormat | undefined): CellBorders => (base === undefined || base.borders === undefined ? {} : base.borders)
|
|
310
|
+
|
|
311
|
+
/** The edge bit each single-side preset targets (the `edgeMaskOf` layout). / 単一辺プリセットが対象とする外周辺ビット (`edgeMaskOf` と同配置)。 */
|
|
312
|
+
const SIDE_BITS = { top: 1, right: 2, bottom: 4, left: 8 } as const
|
|
313
|
+
|
|
314
|
+
/** The default border of the border commands (v0.8 §4.1.13: thin, BORDER_DEFAULT_COLOR). / 罫線コマンドの既定辺 (v0.8 §4.1.13: thin、BORDER_DEFAULT_COLOR)。 */
|
|
315
|
+
const DEFAULT_BORDER: CellBorder = { style: "thin", color: BORDER_DEFAULT_COLOR }
|
|
316
|
+
|
|
317
|
+
/** Structural colour equality through normalizeColor (unparseable never equals). / normalizeColor を通した色の等価 (解釈不能は決して等しくない)。 */
|
|
318
|
+
const sameColor = (a: string | undefined, b: string): boolean => {
|
|
319
|
+
if (a === undefined) {
|
|
320
|
+
return false
|
|
321
|
+
}
|
|
322
|
+
const na = normalizeColor(a)
|
|
323
|
+
return na !== null && na === normalizeColor(b)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The value format a `format*` command writes (the host's locale stamped when declared;
|
|
328
|
+
* `currency` REQUIRES the `formats` prop — the availability gate keeps the row absent without it,
|
|
329
|
+
* so reaching this branch undeclared is a runner bug, reported as a RangeError rather than a
|
|
330
|
+
* substituted currency).
|
|
331
|
+
* format* コマンドが書く値書式 (ホストが locale を宣言していれば刻印。currency は formats prop 必須 —
|
|
332
|
+
* 可用性ゲートが未宣言時は行不在にするため、未宣言でここへ達するのは実行器のバグであり、通貨の代替
|
|
333
|
+
* ではなく RangeError で報告)。
|
|
334
|
+
*/
|
|
335
|
+
const valueFormatOf = (value: ValueKind, formats: CommandFormats | undefined): CellValueFormat => {
|
|
336
|
+
// locale は宣言時だけ刻印 (undefined キーを書かない)
|
|
337
|
+
const locale = formats === undefined || formats.locale === undefined ? {} : { locale: formats.locale }
|
|
338
|
+
switch (value) {
|
|
339
|
+
case "number":
|
|
340
|
+
return { type: "number", decimals: 2, grouping: true, ...locale }
|
|
341
|
+
case "time":
|
|
342
|
+
return { type: "date", pattern: "HH:mm:ss" }
|
|
343
|
+
case "date":
|
|
344
|
+
return { type: "date" }
|
|
345
|
+
case "currency":
|
|
346
|
+
if (formats === undefined) {
|
|
347
|
+
throw new RangeError("[spreadsheet] formatCurrency requires the formats prop (currency)")
|
|
348
|
+
}
|
|
349
|
+
return { type: "currency", currency: formats.currency, decimals: 2, ...locale }
|
|
350
|
+
case "percent":
|
|
351
|
+
return { type: "percent", decimals: 2, ...locale }
|
|
352
|
+
default:
|
|
353
|
+
return { type: "scientific", decimals: 2, ...locale }
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Builds the command runner.
|
|
359
|
+
* コマンド実行器を構築する。
|
|
360
|
+
*/
|
|
361
|
+
export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
362
|
+
// paramsRef パターン: 戻り値のコールバックは identity 安定、最新パラメータはレンダー毎にここへ写す
|
|
363
|
+
const paramsRef = useRef(params)
|
|
364
|
+
paramsRef.current = params
|
|
365
|
+
|
|
366
|
+
/** The write targets: the session cell while editing, else every selected range. / 書込対象: 編集中はセッションセル、それ以外は全選択レンジ。 */
|
|
367
|
+
const targetRanges = useCallback((): readonly CellRange[] => {
|
|
368
|
+
const p = paramsRef.current
|
|
369
|
+
if (p.isEditing()) {
|
|
370
|
+
const active = p.getActive()
|
|
371
|
+
return [makeRange(active, active)]
|
|
372
|
+
}
|
|
373
|
+
return p.getSelection().ranges
|
|
374
|
+
}, [])
|
|
375
|
+
|
|
376
|
+
/** The active cell's BASE format (capability branch). / active セルの BASE 書式 (能力分岐)。 */
|
|
377
|
+
const activeBase = useCallback((): CellFormat | undefined => {
|
|
378
|
+
const p = paramsRef.current
|
|
379
|
+
const active = p.getActive()
|
|
380
|
+
return p.adapter.getCellFormat === undefined ? undefined : p.adapter.getCellFormat(active.row, active.col)
|
|
381
|
+
}, [])
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The format write core: gate → enumerate → mergeFormat memoised per (patch bucket, base
|
|
385
|
+
* identity) → ONE commit with source "format" → sink repaint while editing. `patchOf` receives
|
|
386
|
+
* the cell, its range and its base (already read once here — never a second adapter read).
|
|
387
|
+
* 書式書込の中核: ゲート → 走査 → (パッチ束, base identity) 毎に memo した mergeFormat → source
|
|
388
|
+
* "format" の commit 1 回 → 編集中はシンク再塗り。patchOf はセル・レンジ・base (ここで 1 回読んだ
|
|
389
|
+
* もの — アダプタを二度読みしない) を受け取る。
|
|
390
|
+
*/
|
|
391
|
+
const writeFormat = useCallback(
|
|
392
|
+
(patchOf: (cell: CellCoord, range: CellRange, base: CellFormat | undefined) => { bucket: number; patch: CellFormatPatch }): boolean => {
|
|
393
|
+
const p = paramsRef.current
|
|
394
|
+
const ranges = targetRanges()
|
|
395
|
+
if (!gateSelectionWrite("format", ranges, p.maxOperationCells, p.onOperationRejected)) {
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
// memo: バケット (パッチの同一性) × base identity — パッチはジェスチャ内で定数なので
|
|
399
|
+
// intern された base 1 つにつき mergeFormat は 1 回 (applyEdits 引数の format identity は 1 つ)
|
|
400
|
+
const memo = new Map<number, Map<CellFormat | undefined, CellFormat | undefined>>()
|
|
401
|
+
const edits: CellEdit[] = []
|
|
402
|
+
const read = p.adapter.getCellFormat
|
|
403
|
+
for (const range of ranges) {
|
|
404
|
+
enumerateRangeCells([range], (cell) => {
|
|
405
|
+
const base = read === undefined ? undefined : read(cell.row, cell.col)
|
|
406
|
+
const { bucket, patch } = patchOf(cell, range, base)
|
|
407
|
+
let byBase = memo.get(bucket)
|
|
408
|
+
if (byBase === undefined) {
|
|
409
|
+
byBase = new Map()
|
|
410
|
+
memo.set(bucket, byBase)
|
|
411
|
+
}
|
|
412
|
+
let next: CellFormat | undefined
|
|
413
|
+
if (byBase.has(base)) {
|
|
414
|
+
next = byBase.get(base)
|
|
415
|
+
} else {
|
|
416
|
+
next = mergeFormat(base, patch)
|
|
417
|
+
byBase.set(base, next)
|
|
418
|
+
}
|
|
419
|
+
// 無変化 (identity) は edit を出さない
|
|
420
|
+
if (next === base) {
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
edits.push({ row: cell.row, col: cell.col, format: next === undefined ? null : next })
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
if (edits.length === 0) {
|
|
427
|
+
return true
|
|
428
|
+
}
|
|
429
|
+
if (p.commitEdits(edits, { source: "format" }) === null) {
|
|
430
|
+
return false
|
|
431
|
+
}
|
|
432
|
+
// 編集列: セッションセルの書式が変わったのでシンクを再塗り
|
|
433
|
+
if (p.isEditing()) {
|
|
434
|
+
p.refreshEditPaint()
|
|
435
|
+
}
|
|
436
|
+
return true
|
|
437
|
+
},
|
|
438
|
+
[targetRanges],
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
/** A constant patch (computed once) applied to every target. / 定数パッチ (1 回計算) を全対象へ。 */
|
|
442
|
+
const writeConstantPatch = useCallback((patch: CellFormatPatch): boolean => writeFormat(() => ({ bucket: 0, patch })), [writeFormat])
|
|
443
|
+
|
|
444
|
+
/** Pushes the opt-in announcement after a successful format command. / 書式コマンド成功後の opt-in 告知。 */
|
|
445
|
+
const announceFormat = useCallback((command: SpreadsheetCommand, state: boolean | null): void => {
|
|
446
|
+
const p = paramsRef.current
|
|
447
|
+
if (p.formatCommandAnnouncement !== undefined) {
|
|
448
|
+
p.announce(p.formatCommandAnnouncement({ command, state }))
|
|
449
|
+
}
|
|
450
|
+
}, [])
|
|
451
|
+
|
|
452
|
+
const setBorders = useCallback(
|
|
453
|
+
(preset: BorderPreset, border: CellBorder = DEFAULT_BORDER): boolean => {
|
|
454
|
+
if (preset === "clear") {
|
|
455
|
+
return writeConstantPatch({ borders: null })
|
|
456
|
+
}
|
|
457
|
+
if (preset === "all") {
|
|
458
|
+
return writeConstantPatch({ borders: { top: border, right: border, bottom: border, left: border } })
|
|
459
|
+
}
|
|
460
|
+
// 位置依存 (outer = 全外周辺 / 単一辺 = レンジの当該外周辺だけ — Excel / Sheets のレンジ辺
|
|
461
|
+
// 意味論、分冊 08 §11): 各セル自身の辺を展開し該当辺だけを置く (borders は集合置換 — 内側 /
|
|
462
|
+
// 辺外のセルは辺を保つ)。memo は辺マスク毎で、マスク 0 は identity merge となり edit なし
|
|
463
|
+
const presetMask = preset === "outer" ? 15 : SIDE_BITS[preset]
|
|
464
|
+
return writeFormat((cell, range, base) => {
|
|
465
|
+
const mask = edgeMaskOf(cell, range) & presetMask
|
|
466
|
+
return { bucket: mask, patch: { borders: { ...sidesOf(base), ...sidesOfMask(mask, border) } } }
|
|
467
|
+
})
|
|
468
|
+
},
|
|
469
|
+
[writeConstantPatch, writeFormat],
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
/** Copies one source cell (text / shifted formula + declared aspects, comments excluded) onto targets — the fill handle's per-source rule (`readFillSource` / `fillSourceEdit`). / ソースセル 1 個 (text / シフト数式 + 宣言済み側面、comments 除外) を対象へ複製 — fill ハンドルと同じソース毎規則 (`readFillSource` / `fillSourceEdit`)。 */
|
|
473
|
+
const fillEdits = useCallback((source: CellCoord, targets: readonly CellCoord[], edits: CellEdit[]): void => {
|
|
474
|
+
// ソースは 1 回だけ解決し、対象毎に数式シフト + 側面キーの複製 (ハンドルドラッグと同一規則)
|
|
475
|
+
const resolved = readFillSource(paramsRef.current.adapter, source.row, source.col)
|
|
476
|
+
for (const target of targets) {
|
|
477
|
+
edits.push(fillSourceEdit(resolved, source, target))
|
|
478
|
+
}
|
|
479
|
+
}, [])
|
|
480
|
+
|
|
481
|
+
/** fillDown / fillRight / fillSelection (idle). / fillDown / fillRight / fillSelection (idle)。 */
|
|
482
|
+
const runFill = useCallback(
|
|
483
|
+
(direction: "down" | "right" | "selection"): boolean => {
|
|
484
|
+
const p = paramsRef.current
|
|
485
|
+
const selection = p.getSelection()
|
|
486
|
+
if (!gateSelectionWrite("fill", selection.ranges, p.maxOperationCells, p.onOperationRejected)) {
|
|
487
|
+
return false
|
|
488
|
+
}
|
|
489
|
+
const edits: CellEdit[] = []
|
|
490
|
+
if (direction === "selection") {
|
|
491
|
+
// active の内容を active 以外の全選択セルへ
|
|
492
|
+
const active = selection.active
|
|
493
|
+
const targets: CellCoord[] = []
|
|
494
|
+
enumerateRangeCells(selection.ranges, (cell) => {
|
|
495
|
+
if (cell.row !== active.row || cell.col !== active.col) {
|
|
496
|
+
targets.push(cell)
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
fillEdits(active, targets, edits)
|
|
500
|
+
} else {
|
|
501
|
+
for (const range of selection.ranges) {
|
|
502
|
+
if (direction === "down") {
|
|
503
|
+
const single = range.startRow === range.endRow
|
|
504
|
+
// 1 行のレンジは上隣を複製 (行 0 は上隣が無い — 何もしない)
|
|
505
|
+
if (single && range.startRow === 0) {
|
|
506
|
+
continue
|
|
507
|
+
}
|
|
508
|
+
const sourceRow = single ? range.startRow - 1 : range.startRow
|
|
509
|
+
for (let col = range.startCol; col <= range.endCol; col += 1) {
|
|
510
|
+
const targets: CellCoord[] = []
|
|
511
|
+
for (let row = single ? range.startRow : range.startRow + 1; row <= range.endRow; row += 1) {
|
|
512
|
+
targets.push({ row, col })
|
|
513
|
+
}
|
|
514
|
+
fillEdits({ row: sourceRow, col }, targets, edits)
|
|
515
|
+
}
|
|
516
|
+
} else {
|
|
517
|
+
const single = range.startCol === range.endCol
|
|
518
|
+
// 1 列のレンジは左隣を複製 (列 0 は左隣が無い — 何もしない)
|
|
519
|
+
if (single && range.startCol === 0) {
|
|
520
|
+
continue
|
|
521
|
+
}
|
|
522
|
+
const sourceCol = single ? range.startCol - 1 : range.startCol
|
|
523
|
+
for (let row = range.startRow; row <= range.endRow; row += 1) {
|
|
524
|
+
const targets: CellCoord[] = []
|
|
525
|
+
for (let col = single ? range.startCol : range.startCol + 1; col <= range.endCol; col += 1) {
|
|
526
|
+
targets.push({ row, col })
|
|
527
|
+
}
|
|
528
|
+
fillEdits({ row, col: sourceCol }, targets, edits)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (edits.length === 0) {
|
|
534
|
+
return true
|
|
535
|
+
}
|
|
536
|
+
return p.commitEdits(edits, { source: "fill" }) !== null
|
|
537
|
+
},
|
|
538
|
+
[fillEdits],
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
/** Self-calc over the selection (source "calc"). / 選択の自己計算 (source "calc")。 */
|
|
542
|
+
const runCalc = useCallback((transform: { text: (text: string) => string | null; formula: (formula: string) => string }): boolean => {
|
|
543
|
+
const p = paramsRef.current
|
|
544
|
+
const selection = p.getSelection()
|
|
545
|
+
if (!gateSelectionWrite("calc", selection.ranges, p.maxOperationCells, p.onOperationRejected)) {
|
|
546
|
+
return false
|
|
547
|
+
}
|
|
548
|
+
const edits: CellEdit[] = []
|
|
549
|
+
const adapter = p.adapter
|
|
550
|
+
enumerateRangeCells(selection.ranges, (cell) => {
|
|
551
|
+
// 数式扱いは getCellFormula 宣言時のみ (text 側は "=" を嗅がない)
|
|
552
|
+
const formula = adapter.getCellFormula === undefined ? undefined : adapter.getCellFormula(cell.row, cell.col)
|
|
553
|
+
const next = formula === undefined ? transform.text(adapter.getCellText(cell.row, cell.col)) : transform.formula(formula)
|
|
554
|
+
// null = 変換不能 (非数 / 非真偽値) — そのセルは触らない
|
|
555
|
+
if (next !== null) {
|
|
556
|
+
edits.push({ row: cell.row, col: cell.col, text: next })
|
|
557
|
+
}
|
|
558
|
+
})
|
|
559
|
+
if (edits.length === 0) {
|
|
560
|
+
return true
|
|
561
|
+
}
|
|
562
|
+
return p.commitEdits(edits, { source: "calc" }) !== null
|
|
563
|
+
}, [])
|
|
564
|
+
|
|
565
|
+
/** hideRows / hideColumns / unhideRows / unhideColumns through the resize gates. / hide / unhide (リサイズゲート経由)。 */
|
|
566
|
+
const runHide = useCallback((axisKind: "row" | "col", hide: boolean): boolean => {
|
|
567
|
+
const p = paramsRef.current
|
|
568
|
+
const axis = axisKind === "row" ? p.rowAxis : p.colAxis
|
|
569
|
+
const selection = p.getSelection()
|
|
570
|
+
// 実体化前の保守カウント (レンジ跨ぎの重複は重複のまま数える — useHeaderResize の collectTargets と同じ規律)
|
|
571
|
+
const limit = axisKind === "row" ? Math.floor(p.maxOperationCells / ROW_FANOUT_DIVISOR) : p.maxOperationCells
|
|
572
|
+
let span = 0
|
|
573
|
+
for (const range of selection.ranges) {
|
|
574
|
+
span += axisKind === "row" ? range.endRow - range.startRow + 1 : range.endCol - range.startCol + 1
|
|
575
|
+
}
|
|
576
|
+
if (span > limit) {
|
|
577
|
+
p.onOperationRejected?.("resize", span, "limit")
|
|
578
|
+
return false
|
|
579
|
+
}
|
|
580
|
+
const seen = new Set<number>()
|
|
581
|
+
const entries: ResizeEntry[] = []
|
|
582
|
+
const size = hide ? 0 : axisKind === "row" ? p.defaultRowHeight : p.defaultColumnWidth
|
|
583
|
+
for (const range of selection.ranges) {
|
|
584
|
+
const start = axisKind === "row" ? range.startRow : range.startCol
|
|
585
|
+
const end = axisKind === "row" ? range.endRow : range.endCol
|
|
586
|
+
for (let index = start; index <= end; index += 1) {
|
|
587
|
+
if (seen.has(index)) {
|
|
588
|
+
continue
|
|
589
|
+
}
|
|
590
|
+
seen.add(index)
|
|
591
|
+
// hide は可視トラックだけ、unhide は非表示トラックだけを対象にする
|
|
592
|
+
const hidden = axis.sizeOf(index) === 0
|
|
593
|
+
if (hidden !== hide) {
|
|
594
|
+
entries.push({ index, size })
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const result = gateResizeEntries(axis, entries, null, { maxOperationCells: p.maxOperationCells, rowHiddenLimit: ROW_HIDDEN_LIMIT, axisKind })
|
|
599
|
+
if (!result.ok) {
|
|
600
|
+
p.onOperationRejected?.("resize", result.count, result.reason)
|
|
601
|
+
return false
|
|
602
|
+
}
|
|
603
|
+
// 対象トラックが無ければ幾何に触れない
|
|
604
|
+
if (result.entries.length > 0) {
|
|
605
|
+
p.resizeTracks(axisKind, result.entries)
|
|
606
|
+
}
|
|
607
|
+
return true
|
|
608
|
+
}, [])
|
|
609
|
+
|
|
610
|
+
/** The active cell's hyperlink through the declared aspect (undefined = no link, the aspect is undeclared, or the raw URL fails the allowlist — the read trust boundary, ADR-10). / 宣言済み側面経由の active セルのリンク (undefined = リンク無し、側面未宣言、または生 URL が許可リスト外 — 読み口の信頼境界、ADR-10)。 */
|
|
611
|
+
const activeHyperlink = useCallback((): string | undefined => {
|
|
612
|
+
const p = paramsRef.current
|
|
613
|
+
// 側面未宣言はリンク無しと同じ (能力分岐)
|
|
614
|
+
if (p.adapter.getCellHyperlink === undefined) {
|
|
615
|
+
return undefined
|
|
616
|
+
}
|
|
617
|
+
const active = p.getActive()
|
|
618
|
+
// 信頼境界: 許可外スキームはリンク無し — 開通ゲートにも onHyperlinkOpen にも生 URL を渡さない
|
|
619
|
+
return normalizeAdapterHyperlink(p.adapter.getCellHyperlink(active.row, active.col))
|
|
620
|
+
}, [])
|
|
621
|
+
|
|
622
|
+
/** The capability gate (the keymap row exists only when true — `openHyperlink` with a declared host is cell-aware). / 能力ゲート (true のときだけキーマップ行が存在 — host 宣言時の `openHyperlink` はセル依存)。 */
|
|
623
|
+
const isCommandAvailable = useCallback(
|
|
624
|
+
(command: SpreadsheetCommand): boolean => {
|
|
625
|
+
const p = paramsRef.current
|
|
626
|
+
const adapter = p.adapter
|
|
627
|
+
const spec = COMMAND_SPECS[command]
|
|
628
|
+
const formatDeclared = adapter.getCellFormat !== undefined
|
|
629
|
+
const popoverEnabled = (kind: PopoverKind): boolean => (p.popovers === undefined ? true : p.popovers[kind] !== false)
|
|
630
|
+
switch (spec.kind) {
|
|
631
|
+
case "toggle":
|
|
632
|
+
case "patch":
|
|
633
|
+
case "clear":
|
|
634
|
+
case "borders":
|
|
635
|
+
case "color":
|
|
636
|
+
return formatDeclared
|
|
637
|
+
case "value":
|
|
638
|
+
return formatDeclared && (spec.value !== "currency" || p.formats !== undefined)
|
|
639
|
+
case "popover":
|
|
640
|
+
switch (spec.popover) {
|
|
641
|
+
case "link":
|
|
642
|
+
return adapter.getCellHyperlink !== undefined && popoverEnabled("link")
|
|
643
|
+
case "note":
|
|
644
|
+
return adapter.getCellNote !== undefined && popoverEnabled("note")
|
|
645
|
+
case "comment":
|
|
646
|
+
return adapter.getCellComments !== undefined && popoverEnabled("comment")
|
|
647
|
+
default:
|
|
648
|
+
return adapter.getCellValidation !== undefined && popoverEnabled("list")
|
|
649
|
+
}
|
|
650
|
+
case "openHyperlink":
|
|
651
|
+
// host 宣言時: active にリンクがあるときだけ行が存在 (リンク無しは素通し — README / 分冊 04 §3.2 / 08 §6.3)。
|
|
652
|
+
// 未宣言: link ポップオーバーが開ける限り行あり (Ctrl+K 同等 — リンク無しでも編集器を開く)
|
|
653
|
+
return adapter.getCellHyperlink !== undefined && (p.onHyperlinkOpen !== undefined ? activeHyperlink() !== undefined : popoverEnabled("link"))
|
|
654
|
+
case "paste":
|
|
655
|
+
return spec.options.mode === "formats" ? formatDeclared : spec.options.mode === "formulas" ? adapter.getCellFormula !== undefined : true
|
|
656
|
+
default:
|
|
657
|
+
return true
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
[activeHyperlink],
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
const executeCommand = useCallback(
|
|
664
|
+
(command: SpreadsheetCommand): boolean => {
|
|
665
|
+
const p = paramsRef.current
|
|
666
|
+
if (!isCommandAvailable(command)) {
|
|
667
|
+
return false
|
|
668
|
+
}
|
|
669
|
+
const spec = COMMAND_SPECS[command]
|
|
670
|
+
const editing = p.isEditing()
|
|
671
|
+
// 編集列に無いコマンドは先にセッションを確定する (拒否なら中断)
|
|
672
|
+
if (editing && !EDITING_COLUMN_COMMANDS.has(command) && !p.requestCommit()) {
|
|
673
|
+
return false
|
|
674
|
+
}
|
|
675
|
+
switch (spec.kind) {
|
|
676
|
+
case "toggle": {
|
|
677
|
+
const base = activeBase()
|
|
678
|
+
// active の BASE から 1 回だけ: on なら消去 (null)、off なら true — 混在選択は active に従う
|
|
679
|
+
const on = base !== undefined && base[spec.key] === true
|
|
680
|
+
if (!writeConstantPatch({ [spec.key]: on ? null : true })) {
|
|
681
|
+
return false
|
|
682
|
+
}
|
|
683
|
+
announceFormat(command, !on)
|
|
684
|
+
return true
|
|
685
|
+
}
|
|
686
|
+
case "patch": {
|
|
687
|
+
if (!writeConstantPatch(spec.patch)) {
|
|
688
|
+
return false
|
|
689
|
+
}
|
|
690
|
+
announceFormat(command, null)
|
|
691
|
+
return true
|
|
692
|
+
}
|
|
693
|
+
case "clear": {
|
|
694
|
+
if (!writeConstantPatch(CLEAR_PATCH)) {
|
|
695
|
+
return false
|
|
696
|
+
}
|
|
697
|
+
announceFormat(command, null)
|
|
698
|
+
return true
|
|
699
|
+
}
|
|
700
|
+
case "value": {
|
|
701
|
+
if (!writeConstantPatch({ value: valueFormatOf(spec.value, p.formats) })) {
|
|
702
|
+
return false
|
|
703
|
+
}
|
|
704
|
+
announceFormat(command, null)
|
|
705
|
+
return true
|
|
706
|
+
}
|
|
707
|
+
case "borders": {
|
|
708
|
+
if (!setBorders(spec.preset)) {
|
|
709
|
+
return false
|
|
710
|
+
}
|
|
711
|
+
announceFormat(command, null)
|
|
712
|
+
return true
|
|
713
|
+
}
|
|
714
|
+
case "color": {
|
|
715
|
+
// 目標色: プリセットか colors スロット (スロット不在は行不在と同じ = 何もしない)
|
|
716
|
+
const slots = spec.key === "background" ? p.colors.fill : p.colors.font
|
|
717
|
+
const target = spec.preset !== null ? spec.preset : spec.custom !== null && spec.custom < slots.length ? slots[spec.custom] : undefined
|
|
718
|
+
if (target === undefined) {
|
|
719
|
+
return false
|
|
720
|
+
}
|
|
721
|
+
const base = activeBase()
|
|
722
|
+
// active の BASE 色が目標と等しければ選択全体を消去、違えば目標へ
|
|
723
|
+
const on = sameColor(base === undefined ? undefined : base[spec.key], target)
|
|
724
|
+
if (!writeConstantPatch({ [spec.key]: on ? null : target })) {
|
|
725
|
+
return false
|
|
726
|
+
}
|
|
727
|
+
announceFormat(command, !on)
|
|
728
|
+
return true
|
|
729
|
+
}
|
|
730
|
+
case "calc":
|
|
731
|
+
return runCalc({ text: (text) => scaleDecimalText(text, spec.exponent), formula: (formula) => scaleFormula(formula, spec.exponent) })
|
|
732
|
+
case "sign":
|
|
733
|
+
return runCalc({ text: reverseSignText, formula: reverseSignFormula })
|
|
734
|
+
case "fill": {
|
|
735
|
+
if (editing && spec.direction === "selection") {
|
|
736
|
+
// 編集列: セッション文字列を全選択セルへ 1 applyEdits、セッション閉鎖
|
|
737
|
+
const text = p.getSessionText()
|
|
738
|
+
if (text === null) {
|
|
739
|
+
return false
|
|
740
|
+
}
|
|
741
|
+
const selection = p.getSelection()
|
|
742
|
+
if (!gateSelectionWrite("fill", selection.ranges, p.maxOperationCells, p.onOperationRejected)) {
|
|
743
|
+
return false
|
|
744
|
+
}
|
|
745
|
+
// 検証ゲート (分冊 08 §4.2) は対象セル毎 — 規則はセル毎に違い得る。最初の strict 拒否で
|
|
746
|
+
// 走査を打ち切り applyEdits へ届けない (通知と message ポップオーバーは root の validateCommit が担う)
|
|
747
|
+
const edits: CellEdit[] = []
|
|
748
|
+
let rejected = false
|
|
749
|
+
enumerateRangeCells(selection.ranges, (cell) => {
|
|
750
|
+
if (rejected) {
|
|
751
|
+
return
|
|
752
|
+
}
|
|
753
|
+
if (!p.validateCommit(cell, text).ok) {
|
|
754
|
+
rejected = true
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
edits.push({ row: cell.row, col: cell.col, text })
|
|
758
|
+
})
|
|
759
|
+
// 拒否はセッション維持のまま false (Enter の keep 経路と同じ — Escape で取消 / 修正して再試行)
|
|
760
|
+
if (rejected) {
|
|
761
|
+
return false
|
|
762
|
+
}
|
|
763
|
+
if (p.commitEdits(edits, { source: "fill" }) === null) {
|
|
764
|
+
return false
|
|
765
|
+
}
|
|
766
|
+
// 値は全セルへ書かれた — セッションは破棄で閉じる (二重書込なし)
|
|
767
|
+
p.cancelEdit()
|
|
768
|
+
return true
|
|
769
|
+
}
|
|
770
|
+
return runFill(spec.direction)
|
|
771
|
+
}
|
|
772
|
+
case "popover":
|
|
773
|
+
p.openPopover(spec.popover)
|
|
774
|
+
return true
|
|
775
|
+
case "openHyperlink": {
|
|
776
|
+
if (p.onHyperlinkOpen !== undefined) {
|
|
777
|
+
const url = activeHyperlink()
|
|
778
|
+
// リンク無しは isCommandAvailable が先に落とす (行不在 = 素通し) — ここは型の絞り込みだけ
|
|
779
|
+
if (url === undefined) {
|
|
780
|
+
return false
|
|
781
|
+
}
|
|
782
|
+
p.onHyperlinkOpen(url, p.getActive())
|
|
783
|
+
return true
|
|
784
|
+
}
|
|
785
|
+
// 未宣言: link ポップオーバー (Open ボタンなし)
|
|
786
|
+
p.openPopover("link")
|
|
787
|
+
return true
|
|
788
|
+
}
|
|
789
|
+
case "pasteSpecial": {
|
|
790
|
+
if (p.onPasteSpecialRequest !== undefined) {
|
|
791
|
+
p.onPasteSpecialRequest({ kind: "keyboard", cellRect: p.keyboardCellRect() })
|
|
792
|
+
return true
|
|
793
|
+
}
|
|
794
|
+
// 未宣言: 書式のみ貼り付け (メニュー経路)
|
|
795
|
+
void p.paste({ mode: "formats" })
|
|
796
|
+
return true
|
|
797
|
+
}
|
|
798
|
+
case "paste":
|
|
799
|
+
void p.paste(spec.options)
|
|
800
|
+
return true
|
|
801
|
+
case "insertNow": {
|
|
802
|
+
const text = spec.unit === "date" ? formatLocalDate(p.now()) : formatLocalTime(p.now())
|
|
803
|
+
if (editing) {
|
|
804
|
+
p.insertAtCaret(text)
|
|
805
|
+
return true
|
|
806
|
+
}
|
|
807
|
+
// idle: active セルだけへ 1 applyEdits (選択全体には書かない)、直接確定 (セッションは無い = discarded)
|
|
808
|
+
const active = p.getActive()
|
|
809
|
+
// Excel の Ctrl+; は編集モード経由で Enter 確定が検証される (strict ⇒ 拒否) — 分冊 08 §4.2 の
|
|
810
|
+
// fill / 貼り付け非検証免除はここへ及ばないため、strict 拒否は書込前に判定する
|
|
811
|
+
// (通知と message ポップオーバーは root の validateCommit が担う)
|
|
812
|
+
if (!p.validateCommit(active, text).ok) {
|
|
813
|
+
return false
|
|
814
|
+
}
|
|
815
|
+
return p.commitEdits([{ row: active.row, col: active.col, text }], { source: "edit", editSession: "discarded" }) !== null
|
|
816
|
+
}
|
|
817
|
+
case "hide":
|
|
818
|
+
return runHide(spec.axis, spec.hide)
|
|
819
|
+
default: {
|
|
820
|
+
const h = p.getHighlight()
|
|
821
|
+
switch (spec.target) {
|
|
822
|
+
case "row":
|
|
823
|
+
p.setHighlight({ ...h, row: !h.row })
|
|
824
|
+
return true
|
|
825
|
+
case "column":
|
|
826
|
+
p.setHighlight({ ...h, column: !h.column })
|
|
827
|
+
return true
|
|
828
|
+
case "both": {
|
|
829
|
+
// 両方 on のときだけ off へ、それ以外は両方 on へ
|
|
830
|
+
const on = h.row && h.column
|
|
831
|
+
p.setHighlight({ ...h, row: !on, column: !on })
|
|
832
|
+
return true
|
|
833
|
+
}
|
|
834
|
+
default:
|
|
835
|
+
p.setHighlight({ ...h, intersection: !h.intersection })
|
|
836
|
+
return true
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
[isCommandAvailable, activeHyperlink, activeBase, writeConstantPatch, announceFormat, setBorders, runCalc, runFill, runHide],
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
/** Commit gate shared by the annotation setters. / 注釈 setter 共通の commit ゲート。 */
|
|
845
|
+
const commitGate = useCallback((): boolean => {
|
|
846
|
+
const p = paramsRef.current
|
|
847
|
+
return !(p.isEditing() && !p.requestCommit())
|
|
848
|
+
}, [])
|
|
849
|
+
|
|
850
|
+
const setHyperlink = useCallback(
|
|
851
|
+
(url: string | null): boolean => {
|
|
852
|
+
const p = paramsRef.current
|
|
853
|
+
if (p.adapter.getCellHyperlink === undefined) {
|
|
854
|
+
throw new RangeError("[spreadsheet] setHyperlink requires the getCellHyperlink capability")
|
|
855
|
+
}
|
|
856
|
+
const normalized = url === null ? null : normalizeHyperlink(url)
|
|
857
|
+
if (url !== null && normalized === null) {
|
|
858
|
+
throw new RangeError(`[spreadsheet] setHyperlink received an unnormalisable url ${JSON.stringify(url)}`)
|
|
859
|
+
}
|
|
860
|
+
if (!commitGate()) {
|
|
861
|
+
return false
|
|
862
|
+
}
|
|
863
|
+
const active = p.getActive()
|
|
864
|
+
return p.commitEdits([{ row: active.row, col: active.col, hyperlink: normalized }], { source: "annotate" }) !== null
|
|
865
|
+
},
|
|
866
|
+
[commitGate],
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
const setNote = useCallback(
|
|
870
|
+
(text: string | null): boolean => {
|
|
871
|
+
const p = paramsRef.current
|
|
872
|
+
if (p.adapter.getCellNote === undefined) {
|
|
873
|
+
throw new RangeError("[spreadsheet] setNote requires the getCellNote capability")
|
|
874
|
+
}
|
|
875
|
+
if (!commitGate()) {
|
|
876
|
+
return false
|
|
877
|
+
}
|
|
878
|
+
const active = p.getActive()
|
|
879
|
+
return p.commitEdits([{ row: active.row, col: active.col, note: text }], { source: "annotate" }) !== null
|
|
880
|
+
},
|
|
881
|
+
[commitGate],
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
const addComment = useCallback(
|
|
885
|
+
(input: CellCommentInput): boolean => {
|
|
886
|
+
const p = paramsRef.current
|
|
887
|
+
const read = p.adapter.getCellComments
|
|
888
|
+
if (read === undefined) {
|
|
889
|
+
throw new RangeError("[spreadsheet] addComment requires the getCellComments capability")
|
|
890
|
+
}
|
|
891
|
+
if (!commitGate()) {
|
|
892
|
+
return false
|
|
893
|
+
}
|
|
894
|
+
const active = p.getActive()
|
|
895
|
+
// 既存スレッド (id つき) を verbatim に保ち input を末尾へ — 1 スレッドとして書く
|
|
896
|
+
const existing = read(active.row, active.col)
|
|
897
|
+
const thread: CellCommentInput[] = existing === undefined ? [input] : [...existing, input]
|
|
898
|
+
return p.commitEdits([{ row: active.row, col: active.col, comments: thread }], { source: "annotate" }) !== null
|
|
899
|
+
},
|
|
900
|
+
[commitGate],
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
const setValidation = useCallback(
|
|
904
|
+
(rule: DataValidationRule | null): boolean => {
|
|
905
|
+
const p = paramsRef.current
|
|
906
|
+
if (p.adapter.getCellValidation === undefined) {
|
|
907
|
+
throw new RangeError("[spreadsheet] setValidation requires the getCellValidation capability")
|
|
908
|
+
}
|
|
909
|
+
if (!commitGate()) {
|
|
910
|
+
return false
|
|
911
|
+
}
|
|
912
|
+
const ranges = p.getSelection().ranges
|
|
913
|
+
if (!gateSelectionWrite("annotate", ranges, p.maxOperationCells, p.onOperationRejected)) {
|
|
914
|
+
return false
|
|
915
|
+
}
|
|
916
|
+
const edits: CellEdit[] = []
|
|
917
|
+
enumerateRangeCells(ranges, (cell) => {
|
|
918
|
+
edits.push({ row: cell.row, col: cell.col, validation: rule })
|
|
919
|
+
})
|
|
920
|
+
return p.commitEdits(edits, { source: "annotate" }) !== null
|
|
921
|
+
},
|
|
922
|
+
[commitGate],
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
const applyFormat = useCallback(
|
|
926
|
+
(patch: CellFormatPatch): boolean => {
|
|
927
|
+
if (paramsRef.current.adapter.getCellFormat === undefined) {
|
|
928
|
+
throw new RangeError("[spreadsheet] applyFormat requires the getCellFormat capability")
|
|
929
|
+
}
|
|
930
|
+
return writeConstantPatch(patch)
|
|
931
|
+
},
|
|
932
|
+
[writeConstantPatch],
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
const setBordersPublic = useCallback(
|
|
936
|
+
(preset: BorderPreset, border?: CellBorder): boolean => {
|
|
937
|
+
if (paramsRef.current.adapter.getCellFormat === undefined) {
|
|
938
|
+
throw new RangeError("[spreadsheet] setBorders requires the getCellFormat capability")
|
|
939
|
+
}
|
|
940
|
+
return setBorders(preset, border)
|
|
941
|
+
},
|
|
942
|
+
[setBorders],
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
return useMemo(() => ({ executeCommand, applyFormat, setBorders: setBordersPublic, setHyperlink, setNote, addComment, setValidation, isCommandAvailable }), [executeCommand, applyFormat, setBordersPublic, setHyperlink, setNote, addComment, setValidation, isCommandAvailable])
|
|
946
|
+
}
|