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