@aiquants/spreadsheet 0.9.0 → 0.10.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 +54 -0
- package/README.md +74 -5
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +129 -7
- package/dist/index.d.ts +129 -7
- package/dist/index.js +4318 -4140
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/package.json +5 -5
- package/src/Spreadsheet.tsx +152 -12
- package/src/clipboard/tiling.ts +106 -16
- package/src/clipboard/useClipboard.ts +120 -7
- package/src/commands/useCommands.ts +226 -42
- package/src/editor/useCellEditor.ts +62 -13
- package/src/grid/GridBody.tsx +29 -3
- package/src/index.ts +5 -1
- package/src/keyboard/useGridKeyboard.ts +24 -3
- package/src/model/dataAdapter.ts +121 -2
- package/src/model/labels.ts +8 -2
- package/src/overlay/useFillHandle.ts +35 -2
- package/src/popover/CommentPopover.tsx +14 -2
- package/src/popover/LinkPopover.tsx +15 -3
- package/src/popover/NotePopover.tsx +24 -6
- package/src/popover/usePopovers.ts +89 -11
|
@@ -18,7 +18,18 @@
|
|
|
18
18
|
* `pasteSpecial.ts`, tiling in the generic `expandRichPaste`, sizes through `gateResizeEntries`
|
|
19
19
|
* (non-undo geometry). Every v0.8 parameter is a capability declaration — absent ⇒ the v0.7 branch
|
|
20
20
|
* byte-for-byte (TSV-only adapters — the commit seam `commitPaste` is the one required member). rearm stays with the setSelection
|
|
21
|
-
* wrapper (§6.9-1 no-double-actor rule).
|
|
21
|
+
* wrapper (§6.9-1 no-double-actor rule). v0.10 read-only walls (whole-operation, before any
|
|
22
|
+
* commit, through the `rejectReadOnly` funnel): G4 refuses ARMING a cut whose source range
|
|
23
|
+
* contains a read-only cell (preventDefault, zero setData, armed clip unchanged, before the
|
|
24
|
+
* multi-range advisory); G5 refuses a paste whose shared target rect (`computePasteTargetRect`,
|
|
25
|
+
* transpose-aware) contains one — only for modes writing a cell aspect (CF-only / columnWidths-only
|
|
26
|
+
* pass); G6a/G6b re-check the cut-move target block and the CURRENT source adapter state before
|
|
27
|
+
* EITHER commit (the cross-sheet source has no dataVersion channel — the move-time re-read is its
|
|
28
|
+
* only wall). Capability absent ⇒ all four compile to the previous path byte-identically.
|
|
29
|
+
* v0.10 paste feedback (plan §3.1): `onPasteResolved(outcome, channel)` — the menu channel is
|
|
30
|
+
* the promise's 1:1 mirror at the single `paste` wrap site, the event channel fires once per
|
|
31
|
+
* `runPaste`-reaching gesture; the drag-freeze and null-`clipboardData` pre-reach branches stay
|
|
32
|
+
* deliberately silent. Prop absent ⇒ never called.
|
|
22
33
|
*
|
|
23
34
|
* シンク上のクリップボード結線 (§6.5.4、v0.8 §4.2.5 / ADR-14)。copy / cut は 3 フレーバー —
|
|
24
35
|
* `text/plain` (素の TSV)、`text/html` (lossless payload を先頭属性 `data-aqsp-clip` に運ぶ相互運用
|
|
@@ -35,7 +46,16 @@
|
|
|
35
46
|
* `navigator.clipboard.read({ unsanitized: ["text/html"] })`) から、変換は `pasteSpecial.ts`、タイル
|
|
36
47
|
* は generic `expandRichPaste`、寸法は `gateResizeEntries` (非 undo 幾何)。v0.8 のパラメータは全て
|
|
37
48
|
* 能力宣言 — 不在なら v0.7 分岐そのまま (TSV のみのアダプタ — commit シーム `commitPaste` だけが必須メンバー)。rearm は
|
|
38
|
-
* setSelection ラッパーの専有 (§6.9-1)。
|
|
49
|
+
* setSelection ラッパーの専有 (§6.9-1)。v0.10 の読み取り専用壁 (操作全体・両 commit より前・
|
|
50
|
+
* `rejectReadOnly` 漏斗経由): G4 は元レンジに読み取り専用セルを含む cut の**武装**を拒否
|
|
51
|
+
* (preventDefault・setData 0 回・装填クリップ不変・複数レンジ通知の前)、G5 は共有ターゲット矩形
|
|
52
|
+
* (`computePasteTargetRect` — 転置対応) に含む貼り付けを拒否 (セル側面を書くモードだけ — CF-only /
|
|
53
|
+
* columnWidths-only は素通し)、G6a/G6b は cut-move の移動先ブロックと元アダプタの**今の**状態を
|
|
54
|
+
* 両 commit より前に再検査 (跨ぎ元シートは dataVersion チャネルが無く移動時再読が唯一の壁)。
|
|
55
|
+
* 能力未宣言なら 4 壁とも従来経路とバイト同一。v0.10 貼り付けフィードバック (計画 §3.1):
|
|
56
|
+
* `onPasteResolved(outcome, channel)` — menu チャネルは単一ラップサイト `paste` での promise
|
|
57
|
+
* 1:1 鏡、event チャネルは `runPaste` 到達ゲスチャ毎 1 回。ドラッグ凍結と `clipboardData` null
|
|
58
|
+
* の到達前 2 分岐は意図的無言。prop 未指定なら一度も呼ばれない。
|
|
39
59
|
*/
|
|
40
60
|
|
|
41
61
|
import { type RefObject, useCallback, useEffect, useMemo, useRef } from "react"
|
|
@@ -45,7 +65,7 @@ import { isWholeCols, isWholeRows, normalizeAdapterHyperlink, normalizeHyperlink
|
|
|
45
65
|
import { type CellCoord, type CellRange, countRangeCells, enumerateRangeCells, makeRange, rangeSize, type SpreadsheetOperation } from "../model/cellCoord.ts"
|
|
46
66
|
import type { CellFormat } from "../model/cellFormat.ts"
|
|
47
67
|
import type { ConditionalFormatRule, ConditionalFormatRuleInput } from "../model/conditionalFormat.ts"
|
|
48
|
-
import { type AspectCapabilities, aspectCapabilities, type CellEdit, type EditRejection, type EditSource, resolveEditText, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
68
|
+
import { type AspectCapabilities, aspectCapabilities, type CellEdit, type EditRejection, type EditSource, type ReadOnlyRejection, rectContainsReadOnly, resolveEditText, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
49
69
|
import { toR1C1 } from "../model/formulaRefs.ts"
|
|
50
70
|
import type { SelectionState } from "../selection/selectionState.ts"
|
|
51
71
|
import {
|
|
@@ -67,7 +87,7 @@ import {
|
|
|
67
87
|
} from "./clipPayload.ts"
|
|
68
88
|
import { parseHtmlTable, serializeHtmlTable } from "./htmlInterop.ts"
|
|
69
89
|
import { DEFAULT_PASTE_OPTIONS, MODE_ASPECTS, type PasteAspects, type PasteOptions, patchFromClipCell, stripUndeclaredAspects, translateConditionalFormats, transposePayload } from "./pasteSpecial.ts"
|
|
70
|
-
import { expandRichPaste } from "./tiling.ts"
|
|
90
|
+
import { computePasteTargetRect, expandRichPaste } from "./tiling.ts"
|
|
71
91
|
import { measureTsv, parseTsv, serializeTsv } from "./tsv.ts"
|
|
72
92
|
|
|
73
93
|
/** The armed clip surface a consumer sees (`onClipChange` / `getClip`): `foreign` = the record belongs to another adapter (sheet switch kept a cut). / consumer が見る装填クリップ (`onClipChange` / `getClip`): `foreign` = 記録が別アダプタのもの (シート切替が cut を保持)。 */
|
|
@@ -136,6 +156,32 @@ export type UseClipboardParams = {
|
|
|
136
156
|
onClipboardUnavailable?: (cause: ClipboardUnavailableCause) => void
|
|
137
157
|
/** Source-side rejection of a cross-sheet cut (the target commit already stands). / 跨ぎ cut の元側拒否 (対象 commit は立っている)。 */
|
|
138
158
|
onEditRejected?: (error: unknown, rejection: EditRejection) => void
|
|
159
|
+
/**
|
|
160
|
+
* v0.10 read-only refusal funnel (required seam — the root always wires it; identity-stable,
|
|
161
|
+
* read through paramsRef): G4 (a refused cut arming, `{op:"cut"}`), G5 (a refused paste over
|
|
162
|
+
* the shared target rect, `{op:"paste"}`) and G6a/G6b (the cut-move target / source walls,
|
|
163
|
+
* `{op:"paste"}` / `{op:"cut"}`) fire it once per refused gesture. Capability absent ⇒ never
|
|
164
|
+
* called (byte identity).
|
|
165
|
+
* v0.10 読み取り専用拒否漏斗 (必須 seam — root が常に渡す。identity 安定、paramsRef 読み):
|
|
166
|
+
* G4 (cut 武装拒否 — `{op:"cut"}`)、G5 (共有ターゲット矩形への貼付拒否 — `{op:"paste"}`)、
|
|
167
|
+
* G6a/G6b (cut-move の移動先 / 元側の両壁 — `{op:"paste"}` / `{op:"cut"}`) が拒否ジェスチャ毎
|
|
168
|
+
* 1 回発火する。能力未宣言なら一度も呼ばれない (バイト恒等)。
|
|
169
|
+
*/
|
|
170
|
+
rejectReadOnly: (rejection: ReadOnlyRejection) => void
|
|
171
|
+
/**
|
|
172
|
+
* v0.10 paste feedback (plan §3.1): the menu channel mirrors `paste(options)`'s resolve value
|
|
173
|
+
* 1:1 from the single wrap site (read refusals and the write-time re-gate included); the
|
|
174
|
+
* event channel fires once per gesture that REACHES `runPaste` — the two pre-reach gate
|
|
175
|
+
* consumptions (drag-freeze, null `clipboardData`) stay deliberately silent. Cause-specific
|
|
176
|
+
* callbacks (`onClipboardUnavailable` / `onOperationRejected` / `onReadOnlyRejected`) fire
|
|
177
|
+
* FIRST; the outcome is the trailing summary. Absent ⇒ never called (byte identity).
|
|
178
|
+
* v0.10 貼り付けフィードバック (計画 §3.1): menu チャネルは `paste(options)` の resolve 値の
|
|
179
|
+
* 1:1 鏡 (単一ラップサイト — 読み拒否・書込時再ゲートも含む)、event チャネルは `runPaste` に
|
|
180
|
+
* **到達した**ゲスチャ毎 1 回 (到達前 2 分岐 — ドラッグ凍結消費と `clipboardData` null — は
|
|
181
|
+
* 意図的無言)。原因特定系 callback が先、outcome は後追いの要約。未指定なら一度も呼ばれない
|
|
182
|
+
* (バイト恒等)。
|
|
183
|
+
*/
|
|
184
|
+
onPasteResolved?: (outcome: PasteOutcome, channel: ClipChannel) => void
|
|
139
185
|
}
|
|
140
186
|
|
|
141
187
|
/** The clip surface (§6.5.4-4 — copy/cut unified in v0.3.2, widened in v0.8). / クリップ状態の公開面 (v0.3.2 で copy / cut 統一、v0.8 で拡張)。 */
|
|
@@ -192,6 +238,18 @@ export const estimateTiledCells = (rows: number, cols: number, target: CellRange
|
|
|
192
238
|
/** True for the plain Ctrl+V options — the only paste that may move a cut (transforms are copy semantics). / 素の Ctrl+V オプションだけ真 — cut を move できる唯一の貼り付け (変換は copy 意味論)。 */
|
|
193
239
|
const isPlainPaste = (options: PasteOptions): boolean => options.mode === "all" && options.operation === undefined && options.transpose !== true && options.skipBlanks !== true
|
|
194
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Whether a paste mode writes any CELL aspect (v0.10 G5 wall condition): text / formula /
|
|
243
|
+
* format / borders / note / comments / hyperlink / validation. `cf` and `sizes` are the
|
|
244
|
+
* NON-cell channels — a CF-only or columnWidths-only mode never walks the read-only wall
|
|
245
|
+
* (sheet-level rules and geometry are not cell content, ADR-28 out-of-scope registration).
|
|
246
|
+
* 貼り付けモードがセル側面 (text / formula / format / borders / note / comments / hyperlink /
|
|
247
|
+
* validation のいずれか) を書くか (v0.10 G5 の壁条件)。`cf` と `sizes` は非セルチャネル —
|
|
248
|
+
* CF だけ / 列幅だけのモードは読み取り専用壁を walk しない (シート水準の規則と幾何はセル内容
|
|
249
|
+
* ではない — ADR-28 のスコープ外登記)。
|
|
250
|
+
*/
|
|
251
|
+
const hasCellAspect = (aspects: PasteAspects): boolean => aspects.text || aspects.formula || aspects.format || aspects.borders || aspects.note || aspects.comments || aspects.hyperlink || aspects.validation
|
|
252
|
+
|
|
195
253
|
/**
|
|
196
254
|
* The event channel's flavours: the DataTransfer types and a `getData` memoised per type for the
|
|
197
255
|
* gesture (a paste event's `getData` re-materialises the string on every call — 91 ms for 15.8 MB).
|
|
@@ -633,6 +691,14 @@ export const useClipboard = (params: UseClipboardParams): ClipboardController =>
|
|
|
633
691
|
p.onOperationRejected?.(op, count, "limit")
|
|
634
692
|
return null
|
|
635
693
|
}
|
|
694
|
+
// G4 (v0.10 §3-6): cut の武装壁 — 上限ゲート直後・複数レンジ通知の前 (拒否 cut に advisory を
|
|
695
|
+
// 出さない)。preventDefault はネイティブ cut がシンクの " " を OS クリップボードへ書くのも防ぎ、
|
|
696
|
+
// setData 0 回 = OS クリップボードも装填クリップも不変。copy は無条件 ALLOW (§3-9)
|
|
697
|
+
if (op === "cut" && rectContainsReadOnly(p.adapter, range)) {
|
|
698
|
+
event.preventDefault()
|
|
699
|
+
p.rejectReadOnly({ kind: "operation", op: "cut", cellCount: count })
|
|
700
|
+
return null
|
|
701
|
+
}
|
|
636
702
|
// 複数レンジ選択は末尾レンジのみ + 通知 (明示逸脱 — Excel の整列連結コピーは非対応 §6.5.4-3)
|
|
637
703
|
if (selection.ranges.length > 1) {
|
|
638
704
|
p.onCopyMultiRange?.()
|
|
@@ -746,6 +812,20 @@ export const useClipboard = (params: UseClipboardParams): ClipboardController =>
|
|
|
746
812
|
p.onOperationRejected?.("paste", cellCount, "bounds")
|
|
747
813
|
return "rejected"
|
|
748
814
|
}
|
|
815
|
+
// G6a (v0.10 §3-7): 移動先の読み取り専用壁 — staleness bounds 拒否の後・遅延読みの前 (両 commit
|
|
816
|
+
// より前)。cut はタイルしない: 矩形は共有幾何へ 1×1 ターゲット (= 原点 1 回展開) を渡した装填
|
|
817
|
+
// ブロックそのもの (ADR-28 rule 5 — bounds 通過済みなので clamp は恒等で null は構造的に不能)
|
|
818
|
+
const targetRect = computePasteTargetRect({ rows: snapshot.rows, cols: snapshot.cols }, makeRange(origin, origin), origin, adapter.rowCount, adapter.colCount)
|
|
819
|
+
if (targetRect !== null && rectContainsReadOnly(adapter, targetRect)) {
|
|
820
|
+
p.rejectReadOnly({ kind: "operation", op: "paste", cellCount: snapshot.rows * snapshot.cols })
|
|
821
|
+
return "rejected" // クリップは維持 (bounds 拒否と同じ品行)
|
|
822
|
+
}
|
|
823
|
+
// G6b (v0.10 §3-8): 元レンジの移動時再検査 — 装填後の flip と跨ぎ元シート (dataVersion チャネルの
|
|
824
|
+
// 無い唯一の面 — ADR-28 rule 4) に対する壁。offending セルは元 = payload は {op:"cut"} (G4 と同文言)
|
|
825
|
+
if (rectContainsReadOnly(armed.sourceAdapter, armed.range)) {
|
|
826
|
+
p.rejectReadOnly({ kind: "operation", op: "cut", cellCount: countRangeCells([armed.range]) })
|
|
827
|
+
return "rejected"
|
|
828
|
+
}
|
|
749
829
|
// 遅延読み: Ctrl+X 後の書式 / 注釈 commit を運ぶ (ゲートは snapshot 寸法で済んでいる)
|
|
750
830
|
const payload = armedPayload(armed)
|
|
751
831
|
const caps = aspectCapabilities(adapter)
|
|
@@ -925,6 +1005,18 @@ export const useClipboard = (params: UseClipboardParams): ClipboardController =>
|
|
|
925
1005
|
const caps = aspectCapabilities(adapter)
|
|
926
1006
|
const shaped = stripUndeclaredAspects(transpose ? transposePayload(payload) : payload, caps)
|
|
927
1007
|
const aspects = MODE_ASPECTS[options.mode]
|
|
1008
|
+
// G5 (v0.10 §3-4): 読み取り専用壁 — cut ルーティング / 転置 / 側面剥離の後・実体化
|
|
1009
|
+
// (expandRichPaste) の前。壁はセル側面を書くモードだけ (CF-only / columnWidths-only は
|
|
1010
|
+
// walk なしで ALLOW — §3-5)。矩形はタイラーと同一の共有幾何 (computePasteTargetRect —
|
|
1011
|
+
// ADR-28 rule 5、寸法は転置済み shaped のもの)。null (基点グリッド外) は walk せず従来の
|
|
1012
|
+
// "empty" 経路へ落とす。拒否はクリップ / 選択 / CF / サイズ全チャネル不動 (whole gesture)
|
|
1013
|
+
if (hasCellAspect(aspects)) {
|
|
1014
|
+
const wallRect = computePasteTargetRect({ rows: shaped.rows, cols: shaped.cols }, target, selection.active, adapter.rowCount, adapter.colCount)
|
|
1015
|
+
if (wallRect !== null && rectContainsReadOnly(adapter, wallRect)) {
|
|
1016
|
+
p.rejectReadOnly({ kind: "operation", op: "paste", cellCount: countRangeCells([wallRect]) })
|
|
1017
|
+
return "rejected"
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
928
1020
|
const cursor: PasteCursor = {
|
|
929
1021
|
row: 0,
|
|
930
1022
|
col: 0,
|
|
@@ -995,7 +1087,7 @@ export const useClipboard = (params: UseClipboardParams): ClipboardController =>
|
|
|
995
1087
|
* `onClipboardUnavailable` を鳴らして `"rejected"` (promise は決して reject しない)。イベント
|
|
996
1088
|
* 経路の凍結 / 編集中 / 無効ゲートは書込時点で適用。
|
|
997
1089
|
*/
|
|
998
|
-
const
|
|
1090
|
+
const pasteInner = useCallback(
|
|
999
1091
|
async (options: PasteOptions): Promise<PasteOutcome> => {
|
|
1000
1092
|
const unavailable = (cause: ClipboardUnavailableCause): PasteOutcome => {
|
|
1001
1093
|
paramsRef.current.onClipboardUnavailable?.(cause)
|
|
@@ -1051,6 +1143,24 @@ export const useClipboard = (params: UseClipboardParams): ClipboardController =>
|
|
|
1051
1143
|
[runPaste],
|
|
1052
1144
|
)
|
|
1053
1145
|
|
|
1146
|
+
/**
|
|
1147
|
+
* The public menu paste: `pasteInner`'s resolve value mirrored 1:1 to `onPasteResolved`
|
|
1148
|
+
* from this SINGLE wrap site (v0.10 plan §3.1) — `handle.paste` and the command dispatch's
|
|
1149
|
+
* `void paste(...)` both route here, so every menu gesture is mirrored exactly once.
|
|
1150
|
+
* 公開メニュー貼り付け: `pasteInner` の resolve 値をこの**単一**ラップサイトから
|
|
1151
|
+
* `onPasteResolved` へ 1:1 で鏡映 (v0.10 計画 §3.1)。`handle.paste` もコマンド発行の
|
|
1152
|
+
* `void paste(...)` もここを通るため、menu ゲスチャ毎ちょうど 1 回。
|
|
1153
|
+
*/
|
|
1154
|
+
const paste = useCallback(
|
|
1155
|
+
async (options: PasteOptions): Promise<PasteOutcome> => {
|
|
1156
|
+
const outcome = await pasteInner(options)
|
|
1157
|
+
// 原因特定系 callback (pasteInner 内で発火済み) が先、outcome の要約が後 (§3.1 の登記順)
|
|
1158
|
+
paramsRef.current.onPasteResolved?.(outcome, "menu")
|
|
1159
|
+
return outcome
|
|
1160
|
+
},
|
|
1161
|
+
[pasteInner],
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1054
1164
|
const { enabled, sinkRef } = params
|
|
1055
1165
|
useEffect(() => {
|
|
1056
1166
|
const sink = sinkRef.current
|
|
@@ -1121,11 +1231,14 @@ export const useClipboard = (params: UseClipboardParams): ClipboardController =>
|
|
|
1121
1231
|
armedRef.current = null
|
|
1122
1232
|
}
|
|
1123
1233
|
const data = clipboardDataOf(event)
|
|
1124
|
-
// clipboardData の無い paste イベントは読むものが無い (v0.7 の空文字 no-op と同じ
|
|
1234
|
+
// clipboardData の無い paste イベントは読むものが無い (v0.7 の空文字 no-op と同じ —
|
|
1235
|
+
// 到達前分岐は意図的無言、onPasteResolved は鳴らさない)
|
|
1125
1236
|
if (data === null) {
|
|
1126
1237
|
return
|
|
1127
1238
|
}
|
|
1128
|
-
runPaste(
|
|
1239
|
+
// event チャネルの発火点は runPaste 到達ゲスチャ毎にここ 1 箇所 (v0.10 計画 §3.1)
|
|
1240
|
+
const outcome = runPaste(flavoursOf(data), "event", options)
|
|
1241
|
+
p.onPasteResolved?.(outcome, "event")
|
|
1129
1242
|
}
|
|
1130
1243
|
|
|
1131
1244
|
sink.addEventListener("copy", onCopy)
|
|
@@ -39,6 +39,15 @@
|
|
|
39
39
|
* - Return value: true = committed (or nothing needed writing), false = gate rejection / adapter
|
|
40
40
|
* rejection / command unavailable / commit-gate refusal. Every callback is identity-stable
|
|
41
41
|
* (params are read through `paramsRef`).
|
|
42
|
+
* - v0.10 read-only walls (design ADR-28/30, all through the dataAdapter helpers, refusals via the
|
|
43
|
+
* required `rejectReadOnly` funnel): G8 `runFill` two-phase collect → walk → materialise (the
|
|
44
|
+
* walk covers the producer's own targets — a read-only SOURCE stays legal), G9 editing
|
|
45
|
+
* `fillSelection`, G10 `writeFormat` (walks the invoked target ranges, never the selection),
|
|
46
|
+
* G11 `runCalc`, G12 idle insertNow, G13 the four annotate setters (commit-gate-first kept),
|
|
47
|
+
* and G15 — ONE shared pre-check for the four popover-open command ids between the availability
|
|
48
|
+
* bail and the dispatch-level `requestCommit` (content-present annotate chords fall through and
|
|
49
|
+
* open the viewer; content-absent and the list chord consume + refuse without force-committing).
|
|
50
|
+
* Capability undeclared = every wall compiles to the previous behaviour byte-identically.
|
|
42
51
|
*
|
|
43
52
|
* コマンド実行器 (v0.8 §4.1.13 — ADR-17)。70 の SpreadsheetCommand 全件の executeCommand、handle の
|
|
44
53
|
* 書込群 (applyFormat / setBorders / setHyperlink / setNote / addComment / setValidation)、能力
|
|
@@ -67,7 +76,13 @@
|
|
|
67
76
|
* トグルは setHighlight。formatCommandAnnouncement (opt-in) は書式コマンド成功後に announce へ
|
|
68
77
|
* (state はトグルの適用後状態、非トグルは null)。戻り値: true = commit 成功 (書くものが無い場合も)、
|
|
69
78
|
* false = ゲート拒否 / アダプタ拒否 / 不可用 / commit ゲート拒否。全コールバックは identity 安定
|
|
70
|
-
* (パラメータは paramsRef 経由で読む)。
|
|
79
|
+
* (パラメータは paramsRef 経由で読む)。v0.10 の読み取り専用壁 (設計 ADR-28/30、全て dataAdapter
|
|
80
|
+
* ヘルパ経由、拒否は必須 `rejectReadOnly` 漏斗): G8 runFill の 2 相化 (収集 → walk → 実体化 —
|
|
81
|
+
* walk はプロデューサ自身の targets、読み取り専用ソースは合法のまま)、G9 編集中 fillSelection、
|
|
82
|
+
* G10 writeFormat (呼出しパラメータを walk — 選択ではない)、G11 runCalc、G12 idle insertNow、
|
|
83
|
+
* G13 注釈 setter ×4 (commit-gate-first 維持)、G15 = popover 系 4 コマンド id の単一 pre-check
|
|
84
|
+
* (availability bail と dispatch 水準 requestCommit の間 — 内容有の注釈チョードはビューア開通、
|
|
85
|
+
* 内容無と list は force-commit なしの消費拒否)。能力未宣言 = 全壁が従来挙動と byte 恒等。
|
|
71
86
|
*/
|
|
72
87
|
|
|
73
88
|
import { useCallback, useMemo, useRef } from "react"
|
|
@@ -80,7 +95,7 @@ import { type CellCommentInput, normalizeAdapterHyperlink, normalizeHyperlink }
|
|
|
80
95
|
import { type CellCoord, type CellRange, countRangeCells, enumerateRangeCells, makeRange, type SpreadsheetOperation } from "../model/cellCoord.ts"
|
|
81
96
|
import { BORDER_DEFAULT_COLOR, type CellBorder, type CellBorders, type CellFormat, type CellFormatPatch, type CellValueFormat, mergeFormat } from "../model/cellFormat.ts"
|
|
82
97
|
import { normalizeColor } from "../model/cellPaint.ts"
|
|
83
|
-
import type
|
|
98
|
+
import { type CellEdit, type EditRejection, isReadOnlyCell, type ReadOnlyRejection, rangesContainReadOnly, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
84
99
|
import type { DataValidationRule, ValidationVerdict } from "../model/dataValidation.ts"
|
|
85
100
|
import type { PopoverKind, PopoverOverrides } from "../popover/usePopovers.ts"
|
|
86
101
|
import type { SelectionState } from "../selection/selectionState.ts"
|
|
@@ -118,6 +133,15 @@ export type UseCommandsParams = {
|
|
|
118
133
|
refreshEditPaint: () => void
|
|
119
134
|
maxOperationCells: number
|
|
120
135
|
onOperationRejected?: (op: SpreadsheetOperation, cellCount: number, reason: "limit" | "bounds") => void
|
|
136
|
+
/**
|
|
137
|
+
* v0.10 read-only refusal funnel (required seam — the root always wires it; identity-stable,
|
|
138
|
+
* read through paramsRef): the producer walls G8–G13 and the popover-chord pre-check G15
|
|
139
|
+
* fire it once per refused gesture. Capability absent ⇒ never called (byte identity).
|
|
140
|
+
* v0.10 読み取り専用拒否漏斗 (必須 seam — root が常に渡す。identity 安定、paramsRef 読み):
|
|
141
|
+
* プロデューサ壁 G8–G13 と popover チョード pre-check G15 が拒否ジェスチャ毎 1 回発火する。
|
|
142
|
+
* 能力未宣言なら一度も呼ばれない (バイト恒等)。
|
|
143
|
+
*/
|
|
144
|
+
rejectReadOnly: (rejection: ReadOnlyRejection) => void
|
|
121
145
|
/** Custom colour slots (5 each) for fillColorCustomN / fontColorCustomN. / fillColorCustomN / fontColorCustomN の色スロット (各 5)。 */
|
|
122
146
|
colors: { fill: readonly string[]; font: readonly string[] }
|
|
123
147
|
/** The formatCurrency currency / rendering locale; undefined ⇒ formatCurrency is unavailable (passes through). / formatCurrency の通貨 / locale。未宣言なら formatCurrency は行不在 (素通し)。 */
|
|
@@ -395,6 +419,13 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
395
419
|
if (!gateSelectionWrite("format", ranges, p.maxOperationCells, p.onOperationRejected)) {
|
|
396
420
|
return false
|
|
397
421
|
}
|
|
422
|
+
// G10 (v0.10 §6.9): 読み取り専用壁 — walk は**呼出しパラメータ** (targetRanges) であって選択では
|
|
423
|
+
// ない (編集列トグルはセッションセル 1×1 を渡す — 古い混在選択で拒否しない。設計 rule 3)。
|
|
424
|
+
// 限度ゲートの後 = O(1) 算術が O(cells) walk を守る既存拒否ランク
|
|
425
|
+
if (rangesContainReadOnly(p.adapter, ranges)) {
|
|
426
|
+
p.rejectReadOnly({ kind: "operation", op: "format", cellCount: countRangeCells(ranges) })
|
|
427
|
+
return false
|
|
428
|
+
}
|
|
398
429
|
// memo: バケット (パッチの同一性) × base identity — パッチはジェスチャ内で定数なので
|
|
399
430
|
// intern された base 1 つにつき mergeFormat は 1 回 (applyEdits 引数の format identity は 1 つ)
|
|
400
431
|
const memo = new Map<number, Map<CellFormat | undefined, CellFormat | undefined>>()
|
|
@@ -478,7 +509,16 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
478
509
|
}
|
|
479
510
|
}, [])
|
|
480
511
|
|
|
481
|
-
/**
|
|
512
|
+
/**
|
|
513
|
+
* fillDown / fillRight / fillSelection (idle) — two phases since v0.10 (§0.2 R2): phase 1
|
|
514
|
+
* collects the (source, targets) pairs with coordinate arithmetic only (no reads, no edits),
|
|
515
|
+
* the G8 read-only walk runs over the collected targets, and only a passing gesture
|
|
516
|
+
* materialises (readFillSource + fillSourceEdit) and commits.
|
|
517
|
+
* fillDown / fillRight / fillSelection (idle) — v0.10 から 2 相 (§0.2 R2): 第 1 相は座標算術
|
|
518
|
+
* だけで (source, targets) 対を収集 (読みも edits 生成もしない)、G8 の読み取り専用 walk を
|
|
519
|
+
* 収集済み targets に掛け、通過したジェスチャだけが実体化 (readFillSource + fillSourceEdit)
|
|
520
|
+
* して commit する。
|
|
521
|
+
*/
|
|
482
522
|
const runFill = useCallback(
|
|
483
523
|
(direction: "down" | "right" | "selection"): boolean => {
|
|
484
524
|
const p = paramsRef.current
|
|
@@ -486,7 +526,8 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
486
526
|
if (!gateSelectionWrite("fill", selection.ranges, p.maxOperationCells, p.onOperationRejected)) {
|
|
487
527
|
return false
|
|
488
528
|
}
|
|
489
|
-
|
|
529
|
+
// 第 1 相: (source, targets) 対の収集のみ — 対の順序は従来の fillEdits 呼出し順と同一 (byte 恒等)
|
|
530
|
+
const pairs: { source: CellCoord; targets: CellCoord[] }[] = []
|
|
490
531
|
if (direction === "selection") {
|
|
491
532
|
// active の内容を active 以外の全選択セルへ
|
|
492
533
|
const active = selection.active
|
|
@@ -496,7 +537,7 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
496
537
|
targets.push(cell)
|
|
497
538
|
}
|
|
498
539
|
})
|
|
499
|
-
|
|
540
|
+
pairs.push({ source: active, targets })
|
|
500
541
|
} else {
|
|
501
542
|
for (const range of selection.ranges) {
|
|
502
543
|
if (direction === "down") {
|
|
@@ -511,7 +552,7 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
511
552
|
for (let row = single ? range.startRow : range.startRow + 1; row <= range.endRow; row += 1) {
|
|
512
553
|
targets.push({ row, col })
|
|
513
554
|
}
|
|
514
|
-
|
|
555
|
+
pairs.push({ source: { row: sourceRow, col }, targets })
|
|
515
556
|
}
|
|
516
557
|
} else {
|
|
517
558
|
const single = range.startCol === range.endCol
|
|
@@ -525,11 +566,31 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
525
566
|
for (let col = single ? range.startCol : range.startCol + 1; col <= range.endCol; col += 1) {
|
|
526
567
|
targets.push({ row, col })
|
|
527
568
|
}
|
|
528
|
-
|
|
569
|
+
pairs.push({ source: { row, col: sourceCol }, targets })
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// G8 (v0.10 §6.9): 収集済み対の全 targets を 1 サイトで walk (最初の hit で短絡)。読み取り
|
|
575
|
+
// 専用**ソース**は walk 外 = 合法のまま (Excel 同等 — RC-G8)。拒否 cellCount = Σ targets.length
|
|
576
|
+
// (overlap-naive — ONE 語彙)。実体化 (ソース読み) より前に立つ
|
|
577
|
+
for (const pair of pairs) {
|
|
578
|
+
for (const target of pair.targets) {
|
|
579
|
+
if (isReadOnlyCell(p.adapter, target.row, target.col)) {
|
|
580
|
+
let cellCount = 0
|
|
581
|
+
for (const counted of pairs) {
|
|
582
|
+
cellCount += counted.targets.length
|
|
529
583
|
}
|
|
584
|
+
p.rejectReadOnly({ kind: "operation", op: "fill", cellCount })
|
|
585
|
+
return false
|
|
530
586
|
}
|
|
531
587
|
}
|
|
532
588
|
}
|
|
589
|
+
// 第 2 相: 実体化 — ソース毎に 1 回読み、対象へ複製 (従来と同じ呼出し順)
|
|
590
|
+
const edits: CellEdit[] = []
|
|
591
|
+
for (const pair of pairs) {
|
|
592
|
+
fillEdits(pair.source, pair.targets, edits)
|
|
593
|
+
}
|
|
533
594
|
if (edits.length === 0) {
|
|
534
595
|
return true
|
|
535
596
|
}
|
|
@@ -538,6 +599,60 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
538
599
|
[fillEdits],
|
|
539
600
|
)
|
|
540
601
|
|
|
602
|
+
/**
|
|
603
|
+
* Editing-column Ctrl+Enter (`fillSelection` with an open session): writes the session text
|
|
604
|
+
* to every selected cell as ONE applyEdits and closes the session. Order: limit gate → the
|
|
605
|
+
* G9 read-only wall (v0.10 — whole refusal, session kept, `validateCommit` unreached) → the
|
|
606
|
+
* per-target validation walk (the first strict refusal aborts before the adapter) → commit →
|
|
607
|
+
* `cancelEdit` (no double write).
|
|
608
|
+
* 編集列 Ctrl+Enter (セッション中の fillSelection): セッション文字列を全選択セルへ 1 applyEdits
|
|
609
|
+
* で書き、セッションを閉じる。順序: 限度ゲート → G9 読み取り専用壁 (v0.10 — 全体拒否・
|
|
610
|
+
* セッション保持・validateCommit 不到達) → 対象セル毎の検証 walk (最初の strict 拒否で adapter
|
|
611
|
+
* 前に中断) → commit → cancelEdit (二重書込なし)。
|
|
612
|
+
*/
|
|
613
|
+
const runFillSelectionEditing = useCallback((): boolean => {
|
|
614
|
+
const p = paramsRef.current
|
|
615
|
+
// 編集列: セッション文字列を全選択セルへ 1 applyEdits、セッション閉鎖
|
|
616
|
+
const text = p.getSessionText()
|
|
617
|
+
if (text === null) {
|
|
618
|
+
return false
|
|
619
|
+
}
|
|
620
|
+
const selection = p.getSelection()
|
|
621
|
+
if (!gateSelectionWrite("fill", selection.ranges, p.maxOperationCells, p.onOperationRejected)) {
|
|
622
|
+
return false
|
|
623
|
+
}
|
|
624
|
+
// G9 (v0.10 §6.9): 読み取り専用壁 — validateCommit walk より前・セッション保持で
|
|
625
|
+
// 全体拒否 (書込集合 = 選択そのもの、意図した粗さ — 設計 rule 3)
|
|
626
|
+
if (rangesContainReadOnly(p.adapter, selection.ranges)) {
|
|
627
|
+
p.rejectReadOnly({ kind: "operation", op: "fill", cellCount: countRangeCells(selection.ranges) })
|
|
628
|
+
return false
|
|
629
|
+
}
|
|
630
|
+
// 検証ゲート (分冊 08 §4.2) は対象セル毎 — 規則はセル毎に違い得る。最初の strict 拒否で
|
|
631
|
+
// 走査を打ち切り applyEdits へ届けない (通知と message ポップオーバーは root の validateCommit が担う)
|
|
632
|
+
const edits: CellEdit[] = []
|
|
633
|
+
let rejected = false
|
|
634
|
+
enumerateRangeCells(selection.ranges, (cell) => {
|
|
635
|
+
if (rejected) {
|
|
636
|
+
return
|
|
637
|
+
}
|
|
638
|
+
if (!p.validateCommit(cell, text).ok) {
|
|
639
|
+
rejected = true
|
|
640
|
+
return
|
|
641
|
+
}
|
|
642
|
+
edits.push({ row: cell.row, col: cell.col, text })
|
|
643
|
+
})
|
|
644
|
+
// 拒否はセッション維持のまま false (Enter の keep 経路と同じ — Escape で取消 / 修正して再試行)
|
|
645
|
+
if (rejected) {
|
|
646
|
+
return false
|
|
647
|
+
}
|
|
648
|
+
if (p.commitEdits(edits, { source: "fill" }) === null) {
|
|
649
|
+
return false
|
|
650
|
+
}
|
|
651
|
+
// 値は全セルへ書かれた — セッションは破棄で閉じる (二重書込なし)
|
|
652
|
+
p.cancelEdit()
|
|
653
|
+
return true
|
|
654
|
+
}, [])
|
|
655
|
+
|
|
541
656
|
/** Self-calc over the selection (source "calc"). / 選択の自己計算 (source "calc")。 */
|
|
542
657
|
const runCalc = useCallback((transform: { text: (text: string) => string | null; formula: (formula: string) => string }): boolean => {
|
|
543
658
|
const p = paramsRef.current
|
|
@@ -545,6 +660,11 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
545
660
|
if (!gateSelectionWrite("calc", selection.ranges, p.maxOperationCells, p.onOperationRejected)) {
|
|
546
661
|
return false
|
|
547
662
|
}
|
|
663
|
+
// G11 (v0.10 §6.9): 読み取り専用壁 — 限度ゲート後・列挙 (アダプタ読み) 前の全体拒否
|
|
664
|
+
if (rangesContainReadOnly(p.adapter, selection.ranges)) {
|
|
665
|
+
p.rejectReadOnly({ kind: "operation", op: "calc", cellCount: countRangeCells(selection.ranges) })
|
|
666
|
+
return false
|
|
667
|
+
}
|
|
548
668
|
const edits: CellEdit[] = []
|
|
549
669
|
const adapter = p.adapter
|
|
550
670
|
enumerateRangeCells(selection.ranges, (cell) => {
|
|
@@ -660,6 +780,68 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
660
780
|
[activeHyperlink],
|
|
661
781
|
)
|
|
662
782
|
|
|
783
|
+
/**
|
|
784
|
+
* G15 (v0.10 — design ADR-30): the SINGLE read-only pre-check for the popover-opening
|
|
785
|
+
* dispatches — the four popover-open command ids (`spec.kind === "popover"`) plus the
|
|
786
|
+
* `openHyperlink` host-undeclared fallback, whose execute arm opens the SAME link popover
|
|
787
|
+
* (Ctrl+K equivalent) and therefore walks the link arm of this gate. Returns false when the
|
|
788
|
+
* chord was consumed AND refused (the dispatcher aborts BEFORE its session `requestCommit` —
|
|
789
|
+
* doomed-open), true when dispatch may proceed: non-popover-opening commands (including
|
|
790
|
+
* `openHyperlink` with a declared host — a pure read), editable cells, and the
|
|
791
|
+
* content-PRESENT annotate chords (the normal open path then serves the viewer — S3 carries
|
|
792
|
+
* `cellReadOnly`, G14a is the wall; opening is a success, no callback). Content presence
|
|
793
|
+
* reads ONLY the command's own capability member (availability already guarantees the
|
|
794
|
+
* declaration); an empty note string / empty comment thread counts as ABSENT (design §8).
|
|
795
|
+
* `openValidationList` is always refused (the list surface has no view value).
|
|
796
|
+
* G15 (v0.10 — 設計 ADR-30): popover を開くディスパッチの**単一** pre-check — popover 系
|
|
797
|
+
* 4 コマンド id (`spec.kind === "popover"`) に加え、execute 腕が**同じ** link ポップオーバーを
|
|
798
|
+
* 開く `openHyperlink` の host 未宣言 fallback (Ctrl+K 同等) も link 腕を通る。false =
|
|
799
|
+
* チョード消費 + 拒否 (dispatcher はセッション requestCommit の**前**に中断 — doomed-open)、
|
|
800
|
+
* true = 続行 (popover を開かないコマンド / host 宣言時の `openHyperlink` (純粋な読み) /
|
|
801
|
+
* 編集可能セル / 内容**有**の注釈チョード — 通常 open 経路がビューアを開く。S3 が
|
|
802
|
+
* cellReadOnly を運び G14a が壁。開通は成功で callback なし)。内容有無は当該コマンドの能力
|
|
803
|
+
* メンバーだけを読む (availability が宣言を保証済み)。空文字ノート / 空スレッドは「無」扱い
|
|
804
|
+
* (設計 §8)。openValidationList は常に拒否 (list 面に閲覧価値は無い)。
|
|
805
|
+
*/
|
|
806
|
+
const gateReadOnlyChord = useCallback(
|
|
807
|
+
(spec: CommandSpec): boolean => {
|
|
808
|
+
const p = paramsRef.current
|
|
809
|
+
// 壁の対象 = execute 腕が開く popover 種。openHyperlink の host 未宣言 fallback は
|
|
810
|
+
// Ctrl+K 同等の link open (宣言時は読み = 対象外。リンク無しの行不在は availability が先に落とす)
|
|
811
|
+
const popover: PopoverKind | undefined = spec.kind === "popover" ? spec.popover : spec.kind === "openHyperlink" && p.onHyperlinkOpen === undefined ? "link" : undefined
|
|
812
|
+
if (popover === undefined) {
|
|
813
|
+
return true
|
|
814
|
+
}
|
|
815
|
+
const active = p.getActive()
|
|
816
|
+
if (!isReadOnlyCell(p.adapter, active.row, active.col)) {
|
|
817
|
+
return true
|
|
818
|
+
}
|
|
819
|
+
if (popover === "list") {
|
|
820
|
+
// list 面に閲覧価値は無い (対話 = 書込そのもの) — 常に消費拒否
|
|
821
|
+
p.rejectReadOnly({ kind: "edit", cell: active, editSession: "none" })
|
|
822
|
+
return false
|
|
823
|
+
}
|
|
824
|
+
// 注釈 3 種は内容の有無で分岐 (このパッケージに独立ビューアは無い — WCAG 2.1.1)
|
|
825
|
+
let present: boolean
|
|
826
|
+
if (popover === "link") {
|
|
827
|
+
present = activeHyperlink() !== undefined
|
|
828
|
+
} else if (popover === "note") {
|
|
829
|
+
const note = p.adapter.getCellNote === undefined ? undefined : p.adapter.getCellNote(active.row, active.col)
|
|
830
|
+
present = note !== undefined && note !== ""
|
|
831
|
+
} else {
|
|
832
|
+
const thread = p.adapter.getCellComments === undefined ? undefined : p.adapter.getCellComments(active.row, active.col)
|
|
833
|
+
present = thread !== undefined && thread.length > 0
|
|
834
|
+
}
|
|
835
|
+
if (!present) {
|
|
836
|
+
// 内容無 = 純粋な追加意図 (doomed empty editor) — 消費拒否、requestCommit 不実行
|
|
837
|
+
p.rejectReadOnly({ kind: "operation", op: "annotate", cellCount: 1 })
|
|
838
|
+
return false
|
|
839
|
+
}
|
|
840
|
+
return true
|
|
841
|
+
},
|
|
842
|
+
[activeHyperlink],
|
|
843
|
+
)
|
|
844
|
+
|
|
663
845
|
const executeCommand = useCallback(
|
|
664
846
|
(command: SpreadsheetCommand): boolean => {
|
|
665
847
|
const p = paramsRef.current
|
|
@@ -667,6 +849,14 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
667
849
|
return false
|
|
668
850
|
}
|
|
669
851
|
const spec = COMMAND_SPECS[command]
|
|
852
|
+
// G15 (v0.10 §6.9 / 設計 ADR-30): popover を開くディスパッチ (popover 系 4 コマンド id +
|
|
853
|
+
// openHyperlink の host 未宣言 fallback) の**単一** pre-check — 座席は availability bail と
|
|
854
|
+
// dispatch 水準 requestCommit の**間** (doomed-open 原則: 拒否腕は無関係セッションを
|
|
855
|
+
// force-commit しない)。チョードは keymap が execute 前に消費済み。
|
|
856
|
+
// imperative 経路 (handle.executeCommand / commandDispatch.execute) も同関数で自動継承
|
|
857
|
+
if (!gateReadOnlyChord(spec)) {
|
|
858
|
+
return false
|
|
859
|
+
}
|
|
670
860
|
const editing = p.isEditing()
|
|
671
861
|
// 編集列に無いコマンドは先にセッションを確定する (拒否なら中断)
|
|
672
862
|
if (editing && !EDITING_COLUMN_COMMANDS.has(command) && !p.requestCommit()) {
|
|
@@ -733,39 +923,7 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
733
923
|
return runCalc({ text: reverseSignText, formula: reverseSignFormula })
|
|
734
924
|
case "fill": {
|
|
735
925
|
if (editing && spec.direction === "selection") {
|
|
736
|
-
|
|
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
|
|
926
|
+
return runFillSelectionEditing()
|
|
769
927
|
}
|
|
770
928
|
return runFill(spec.direction)
|
|
771
929
|
}
|
|
@@ -782,7 +940,8 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
782
940
|
p.onHyperlinkOpen(url, p.getActive())
|
|
783
941
|
return true
|
|
784
942
|
}
|
|
785
|
-
// 未宣言: link ポップオーバー (Open ボタンなし)
|
|
943
|
+
// 未宣言: link ポップオーバー (Open ボタンなし)。読み取り専用 × リンク無しの
|
|
944
|
+
// doomed empty editor は G15 の link 腕が拒否済み (ここに届くのは開通できる open のみ)
|
|
786
945
|
p.openPopover("link")
|
|
787
946
|
return true
|
|
788
947
|
}
|
|
@@ -806,6 +965,11 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
806
965
|
}
|
|
807
966
|
// idle: active セルだけへ 1 applyEdits (選択全体には書かない)、直接確定 (セッションは無い = discarded)
|
|
808
967
|
const active = p.getActive()
|
|
968
|
+
// G12 (v0.10 §6.9): 読み取り専用壁 — セッションレス "edit" 書込も validateCommit より前で拒否
|
|
969
|
+
if (isReadOnlyCell(p.adapter, active.row, active.col)) {
|
|
970
|
+
p.rejectReadOnly({ kind: "edit", cell: active, editSession: "none" })
|
|
971
|
+
return false
|
|
972
|
+
}
|
|
809
973
|
// Excel の Ctrl+; は編集モード経由で Enter 確定が検証される (strict ⇒ 拒否) — 分冊 08 §4.2 の
|
|
810
974
|
// fill / 貼り付け非検証免除はここへ及ばないため、strict 拒否は書込前に判定する
|
|
811
975
|
// (通知と message ポップオーバーは root の validateCommit が担う)
|
|
@@ -838,7 +1002,7 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
838
1002
|
}
|
|
839
1003
|
}
|
|
840
1004
|
},
|
|
841
|
-
[isCommandAvailable, activeHyperlink, activeBase, writeConstantPatch, announceFormat, setBorders, runCalc, runFill, runHide],
|
|
1005
|
+
[isCommandAvailable, gateReadOnlyChord, activeHyperlink, activeBase, writeConstantPatch, announceFormat, setBorders, runCalc, runFill, runFillSelectionEditing, runHide],
|
|
842
1006
|
)
|
|
843
1007
|
|
|
844
1008
|
/** Commit gate shared by the annotation setters. / 注釈 setter 共通の commit ゲート。 */
|
|
@@ -861,6 +1025,11 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
861
1025
|
return false
|
|
862
1026
|
}
|
|
863
1027
|
const active = p.getActive()
|
|
1028
|
+
// G13 (v0.10 §6.9): 読み取り専用壁 — commit-gate-first は登記済み品行 (§3 preamble)、壁は commit 前
|
|
1029
|
+
if (isReadOnlyCell(p.adapter, active.row, active.col)) {
|
|
1030
|
+
p.rejectReadOnly({ kind: "operation", op: "annotate", cellCount: 1 })
|
|
1031
|
+
return false
|
|
1032
|
+
}
|
|
864
1033
|
return p.commitEdits([{ row: active.row, col: active.col, hyperlink: normalized }], { source: "annotate" }) !== null
|
|
865
1034
|
},
|
|
866
1035
|
[commitGate],
|
|
@@ -876,6 +1045,11 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
876
1045
|
return false
|
|
877
1046
|
}
|
|
878
1047
|
const active = p.getActive()
|
|
1048
|
+
// G13 (v0.10 §6.9): 読み取り専用壁 — commit 前の単一ターゲット walk
|
|
1049
|
+
if (isReadOnlyCell(p.adapter, active.row, active.col)) {
|
|
1050
|
+
p.rejectReadOnly({ kind: "operation", op: "annotate", cellCount: 1 })
|
|
1051
|
+
return false
|
|
1052
|
+
}
|
|
879
1053
|
return p.commitEdits([{ row: active.row, col: active.col, note: text }], { source: "annotate" }) !== null
|
|
880
1054
|
},
|
|
881
1055
|
[commitGate],
|
|
@@ -892,6 +1066,11 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
892
1066
|
return false
|
|
893
1067
|
}
|
|
894
1068
|
const active = p.getActive()
|
|
1069
|
+
// G13 (v0.10 §6.9): 読み取り専用壁 — 既存スレッド読み (実体化) より前で拒否
|
|
1070
|
+
if (isReadOnlyCell(p.adapter, active.row, active.col)) {
|
|
1071
|
+
p.rejectReadOnly({ kind: "operation", op: "annotate", cellCount: 1 })
|
|
1072
|
+
return false
|
|
1073
|
+
}
|
|
895
1074
|
// 既存スレッド (id つき) を verbatim に保ち input を末尾へ — 1 スレッドとして書く
|
|
896
1075
|
const existing = read(active.row, active.col)
|
|
897
1076
|
const thread: CellCommentInput[] = existing === undefined ? [input] : [...existing, input]
|
|
@@ -913,6 +1092,11 @@ export const useCommands = (params: UseCommandsParams): CommandRunner => {
|
|
|
913
1092
|
if (!gateSelectionWrite("annotate", ranges, p.maxOperationCells, p.onOperationRejected)) {
|
|
914
1093
|
return false
|
|
915
1094
|
}
|
|
1095
|
+
// G13 (v0.10 §6.9): 読み取り専用壁 — setValidation だけ選択レンジ walk (限度ゲート後・列挙前)
|
|
1096
|
+
if (rangesContainReadOnly(p.adapter, ranges)) {
|
|
1097
|
+
p.rejectReadOnly({ kind: "operation", op: "annotate", cellCount: countRangeCells(ranges) })
|
|
1098
|
+
return false
|
|
1099
|
+
}
|
|
916
1100
|
const edits: CellEdit[] = []
|
|
917
1101
|
enumerateRangeCells(ranges, (cell) => {
|
|
918
1102
|
edits.push({ row: cell.row, col: cell.col, validation: rule })
|