@aiquants/spreadsheet 0.10.0 → 0.12.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 +77 -0
- package/README.md +14 -3
- package/dist/index.cjs +13 -13
- package/dist/index.d.cts +154 -14
- package/dist/index.d.ts +154 -14
- package/dist/index.js +3979 -3899
- package/package.json +1 -1
- package/src/Spreadsheet.tsx +196 -7
- package/src/editor/useCellEditor.ts +6 -1
- package/src/grid/CellView.tsx +22 -5
- package/src/grid/GridBody.tsx +46 -9
- package/src/index.ts +1 -1
- package/src/keyboard/useGridKeyboard.ts +9 -4
- package/src/model/dataAdapter.ts +98 -7
- package/src/popover/usePopovers.ts +22 -16
- package/src/selection/selectionState.ts +28 -1
package/src/model/dataAdapter.ts
CHANGED
|
@@ -266,10 +266,12 @@ export type SpreadsheetDataAdapter = {
|
|
|
266
266
|
* string only — editing keeps the raw getCellText (Excel semantics) and copy/TSV carry the raw
|
|
267
267
|
* value too (a DELIBERATE deviation from Excel, which copies the display — raw preserves the
|
|
268
268
|
* numeric round-trip). Autofit measures the FORMATTED display, and screen-reader
|
|
269
|
-
* announcements receive both the raw text and the display string. `renderCell` (
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* renderCell
|
|
269
|
+
* announcements receive both the raw text and the display string. A `renderCell` (prop)
|
|
270
|
+
* cell that returns custom output shows that output instead of the built-in VALUE format
|
|
271
|
+
* (raw `text` and the formatted `displayText` both flow to the custom renderer); a cell whose
|
|
272
|
+
* renderCell returns `undefined` falls back to the formatted default (v0.12 per-cell opt-out),
|
|
273
|
+
* and the resolved ALIGNMENT and paint are cell properties that apply under renderCell
|
|
274
|
+
* regardless. Every format is fail-soft: a non-numeric value in a numeric format, an
|
|
273
275
|
* invalid date, an out-of-range Intl option or an unparseable colour renders / paints
|
|
274
276
|
* nothing for that key. Return a shared/interned CellFormat per column (or per cell object)
|
|
275
277
|
* to avoid per-cell allocation and to keep the paint intern stable; because return values
|
|
@@ -283,9 +285,10 @@ export type SpreadsheetDataAdapter = {
|
|
|
283
285
|
* **塗り書式** (この base ⊕ 条件付き書式)。値書式は表示文字列にのみ適用し、編集は素の
|
|
284
286
|
* getCellText (Excel 意味論)、コピー / TSV も素の値を運ぶ (Excel は表示を写すため**意図的な
|
|
285
287
|
* 逸脱** — 素値は数値の往復を保つ)。autofit は**整形後の表示**を計測し、読み上げには素の
|
|
286
|
-
* text と表示文字列の両方を渡す。`renderCell` (prop)
|
|
287
|
-
* text
|
|
288
|
-
*
|
|
288
|
+
* text と表示文字列の両方を渡す。`renderCell` (prop) がカスタム出力を返したセルは組み込み
|
|
289
|
+
* 「値書式」の代わりにその出力を描く (素の text と整形後 displayText の両方がカスタム
|
|
290
|
+
* レンダラーへ)。undefined を返したセルは整形済みの既定描画へ降りる (v0.12 のセル単位
|
|
291
|
+
* オプトアウト)。解決済み「揃え」と塗りはセル固有属性として renderCell 下でも常に効く。全書式は fail-soft — 数値書式の非数値・無効な日付・範囲外 Intl オプション・解釈
|
|
289
292
|
* 不能な色はそのキーだけ何も描かない。列毎 (またはセル object 毎) に共有 CellFormat を返すと
|
|
290
293
|
* セル毎の確保を避け塗りの intern も安定する。戻り値は (getCellText 同様) 再スライス時のみ
|
|
291
294
|
* 再読込されるため、書式変更の反映は dataVersion 増分 (またはアダプタ差替) で行う。
|
|
@@ -398,6 +401,31 @@ export type SpreadsheetDataAdapter = {
|
|
|
398
401
|
* (getCellText と同格、描画パスでも呼ばれるため)。未宣言 = 全セル編集可 = 従来挙動とバイト同一。
|
|
399
402
|
*/
|
|
400
403
|
isCellReadOnly?: (row: number, col: number) => boolean
|
|
404
|
+
/**
|
|
405
|
+
* OPTIONAL capability (v0.12): host-owned `data-*` attributes for one cell, merged verbatim
|
|
406
|
+
* onto the PACKAGE-OWNED cell element (`[data-aqsp-cell]`) so host CSS can target cell
|
|
407
|
+
* state, `:hover` and theme tokens without wrapper elements inside the cell. The package
|
|
408
|
+
* never interprets the values. Validated fail-fast at render (RangeError): the return must
|
|
409
|
+
* be a plain record or `null` (a JS host's `undefined` / array / Map is rejected by name,
|
|
410
|
+
* never a bare TypeError), every key must be a lowercase `data-*` name
|
|
411
|
+
* (`data-[a-z0-9-]+`), the package- and engine-emitted cell attributes (`data-aqsp*`,
|
|
412
|
+
* `data-aqvs*`, `data-row`, `data-col`) are reserved, and every value must be a string
|
|
413
|
+
* (React would otherwise stringify objects or drop `undefined` silently). Read once per
|
|
414
|
+
* painted cell per slice (the `getCellText` cost budget — keep it O(1)); publish changes
|
|
415
|
+
* via `dataVersion`. `null` = no extra attributes for this cell. Absence = the previous
|
|
416
|
+
* version's cell DOM byte-identically.
|
|
417
|
+
* 任意能力 (v0.12): セル 1 つへのホスト所有 `data-*` 属性 — パッケージ所有のセル要素
|
|
418
|
+
* (`[data-aqsp-cell]`) へそのまま載せる継ぎ目。セル内に器 (ラッパー要素) を敷かずにホスト
|
|
419
|
+
* CSS がセル状態・`:hover`・テーマトークンを掴むための口で、値の解釈はパッケージが
|
|
420
|
+
* 持たない。render 時 fail-fast 検証 (RangeError): 戻りはプレーンな record か `null`
|
|
421
|
+
* (JS ホストの `undefined` / 配列 / Map は名指しで拒否 — 生 TypeError にしない)、キーは
|
|
422
|
+
* 小文字 `data-*` (`data-[a-z0-9-]+`) のみ、パッケージ / エンジン放出のセル属性
|
|
423
|
+
* (`data-aqsp*` / `data-aqvs*` / `data-row` / `data-col`) は予約、値は string のみ (React の
|
|
424
|
+
* 暗黙文字列化や `undefined` の無言脱落を許さない)。描画セル毎・スライス毎に 1 読み
|
|
425
|
+
* (getCellText と同格の予算 — O(1) に保つ)。変更は dataVersion で公開。null = このセルに
|
|
426
|
+
* 追加属性なし。未宣言 = 従来のセル DOM とバイト同一。
|
|
427
|
+
*/
|
|
428
|
+
getCellAttributes?: (row: number, col: number) => Readonly<Record<string, string>> | null
|
|
401
429
|
/**
|
|
402
430
|
* OPTIONAL capability (v0.8): the numeric minimum / maximum over a range, for colorScale
|
|
403
431
|
* rules whose end points omit `value`; null = the range holds no numeric value. Memoised by
|
|
@@ -553,6 +581,69 @@ export const readOnlyDeclared = (adapter: SpreadsheetDataAdapter): boolean => ad
|
|
|
553
581
|
*/
|
|
554
582
|
export const isReadOnlyCell = (adapter: SpreadsheetDataAdapter, row: number, col: number): boolean => adapter.isCellReadOnly?.(row, col) === true
|
|
555
583
|
|
|
584
|
+
/** Host cell-attribute key grammar (v0.12): lowercase `data-*` only. / ホストセル属性キーの文法 (v0.12): 小文字 `data-*` のみ。 */
|
|
585
|
+
const CELL_ATTRIBUTE_KEY = /^data-[a-z0-9-]+$/
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Whether a value is a plain record (object literal or null-prototype object — not an array,
|
|
589
|
+
* Map, class instance or primitive): the only shape `getCellAttributes` may return besides null.
|
|
590
|
+
* プレーンな record (object リテラルか null プロトタイプ object — 配列 / Map / class インスタンス /
|
|
591
|
+
* プリミティブではない) か: `getCellAttributes` が null 以外で返してよい唯一の形。
|
|
592
|
+
*/
|
|
593
|
+
const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
|
|
594
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
595
|
+
return false
|
|
596
|
+
}
|
|
597
|
+
const proto = Object.getPrototypeOf(value)
|
|
598
|
+
return proto === Object.prototype || proto === null
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Resolves the host cell-attribute capability for one cell: `null` in O(1) without calling
|
|
603
|
+
* anything when the capability is absent (the byte-identity lever), otherwise the host's record
|
|
604
|
+
* verbatim after fail-fast validation — the return must be a plain record (a JS host's
|
|
605
|
+
* `undefined` / array / Map / primitive is rejected by name, never a bare TypeError), every key
|
|
606
|
+
* must be a lowercase `data-*` name that does not shadow a package- or engine-emitted cell
|
|
607
|
+
* attribute (`data-aqsp*` / `data-aqvs*` / `data-row` / `data-col` — the engine spreads its own
|
|
608
|
+
* `data-aqvs-*` band stamps AFTER the host props, so a colliding host key would win in the
|
|
609
|
+
* scroll band and lose in the frozen bands), and every value must be a string. Throws a
|
|
610
|
+
* RangeError naming the offending shape / key. A throw from the host callback is a host bug and
|
|
611
|
+
* propagates (no try/catch — Strict No-Fallback).
|
|
612
|
+
* セル 1 つのホスト属性能力の解決: 能力未宣言なら何も呼ばず O(1) で null (バイト恒等の支点)。
|
|
613
|
+
* 宣言時は fail-fast 検証 — 戻りはプレーンな record (JS ホストの `undefined` / 配列 / Map /
|
|
614
|
+
* プリミティブは名指しで拒否、生 TypeError にしない)、キーは小文字 `data-*` かつパッケージ /
|
|
615
|
+
* エンジン放出のセル属性 (`data-aqsp*` / `data-aqvs*` / `data-row` / `data-col`) を影書きしない
|
|
616
|
+
* (エンジンは自身の `data-aqvs-*` 帯スタンプをホスト props の**後**に spread するため、衝突キーは
|
|
617
|
+
* スクロール帯では勝ち凍結帯では負ける)、値は string のみ — の後、ホストの record をそのまま
|
|
618
|
+
* 返す。違反は形 / キーを名指しの RangeError。ホスト callback の throw はホストのバグとして
|
|
619
|
+
* 素通し (try/catch しない)。
|
|
620
|
+
*/
|
|
621
|
+
export const resolveCellAttributes = (adapter: SpreadsheetDataAdapter, row: number, col: number): Readonly<Record<string, string>> | null => {
|
|
622
|
+
if (adapter.getCellAttributes === undefined) {
|
|
623
|
+
return null
|
|
624
|
+
}
|
|
625
|
+
const attrs = adapter.getCellAttributes(row, col)
|
|
626
|
+
if (attrs === null) {
|
|
627
|
+
return null
|
|
628
|
+
}
|
|
629
|
+
// 型外の戻り (JS ホスト) は Object.keys の生 TypeError や Map の無言「空属性」でなく名指しで拒否する
|
|
630
|
+
if (!isPlainRecord(attrs)) {
|
|
631
|
+
throw new RangeError(`[spreadsheet] getCellAttributes must return a plain data-* record or null (got ${Object.prototype.toString.call(attrs)})`)
|
|
632
|
+
}
|
|
633
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
634
|
+
if (!CELL_ATTRIBUTE_KEY.test(key)) {
|
|
635
|
+
throw new RangeError(`[spreadsheet] getCellAttributes key must be a lowercase data-* name (got ${JSON.stringify(key)})`)
|
|
636
|
+
}
|
|
637
|
+
if (key.startsWith("data-aqsp") || key.startsWith("data-aqvs") || key === "data-row" || key === "data-col") {
|
|
638
|
+
throw new RangeError(`[spreadsheet] getCellAttributes key ${JSON.stringify(key)} shadows a package- or engine-emitted cell attribute`)
|
|
639
|
+
}
|
|
640
|
+
if (typeof value !== "string") {
|
|
641
|
+
throw new RangeError(`[spreadsheet] getCellAttributes value for ${JSON.stringify(key)} must be a string (got ${typeof value})`)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return attrs
|
|
645
|
+
}
|
|
646
|
+
|
|
556
647
|
/**
|
|
557
648
|
* Whether a grid-clamped rect contains at least one read-only cell: rectangular walk with
|
|
558
649
|
* short-circuit on first hit; `false` in O(1) when the capability is absent. For the
|
|
@@ -120,8 +120,8 @@ export type PopoverController = {
|
|
|
120
120
|
isKindEnabled: (kind: PopoverKind) => boolean
|
|
121
121
|
/** Opens a kind on the ACTIVE cell; RangeError when the kind is disabled or its capability undeclared. / active セルで kind を開く。無効 / 能力未宣言は RangeError。 */
|
|
122
122
|
open: (kind: PopoverKind, options?: PopoverOpenOptions) => void
|
|
123
|
-
/** Closes the interactive popover (idempotent) and returns focus to the sink. / 対話ポップオーバーを閉じ (冪等)
|
|
124
|
-
close: () => void
|
|
123
|
+
/** Closes the interactive popover (idempotent) and returns focus to the sink — unless `{ restoreFocus: false }`, which the host-driven count-shrink seam passes while focus sits outside the grid (a filter input) so the host's keystrokes are not stolen; the sink is re-armed when idle either way. / 対話ポップオーバーを閉じ (冪等) シンクへフォーカスを返す — `{ restoreFocus: false }` なら返さない (ホスト起点の行数縮小シームが、フォーカスがグリッド外 (絞り込み入力欄) にある間に渡す — ホストの打鍵先を奪わない)。idle なら再武装はどちらでも行う。 */
|
|
124
|
+
close: (options?: { restoreFocus: boolean }) => void
|
|
125
125
|
/** The open interactive kind or null. / 開いている対話 kind か null。 */
|
|
126
126
|
isOpen: () => PopoverKind | null
|
|
127
127
|
/** rAF-coalesced hover entry for the note tip by cell (null = pointer left the cells); the body's own pointermove path queues the client point and hit-tests inside the frame. / note tip のセル指定ホバー入口 (rAF 合流。null = セル領域から離脱)。body の pointermove 経路は client 座標を積みフレーム内でヒットテストする。 */
|
|
@@ -458,20 +458,26 @@ export const usePopovers = (params: UsePopoversParams): PopoverController => {
|
|
|
458
458
|
const hideTip = useCallback((): boolean => hideTipNow(), [hideTipNow])
|
|
459
459
|
|
|
460
460
|
/** Closes the interactive popover (idempotent) and returns focus to the sink. / 対話ポップオーバーを閉じ (冪等) フォーカスをシンクへ返す。 */
|
|
461
|
-
const close = useCallback(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
461
|
+
const close = useCallback(
|
|
462
|
+
(options?: { restoreFocus: boolean }) => {
|
|
463
|
+
if (openKindRef.current === null) {
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
const p = paramsRef.current
|
|
467
|
+
openKindRef.current = null
|
|
468
|
+
setContext(null)
|
|
469
|
+
writeAnchors({ interactive: null })
|
|
470
|
+
// フォーカス復帰 (スクロールさせない) — ホスト起点の閉鎖 (行数縮小) がグリッド外の打鍵先を守るときだけ省く。
|
|
471
|
+
// idle なら再武装 (編集セッションの値は守る) はどちらでも行う (rearm はフォーカス中立)
|
|
472
|
+
if (options === undefined || options.restoreFocus) {
|
|
473
|
+
p.focusSink({ preventScroll: true })
|
|
474
|
+
}
|
|
475
|
+
if (!p.isEditing()) {
|
|
476
|
+
p.rearmSink()
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
[writeAnchors],
|
|
480
|
+
)
|
|
475
481
|
|
|
476
482
|
/** Places `ctx` on the interactive anchor (replacing whatever was open). / ctx を対話アンカーへ載せる (開いていたものは置換)。 */
|
|
477
483
|
const present = useCallback(
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* の (c) は pressCell 1×1 を再構築するため等価でも新オブジェクト — ラッパーの構造 bail が受ける。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { type CellCoord, type CellRange, coordEquals, makeRange, rangeContains, type SpreadsheetDirection } from "../model/cellCoord.ts"
|
|
14
|
+
import { type CellCoord, type CellRange, clampCoord, coordEquals, makeRange, rangeContains, type SpreadsheetDirection } from "../model/cellCoord.ts"
|
|
15
15
|
|
|
16
16
|
/** Selection state (tail of ranges = the "current range"). / 選択状態 (末尾が現在レンジ)。 */
|
|
17
17
|
export type SelectionState = {
|
|
@@ -38,6 +38,33 @@ const rangeEquals = (a: CellRange, b: CellRange): boolean => a.startRow === b.st
|
|
|
38
38
|
*/
|
|
39
39
|
export const selectionStateEquals = (a: SelectionState, b: SelectionState): boolean => a.ranges.length === b.ranges.length && a.ranges.every((range, i) => rangeEquals(range, b.ranges[i])) && coordEquals(a.anchor, b.anchor) && coordEquals(a.active, b.active)
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Clamps a selection into a NON-EMPTY grid whose counts shrank under it (same adapter — a
|
|
43
|
+
* filter / category / period change removed rows): every range corner, the anchor and the
|
|
44
|
+
* active cell are pulled onto the last row / column, and a range that lay entirely beyond the
|
|
45
|
+
* edge collapses onto it. Returns the INPUT object when nothing was out of bounds (identity
|
|
46
|
+
* contract → the §6.9-1 wrapper's Object.is bail = no notification) and keeps the identity of
|
|
47
|
+
* every range that was inside. Throws RangeError on an empty grid (the caller owns the empty
|
|
48
|
+
* path — 0 rows have no A1).
|
|
49
|
+
* 縮小した非空グリッドへ選択を畳む (同一アダプタ — 絞り込み / カテゴリ / 期間の変更で行が減った):
|
|
50
|
+
* 各レンジの隅・anchor・active を末尾行 / 列へ引き寄せ、丸ごと外にあったレンジは端へ潰れる。範囲外が
|
|
51
|
+
* 無ければ入力 object をそのまま返し (identity 契約 → §6.9-1 ラッパーの Object.is bail = 通知なし)、
|
|
52
|
+
* 範囲内のレンジは identity を保つ。空グリッドは RangeError (空経路は呼び手所有 — 0 行に A1 は無い)。
|
|
53
|
+
*/
|
|
54
|
+
export const clampSelectionToGrid = (state: SelectionState, rowCount: number, colCount: number): SelectionState => {
|
|
55
|
+
const inside = (cell: CellCoord): boolean => cell.row < rowCount && cell.col < colCount
|
|
56
|
+
const rangeInside = (range: CellRange): boolean => range.endRow < rowCount && range.endCol < colCount
|
|
57
|
+
if (inside(state.anchor) && inside(state.active) && state.ranges.every(rangeInside)) {
|
|
58
|
+
return state
|
|
59
|
+
}
|
|
60
|
+
const clamp = (cell: CellCoord): CellCoord => clampCoord(cell, rowCount, colCount)
|
|
61
|
+
return {
|
|
62
|
+
ranges: state.ranges.map((range) => (rangeInside(range) ? range : makeRange(clamp({ row: range.startRow, col: range.startCol }), clamp({ row: range.endRow, col: range.endCol })))),
|
|
63
|
+
anchor: inside(state.anchor) ? state.anchor : clamp(state.anchor),
|
|
64
|
+
active: inside(state.active) ? state.active : clamp(state.active),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
/** Replaces the tail range, preserving identity when unchanged. / 現在レンジのみ置換 (等価なら入力を返す)。 */
|
|
42
69
|
const replaceCurrentRange = (state: SelectionState, next: CellRange): SelectionState => {
|
|
43
70
|
// identity 保存契約 (冒頭): 無変化なら新オブジェクトを作らない — §6.9-1 の Object.is bail の根拠
|