@aiquants/spreadsheet 0.12.0 → 0.13.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 +60 -19
- package/dist/index.cjs +13 -13
- package/dist/index.d.cts +113 -119
- package/dist/index.d.ts +113 -119
- package/dist/index.js +3457 -3328
- package/package.json +3 -3
- package/src/Spreadsheet.tsx +68 -13
- package/src/grid/GridBody.tsx +30 -1
- package/src/index.ts +1 -1
- package/src/model/dataValidation.ts +84 -67
- package/src/model/labels.ts +313 -101
- package/src/model/validationMessages.ts +203 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiquants/spreadsheet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Excel-grade spreadsheet grid for React: multi-cell selection, TSV/HTML clipboard, in-place editing on top of @aiquants/virtualscroll",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
"LICENSE.md"
|
|
35
35
|
],
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@aiquants/virtualscroll": "^3.
|
|
37
|
+
"@aiquants/virtualscroll": "^3.7.0",
|
|
38
38
|
"react": "^19.2.7",
|
|
39
39
|
"react-dom": "^19.2.7"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@aiquants/virtualscroll": "3.
|
|
42
|
+
"@aiquants/virtualscroll": "3.7.0",
|
|
43
43
|
"@playwright/test": "^1.59.1",
|
|
44
44
|
"@tailwindcss/cli": "^4.1.18",
|
|
45
45
|
"@testing-library/jest-dom": "^6.9.1",
|
package/src/Spreadsheet.tsx
CHANGED
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
* clipboard / keyboard (executeCommandRef / openPopoverRef / hideNoteTipRef / pasteRef /
|
|
28
28
|
* pendingPasteOptionsRef / popover anchors).
|
|
29
29
|
*
|
|
30
|
+
* UI chrome locale: the `locale` prop is resolved once (`resolveVirtualScrollLocale` — undefined ⇒
|
|
31
|
+
* "en", unknown ⇒ RangeError at render) and drives the label catalog, the built-in validation
|
|
32
|
+
* messages and — through a value-stable engine subset — the grid scrollbars.
|
|
33
|
+
*
|
|
30
34
|
* v0.8 (計画 §4.2.13 — フック順は設計 v5): 軸モデル → viewStore → ズーム → オートスクローラー
|
|
31
35
|
* 6 台 (6 台目は参照ドラッグ用) → 選択エンジン → ヘッダー機械 ×2 → ヘッダーリサイズ → fill →
|
|
32
36
|
* 参照ドラッグ (usePointDrag) → 合成ドラッグ面 (isDraggingAny / abortDragAny / "point" 込みの
|
|
@@ -37,9 +41,13 @@
|
|
|
37
41
|
* ポインティングの ref 辺 (editor → engine / ヘッダー / 参照ドラッグ / body / コーナー / ズーム死空間)、
|
|
38
42
|
* コマンド / ポップオーバー / クリップボード / キーボード間の ref 辺 (executeCommandRef /
|
|
39
43
|
* openPopoverRef / hideNoteTipRef / pasteRef / pendingPasteOptionsRef / ポップオーバーアンカー) を所有する。
|
|
44
|
+
*
|
|
45
|
+
* UI クロームロケール: `locale` prop を 1 回だけ解決し (`resolveVirtualScrollLocale` — undefined は
|
|
46
|
+
* "en"、未知は render 中 RangeError)、ラベルカタログ・組み込み検証メッセージ・値安定なエンジン部分集合
|
|
47
|
+
* 経由のグリッドスクロールバーを駆動する。
|
|
40
48
|
*/
|
|
41
49
|
|
|
42
|
-
import type
|
|
50
|
+
import { resolveVirtualScrollLocale, type ScrollBarTapCircleOptions, VIRTUAL_SCROLL_LABEL_KEYS, type VirtualGridHandle, type VirtualGridRange, type VirtualScrollLabels } from "@aiquants/virtualscroll"
|
|
43
51
|
import type { ProfilerOnRenderCallback } from "react"
|
|
44
52
|
import { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"
|
|
45
53
|
import type { ClipChannel } from "./clipboard/clipPayload.ts"
|
|
@@ -67,7 +75,7 @@ import { type CellEdit, type EditContext, type EditRejection, type EditSource, i
|
|
|
67
75
|
import { type DataValidationRule, type ValidationVerdict, validateCellText } from "./model/dataValidation.ts"
|
|
68
76
|
import type { ReferenceAxis } from "./model/formulaRefs.ts"
|
|
69
77
|
import { colToViewportX, rowToViewportY } from "./model/frozenPartition.ts"
|
|
70
|
-
import { resolveLabels, type SpreadsheetLabelOverrides } from "./model/labels.ts"
|
|
78
|
+
import { pickEngineLabels, resolveLabels, type SpreadsheetLabelOverrides, type SpreadsheetLocale } from "./model/labels.ts"
|
|
71
79
|
import { createViewStore, type ViewStore } from "./model/viewStore.ts"
|
|
72
80
|
import { DEFAULT_HIGHLIGHT_COLORS, type DragFrameMode, type HighlightColors, type HighlightState, type ResizeGuide, SelectionOverlay } from "./overlay/SelectionOverlay.tsx"
|
|
73
81
|
import { useFillHandle } from "./overlay/useFillHandle.ts"
|
|
@@ -437,14 +445,30 @@ export type SpreadsheetProps = {
|
|
|
437
445
|
* コマンドの AT 通知 (opt-in、v0.8 §1.9)。`state` はトグル後状態 (非トグルは null)。live region へ。
|
|
438
446
|
*/
|
|
439
447
|
formatCommandAnnouncement?: (ctx: { command: SpreadsheetCommand; state: boolean | null }) => string
|
|
448
|
+
/**
|
|
449
|
+
* UI chrome language ("en" | "ja"; default "en"): selects the label catalog, the built-in
|
|
450
|
+
* data-validation messages, and is forwarded to the grid scrollbars (arrow labels,
|
|
451
|
+
* scroll-to-edge pills, "No items"). An unsupported value throws a RangeError during render —
|
|
452
|
+
* there is no language negotiation: hosts map `navigator.language` themselves. Unrelated to
|
|
453
|
+
* `formats.locale`, which is the BCP-47 value-formatting locale ("ja-JP" is NOT a valid value
|
|
454
|
+
* here).
|
|
455
|
+
* UI クローム言語 ("en" | "ja"、既定 "en"): ラベルカタログと組み込みデータ検証メッセージを選び、
|
|
456
|
+
* グリッドのスクロールバー (矢印ラベル・端スクロールピル・"No items") へ素通し。非対応値は render 中
|
|
457
|
+
* RangeError — 言語ネゴシエーションなし (`navigator.language` の写像はホスト側)。値書式の BCP-47
|
|
458
|
+
* ロケールである `formats.locale` とは無関係 ("ja-JP" はここでは不正値)。
|
|
459
|
+
*/
|
|
460
|
+
locale?: SpreadsheetLocale
|
|
440
461
|
/**
|
|
441
462
|
* UI label vocabulary (v0.8 §1.6) — folds the former `sinkAriaLabel` / `rootAriaLabel` props
|
|
442
|
-
* (clean cut-over).
|
|
443
|
-
*
|
|
444
|
-
*
|
|
463
|
+
* (clean cut-over). 28 keys = the 7 engine chrome keys (scrollUp / scrollDown / scrollLeft /
|
|
464
|
+
* scrollRight / scrollToTop / scrollToBottom / noItems) + 21 own keys. An omitted prop or an
|
|
465
|
+
* explicitly `undefined` key keeps the value of the `locale` catalog. An unknown key or an
|
|
466
|
+
* empty / blank value throws a RangeError during render, and the resolved `sinkAriaLabel` MUST
|
|
467
|
+
* mention "Escape" (WCAG 2.1.2 escape advisement, §10-4).
|
|
445
468
|
* UI ラベル語彙 (v0.8 §1.6) — 旧 sinkAriaLabel / rootAriaLabel prop を畳込み (クリーンカットオーバー)。
|
|
446
|
-
*
|
|
447
|
-
*
|
|
469
|
+
* 28 キー = エンジンクローム 7 キー (scrollUp / scrollDown / scrollLeft / scrollRight / scrollToTop /
|
|
470
|
+
* scrollToBottom / noItems) + 固有 21 キー。prop 省略 / 明示 undefined のキーは `locale` のカタログ値。
|
|
471
|
+
* 未知キー・空 / 空白値は render 中 RangeError、解決後の sinkAriaLabel は "Escape" を含むこと (§10-4)。
|
|
448
472
|
*/
|
|
449
473
|
labels?: SpreadsheetLabelOverrides
|
|
450
474
|
/**
|
|
@@ -505,7 +529,7 @@ export type SpreadsheetProps = {
|
|
|
505
529
|
onClipChange?: (clip: ClipInfo | null) => void
|
|
506
530
|
/** Hyperlink navigation seam (Alt+Enter, the link popover's Open button, and Ctrl / Meta+click on a link cell — spec 08 §11 D-53) — undeclared ⇒ no navigation, no Open button, no pointer gesture. / ハイパーリンク遷移シーム (Alt+Enter / Open ボタン / リンクセルへの Ctrl / Meta+クリック — 分冊 08 §11 D-53)。未宣言 = 遷移なし・Open ボタンなし・ポインタジェスチャなし。 */
|
|
507
531
|
onHyperlinkOpen?: (url: string, cell: CellCoord) => void
|
|
508
|
-
/** Strict validation rejection notice (v0.8 §1.9) — the commit never reaches the adapter; the message popover shows the rejection. / strict 検証拒否の通知 (commit はアダプタへ届かず、message ポップオーバーが拒否を示す)
|
|
532
|
+
/** Strict validation rejection notice (v0.8 §1.9) — the commit never reaches the adapter; the message popover shows the rejection. `message` is `rule.errorMessage` when declared, otherwise the built-in message in the active `locale`. / strict 検証拒否の通知 (commit はアダプタへ届かず、message ポップオーバーが拒否を示す)。`message` は `rule.errorMessage` 宣言時はそれ、未宣言なら現在の `locale` の組み込みメッセージ。 */
|
|
509
533
|
onValidationRejected?: (ctx: { cell: CellCoord; rule: DataValidationRule; text: string; message: string }) => void
|
|
510
534
|
/** Ctrl+Alt+V request with the active cell's viewport rect (visual px, keyboard-origin shape of onCellContextMenu); undeclared ⇒ `paste({ mode: "formats" })`. / Ctrl+Alt+V の要求通知 (未宣言なら書式のみ貼り付け)。 */
|
|
511
535
|
onPasteSpecialRequest?: (origin: { kind: "keyboard"; cellRect: { top: number; left: number; width: number; height: number } }) => void
|
|
@@ -715,6 +739,28 @@ const requirePasteSheet = (sheet: PasteSheet | undefined): PasteSheet => {
|
|
|
715
739
|
return sheet
|
|
716
740
|
}
|
|
717
741
|
|
|
742
|
+
/**
|
|
743
|
+
* Keeps the previous engine label object while all 7 strings are value-equal, so the object handed
|
|
744
|
+
* to the memoized GridBody changes identity only when a string changes (a host passing an inline
|
|
745
|
+
* `labels={{…}}` must not re-render the grid body on every shell render).
|
|
746
|
+
* 7 文字列がすべて値等価の間は前回のエンジンラベル object を保持 — memo 化された GridBody へ渡す
|
|
747
|
+
* object の identity は文字列が変わった時だけ変わる (インライン `labels={{…}}` のホストでも shell
|
|
748
|
+
* 再レンダー毎に本体を再描画させない)。
|
|
749
|
+
*
|
|
750
|
+
* @param next - The engine labels resolved in this render / 今回の render で解決したエンジンラベル
|
|
751
|
+
* @returns `next`, or the previous object when every key is value-equal / `next`、全キー値等価なら前回の object
|
|
752
|
+
*/
|
|
753
|
+
const useValueStableScrollLabels = (next: VirtualScrollLabels): VirtualScrollLabels => {
|
|
754
|
+
const stableRef = useRef<VirtualScrollLabels | null>(null)
|
|
755
|
+
const previous = stableRef.current
|
|
756
|
+
if (previous !== null && VIRTUAL_SCROLL_LABEL_KEYS.every((key) => previous[key] === next[key])) {
|
|
757
|
+
return previous
|
|
758
|
+
}
|
|
759
|
+
// レンダー時代入: 値等価比較の基準だけを保持し、副作用は持たない (破棄された render でも比較結果は同じ)
|
|
760
|
+
stableRef.current = next
|
|
761
|
+
return next
|
|
762
|
+
}
|
|
763
|
+
|
|
718
764
|
/** Initial A1 selection. / 初期 A1 選択。 */
|
|
719
765
|
const INITIAL_SELECTION: SelectionState = {
|
|
720
766
|
ranges: [makeRange({ row: 0, col: 0 }, { row: 0, col: 0 })],
|
|
@@ -757,6 +803,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
757
803
|
formatCellAnnouncement,
|
|
758
804
|
formatSelectionAnnouncement,
|
|
759
805
|
formatCommandAnnouncement,
|
|
806
|
+
locale,
|
|
760
807
|
labels,
|
|
761
808
|
shortcuts,
|
|
762
809
|
colors,
|
|
@@ -797,8 +844,12 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
797
844
|
const viewStore = viewStoreRef.current
|
|
798
845
|
|
|
799
846
|
// ---- prop 時検証 (v0.8 §4.2.13 — clampZoom と同じく render 中に RangeError を投げる) ----
|
|
847
|
+
// 非対応ロケールはここで render 中 RangeError — 以降の検証メッセージ / GridBody は解決済み値だけを受ける
|
|
848
|
+
const uiLocale = resolveVirtualScrollLocale(locale)
|
|
800
849
|
// ラベルの畳込み: resolveLabels が Escape 告知 (§10-4) を検証する (RC22)
|
|
801
|
-
const resolvedLabels = useMemo(() => resolveLabels(labels), [labels])
|
|
850
|
+
const resolvedLabels = useMemo(() => resolveLabels(locale, labels), [locale, labels])
|
|
851
|
+
// GridBody は memo 境界 — インライン labels で毎回新 object になっても 7 文字列が同じなら identity を保つ
|
|
852
|
+
const scrollLabels = useValueStableScrollLabels(pickEngineLabels(resolvedLabels))
|
|
802
853
|
// コマンド表: validateShortcuts の 5 条件 (+ 未知キー) は consumer の設定誤り — render 中に表面化
|
|
803
854
|
const commandTable = useMemo(() => validateShortcuts(shortcuts), [shortcuts])
|
|
804
855
|
// カスタム色スロット (各軸ちょうど 5 本の #rrggbb)
|
|
@@ -1897,10 +1948,12 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1897
1948
|
* The validation gate (v0.8 §4.2.6 / 分冊 08 §4.2), shared by the editor's changed commit and
|
|
1898
1949
|
* the editing-column `fillSelection` (which runs it per target cell): no rule / non-strict ⇒
|
|
1899
1950
|
* ok (the cell keeps its invalid mark); only a strict rejection stops the commit and reports
|
|
1900
|
-
* through `onValidationRejected` + the `message` popover (`rejected`).
|
|
1951
|
+
* through `onValidationRejected` + the `message` popover (`rejected`). The verdict message is
|
|
1952
|
+
* `rule.errorMessage` when declared, otherwise the built-in message in the resolved UI locale.
|
|
1901
1953
|
* 検証ゲート (v0.8 §4.2.6 / 分冊 08 §4.2) — エディタの変更ある確定と編集列 fillSelection (対象セル毎)
|
|
1902
1954
|
* が共有。規則なし / 非 strict は受理 (セルは不正表示のまま)、strict の拒否だけが commit を止めて
|
|
1903
|
-
* onValidationRejected + message ポップオーバー (rejected)
|
|
1955
|
+
* onValidationRejected + message ポップオーバー (rejected) を出す。判定メッセージは `rule.errorMessage`
|
|
1956
|
+
* 宣言時はそれ、未宣言なら解決済み UI ロケールの組み込みメッセージ。
|
|
1904
1957
|
*/
|
|
1905
1958
|
const validateCommit = useCallback(
|
|
1906
1959
|
(cell: CellCoord, text: string): ValidationVerdict => {
|
|
@@ -1909,7 +1962,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1909
1962
|
return OK_VERDICT
|
|
1910
1963
|
}
|
|
1911
1964
|
const evaluateFormula = adapter.evaluateFormula
|
|
1912
|
-
const verdict = validateCellText(rule, text, evaluateFormula === undefined ? undefined : (formula) => evaluateFormula(formula, cell))
|
|
1965
|
+
const verdict = validateCellText(rule, text, uiLocale, evaluateFormula === undefined ? undefined : (formula) => evaluateFormula(formula, cell))
|
|
1913
1966
|
if (verdict.ok || rule.strict !== true) {
|
|
1914
1967
|
return OK_VERDICT
|
|
1915
1968
|
}
|
|
@@ -1920,7 +1973,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1920
1973
|
}
|
|
1921
1974
|
return verdict
|
|
1922
1975
|
},
|
|
1923
|
-
[adapter, onValidationRejected],
|
|
1976
|
+
[adapter, onValidationRejected, uiLocale],
|
|
1924
1977
|
)
|
|
1925
1978
|
const editor = useCellEditor({
|
|
1926
1979
|
enabled: !isEmpty,
|
|
@@ -2777,6 +2830,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2777
2830
|
cfIndexRef={cfIndexRef}
|
|
2778
2831
|
cfMemoRef={cfMemoRef}
|
|
2779
2832
|
isPointing={isPointing}
|
|
2833
|
+
locale={uiLocale}
|
|
2834
|
+
scrollLabels={scrollLabels}
|
|
2780
2835
|
/>
|
|
2781
2836
|
<SelectionOverlay
|
|
2782
2837
|
rowAxis={rowAxis}
|
package/src/grid/GridBody.tsx
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
* helper (ADR-31 — no package CSS; capability absent ⇒ O(1) false ⇒ keys absent ⇒ the DOM
|
|
29
29
|
* stays byte-identical).
|
|
30
30
|
*
|
|
31
|
+
* UI chrome locale: the resolved `locale` and the value-stable engine subset `scrollLabels` are
|
|
32
|
+
* forwarded to VirtualGrid, which localizes the embedded vertical bar, the horizontal bar and the
|
|
33
|
+
* degenerate-band "No items" — no lang attribute, no new data-* hook.
|
|
34
|
+
*
|
|
31
35
|
* グリッド本体。VirtualGrid 結線 (両軸) とセルレンダラーを所有する。v0.4 から横軸は VirtualGrid
|
|
32
36
|
* の所有 (量子化列アンカー + 残差 transform + 横バー) — v0.3 の列スタック (useColumnWindow /
|
|
33
37
|
* --aqsp-hx CSS 変数 / 軸原点絶対 left / 自前 HScrollBar) は設計計画 §7-1 に従い削除。
|
|
@@ -50,9 +54,12 @@
|
|
|
50
54
|
* v0.10 読み取り専用セル (計画 §6.5、S1): `getCellProps` が塗られた読み取り専用セルへ
|
|
51
55
|
* `aria-readonly="true"` + `data-aqsp-readonly=""` を `isReadOnlyCell` ヘルパ経由で条件スタンプ
|
|
52
56
|
* する (ADR-31 — パッケージ CSS なし。能力未宣言 = O(1) false = キー不在 = DOM バイト同一)。
|
|
57
|
+
*
|
|
58
|
+
* UI クロームロケール: 解決済み `locale` と値安定なエンジン部分集合 `scrollLabels` を VirtualGrid へ
|
|
59
|
+
* 素通しし、埋込み縦バー・横バー・退化帯の "No items" を現地化する (lang 属性・新規 data-* フックなし)。
|
|
53
60
|
*/
|
|
54
61
|
|
|
55
|
-
import { type ScrollBarTapCircleOptions, VirtualGrid, type VirtualGridHandle, type VirtualGridRange } from "@aiquants/virtualscroll"
|
|
62
|
+
import { type ScrollBarTapCircleOptions, VirtualGrid, type VirtualGridHandle, type VirtualGridRange, type VirtualScrollLabels } from "@aiquants/virtualscroll"
|
|
56
63
|
import { type HTMLAttributes, memo, type ProfilerOnRenderCallback, type ReactNode, type RefObject, useCallback, useMemo, useRef, useSyncExternalStore } from "react"
|
|
57
64
|
import { type AxisModel, nextVisibleIndex } from "../model/axisModel.ts"
|
|
58
65
|
import { normalizeAdapterHyperlink } from "../model/cellAnnotations.ts"
|
|
@@ -61,6 +68,7 @@ import { type CellPaint, resolveCellPaint } from "../model/cellPaint.ts"
|
|
|
61
68
|
import { type CfIndex, type CfMemo, createCfMemo, resolvePaintedFormat } from "../model/conditionalFormat.ts"
|
|
62
69
|
import { isReadOnlyCell, lazyCellText, resolveCellAlign, resolveCellAttributes, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
63
70
|
import { isVisiblyInvalid } from "../model/dataValidation.ts"
|
|
71
|
+
import type { SpreadsheetLocale } from "../model/labels.ts"
|
|
64
72
|
import { SCROLLBAR_WIDTH, type ViewStore } from "../model/viewStore.ts"
|
|
65
73
|
import { CellView, type RenderCellContext } from "./CellView.tsx"
|
|
66
74
|
|
|
@@ -338,6 +346,23 @@ type GridBodyProps = {
|
|
|
338
346
|
background?: ReactNode
|
|
339
347
|
/** Rendering-truncation signal (engine `onRenderTruncated` pass-through — trailing columns dropped under the cell cap). / 描画打切り通知 (エンジン `onRenderTruncated` の素通し — セル上限下の末尾列脱落)。 */
|
|
340
348
|
onRenderTruncated?: (info: { renderedCells: number; droppedCols: number }) => void
|
|
349
|
+
/**
|
|
350
|
+
* The RESOLVED UI chrome locale (never undefined — the root resolves the `locale` prop through
|
|
351
|
+
* `resolveVirtualScrollLocale`), forwarded to VirtualGrid's `locale`.
|
|
352
|
+
* 解決済みの UI クロームロケール (undefined にならない — root が `locale` prop を
|
|
353
|
+
* `resolveVirtualScrollLocale` で解決)。VirtualGrid の `locale` へ素通し。
|
|
354
|
+
*/
|
|
355
|
+
locale: SpreadsheetLocale
|
|
356
|
+
/**
|
|
357
|
+
* The resolved engine chrome strings (the 7 VirtualScrollLabels keys: scroll arrows,
|
|
358
|
+
* scroll-to-edge pills, "No items"), forwarded to VirtualGrid's `labels`. MUST be value-stable:
|
|
359
|
+
* its identity may change only when one of the 7 strings changes, otherwise every shell render
|
|
360
|
+
* pierces the memo boundary.
|
|
361
|
+
* 解決済みエンジンクローム文言 (VirtualScrollLabels の 7 キー: スクロール矢印・端スクロールピル・
|
|
362
|
+
* "No items")。VirtualGrid の `labels` へ素通し。値安定であること (7 文字列のどれかが変わった時だけ
|
|
363
|
+
* identity が変わる — さもなくば shell 再レンダー毎に memo 境界を貫通)。
|
|
364
|
+
*/
|
|
365
|
+
scrollLabels: VirtualScrollLabels
|
|
341
366
|
}
|
|
342
367
|
|
|
343
368
|
/**
|
|
@@ -373,6 +398,8 @@ const GridBodyInner = ({
|
|
|
373
398
|
overscanCols,
|
|
374
399
|
background,
|
|
375
400
|
onRenderTruncated,
|
|
401
|
+
locale,
|
|
402
|
+
scrollLabels,
|
|
376
403
|
}: GridBodyProps): ReactNode => {
|
|
377
404
|
const internalBodyRef = useRef<HTMLDivElement | null>(null)
|
|
378
405
|
// 外部 ref (エンジン結線用) と内部 ref の合成
|
|
@@ -554,6 +581,8 @@ const GridBodyInner = ({
|
|
|
554
581
|
background={background}
|
|
555
582
|
onRenderTruncated={onRenderTruncated}
|
|
556
583
|
__maxRenderedCells={__maxRenderedCells}
|
|
584
|
+
locale={locale}
|
|
585
|
+
labels={scrollLabels}
|
|
557
586
|
callbackThrottleMs={0}
|
|
558
587
|
onScroll={onScroll}
|
|
559
588
|
onRangeChange={onRangeChange}
|
package/src/index.ts
CHANGED
|
@@ -110,7 +110,7 @@ export {
|
|
|
110
110
|
validateCellText,
|
|
111
111
|
} from "./model/dataValidation.ts"
|
|
112
112
|
export { fromR1C1, shiftFormula, toR1C1 } from "./model/formulaRefs.ts"
|
|
113
|
-
export {
|
|
113
|
+
export { resolveLabel, resolveLabels, SPREADSHEET_LABEL_CATALOGS, type SpreadsheetLabelOverrides, type SpreadsheetLabels, type SpreadsheetLocale } from "./model/labels.ts"
|
|
114
114
|
export { DEFAULT_HIGHLIGHT_COLORS, type HighlightColors, type HighlightState } from "./overlay/SelectionOverlay.tsx"
|
|
115
115
|
// 既定ポップオーバー 6 種 — renderPopover の部品 (kind 別に差し替え、残りは既定を組み合わせる)
|
|
116
116
|
export { CommentPopover, type CommentPopoverProps } from "./popover/CommentPopover.tsx"
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
* - INTERNED list Sets per rule object (WeakMap identity), so the paint-time `list` check is O(1)
|
|
27
27
|
* even for a 10k-item list; a fresh rule object interns a fresh Set.
|
|
28
28
|
* - MESSAGE resolution: `errorMessage` when the consumer set it (an empty string is honoured as
|
|
29
|
-
* is), else
|
|
29
|
+
* is), else the built-in summary of the rule in the caller's REQUIRED `locale`
|
|
30
|
+
* (validationMessages.ts); an unsupported locale throws RangeError before any verdict.
|
|
30
31
|
* - `custom` needs the caller's `evaluate`; without it the rule is not evaluated (verdict ok), and
|
|
31
32
|
* truth = `true` or a finite non-zero number (the conditionalFormat customFormula convention).
|
|
32
33
|
* - Matching is exact (case-sensitive, untrimmed) for `list` / `text` / `checkbox`; textLength
|
|
@@ -43,11 +44,15 @@
|
|
|
43
44
|
* タプル比較 (between は両端含み)、不完全な規則 (解釈不能な境界・between の b 欠落・needle 欠落) は
|
|
44
45
|
* 不活性 = 全受理 (書きかけの規則でセルを閉じ込めない、境界の入替もしない)、語彙外の type / op は
|
|
45
46
|
* consumer バグとして text に依らず RangeError、list の Set は規則 object 毎に WeakMap intern、
|
|
46
|
-
* メッセージは errorMessage (空文字も尊重)
|
|
47
|
+
* メッセージは errorMessage (空文字も尊重) が無ければ呼び手必須の locale による組込み要約
|
|
48
|
+
* (validationMessages.ts — 非対応ロケールは判定前に RangeError)、custom は evaluate 無しなら
|
|
47
49
|
* 未評価 (ok)・真 = true か有限非 0 数、照合は完全一致 (大文字小文字区別・trim 無し)、textLength は
|
|
48
50
|
* コードポイント数、isEmail は WHATWG 文法 + ドット付きドメイン必須、isUrl は WHATWG 絶対 http(s) URL。
|
|
49
51
|
*/
|
|
50
52
|
|
|
53
|
+
import { describeLabelValue, type SpreadsheetLocale } from "./labels.ts"
|
|
54
|
+
import { VALIDATION_MESSAGE_CATALOGS, type ValidationMessageCatalog } from "./validationMessages.ts"
|
|
55
|
+
|
|
51
56
|
/** Ordered comparison vocabulary shared by number / textLength / date rules; between / notBetween include both bounds. / number / textLength / date 規則が共有する比較語彙。between / notBetween は両端含み。 */
|
|
52
57
|
export type ValidationCompareOp = "gt" | "gte" | "lt" | "lte" | "eq" | "neq" | "between" | "notBetween"
|
|
53
58
|
|
|
@@ -127,15 +132,6 @@ type Ordering = -1 | 0 | 1
|
|
|
127
132
|
/** Resolved bounds of an ordered rule; `b` is null when the op does not use it. / 順序規則の解決済み境界。op が使わないとき b は null。 */
|
|
128
133
|
type Bounds<T> = { a: T; b: T | null }
|
|
129
134
|
|
|
130
|
-
/** Rule-family wording for the four ordered ops (the other four are shared). / 順序 4 op の規則族別の言い回し (残る 4 つは共通)。 */
|
|
131
|
-
type OrderWords = { readonly gt: string; readonly gte: string; readonly lt: string; readonly lte: string }
|
|
132
|
-
|
|
133
|
-
/** Wording for number / textLength messages. / number / textLength メッセージの言い回し。 */
|
|
134
|
-
const NUMERIC_WORDS: OrderWords = { gt: "greater than", gte: "greater than or equal to", lt: "less than", lte: "less than or equal to" }
|
|
135
|
-
|
|
136
|
-
/** Wording for date messages. / date メッセージの言い回し。 */
|
|
137
|
-
const DATE_WORDS: OrderWords = { gt: "after", gte: "on or after", lt: "before", lte: "on or before" }
|
|
138
|
-
|
|
139
135
|
/**
|
|
140
136
|
* Parses a strict decimal string into its exact normalised form: trims, applies the
|
|
141
137
|
* cellFormat.ts grammar (hex / grouping / empty rejected, exponent overflow to ±Infinity
|
|
@@ -367,9 +363,16 @@ const checkboxStrings = (rule: CheckboxValidationRule): { checked: string; unche
|
|
|
367
363
|
|
|
368
364
|
/**
|
|
369
365
|
* Rejects a rule outside the vocabulary (unknown `type`, or an unknown `op` on number /
|
|
370
|
-
* textLength / date / text) with RangeError — a consumer bug surfaces regardless of the text.
|
|
366
|
+
* textLength / date / text) with RangeError — a consumer bug surfaces regardless of the text. The
|
|
367
|
+
* rejected value is described by `describeLabelValue`, so a Symbol / null-prototype `type` or `op`
|
|
368
|
+
* still yields the RangeError (never a TypeError from the message itself).
|
|
371
369
|
* 語彙外の規則 (未知 type、number / textLength / date / text の未知 op) を RangeError で拒否 —
|
|
372
|
-
* consumer バグは text
|
|
370
|
+
* consumer バグは text に依らず表面化。拒否値は `describeLabelValue` で説明するため、Symbol / null
|
|
371
|
+
* プロトタイプの type や op でもメッセージ組立て自体が TypeError を投げず RangeError のまま。
|
|
372
|
+
*
|
|
373
|
+
* @param rule - The rule to check / 検査する規則
|
|
374
|
+
* @returns Nothing; returns normally for an in-vocabulary rule / なし (語彙内の規則なら正常終了)
|
|
375
|
+
* @throws {RangeError} When the rule's `type` / `op` is outside the vocabulary / 規則の type / op が語彙外の場合
|
|
373
376
|
*/
|
|
374
377
|
const assertRuleShape = (rule: DataValidationRule): void => {
|
|
375
378
|
switch (rule.type) {
|
|
@@ -381,16 +384,16 @@ const assertRuleShape = (rule: DataValidationRule): void => {
|
|
|
381
384
|
case "textLength":
|
|
382
385
|
case "date":
|
|
383
386
|
if (!COMPARE_OPS.has(rule.op)) {
|
|
384
|
-
throw new RangeError(`[spreadsheet] unknown data-validation op
|
|
387
|
+
throw new RangeError(`[spreadsheet] unknown data-validation op ${describeLabelValue(rule.op)} for type "${rule.type}"`)
|
|
385
388
|
}
|
|
386
389
|
return
|
|
387
390
|
case "text":
|
|
388
391
|
if (!TEXT_OPS.has(rule.op)) {
|
|
389
|
-
throw new RangeError(`[spreadsheet] unknown data-validation op
|
|
392
|
+
throw new RangeError(`[spreadsheet] unknown data-validation op ${describeLabelValue(rule.op)} for type "text"`)
|
|
390
393
|
}
|
|
391
394
|
return
|
|
392
395
|
default:
|
|
393
|
-
throw new RangeError(`[spreadsheet] unknown data-validation rule type
|
|
396
|
+
throw new RangeError(`[spreadsheet] unknown data-validation rule type ${describeLabelValue((rule as { type: unknown }).type)}`)
|
|
394
397
|
}
|
|
395
398
|
}
|
|
396
399
|
|
|
@@ -427,68 +430,61 @@ const accepts = (rule: DataValidationRule, text: string, evaluate: ValidationEva
|
|
|
427
430
|
return text === "" || ruleHolds(rule, text, evaluate)
|
|
428
431
|
}
|
|
429
432
|
|
|
430
|
-
/** Phrase for an ordered op with the family wording: "greater than 5", "between 1 and 9", ... / 順序 op の言い回し (規則族の語彙付き)。 */
|
|
431
|
-
const comparePhrase = (op: ValidationCompareOp, a: string, b: string | undefined, words: OrderWords): string => {
|
|
432
|
-
switch (op) {
|
|
433
|
-
case "gt":
|
|
434
|
-
case "gte":
|
|
435
|
-
case "lt":
|
|
436
|
-
case "lte":
|
|
437
|
-
return `${words[op]} ${a}`
|
|
438
|
-
case "eq":
|
|
439
|
-
return `equal to ${a}`
|
|
440
|
-
case "neq":
|
|
441
|
-
return `not equal to ${a}`
|
|
442
|
-
case "between":
|
|
443
|
-
return `between ${a} and ${b}`
|
|
444
|
-
default:
|
|
445
|
-
// 残る順序 op は notBetween
|
|
446
|
-
return `not between ${a} and ${b}`
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
433
|
/** Trims an optional bound string for display. / 表示用に任意の境界文字列を trim。 */
|
|
451
434
|
const trimBound = (bound: string | undefined): string | undefined => (bound === undefined ? undefined : bound.trim())
|
|
452
435
|
|
|
453
|
-
/**
|
|
454
|
-
|
|
436
|
+
/**
|
|
437
|
+
* Built-in summary of a rule in one locale's catalog, used when the consumer set no
|
|
438
|
+
* `errorMessage`. The bounds are normalised here (number / date bounds trimmed, textLength bounds
|
|
439
|
+
* stringified, checkbox strings defaulted) so every locale's template sees the same values.
|
|
440
|
+
* consumer が errorMessage を置かないときの、1 ロケールのカタログによる規則の組込み要約。境界の正規化
|
|
441
|
+
* (number / date は trim、textLength は文字列化、checkbox は既定適用) をここで行い、全ロケールの
|
|
442
|
+
* テンプレートが同じ値を受け取る。
|
|
443
|
+
*
|
|
444
|
+
* @param rule - The shape-checked rule that rejected the text / text を拒否した形検査済みの規則
|
|
445
|
+
* @param catalog - The message catalog of the verdict's locale / 判定ロケールのメッセージカタログ
|
|
446
|
+
* @returns The built-in message / 組込みメッセージ
|
|
447
|
+
*/
|
|
448
|
+
const messageOf = (rule: DataValidationRule, catalog: ValidationMessageCatalog): string => {
|
|
455
449
|
switch (rule.type) {
|
|
456
450
|
case "list":
|
|
457
|
-
return
|
|
451
|
+
return catalog.list
|
|
458
452
|
case "number":
|
|
459
|
-
return
|
|
453
|
+
return catalog.number[rule.op](rule.a.trim(), trimBound(rule.b))
|
|
460
454
|
case "textLength":
|
|
461
|
-
return
|
|
455
|
+
return catalog.textLength[rule.op](String(rule.a), rule.b === undefined ? undefined : String(rule.b))
|
|
462
456
|
case "date":
|
|
463
|
-
return
|
|
457
|
+
return catalog.date[rule.op](rule.a.trim(), trimBound(rule.b))
|
|
464
458
|
case "text":
|
|
465
|
-
return
|
|
459
|
+
return textMessageOf(rule, catalog.text)
|
|
466
460
|
case "checkbox": {
|
|
467
461
|
const strings = checkboxStrings(rule)
|
|
468
|
-
return
|
|
462
|
+
return catalog.checkbox(strings.checked, strings.unchecked)
|
|
469
463
|
}
|
|
470
464
|
default:
|
|
471
465
|
// 残る規則種別は custom
|
|
472
|
-
return
|
|
466
|
+
return catalog.custom(rule.formula)
|
|
473
467
|
}
|
|
474
468
|
}
|
|
475
469
|
|
|
476
|
-
/**
|
|
477
|
-
|
|
470
|
+
/**
|
|
471
|
+
* Built-in summary of a text rule from a catalog's `text` templates (needle ops embed `value`; the
|
|
472
|
+
* shape checks are fixed sentences).
|
|
473
|
+
* カタログの text テンプレートによる text 規則の組込み要約 (needle 系は value を埋め込み、形式検査は固定文)。
|
|
474
|
+
*
|
|
475
|
+
* @param rule - The shape-checked text rule / 形検査済みの text 規則
|
|
476
|
+
* @param templates - The `text` templates of the verdict's locale / 判定ロケールの text テンプレート
|
|
477
|
+
* @returns The built-in message / 組込みメッセージ
|
|
478
|
+
*/
|
|
479
|
+
const textMessageOf = (rule: TextValidationRule, templates: ValidationMessageCatalog["text"]): string => {
|
|
478
480
|
switch (rule.op) {
|
|
479
|
-
case "contains":
|
|
480
|
-
return `Value must contain "${rule.value}".`
|
|
481
|
-
case "notContains":
|
|
482
|
-
return `Value must not contain "${rule.value}".`
|
|
483
|
-
case "startsWith":
|
|
484
|
-
return `Value must start with "${rule.value}".`
|
|
485
|
-
case "endsWith":
|
|
486
|
-
return `Value must end with "${rule.value}".`
|
|
487
481
|
case "isEmail":
|
|
488
|
-
return
|
|
482
|
+
return templates.isEmail
|
|
483
|
+
case "isUrl":
|
|
484
|
+
return templates.isUrl
|
|
489
485
|
default:
|
|
490
|
-
//
|
|
491
|
-
return
|
|
486
|
+
// needle 系は value 欠落なら不活性で拒否に至らないため、ここへ来るとき value は常にある
|
|
487
|
+
return templates[rule.op](rule.value as string)
|
|
492
488
|
}
|
|
493
489
|
}
|
|
494
490
|
|
|
@@ -513,20 +509,41 @@ export const listValuesOf = (rule: ListValidationRule): ReadonlySet<string> => {
|
|
|
513
509
|
}
|
|
514
510
|
|
|
515
511
|
/**
|
|
516
|
-
* Validates committed text against a rule.
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
512
|
+
* Validates committed text against a rule. The locale is checked FIRST (before the rule and the
|
|
513
|
+
* text), so an unsupported locale fails fast even for a passing text — this pure function has no
|
|
514
|
+
* hidden default locale. Blank always passes; `custom` is evaluated only when the caller passes
|
|
515
|
+
* `evaluate` (commit time — paint omits it); an incomplete rule (unparseable bound, missing `b` /
|
|
516
|
+
* `value`) is inert; a rule outside the vocabulary throws RangeError. Message = `rule.errorMessage`
|
|
517
|
+
* when set (the empty string included), else the built-in summary in the given locale.
|
|
518
|
+
* 確定テキストの規則検証。ロケールを最初に (規則と text より前に) 検査するため、非対応ロケールは
|
|
519
|
+
* 合格する text でも fail-fast (純関数に隠れた既定ロケールは無い)。空文字は常に合格、custom は呼び手が
|
|
520
|
+
* evaluate を渡すとき (確定時 — 描画は渡さない) だけ評価、不完全な規則 (解釈不能な境界・b / value 欠落) は
|
|
521
|
+
* 不活性、語彙外の規則は RangeError。メッセージは errorMessage があればそれ (空文字も含む)、無ければ
|
|
522
|
+
* 指定ロケールの組込み要約。
|
|
523
|
+
*
|
|
524
|
+
* @param rule - The cell's data-validation rule / セルのデータ検証規則
|
|
525
|
+
* @param text - The committed text / 確定テキスト
|
|
526
|
+
* @param locale - The UI chrome locale of the built-in message ("en" | "ja"; required) / 組込みメッセージの UI クロームロケール ("en" | "ja"、必須)
|
|
527
|
+
* @param evaluate - The consumer's formula evaluator for `custom` rules (omit to leave them unevaluated) / custom 規則用の consumer の数式評価器 (省略で未評価)
|
|
528
|
+
* @returns The frozen shared ok verdict, or a rejection carrying the resolved message / 共有の凍結 ok 判定、または解決済みメッセージ付きの拒否
|
|
529
|
+
* @throws {RangeError} When `locale` is not a supported SpreadsheetLocale (`undefined`, `null`,
|
|
530
|
+
* `"EN"`, `"ja-JP"`, ...), or the rule's `type` / `op` is outside the vocabulary / `locale` が
|
|
531
|
+
* 対応 SpreadsheetLocale でない場合 (`undefined`、`null`、`"EN"`、`"ja-JP"` など)、または規則の
|
|
532
|
+
* type / op が語彙外の場合
|
|
523
533
|
*/
|
|
524
|
-
export const validateCellText = (rule: DataValidationRule, text: string, evaluate?: ValidationEvaluate): ValidationVerdict => {
|
|
534
|
+
export const validateCellText = (rule: DataValidationRule, text: string, locale: SpreadsheetLocale, evaluate?: ValidationEvaluate): ValidationVerdict => {
|
|
535
|
+
// 型を外れた呼び手 (JS・as キャスト) の undefined を "en" へ黙って寄せると、言語の取り違えが表面化しない
|
|
536
|
+
if (typeof locale !== "string" || !Object.hasOwn(VALIDATION_MESSAGE_CATALOGS, locale)) {
|
|
537
|
+
const allowed = Object.keys(VALIDATION_MESSAGE_CATALOGS)
|
|
538
|
+
.map((candidate) => JSON.stringify(candidate))
|
|
539
|
+
.join(", ")
|
|
540
|
+
throw new RangeError(`[spreadsheet] validateCellText locale must be one of ${allowed}; got ${describeLabelValue(locale)}`)
|
|
541
|
+
}
|
|
525
542
|
if (accepts(rule, text, evaluate)) {
|
|
526
543
|
return VALID
|
|
527
544
|
}
|
|
528
545
|
// errorMessage は「置かれているか」だけを見る (空文字も consumer の意思として尊重)
|
|
529
|
-
return { ok: false, message: rule.errorMessage === undefined ?
|
|
546
|
+
return { ok: false, message: rule.errorMessage === undefined ? messageOf(rule, VALIDATION_MESSAGE_CATALOGS[locale]) : rule.errorMessage }
|
|
530
547
|
}
|
|
531
548
|
|
|
532
549
|
/** Whether a rule can be judged at paint time without a formula evaluator (every type but `custom`). / 数式評価器なしに描画時判定できる規則か (custom 以外の全型)。 */
|