@threestarsoftware/tssgrid 0.0.1

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/README.md ADDED
@@ -0,0 +1,1091 @@
1
+ # TssGrid
2
+
3
+ 軽量・ノービルド・**日本語IMEに強い**編集グリッド(vanilla JS / 依存なし / MIT)。
4
+
5
+ Excel ライクなセル入力・範囲選択・フィル(連番/2D)・コピペ(Excel相互)・Undo/Redo を、
6
+ バンドラ無し・`<script>` 一発で使える小さなコンポーネントとして提供する。
7
+
8
+ ## なぜ作ったか
9
+
10
+ 無料の Web グリッドは「日本語IMEが弱い」「商用は有料化」「中国製で採用しづらい」のどれかに当たりがち。
11
+ TssGrid は **IME堅牢 × ノービルド × MIT** を満たすことを目的にしている。
12
+
13
+ 主要グリッド(Handsontable / AG Grid / jspreadsheet CE / SpreadJS)との**機能比較と立ち位置**は → [`examples/compare.html`](https://tssgrid.threestarsoftware.co.jp/examples/compare.html)(他社が強い領域=TssGridの伸びしろも正直に併記)。
14
+
15
+ ## 導入事例(Showcase)— 使ってる方へ 📣
16
+
17
+ TssGrid を使っている会社・ブランドを [`examples/showcase.html`](https://tssgrid.threestarsoftware.co.jp/examples/showcase.html) に**無料・承認制**で掲載します(個人・小規模も歓迎)。掲載すると**サイトURLへリンク**(被リンク)し、相互に宣伝になります。
18
+ - **申請**: [導入事例の掲載申請(GitHub Issue)](https://github.com/threestarsoftware/TssGrid/issues/new?template=showcase.yml) から、会社名+ロゴ(PNG/JPG・200KB目安)を送るだけ。内容を確認のうえ承認・掲載します(スパム防止のため承認制)。
19
+ - **バッジ**: 採用いただいた方は `Powered by TssGrid` バッジを自社サイトに貼っていただけると嬉しいです(任意)。
20
+
21
+ > 仕組み: 申請(Issue) → メンテナが `showcase/adopters.js` に追記(=承認)→ ページに表示。サーバー不要・GitHub だけで完結。
22
+
23
+ ## 特長
24
+
25
+ - **IME堅牢**: アクティブセルに常時フォーカスした本物の `<input>` を重ねる方式。「あ」が「a」に化けない / 変換確定の Enter でセルが飛ばない。
26
+ - **ノービルド**: 素の HTML/JS。`file://` で直接開ける(外部Excelからの貼り付けのみ http 推奨。後述)。
27
+ - **依存なし / MIT**: 商用利用可。
28
+ - 範囲選択(ドラッグ / Shift+矢印 / Shift+クリック)、フィルハンドル(連番 / 2D矩形)、Excel相互コピペ、Undo/Redo。
29
+ - **Undo を上位で共有**: `HistoryManager` を外出しにし、複数グリッドへ同じインスタンスを注入すると **1本の Undo タイムライン**になる。
30
+
31
+ ## クイックスタート
32
+
33
+ ```html
34
+ <link rel="stylesheet" href="src/tssgrid.css">
35
+ <div id="grid"></div>
36
+ <script src="src/tssgrid.js"></script>
37
+ <script>
38
+ const grid = new TssGrid(document.getElementById('grid'), {
39
+ headers: ['名前', 'よみ', '数値'],
40
+ data: [['彩瀬', 'あやせ', '1'], ['', '', '2']],
41
+ });
42
+ // 値の取得
43
+ // grid.getData() -> string[][]
44
+ </script>
45
+ ```
46
+
47
+ **npm / バンドラ(Vite・Webpack・Next・Nuxt・Angular …)**:
48
+
49
+ ```bash
50
+ npm i @threestarsoftware/tssgrid
51
+ ```
52
+ ```js
53
+ import { TssGrid, HistoryManager } from '@threestarsoftware/tssgrid';
54
+ import '@threestarsoftware/tssgrid/tssgrid.css';
55
+ // プラグインは個別に: import '@threestarsoftware/tssgrid/plugins/tss-merge.js'
56
+ ```
57
+ **型定義 (`.d.ts`) 同梱**なので TypeScript ではそのまま補完が効きます(APIリファレンス → [tssgrid…/api](https://tssgrid.threestarsoftware.co.jp/api.html))。
58
+
59
+ **Node / CommonJS**:
60
+
61
+ ```js
62
+ const { TssGrid, HistoryManager } = require('@threestarsoftware/tssgrid');
63
+ ```
64
+
65
+ (UMD。`<script>` 読み込みでは `window.TssGrid` / `window.HistoryManager`。CDN は `https://cdn.jsdelivr.net/npm/@threestarsoftware/tssgrid`。)
66
+
67
+ ## フレームワークで使う(React / Vue / Angular)
68
+
69
+ 専用ラッパーは**作りません**。vanilla on DOM なので CRA/Vite/Next/Webpack/Angular/Nuxt でそのまま動きます。コツは **マウント時に生成し、アンマウント時に `grid.destroy()`** するだけ(リスナ・ResizeObserver・DOM を後始末。idempotent)。
70
+
71
+ **React**:
72
+ ```jsx
73
+ import { useEffect, useRef } from 'react';
74
+ import { TssGrid } from '@threestarsoftware/tssgrid';
75
+ import '@threestarsoftware/tssgrid/tssgrid.css';
76
+
77
+ function Grid({ columns, data }) {
78
+ const ref = useRef(null);
79
+ useEffect(() => {
80
+ const grid = new TssGrid(ref.current, { columns, data });
81
+ return () => grid.destroy(); // 後始末
82
+ }, []);
83
+ return <div ref={ref} />;
84
+ }
85
+ ```
86
+
87
+ **Vue 3**:
88
+ ```vue
89
+ <script setup>
90
+ import { onMounted, onBeforeUnmount, ref } from 'vue';
91
+ import { TssGrid } from '@threestarsoftware/tssgrid';
92
+ import '@threestarsoftware/tssgrid/tssgrid.css';
93
+ const el = ref(null); let grid;
94
+ onMounted(() => { grid = new TssGrid(el.value, { columns, data }); });
95
+ onBeforeUnmount(() => grid?.destroy());
96
+ </script>
97
+ <template><div ref="el"></div></template>
98
+ ```
99
+
100
+ **Angular**:
101
+ ```ts
102
+ import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
103
+ import { TssGrid } from '@threestarsoftware/tssgrid';
104
+
105
+ @Component({ selector: 'app-grid', template: '<div #host></div>' })
106
+ export class GridComponent implements AfterViewInit, OnDestroy {
107
+ @ViewChild('host') host!: ElementRef;
108
+ private grid!: TssGrid;
109
+ ngAfterViewInit() { this.grid = new TssGrid(this.host.nativeElement, { columns, data }); }
110
+ ngOnDestroy() { this.grid.destroy(); }
111
+ }
112
+ ```
113
+
114
+ ## 複数グリッドで Undo を共有
115
+
116
+ ```js
117
+ const history = new HistoryManager();
118
+ new TssGrid(elA, { name: '甲', history, headers, data });
119
+ new TssGrid(elB, { name: '乙', history, headers, data });
120
+ // 甲乙どちらで編集しても、Ctrl+Z は「やった順の逆」で戻り、該当グリッドが自動でフォーカスされる。
121
+ // history.undo() / history.redo() を上位の Undo ボタンに繋げてもよい。
122
+ ```
123
+
124
+ `history` を渡さなければグリッドが自前で `HistoryManager` を生成する(単体動作。コードは同じ)。
125
+
126
+ ## データの持ち方(配列 / オブジェクト配列)
127
+
128
+ `data` は **2次元配列**(`string[][]`)でも **オブジェクト配列**(`object[]`)でも渡せます(**自動判別**)。列にキーを紐付けると、列名でアクセスできます。
129
+
130
+ ```js
131
+ const grid = new TssGrid(el, {
132
+ columns: [
133
+ { data: '品番' }, // ← columns[c].data で列にキーを紐付け
134
+ { data: '品名' },
135
+ { data: '数量', type: 'number', thousands: true },
136
+ ],
137
+ data: [
138
+ { 品番: 'P-100', 品名: 'ボルト', 数量: '1200', id: 42 }, // ← オブジェクト配列で渡せる
139
+ { 品番: 'P-101', 品名: 'ナット', 数量: '1200', id: 43 },
140
+ ],
141
+ });
142
+
143
+ grid.getValue(0, '品番'); // → 'P-100'(列名 or 列index でアクセス)
144
+ grid.setValue(0, '数量', '5'); // → 列名で書き込み(Undo 可)
145
+ grid.getRow(0); // → {品番:'P-100', 品名:'ボルト', 数量:'1200', id:42}(1行を横断)
146
+ grid.getColumn('品番'); // → ['P-100','P-101',...](getRow の対。1列を縦断)
147
+ grid.getRows(); // → [{品番:'P-100', ...}, ...](全行をオブジェクト配列で)
148
+ grid.getColumns(); // → {品番:['P-100',...], 品名:[...], 数量:[...]}(列ごとの値配列)
149
+
150
+ grid.toCSV(); // → CSV文字列(RFC4180クォート / 既定は素の値・空行除外)
151
+ grid.downloadCSV('在庫.csv'); // → UTF-8 BOM付きで即ダウンロード(Excel日本語が文字化けしない)
152
+ // JSON出力は専用APIなし= JSON.stringify(grid.getRows()) で足ります(JSONは標準、CSVは非標準なので内蔵)
153
+ ```
154
+
155
+ - **CSV出力**: `toCSV(opts)` / `downloadCSV(filename, opts)`。`opts`: `headers`(既定true) / `formatted`(既定false=素の値で数値はそのまま, trueで表示形 `¥1,234`) / `skipEmpty`(既定true=空行除外) / `eol`(既定 `'\r\n'`)。`"`/カンマ/改行を含むセルは自動でクォート。**CSV入力(読込)は非内蔵**=Excelからの貼り付けで取り込めるため(パースが必要なら PapaParse 等を)。
156
+ - **Excel(xlsx)出力**: コア非搭載(zipバイナリで重いため)。`getRows()` + **ExcelJS**(MIT, CDN+SRI)でサンプル提供。数値を数値のまま・太字ヘッダ・`¥#,##0`書式・ヘッダ行固定まで付けられます → 動く例: [`examples/excel-export.html`](https://tssgrid.threestarsoftware.co.jp/examples/excel-export.html)。
157
+ - **Excel(xlsx)読込**: 同じく ExcelJS でサンプル提供。**データ(セルの値)のみ**取り込み(書式・数式は無視=**数式は結果値**、日付は ISO)。1行目を見出し、2行目以降を `setData` に → 動く例: [`examples/excel-import.html`](https://tssgrid.threestarsoftware.co.jp/examples/excel-import.html)(外部ファイル不要の「生成→読込」round-trip 付き)。CSV と違い ExcelJS がパースを担うので自前実装は不要。
158
+ - **CSV読込**: コア非搭載(簡単な取り込みは Excel からの貼り付けで足りるため)。**PapaParse**(MIT, CDN+SRI)でサンプル提供。素朴な `split(',')` では壊れる**クォート内カンマ・改行・`""` エスケープ**(RFC4180)を正しく解き、**文字コード指定**(Excel の **Shift_JIS** な `.csv` も読める)・**大容量のストリーミング/Worker**まで対応。1行目を見出し、2行目以降を `setData` に → 動く例: [`examples/csv-import.html`](https://tssgrid.threestarsoftware.co.jp/examples/csv-import.html)(クォート/改行入りの「生成→読込」round-trip + ファイル D&D・文字コード切替付き)。
159
+
160
+ - **保存値は常に文字列の2次元配列**を内部に持ち、描画・編集・コピペ・履歴はすべて従来どおり。オブジェクト配列は**入出力時の変換層**です(大きな作りは変えていません)。
161
+ - **`getData()`** は常に2次元配列を返す(後方互換)。**`getRows()`** はオブジェクト配列で返す。
162
+ - **画面に出さないフィールド**(上の `id` など)も `getRows()` で**保持**されます(元オブジェクトを内部に退避。編集・行挿入・削除をしても維持)。
163
+ - 見出し `headers` を省略すると、`columns[c].title`(あれば)→ `data` キー → `A,B,C…` の順で補完します。
164
+ - `getValue` / `setValue` の列指定は **数値index** でも **`data` キー文字列** でも可。`setData(rows)` も配列/オブジェクトを自動判別。
165
+ - 配列形式しか使わない場合は今までどおり。`getRows()` は `data` キーのある列だけを対象にします。
166
+ - **ネストパス対応**: `columns[c].data` は **`'name.last'` のようなドット区切り**も指定できます(Handsontable 互換)。
167
+
168
+ ```js
169
+ new TssGrid(el, {
170
+ columns: [{ data: 'age' }, { data: 'name.last' }, { data: 'name.first' }],
171
+ data: [{ name: { first: '佐藤', last: '一郎' }, age: 25 }, /* … */ ],
172
+ });
173
+ grid.getValue(0, 'name.last'); // → '一郎'
174
+ grid.getRows()[0]; // → { name: { first:'佐藤', last:'一郎' }, age:'25' }(入れ子で復元、非バインドの入れ子フィールドも保持)
175
+ ```
176
+
177
+ ## 整列 / セル単位の読み取り専用(実行時)
178
+
179
+ ```js
180
+ // 整列(range 省略時は現在の選択範囲)。'left'|'center'|'right'|'top'|'middle'|'bottom'|{h,v}|'reset'
181
+ grid.setAlignment('center');
182
+ grid.setAlignment('right', { r0: 0, c0: 2, r1: 5, c1: 2 });
183
+ grid.getAlignment(0, 2); // → { h:'right', v:null }
184
+ new TssGrid(el, { columns: [{ align: 'center', valign: 'middle' }] }); // 列既定
185
+
186
+ // セル単位の読み取り専用(実行時トグル。列の readOnly とは別に効く)
187
+ grid.setCellReadOnly(true); // 選択範囲を読取専用に
188
+ grid.setCellReadOnly(false, { r0: 1, c0: 1, r1: 1, c1: 1 }); // 解除
189
+ ```
190
+
191
+ - 整列はセル上書き>列既定(`columns[c].align`/`valign`)の順。数値列の既定右寄せも上書きできます。
192
+ - どちらも**列の挿入・削除でセルに自動追従**(Undo も整合)。`setData` で新データを入れるとクリアされます。
193
+ - `setAlignment`/`setCellReadOnly` 自体は Undo に積みません(書式・状態のため。`setColWidth` と同様)。
194
+
195
+ ## 並べ替え(ソート)
196
+
197
+ コアにソートUIは持たせていません(業務フォーム志向)。**並べ替えは数行で外から**組めます。
198
+
199
+ ```js
200
+ grid.sortBy('数量', 'asc'); // 列キー or index で並べ替え(数値っぽければ数値比較)
201
+ grid.sortBy(2, 'desc');
202
+ grid.sortRows((a, b) => a.qty - b.qty); // 任意比較(行はオブジェクト/配列)
203
+ ```
204
+ - `sortBy`/`sortRows` は**データ実体を並べ替え**ます。整列・セル readOnly・checkbox・隠しフィールド(`_src`)は**行ごと追従**します。**Undo には積みません**(書式系と同じ割り切り)。
205
+ - **未入力(空)行はソート対象外**=元の順序のまま**最下部に固定**(`minSpareRows` の追加用行が上に来ない)。
206
+ - `clearSort()` で**初期状態(入力順)に戻せます**(`isSorted()` で判定)。行参照ベースなので**編集してもズレず**、ソート後に追加した行は末尾へ、削除済みは無視。
207
+
208
+ **ヘッダクリックで並べ替え(▲▼インジケータ付き)も外部で**組めます(→ 動く例: [`examples/sort.html`](https://tssgrid.threestarsoftware.co.jp/examples/sort.html)):
209
+
210
+ ```js
211
+ let sortCol = -1, dir = 'asc';
212
+ const base = ['品番', '数量', '単価'];
213
+ new TssGrid(el, {
214
+ headers: base.slice(),
215
+ onHeaderClick(c) { // ヘッダクリックを横取り
216
+ dir = (sortCol === c && dir === 'asc') ? 'desc' : 'asc'; sortCol = c;
217
+ grid.headers = base.map((h, i) => i === c ? h + (dir === 'asc' ? ' ▲' : ' ▼') : h); // ▲▼ は文字を足すだけ
218
+ grid.sortBy(c, dir);
219
+ return false; // 既定の列選択は抑止
220
+ },
221
+ });
222
+ ```
223
+
224
+ ### 行のドラッグ移動(rowReorder)
225
+
226
+ `rowReorder: true` で**行ヘッダ左に「つまみ」⠿**が出て、ドラッグで行を並べ替えられます(ドロップ位置に緑のガイド線)。データ実体ごと動くので**値・整列・readOnly・隠しフィールドも追従**、行番号は自動で振り直し(→ 動く例: [`examples/row-reorder.html`](https://tssgrid.threestarsoftware.co.jp/examples/row-reorder.html))。**複数行**は行ヘッダで範囲選択(クリック→Shift+クリック)してから選択内のつまみを掴めば**まとめて移動**します。
227
+
228
+ ```js
229
+ new TssGrid(el, {
230
+ rowReorder: true, // 行ヘッダにつまみを表示
231
+ onAfterRowMove: (from, to, count) => { /* … */ },// 確定後(from=元の先頭, to=新しい先頭, count=行数)
232
+ });
233
+ grid.moveRow(from, to); // 1行(to は 0..ROWS の挿入境界)
234
+ grid.moveRows(r0, r1, to); // 連続する複数行をまとめて移動
235
+ ```
236
+ - 行移動は**Undo/Redo 対応**(`Ctrl+Z`/`Ctrl+Y`。意図的な編集なので戻せる。※`sortBy` の並べ替えは履歴非対象でこちらと別)。
237
+ - `minSpareRows` と併用すると追加用の空行が末尾以外に動くことがあるので、入力途中の表では `rowReorder` を切るか並べ替え確定後に使うのが無難。
238
+
239
+ ### 非破壊フィルタ(`filter` / `clearFilter`)
240
+
241
+ **コア内蔵**の非破壊フィルタ(行インデックスマッパ)。全行は内部の `_allRows`(マスタ)に保持したまま、**可視部分集合だけ描画**します。**10万行でも絞り込んだ数百行だけ描く**ので、仮想スクロール無しで軽い → 動く例: [`examples/filter-live.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-live.html)。
242
+
243
+ ```js
244
+ grid.filter(row => row[2] >= 100); // 述語: (row, masterIndex, src) => bool
245
+ grid.filter({ 部署: '営業' }); // 簡易形: {列キー or 見出し名: 値|関数}(AND)
246
+ grid.clearFilter(); // 解除=全行へ(フィルタ中の編集も残る=非破壊)
247
+ grid.isFiltered(); // 真偽
248
+ grid.getAllRows(); // マスタ全行(フィルタ中でも全件)
249
+ // onFilter: ({count, total, cleared}) => {...}
250
+ ```
251
+ - **非破壊**: フィルタ中に編集した値も、**行参照の共有**でマスタへ自動反映。`clearFilter()` で全件+編集が残る。`rowH`/整列/セル readOnly も非破壊に往復。
252
+ - **フィルタ中の行挿入/削除OK**(マスタへ追従+**Undo対応**/同一フィルタ内)。**ソートはビュー内**で有効。
253
+ - **選択/ナビ/結合/幾何はビューのまま無改変**=フィルタ未使用なら一切コスト無し。
254
+ - 制限(v1): フィルタ中は **列の挿入削除・行移動・セル結合**はロック(解除してから)。`filter`/`clearFilter` は**Undo履歴をクリア**(ビューindex基準の履歴が境界を跨ぐと不整合になるため)。
255
+ - **フィルタUI**(ヘッダの絞り込み等)はコアに入れず**プラグイン** `tss-filter`(下記)で。素朴な `setData` 差し替え方式の旧サンプルは [`examples/filter.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter.html)。
256
+
257
+ ### 同梱プラグイン: ヘッダ オートフィルタ(filter UI)
258
+
259
+ `plugins/tss-filter.js`+`.css`= 各見出しの**ロート(漏斗)アイコン**をクリックで**値チェックリスト+検索**のポップアップ(Excel風。ソートの▲▼と区別できるアイコン)。複数列の条件は **AND**。中身は上の**コア `filter()` を呼ぶだけ**。動く例: [`examples/filter-ui.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-ui.html)。
260
+
261
+ ```html
262
+ <link rel="stylesheet" href="plugins/tss-filter.css">
263
+ <script src="plugins/tss-filter.js"></script>
264
+ ```
265
+ ```js
266
+ plugins: [ TssFilter.plugin() ] // 全列にフィルタUI
267
+ plugins: [ TssFilter.plugin({ columns:['部署', 2] }) ] // 対象列を限定
268
+ grid.getPlugin('filter').clearAll(); // 全条件クリア
269
+ ```
270
+ - 値候補は**マスタ全行から集計**(絞り込み中でも全値が出る)。フィルタ済み列に印。値は HTML エスケープ。
271
+
272
+ ### 同梱プラグイン: 共有マスタ(複数グリッドで1データ)
273
+
274
+ `plugins/tss-shared.js`= **1つのマスタ(同じ行データ)を複数グリッドで共有**。左右を**別フィルタ/別ソート**で表示でき、**片方の編集がもう片方の同じ行へ即反映**(行参照を共有)。A系/B系を並べて突き合わせ編集する用途。動く例: [`examples/shared-master.html`](https://tssgrid.threestarsoftware.co.jp/examples/shared-master.html)。
275
+
276
+ ```html
277
+ <script src="plugins/tss-shared.js"></script>
278
+ ```
279
+ ```js
280
+ const shared = TssShared.create(masterRows, { headers:[...], columns:[...], rowHeaders:true });
281
+ const gridA = shared.attach(hostA, { plugins:[TssFilter.plugin()] });
282
+ const gridB = shared.attach(hostB, { plugins:[TssFilter.plugin()] });
283
+ gridA.filter({ 部署:'営業' }); gridB.filter({ 勤務地:'東京' }); // 各々別フィルタ
284
+ gridA.insertRow(0); gridA.deleteRows(2); // 行の挿入/削除も両ビュー&マスタへ伝播
285
+ shared.getMaster(); // マスタ全行(getRows 形式)。 shared.detach(gridA);
286
+ ```
287
+ - **値編集**は行参照共有で即反映。**行/列の挿入削除**も伝播(構造変更後は他ビューを master から再構成=フィルタは再適用・そのビューのソートはリセット)。
288
+ - **配列データ/オブジェクト配列データ どちらも対応**(オブジェクト時は非表示フィールドも保持)。`minSpareRows:0` 推奨。
289
+ - 連携に [同期スクロール](#同梱プラグイン-同期スクロール左右2表を並べて比較) を足せば「並べて比較+同時スクロール+同時編集」に。
290
+
291
+ ## ショートカット / プラグイン / 破棄
292
+
293
+ **カスタムショートカット**(軽量 shortcut-manager 相当。`context` は `'grid'`=ナビ / `'editor'`=編集中 / `'all'`):
294
+
295
+ ```js
296
+ new TssGrid(el, {
297
+ shortcuts: [
298
+ { keys: 'Ctrl+D', name: 'dup', context: 'grid', handler: (e, { grid }) => grid.insertRow(grid.active.r, 'below') },
299
+ { keys: ['Ctrl+S', 'Ctrl+Shift+S'], handler: () => save() }, // 複数キー可
300
+ ],
301
+ });
302
+ grid.addShortcut({ keys: 'Ctrl+K', handler: () => {} }); // 実行時に追加
303
+ grid.removeShortcut('dup');
304
+ ```
305
+ - マッチすると既定動作を抑止します(`handler` が `true` を返すと既定も続行)。`cmd`/`meta` は `Ctrl` 扱い。
306
+ - 全キーをまとめて捌きたいなら **`onBeforeKeyDown(e)`**(`false` / `preventDefault()` でグリッド既定を止める)。
307
+
308
+ **プラグイン**(軽量 base-plugin 相当。`init(grid)→{destroy?}` の薄い契約):
309
+
310
+ ```js
311
+ const autoSavePlugin = (grid) => {
312
+ const prev = grid.onAfterChange;
313
+ grid.onAfterChange = (ch, src) => { prev && prev(ch, src); localStorage.setItem('grid', JSON.stringify(grid.getData())); };
314
+ return { name: 'autosave', destroy() { grid.onAfterChange = prev; } };
315
+ };
316
+ new TssGrid(el, { plugins: [autoSavePlugin] });
317
+ grid.usePlugin(autoSavePlugin); // 実行時に追加
318
+ TssGrid.registerPlugin('autosave', autoSavePlugin); // 名前登録 → plugins:['autosave'] で使える
319
+ ```
320
+
321
+ **破棄**: SPA でグリッドを出し入れするときは **`grid.destroy()`** で document/window リスナ・ResizeObserver・プラグインを全解除し、DOM も除去します(**リーク防止**)。
322
+
323
+ ## カスタムエディタ
324
+
325
+ 組込エディタ(text/dropdown/checkbox/date/time/number)以外に、**独自の編集UI**を列に差せます。`columns[c].editor` に**薄いオブジェクト契約**を渡すだけ。
326
+
327
+ ```js
328
+ const myEditor = {
329
+ // 編集開始時に呼ばれる。UI を出して、確定で commit(値) / 取消で cancel() を呼ぶ。
330
+ open({ grid, r, c, value, td, commit, cancel }) {
331
+ const inp = document.createElement('input');
332
+ inp.value = value;
333
+ const box = td.getBoundingClientRect();
334
+ Object.assign(inp.style, { position: 'fixed', left: box.left + 'px', top: box.top + 'px', width: box.width + 'px' });
335
+ document.body.appendChild(inp); inp.focus();
336
+ inp.onkeydown = e => { if (e.key === 'Enter') commit(inp.value); if (e.key === 'Escape') cancel(); };
337
+ this._inp = inp;
338
+ },
339
+ close() { this._inp.remove(); }, // 任意。後片付け
340
+ };
341
+
342
+ new TssGrid(el, { columns: [{ editor: myEditor }] });
343
+ // editor は () => ({...}) のファクトリでも可(セルごとに生成したい時)
344
+ ```
345
+
346
+ - `open` が受け取る `{ grid, r, c, value, td, commit, cancel }` だけ知っていれば作れます。F2 / ダブルクリック / 文字入力で開きます。
347
+ - **`commit(value)` は通常の確定経路(pushCmd)を通る**ので、`validator`/`parse`/`format`/読み取り専用がそのまま効き、**Undo にも積まれます**。
348
+ - 編集中に別セルをクリックすると `cancel()` 相当で閉じます。描画(セルの見た目)は従来どおり `format` 等で制御。
349
+
350
+ ### 同梱プラグイン: 休日カレンダー対応の日付ピッカー
351
+
352
+ `plugins/tss-calendar.js`= ネイティブ `<input type=date>` では出来ない**会社の休日カレンダー(JSON)対応**の日付ピッカー。勤怠・シフト・予約などで効きます(動く例: [`examples/calendar.html`](https://tssgrid.threestarsoftware.co.jp/examples/calendar.html))。
353
+
354
+ ```html
355
+ <link rel="stylesheet" href="plugins/tss-calendar.css">
356
+ <script src="plugins/tss-calendar.js"></script>
357
+ ```
358
+ ```js
359
+ const cal = TssCalendar({
360
+ holidays: { '2026-01-01': '元日', '2026-08-13': { name: '夏季休業', type: 'company' } }, // 日付→名称
361
+ weekend: [0, 6], // 既定。土日を色分け
362
+ // min, max, disable:['holiday','weekend'](選択不可), weekLabels, monthLabel …
363
+ });
364
+ new TssGrid(el, { columns: [{ type: 'date', editor: cal, format: 'yyyy/mm/dd' }] });
365
+ ```
366
+ - セル右端に **📅** が出て**シングルクリックで開閉**(再クリックで閉じる=トグル)、開いている間も**フィルハンドルで下へコピー可**(ドロップダウンと同じ操作感)。**年・月はプルダウン**で直接ジャンプ。祝日は赤+**名称ツールチップ**、土日は色分け、キーボード操作(矢印/Enter/Esc/PageUp・Down)。保存値は ISO のまま。
367
+ - 見た目は CSS 変数(`--cal-accent` など)と `tss-cal-*` クラスで自由に(依存ゼロ)。グリッド外でも `cal.openAt(anchorEl, isoValue, onPick)` で単体利用可。
368
+ - `opts`: `icon`(既定 `'📅'`, `''` で消す) / `openOnClick`(既定 true) / `yearMin`・`yearMax`(年プルダウンの範囲) も。
369
+ - 仕組み: カスタムエディタが **`icon` / `openOnClick` を申告**すると、グリッドが右端アイコン表示+シングルクリック起動を行います(自作エディタでも同様に使えます)。
370
+
371
+ ### 同梱プラグイン: 時刻ピッカー(時/分プルダウン)
372
+
373
+ `plugins/tss-time.js`= 業務の時刻入力向けの**時/分プルダウン**ピッカー(クロックフェイスのようなリッチさは省き、任意時刻を素早く)。保存値は常に **24h `'HH:MM'`**(組込 `type:'time'` と互換)。動く例: [`examples/time.html`](https://tssgrid.threestarsoftware.co.jp/examples/time.html)。
374
+
375
+ ```html
376
+ <link rel="stylesheet" href="plugins/tss-time.css">
377
+ <script src="plugins/tss-time.js"></script>
378
+ ```
379
+ ```js
380
+ const t = TssTime({ step: 15, hour12: false }); // 15分刻み / 24h
381
+ new TssGrid(el, { columns: [{ type: 'time', editor: t }] });
382
+ ```
383
+ - `opts`: `step`(分刻み, 既定1) / `hour12`(AM/PM 表示, 既定false) / `icon`(既定 `'🕐'`) / `openOnClick`(既定true) / `className`。
384
+ - 🕐 を**シングルクリックで開閉**、**現在**/**決定**ボタン、キーボード(Enter 決定 / Esc 取消)、外側クリックで採用、フィルで下へコピー。見た目は `--time-*` 変数と `tss-time-*` クラスで自由に。
385
+
386
+ ### 同梱プラグイン: 派生列(formula / computed column)
387
+
388
+ `plugins/tss-formula.js`=「その行の値だけから計算する列」を `columns[c].formula` 関数で書けるようにする。金額=数量×単価、税込=金額×1.1 のような**派生列**を宣言的に。動く例: [`examples/formula.html`](https://tssgrid.threestarsoftware.co.jp/examples/formula.html)。
389
+
390
+ ```html
391
+ <script src="plugins/tss-formula.js"></script>
392
+ ```
393
+ ```js
394
+ new TssGrid(el, {
395
+ columns: [
396
+ { data: 'quantity', type: 'number' },
397
+ { data: 'price', type: 'number' },
398
+ { data: 'amount', type: 'number', formula: (row) => row.quantity * row.price }, // ← 派生列
399
+ { data: 'withTax', type: 'number', formula: (row) => Math.round(row.amount * 1.1) }, // ← 前の formula 結果も使える
400
+ ],
401
+ plugins: [TssFormula], // これだけ。grid.usePlugin(TssFormula) / plugins:['formula'] でも可
402
+ });
403
+ ```
404
+ - `formula` は**行スコープの純粋関数** `(row, {r,c,grid}) => value`(`row`=その行の `{dataキー:値}`)。文字列ではなく**関数**で渡す(`eval` 不要・CSP安全)。
405
+ - formula 列は**自動 readOnly** / **同じ行が変わると自動再計算**(編集・フィル・貼付・Undo/Redo すべて追従)/ 書き込みは履歴に積まない(派生値が独立 Undo 段にならない)/ **columns 定義順に評価**=後ろの formula は前の結果を使える。
406
+ - 値は内部文字列なので算術は JS が数値強制(`'120' * '18' = 2160`)。`+` の連結を避けたいときは `Number(row.x)` を。
407
+ - **列方向の集計(縦の SUM・合計行)は formula の範囲外**=下の `TssTotals` で。
408
+
409
+ ### 同梱プラグイン: 列の集計(totals / 縦の SUM・平均…)
410
+
411
+ `plugins/tss-totals.js`= 列方向の集計(`sum`/`avg`/`count`/`min`/`max`/独自関数)を担う。**計算だけで描画はしない**=結果を `onTotals` で受けて好きな場所へ(フッタ・ラベル・**別の TssGrid の合計表**でも)。`formula` と併用すると派生列の後に集計が連動。動く例: [`examples/formula.html`](https://tssgrid.threestarsoftware.co.jp/examples/formula.html)(明細=formula/合計=totals)。
412
+
413
+ ```html
414
+ <script src="plugins/tss-totals.js"></script>
415
+ ```
416
+ ```js
417
+ new TssGrid(el, {
418
+ columns: [ /* …amount 等… */ ],
419
+ plugins: [ TssFormula, TssTotals({
420
+ columns: { quantity: 'sum', amount: 'sum', price: 'avg' }, // dataキー → 集計種別
421
+ skipEmpty: true, // 全列空の行を除外(既定true)
422
+ onTotals: (t) => { footerEl.textContent = '合計 ' + t.amount; }, // 再計算のたびに呼ばれる
423
+ }) ],
424
+ });
425
+ // 取得は handle 経由でも: grid.getPlugin('totals').getTotals() // → { quantity:…, amount:… }
426
+ ```
427
+ - **コアに合計行を埋めない**理由=置き場が `minSpareRows`/ソート/空行処理と衝突して肥大化するため。集計を外出しして「どこに出すかは自由」にした。
428
+ - formula と併用時は **`plugins: [TssFormula, TssTotals(...)]` の順**(合計を派生列の結果の後に計算するため)。`sum`/`avg`/`min`/`max` は数値セルのみ・`count` は非空セル数。
429
+
430
+ ### 同梱プラグイン: 残高/累計(running total・縦の連鎖)
431
+
432
+ `plugins/tss-running.js`=「**前の行の結果 + この行の増減**」で縦に積む列。`formula`(行内)でも `totals`(全体合計)でも出せない**縦方向の連鎖**を担う。出納帳/通帳の**残高**、在庫受払の**在庫数**、経費・ポイントの**累計**など。動く例: [`examples/running.html`](https://tssgrid.threestarsoftware.co.jp/examples/running.html)(現金出納帳=入金・出金→残高)。
433
+
434
+ ```html
435
+ <script src="plugins/tss-running.js"></script>
436
+ ```
437
+ ```js
438
+ new TssGrid(el, {
439
+ columns: [ /* …deposit, withdraw, balance… */ ],
440
+ plugins: [ TssRunningTotal({
441
+ column: 'balance', initial: 10000, // 残高列(自動readOnly)/前残
442
+ step: (prev, row) => prev + Number(row.deposit||0) - Number(row.withdraw||0),
443
+ }) ],
444
+ // 単純累計なら step の代わりに delta: 'amount'(running += Number(row.amount))
445
+ });
446
+ ```
447
+ - 編集・行の挿入/削除・`setData`(+それらの Undo/Redo)で**上から全部引き直し**。書き込みは履歴に積まない。
448
+ - `column`(残高列) / `initial`(前残) / `delta` か `step` / `skipEmpty`(全列空の行は連鎖を進めず空表示、既定true)。
449
+ - **行ドラッグ移動(rowReorder)併用時**は順序が変わるので `onAfterRowMove` で `handle.recompute()` を呼ぶ。
450
+
451
+ > 「**行=formula/列=totals/縦=running**」の3点が揃う。いずれも `setValueRaw`+`onAfterChange` ラップで動く**プラグイン**。
452
+
453
+ ### 同梱プラグイン: 氏名→フリガナ 自動入力(furigana・辞書不要)
454
+
455
+ `plugins/tss-furigana.js`(**辞書不要**)= 漢字を入力(IME変換)すると**読みが別列に自動で入る**(スマホ連絡先の よみがな 自動入力と同じ)。辞書引きではなく、**IME変換の composition イベントから「変換前のかな」を拾う**方式。TssGrid は**アクティブセルに本物の `<input>` を重ねる**設計なので composition が確実に取れる=「**IMEに強い**」を機能で体現。動く例: [`examples/furigana.html`](https://tssgrid.threestarsoftware.co.jp/examples/furigana.html)。
456
+
457
+ ```html
458
+ <script src="plugins/tss-furigana.js"></script>
459
+ ```
460
+ ```js
461
+ new TssGrid(el, {
462
+ columns: [ { data: 'name', title: '氏名' }, { data: 'kana', title: 'フリガナ' }, /* … */ ],
463
+ plugins: [ TssFurigana({ source: 'name', target: 'kana', katakana: true }) ],
464
+ // 複数ペアは pairs: [{source,target}, …] / ひらがな出力は katakana:false
465
+ });
466
+ ```
467
+ - `source`(漢字を入れる列) → `target`(読みを入れる列)。`target` は **readOnly にしない**=**手修正できる**。
468
+ - **正直な限界**: 手入力(IME変換)した時だけ取れる。**貼り付け・既存の漢字からは取れない**(辞書を持たないため)。「自動入力+手直し」が現実解。
469
+
470
+ ### 同梱プラグイン: セル内ミニ計算式(calc・=12*3)
471
+
472
+ `plugins/tss-calc.js`(**eval/Function 不使用**)= セルに `=12*3+5` と打つと確定時に **41** に。四則 `+ - * /`・括弧・小数対応の**自前再帰下降パーサ**(CSP安全)。`columns[c].parse`(入力変換)seam に乗るだけ。動く例: [`examples/calc.html`](https://tssgrid.threestarsoftware.co.jp/examples/calc.html)。
473
+
474
+ ```html
475
+ <script src="plugins/tss-calc.js"></script>
476
+ ```
477
+ ```js
478
+ // (a) 列の parse に直接(一番素直)
479
+ columns: [ { data: 'amount', type: 'number', parse: TssCalc() } ]
480
+ // (b) プラグインで対象列にまとめて注入(既存 parse があれば合成)
481
+ plugins: [ TssCalc.plugin({ columns: ['quantity', 'price'] }) ] // columns 省略で number 列すべて
482
+ ```
483
+ - `=` 始まりは強制評価、`=` 無しの素の式(`120*1.1`)も計算(`bare:false` で無効化)。**ただの数値・文字は素通り**。構文エラー/0除算は入力を残す(validator が弾く)。
484
+ - `prefix`(既定 `'='`) / `bare`(既定 true) / `decimals`(結果の小数桁)。`formula` と併用すると「式で入力→派生列が再計算」も連動。
485
+
486
+ ### 同梱プラグイン: オートコンプリート(autocomplete エディタ)
487
+
488
+ `plugins/tss-autocomplete.js`= 自由入力+**候補の絞り込み**。固定 dropdown の上位版で「だいたい決まってるが新規もある」項目(取引先・商品名)向け。**IME変換中の Enter は奪わない**(変換確定が最優先)。動く例: [`examples/autocomplete.html`](https://tssgrid.threestarsoftware.co.jp/examples/autocomplete.html)。
489
+
490
+ ```html
491
+ <link rel="stylesheet" href="plugins/tss-autocomplete.css">
492
+ <script src="plugins/tss-autocomplete.js"></script>
493
+ ```
494
+ ```js
495
+ columns: [
496
+ { data: 'client', editor: TssAutocomplete({ source: ['三星商事','三星製作所', …] }) }, // 自由入力可
497
+ { data: 'pay', editor: TssAutocomplete({ source: ['現金','振込', …], strict: true }) }, // 候補のみ
498
+ ];
499
+ // 動的候補: source: (query, {row,r,c,grid}) => string[]
500
+ ```
501
+ - ↑↓で候補移動・Enter確定・Esc取消・クリック選択。`source`(配列 or 関数) / `strict`(候補のみ) / `minChars` / `max` / `match`(`includes`/`startsWith`/関数) / `openOnClick`。
502
+ - カスタムエディタ契約(`editor.open/close`)に乗るだけ。見た目は `tss-ac-*` クラスと `--ac-*` 変数で。
503
+
504
+ ### 同梱プラグイン: セル内スパークライン(sparkline・ミニグラフ)
505
+
506
+ `plugins/tss-sparkline.js`(**SVG自前描画**)= セルの数列を**ミニ折れ線/棒**で描く。`columns[c].html:true` + `format`(format が返す SVG がセルに入る)seam に乗るだけ。Chart.js 等は積まない。動く例: [`examples/sparkline.html`](https://tssgrid.threestarsoftware.co.jp/examples/sparkline.html)(月次売上→動向グラフ・編集でライブ更新)。
507
+
508
+ ```html
509
+ <script src="plugins/tss-sparkline.js"></script>
510
+ ```
511
+ ```js
512
+ // 値 "12,15,9,20,18"(カンマ/空白区切り)→ ミニ折れ線
513
+ { data:'trend', title:'動向', html:true, readOnly:true, format: TssSparkline({ width:96, height:24, fill:'rgba(13,148,136,.12)' }) }
514
+
515
+ // formula で月次列から数列を組み立てると、月を編集→グラフがライブ更新
516
+ { data:'trend', html:true, readOnly:true, format: TssSparkline(),
517
+ formula: (row) => ['m4','m5','m6','m7','m8','m9'].map(k=>row[k]).filter(v=>v!=='').join(',') }
518
+ ```
519
+ - `type`('line'/'bar') / `width` / `height` / `color` / `strokeWidth` / `fill`(折れ線下の塗り) / `min`・`max`(既定は自動)。
520
+ - 数値しか SVG に入れない=安全。`formula`/`html-cells` と同じ仕組みで、**有料グリッドの "セル内グラフ" を無料で**。
521
+
522
+ ### 同梱プラグイン: セル内 横棒グラフ(bar・データバー)
523
+
524
+ `plugins/tss-bar.js`= セルの数値を**横棒**で描く(認知率・進捗%・スコア)。`html:true`+`format` seam。`type:'number'` 併用で**バーのまま編集**(値を打つと伸びる)。動く例: [`examples/bar.html`](https://tssgrid.threestarsoftware.co.jp/examples/bar.html)。
525
+
526
+ ```html
527
+ <script src="plugins/tss-bar.js"></script>
528
+ ```
529
+ ```js
530
+ // 表示専用
531
+ { data:'rate', title:'認知率', html:true, readOnly:true, format: TssBar({ max:100, suffix:'%', decimals:1 }) }
532
+ // バーのまま編集(値を打つと伸びる)+ 閾値で色分け
533
+ { data:'rate', type:'number', html:true, format: TssBar({ max:100, suffix:'%', color: v => v < 40 ? '#dc2626' : '#0d9488' }) }
534
+ ```
535
+ - `min`/`max`(既定0/100) / `color`(文字列 or `(value)=>色`=閾値色分け) / `track` / `height` / `radius` / `showValue` / `decimals` / `suffix` / `labelWidth`。
536
+
537
+ ### 同梱プラグイン: ヒートマップ / 背景データバー(cellStyle)
538
+
539
+ `plugins/tss-heatmap.js`= コアの `cellStyle` フックに乗せる**連続色(ヒートマップ)**と**背景データバー**。`tss-bar.js`(前景の棒+ラベル)と違い、**セル背景**を塗る=**数値はそのまま見せて編集できる**。動く例: [`examples/heatmap.html`](https://tssgrid.threestarsoftware.co.jp/examples/heatmap.html)。
540
+
541
+ ```html
542
+ <script src="plugins/tss-heatmap.js"></script>
543
+ ```
544
+ ```js
545
+ // ヒートマップ: 低→高で背景連続色。文字色は自動コントラスト。
546
+ { data:'sales', type:'number', cellStyle: TssHeatmap({ min:100000, max:400000, colors:['#f8b4b4','#fff','#93c5fd'] }) }
547
+ // 背景データバー: 数値はそのまま・背景に棒(編集可)
548
+ { data:'rate', type:'number', cellStyle: TssDataBar({ min:0, max:100, color:'rgba(13,148,136,.22)' }) }
549
+ ```
550
+ - `TssHeatmap`: `min`/`max`(明示。セル単位呼び出しで列範囲を知らないため)/ `colors`(2色以上のストップ) or `low`/`high` / `text`(自動文字色, 既定on)。
551
+ - `TssDataBar`: `min`/`max` / `color` / `align`('left'/'right')。編集で即更新。
552
+
553
+ > 「セル内グラフ」3点: **前景の折れ線/棒=`html:true`+`format`(tss-sparkline / tss-bar)**、**背景の連続色/バー=`cellStyle`(tss-heatmap)**。どれもノービルド。
554
+
555
+ ### グラフ連携(Chart.js などと)— 編集でライブ更新
556
+
557
+ セル内グラフより大きく見せたいときは、**外部チャートライブラリ(MIT)と連携**。データの持ち主はグリッド、グラフはその"ビュー"です。連携は `onAfterChange` で `grid.getRows()` → `chart.data` → `chart.update()` するだけ。コアには積まず **CDN+SRI のサンプル**として提供(excel-export と同じ位置づけ)。動く例: [`examples/chart-chartjs.html`](https://tssgrid.threestarsoftware.co.jp/examples/chart-chartjs.html)(Chart.js・MIT・編集で折れ線/棒がリアルタイム更新)。
558
+
559
+ ```js
560
+ const chart = new Chart(ctx, { type:'line', data: toChartData(grid) });
561
+ new TssGrid(el, { columns, data, onAfterChange: () => { chart.data = toChartData(grid); chart.update(); } });
562
+ ```
563
+ - **おすすめ(いずれも `<script>`/CDN で no-build)**: **Chart.js**(MIT・定番・バランス◎)/ **ApexCharts**(MIT・モダンで映え)/ **Apache ECharts**(Apache-2.0・多機能)/ **uPlot**(MIT・超軽量)。Highcharts/AG Charts は商用有料なので MIT 方針なら避ける。
564
+
565
+ ### 同梱プラグイン: 2択トグル(toggle・有効|無効)
566
+
567
+ `plugins/tss-toggle.js`+`.css`= ラジオの2択を**並んだセグメント(有効|無効)**で表示、クリックで切替。チェックボックスより**2ラベルが見える**。値は dropdown 同様 **value を保存**。動く例: [`examples/toggle.html`](https://tssgrid.threestarsoftware.co.jp/examples/toggle.html)。
568
+
569
+ ```html
570
+ <link rel="stylesheet" href="plugins/tss-toggle.css">
571
+ <script src="plugins/tss-toggle.js"></script>
572
+ ```
573
+ ```js
574
+ plugins: [ TssToggle.plugin({ columns: {
575
+ status: ['有効', '無効'], // 文字列2つ(value=label)
576
+ pub: [{ value:'1', label:'公開' }, { value:'0', label:'非公開' }], // value/label 分離
577
+ } }) ]
578
+ ```
579
+ - クリックでその側に確定(`setValue`=**検証・Undo を通る**)。アクティブセルで **Space** で反対側へトグル。2値以外の打ち込みは自動 validator で拒否。
580
+ - 単純な ON/OFF 1個なら組込 `type:'checkbox'`、3択以上は `type:'dropdown'`。見た目は `--tg-toggle-accent` 等で。
581
+
582
+ ### 同梱プラグイン: セル結合(merge・横/縦/矩形)
583
+
584
+ `plugins/tss-merge.js`= データセルの**矩形結合**(`colspan`×`rowspan`)。左上の**アンカーが値・編集・選択の対象**で、覆われた従属セルは**描画スキップ+読み取り専用**(貼り付け・フィルは弾く)。動く例: [`examples/merge.html`](https://tssgrid.threestarsoftware.co.jp/examples/merge.html)。(見出しの結合は `nestedHeaders` を参照)
585
+
586
+ ```html
587
+ <script src="plugins/tss-merge.js"></script>
588
+ ```
589
+ ```js
590
+ plugins: [ TssMerge.plugin({ merges: [
591
+ { r:0, c:0, colspan:1, rowspan:2 }, // 縦結合(2行)
592
+ { r:0, c:2, colspan:2 }, // 横結合(rowspan 省略=1)
593
+ ] }) ]
594
+ ```
595
+ | API | 説明 |
596
+ |---|---|
597
+ | `grid.setMerge(r, c, colspan, rowspan?)` | (r,c) を起点に結合。`rowspan` 省略=横結合。従属セルの値はクリア(Excel流)。成功で `true`、範囲外/1×1/既存結合と重なれば `false` |
598
+ | `grid.mergeSelection()` | 選択中の矩形を結合 |
599
+ | `grid.removeMerge(r, c)` | (r,c) を含む結合を解除 |
600
+ | `grid.getMerges()` | `[{r,c,colspan,rowspan}, …]` |
601
+ | `grid.destroyMerged()` | 全結合を解除 |
602
+
603
+ - **Undo 対応**: `setMerge`/`removeMerge`/`mergeSelection`/`destroyMerged` は履歴に積み、結合時にクリアした値も復元(初期 `merges` は設定扱いで非対象)。
604
+ - **行/列の挿入削除**で結合座標が縦横対称に追従(拡張・縮小・解除、Undo 対応)。
605
+ - **割り切り**: ソートは結合中ロック。行移動は**横結合のみ可**(行と一緒に移動)、**縦結合がある間はロック**(行順を崩すと縦結合が分断=auto-split を避ける)。
606
+ - 既知の限界: frozen 境界をまたぐ結合は想定外。結合範囲のコピーは従属位置が空セル。
607
+
608
+ ### 同梱プラグイン: 同期スクロール(syncscroll・左右2表を並べて比較)
609
+
610
+ `plugins/tss-syncscroll.js`= 並べた複数グリッドのスクロールを連動(Excel「並べて比較+同時にスクロール」相当)。A系/B系で別々に絞り込んだ表を**突き合わせながら編集**する用途に。動く例: [`examples/sync-scroll.html`](https://tssgrid.threestarsoftware.co.jp/examples/sync-scroll.html)。
611
+
612
+ ```html
613
+ <script src="plugins/tss-syncscroll.js"></script>
614
+ ```
615
+ ```js
616
+ const link = TssSyncScroll.link([gridA, gridB]); // 既定: 相対・縦横ピクセル
617
+ const link = TssSyncScroll.link([gA, gB], { by: 'row' }); // 行で揃える(行高が違っても対応行が並ぶ)
618
+ const link = TssSyncScroll.link([gA, gB], { relative: false }); // 絶対(スナップして一致)
619
+ link.unlink(); // 連動解除
620
+ ```
621
+ - **`relative: true`(既定・Excel流)**= ON した時点の**位置差を保ったまま移動量(delta)だけ連動**。連動OFF→片方スクロール→ON でも“その場から”続けて同期できる。`false` で絶対スナップ。
622
+ - **`by`**: `'pixel'`(既定・スクロール量) / `'row'`(コアの実測行高キャッシュ `_rowTops` で行を揃える=**左右で行高が違っても**対応行が並ぶ)。
623
+ - **`axis`**: `'both'`(既定) / `'vertical'` / `'horizontal'`。`link()` は `{ unlink, rebase, grids }` を返す(`rebase()`=相対モードで“今の差”を基準化)。
624
+
625
+ ## 実例: 在庫管理ミニアプリ(機能てんこ盛り)
626
+
627
+ TssGrid 1本(+カレンダープラグイン)で組んだ**業務画面のショーケース** → 動く例: [`examples/inventory.html`](https://tssgrid.threestarsoftware.co.jp/examples/inventory.html)。項目を差し替えれば**勤怠・発注・工程**などにも転用できます。
628
+
629
+ 入っている機能: ドロップダウン / 数値書式 / **計算列**(金額=数量×単価, 読み取り専用)/ チェックボックス+ヘッダ全選択 / 休日カレンダー / 入力検証 / ヘッダクリックで並べ替え / 右クリックで行挿入削除・全チェック / 末尾に常に空行 / Excel相互コピペ・Undo / `getRows()` で保存 / **CSV出力**(BOM付)。
630
+
631
+ **計算列**は、読み取り専用セルへ `setValue(r, c, val, /*force*/ true)` で書き込みます(プログラムからの確定的な書き込みは readOnly を貫通=ユーザー編集だけ保護)。
632
+
633
+ ```js
634
+ onAfterChange: (changes) => {
635
+ for (const ch of changes) if (ch.c === 数量 || ch.c === 単価) {
636
+ const amt = String((+grid.getValue(ch.r, 数量) || 0) * (+grid.getValue(ch.r, 単価) || 0));
637
+ grid.setValueRaw(ch.r, 金額, amt); // ★ 計算列は setValueRaw(履歴に積まない派生値・readOnly貫通)
638
+ }
639
+ }
640
+ ```
641
+ > **計算列は `setValueRaw` で書く**のが正解です。`setValueRaw(r,c,val)` は**履歴に積まず・検証も通さず**値を直接セット(readOnly も貫通、表示書式は描画時に適用)。これにより計算結果は**独立した undo ステップにならず**、`undo`/`redo` では入力セルが履歴復元→`onAfterChange` で**自動的に再計算されて追従**します(入力1つ=undo1回・計算列は勝手に付いてくる)。
642
+ > ※ 逆に `setValue(...,true)`(履歴あり)で計算列を書くと、計算結果が別の undo ステップになり、undo/redo 中の再計算が redo スタックを壊します。派生値は必ず `setValueRaw`。
643
+
644
+ ## 実例: 方眼ガント(予定/実績・遅延管理)
645
+
646
+ 専用ライブラリを積まず、**コアだけ**で組んだ**方眼ガント+遅延管理**ツール → 動く例: [`examples/gantt.html`](https://tssgrid.threestarsoftware.co.jp/examples/gantt.html)。依存ゼロ。
647
+
648
+ 入っている表現: **予定/実績の2段バー**(実績が予定より延びると**遅延を赤**で)/ **状態列**(完了・進行中・遅延・未着手)/ **期間&「今日」線** / **進捗データバー**(`cellStyle` だけで描画)/ **曜日+土日色**(`nestedHeaders` のリーフに改行を入れ `buildTable` ラップで色分け)。日付は手入力可(`format`+`parse`+`invalidMode:'keep'` で不正日を赤検証)。
649
+
650
+ > 「ガントの“升目”はグリッドそのもの」という割り切りで、専用コンポーネントなしに**スケジュール表をそのまま画面化**できます。バーをドラッグして日付を動かす版は将来候補(コアは無改変のまま外側で組める)。
651
+
652
+ ## テーマ / 見た目のカスタマイズ
653
+
654
+ 主要な色・サイズは **CSS 変数**にしてあり、業務側で**数個上書きするだけ**で統一感を出せます(フルのテーマエンジンは持たない軽量方針)。
655
+
656
+ ```css
657
+ .tssgrid { --tg-accent: #c0392b; --tg-header-bg: #f3eaea; --tg-font-size: 14px; } /* 全グリッド */
658
+ .myapp .tssgrid { --tg-edit-accent: #7b61ff; } /* 特定業務だけ */
659
+ ```
660
+ 主な変数: `--tg-accent`(選択枠/フィル) / `--tg-edit-accent`(編集枠) / `--tg-border` / `--tg-wrap-border` / `--tg-header-bg`(ヘッダ背景の既定) / `--tg-colheader-bg`(列ヘッダ背景) / `--tg-rowhead-bg`(行番号列背景) / `--tg-header-fg`(列ヘッダ文字)・`--tg-rowhead-fg`(行番号文字) / `--tg-cell-bg` / `--tg-sel-bg`(選択背景) / `--tg-selhdr-bg`(選択中ヘッダ) / `--tg-readonly-bg`・`--tg-readonly-fg` / `--tg-placeholder-fg` / `--tg-invalid-fg`・`--tg-invalid-bg` / `--tg-box-color`・`--tg-box-width`(セル囲み) / `--tg-font-size` / `--tg-radius`。さらに細かくは従来どおり `.tssgrid .tg-xxx { … }` で上書きできます(全 CSS は `.tssgrid` 配下に scope 済み)。
661
+
662
+ - **ヘッダ色**: `--tg-header-bg` で列ヘッダ+行番号列をまとめて、または `--tg-colheader-bg`/`--tg-rowhead-bg` で**別々に**(未指定なら `--tg-header-bg` を継ぐので後方互換)。選択中の列/行ヘッダは `--tg-selhdr-bg` が出るので、濃色テーマではこれも合わせると統一感が出ます。
663
+
664
+ **罫線まわり**: 色はセル全体なら `--tg-border`、セル単位なら `cellClass` で `border-right/bottom-color` を指定(各セルは右・下の罫線を持つ設計)。**任意の1セルを四角く囲む**なら border ではなく**同梱の `.tg-box` クラス**を `cellClass` で付与(`box-shadow: inset` ベースなので **A1・1行目・1列目・端セルでも全4辺を囲め、レイアウトも動きません**)。色/太さは `--tg-box-color`・`--tg-box-width`、用途別は `.tssgrid td.tg-box-warn{ --tg-box-color:#e67e22 }` のように。
665
+
666
+ ```js
667
+ cellClass: (r, c, value, row) => isAlert(r, c) ? 'tg-box tg-box-warn' : '' // 該当セルを橙枠で強調
668
+ ```
669
+
670
+ ## 列の表示/非表示
671
+
672
+ 列を**プログラムから**隠せます(メニュー項目は付きません。`hiddenColumns` で初期指定も可)。
673
+
674
+ ```js
675
+ grid.hideColumn(1); // 列1を隠す
676
+ grid.showColumn(1); // 再表示
677
+ grid.toggleColumn(1); // 切替
678
+ grid.isColumnHidden(1); // → true/false
679
+ grid.getHiddenColumns(); // → [1, 3]
680
+ new TssGrid(el, { hiddenColumns: [1] }); // 初期で隠す
681
+ ```
682
+
683
+ - 隠し列は**畳まれ**(幅0)、**カーソル移動は隣の可視列へスキップ**、幅計算・固定列オフセットからも除外されます。
684
+ - **コピーは隠し列を除外**し、**貼り付けは可視列へ**流し込みます(見たままをコピペ)。
685
+ - **データは消えません**(表示だけ。`getData()`/`getRows()` には隠し列の値も入ります)。
686
+ - 最低1列は残ります(全列は隠せません)。列の挿入・削除をしても隠し状態のインデックスは自動で追従します。
687
+
688
+ ## API
689
+
690
+ ### `new TssGrid(container, options)`
691
+
692
+ | option | 型 | 既定 | 説明 |
693
+ |---|---|---|---|
694
+ | `headers` | `string[]` | `['A','B','C']` | 列見出し |
695
+ | `data` | `string[][]` | — | 初期データ(行 × 列) |
696
+ | `history` | `HistoryManager` | 自前生成 | 渡すと Undo を共有 |
697
+ | `name` | `string` | `'grid'` | 履歴ラベル / 識別 |
698
+ | `showDump` | `boolean` | `false` | データ確認用の表示 |
699
+ | `enterMoves` | `'down'\|'up'\|'right'\|'left'\|'none'` | `'down'` | Enter 確定後の移動方向 |
700
+ | `tabMoves` | `'down'\|'up'\|'right'\|'left'\|'none'` | `'right'` | Tab 確定後の移動方向 |
701
+ | `initialCell` | `[r,c]` or `{r,c}` | `[0,0]` | 初期カーソル位置。`c` は index か `data` キー(例 `{r:0, c:'start_hours'}`) |
702
+ | `nextCell` | `({r,c,key,shift})=>{r,c}\|null` | — | **入力フロー制御**: Enter/Tab の移動先を上書き(`null` で既定方向)。**入力セルだけを論理順で巡回**でき、readonly 列を飛ばせる(業務フォーム向け)|
703
+ | `columns` | `ColumnDef[]` | `[]` | 列ごとのセルタイプ / バリデータ(下記) |
704
+ | `invalidMode` | `'revert'\|'keep'` | `'revert'` | 検証 NG 時。`revert`=元の値に戻す / `keep`=入力値を残してセルを赤く警告 |
705
+ | `invalidTitle` | `boolean` | `true` | `keep` 時、赤セルに validator メッセージを `title=`(ホバーで理由)。`false` でOFF。全件は `getInvalidCells()` |
706
+ | `messages` | `{code:…}\|fn` | — | エラーの**共通メッセージカタログ**。`validator` が `{code,params,level}` を返すと本文を解決(`{label}`/`{value}`/params 差込)。本文を外部JSON/CSV/XMLで一元管理 |
707
+ | `pasteOverflow` | `'clip'\|'error'` | `'clip'` | 貼り付けがグリッド外に超過した時。`clip`=はみ出し分を切り捨て / `error`=貼り付け中止 |
708
+ | `colWidths` / `rowHeights` | `number[]` \| `{[key]:number}` | `[]` | 列幅 / 行高の初期値(px)。未指定の要素は既定値。`colWidths` は**配列(位置指定)でも、`{ dataキー: px }` のオブジェクト(列ID指定)でも可**=後者は列を入れ替えても幅が追従する。列定義側に `columns[c].width` で同居指定も可(並べ替え・コピペで幅が一緒に動く=最も壊れにくい)。優先度: 実行時リサイズ/明示 `colWidths` > `columns[c].width` > 既定 |
709
+ | `defaultColWidth` / `defaultRowHeight` | `number` | `120` / `28` | 既定の列幅 / 行高(px) |
710
+ | `minColWidth` / `minRowHeight` | `number` | `30` / `18` | リサイズの下限(px) |
711
+ | `resizeMode` | `'preview'\|'live'` | `'preview'` | リサイズの見せ方。`preview`=Excel風に線で予告→離して確定 / `live`=即時反映 |
712
+ | `frozenCols` | `number` | `0` | 左から固定する列数(横スクロールしても残る)。`freezeCols(n)` で実行時変更も可 |
713
+ | `frozenRows` | `number` | `0` | 上から固定する行数(縦スクロールしても残る)。`freezeRows(n)` で実行時変更も可。`frozenCols` 併用で**四隅固定** → 動く例: [`examples/frozen-rows.html`](https://tssgrid.threestarsoftware.co.jp/examples/frozen-rows.html) |
714
+ | `nestedHeaders` | `array[]` | — | **ヘッダ結合**(複数段見出し)。各段は配列で要素は文字列 or `{label,colspan}`。最下段が**リーフ=実列**(並べ替え・全選択チェック等は機能)。上段グループのクリックで範囲列を選択 → 動く例: [`examples/nested-headers.html`](https://tssgrid.threestarsoftware.co.jp/examples/nested-headers.html) |
715
+ | `width` / `height` | `number\|string` | — | 枠(wrap)の幅/高さ。**指定するとテーブル自身に横/縦スクロールバーが出る**(未指定だと枠が内容幅まで広がりページ側がスクロール)。固定行/列・縦横スクロールを使うなら指定推奨 |
716
+ | `resizeCols` / `resizeRows` | `boolean` | `true` | 列幅/行高のリサイズ可否(全体)。`false` でグリップを出さない |
717
+ | `cursor` | `boolean` | `true` | 選択カーソル(青の選択枠/範囲/セルハイライト/フィルハンドル)を表示するか。`false` で**表示専用グリッド向けに非表示**+セルカーソルも既定に(内部の選択は保持=コピーは可)。合計表・サマリ等に |
718
+ | `columns[c].width` | `number` | — | 列ごとの初期幅(px)。**列定義に同居するので並べ替え・コピペで幅が一緒に動く**(`colWidths` より壊れにくい)。実行時リサイズ・明示 `colWidths` があればそちらが優先 |
719
+ | `columns[c].resizable` | `boolean` | `true` | 列ごとの幅ロック。`false` でその列だけリサイズ禁止 |
720
+ | `stretchH` | `'none'\|'last'\|'all'` | `'none'` | 列を枠幅まで伸ばす。`'last'`=余りを最終列へ / `'all'`=全列で配分(`width` 指定時に効く) |
721
+ | `rowHeaders` / `colHeaders` | `boolean` | `true` | 行番号列 / ヘッダ行の表示。`false` で隠す |
722
+ | `hiddenColumns` | `number[]` | `[]` | 初期で隠す列インデックス。実行時は `hideColumn`/`showColumn`/`toggleColumn`(→ [列の表示/非表示](#列の表示非表示)) |
723
+ | `wordWrap` | `boolean` | `false` | セル内で折り返し(行高は内容に合わせ伸びる)。`columns[c].wordWrap` で列単位も可 |
724
+ | `placeholder` | `string` | — | 空セルに薄いヒント文字(`getData` には入らない)。`columns[c].placeholder` で列単位も可 |
725
+ | `className` | `string` | — | コンテナ(`.tssgrid`)に付ける任意クラス(テーマ付け) |
726
+ | `fillHandle` | `boolean \| {direction,autoInsertRow}` | `true` | **`false`でフィル完全OFF**(ハンドルを出さない=ドラッグでの消去/上書きなし。フィルの起点はハンドルのみなので fill 自体が無効に)。`{direction:'vertical'\|'horizontal'\|'both', autoInsertRow:true}`で詳細指定(下へドラッグで行追加)。設定/表示専用グリッドで誤フィルを防ぐのに有効 |
727
+ | `onBeforeAutofill` / `onAfterAutofill` | `(src,target)=>…` | — | フィル前後。before は `false` で取消。`src`/`target` = `{r0,c0,r1,c1}` |
728
+ | `autoColumnSize` | `boolean \| number` | `false` | 初期化時に列幅を内容に合わせて自動調整(数値=上限px)。`autoSizeColumn`/`autoSizeAllColumns` API・列境界ダブルクリックでも |
729
+ | `columns[c].align` / `valign` | `string` | — | 列既定の整列(→ [整列](#整列--セル単位の読み取り専用実行時)) |
730
+ | `columns[c].editor` | `object \| ()=>object` | — | カスタム編集UI(→ [カスタムエディタ](#カスタムエディタ)) |
731
+ | `copyPaste` | `boolean` | `true` | `false` でコピー/カット/ペーストを無効化 |
732
+ | `minSpareRows` | `number` | `0` | 末尾に常に確保する空行数(Excel風に下へ打ち足せる) |
733
+ | `autoWrapRow` / `autoWrapCol` | `boolean` | `false` | Tab で行末→次行頭 / Enter で列末→次列頭に折り返し |
734
+ | `minRows` / `maxRows` | `number` | — | 行数の下限/上限(min は初期パディング、削除の床。max は挿入の天井) |
735
+ | `minCols` / `maxCols` | `number` | — | 列数の下限/上限 |
736
+ | `readOnly` | `boolean` | `false` | **グリッド全体**を読み取り専用に(ビューア用途) |
737
+ | `columns[c].readOnly` | `boolean \| (value,{r,c})=>boolean` | — | **列単位**の読み取り専用。関数を渡すとセル単位で判定(→ [読み取り専用](#読み取り専用readonly)) |
738
+ | `onChange` | `(grid)=>void` | — | データ変更時に呼ばれる(後方互換) |
739
+ | `onInvalid` | `(rejections, source)=>void` | — | 列ルールで弾かれた時。`rejections = [{ r, c, value, message }]` |
740
+ | `onReadOnly` | `(blocked, source)=>void` | — | 読み取り専用セルへの変更を弾いた時。`blocked = [{ r, c, value }]` |
741
+ | `contextMenu` | `boolean \| Array \| {items}` | `true` | 右クリックメニュー。`true`=既定項目 / `false`=無効 / **配列**=項目を構成(組込キー+カスタム項目)(→ [右クリックメニュー](#右クリックメニュー行列の挿入削除)) |
742
+ | `allowInsertRows` / `allowDeleteRows` | `boolean` | `true` | 行の挿入 / 削除の可否(メニュー項目の表示も連動) |
743
+ | `allowInsertCols` / `allowDeleteCols` | `boolean` | `true` | 列の挿入 / 削除の可否 |
744
+ | `onStructureChange` | `(info)=>void` | — | 行/列の挿入・削除後。`info = { type, ... , undo? }` |
745
+ | `onBeforeStructureChange` | `(info)=>false\|void` | — | 行/列の挿入・削除の前。`false` で中止 |
746
+ | `onAfterPaste` | `(data,{range})=>void` | — | 貼り付け確定後 |
747
+ | `onBeforeCopy` / `onBeforeCut` | `(data,{range})=>false\|string[][]\|void` | — | コピー/カットの前。`false` で取消・配列で差替え |
748
+ | `onAfterCopy` | `(data,{range,cut})=>void` | — | クリップボード書込後(cut でも発火) |
749
+ | `onBeforeSelectionChange` | `({range,active,extent})=>false\|void` | — | 選択変更の前。`false` で中止 |
750
+ | `onBeforeKeyDown` | `(e)=>false\|void` | — | キーをグリッドより先に。`false` / `preventDefault()` で既定を止める |
751
+ | `onHeaderClick` | `(c, e)=>false\|void` | — | 列ヘッダのクリック。`false` で既定(列選択)を抑止(ソートUI等を外部で組む用) |
752
+ | `shortcuts` | `Array` | `[]` | カスタムショートカット(→ [ショートカット / プラグイン](#ショートカット--プラグイン--破棄)) |
753
+ | `plugins` | `Array` | `[]` | プラグイン(関数 or 登録名)。`init(grid)` で初期化 |
754
+ | `onPasteOverflow` | `(info)=>void` | — | `pasteOverflow:'error'` で超過時。`info = { anchor:{r,c}, height, width, rows, cols, overRows, overCols }` |
755
+
756
+ `enterMoves` / `tabMoves` は **Shift 押下で逆方向**に移動します(例: 既定で Shift+Enter は上、Shift+Tab は左)。プロパティとして実行時に書き換えても次のキー入力から反映されます(例: `grid.enterMoves = 'right'`)。
757
+
758
+ **入力フロー制御(`nextCell`)**: Enter/Tab の移動先を上書きして、**入力セルだけを論理順で巡回**できます(readonly の計算列を飛ばす/行末で次行の先頭入力へ)。"Excel ではなくシステム" だからできる、フォームに近い入力体験(→ 実例: [`examples/attendance.html`](https://tssgrid.threestarsoftware.co.jp/examples/attendance.html)):
759
+
760
+ ```js
761
+ const FLOW = [3, 4, 5, 13]; // 出勤→退勤→所定外休→備考(入力列だけ)
762
+ new TssGrid(el, {
763
+ initialCell: { r: 0, c: 3 }, // 初期カーソル=1日目の出勤
764
+ nextCell: ({ r, c, shift }) => {
765
+ const i = FLOW.indexOf(c);
766
+ if (i < 0) return null; // 入力列以外は既定移動
767
+ return i < FLOW.length - 1 ? { r, c: FLOW[i + 1] } : { r: r + 1, c: FLOW[0] }; // 備考→次行の出勤
768
+ },
769
+ });
770
+ ```
771
+
772
+ メソッド: `getData()` / `getRows()` / `getRow(r)` / `getColumn(cOrKey)` / `getColumns()` / `setData(rows)` / `getValue(r,cOrKey)` / `setValue(r,cOrKey,val,force?)` / `setValueRaw(r,cOrKey,val)`(履歴に積まない派生値書き込み・計算列用) / `hideColumn(c)` / `showColumn(c)` / `toggleColumn(c)` / `isColumnHidden(c)` / `getHiddenColumns()` / `setAlignment(align,range?)` / `getAlignment(r,c)` / `setCellReadOnly(flag,range?)` / `autoSizeColumn(c)` / `autoSizeAllColumns()` / `sortBy(cOrKey, dir)` / `sortRows(cmp)` / `addShortcut(s)` / `removeShortcut(name)` / `usePlugin(fn)` / `getPlugin(name)` / `destroy()` / `setActive(r,c)` / `selectRow(r)` / `selectCol(c)` / `selectAll()` / `setColWidth(c,px)` / `setRowHeight(r,px)` / `freezeCols(n)` / `freezeRows(n)` / `moveRow(from,to)` / `moveRows(r0,r1,to)` / `clearSort()` / `toCSV(opts)` / `downloadCSV(name,opts)` / `insertRow(ri,'above'|'below')` / `deleteRows(r0,r1?)` / `insertCol(ci,'left'|'right')` / `deleteCols(c0,c1?)` / `redraw()`(列定義変更後の再描画, データ・選択は保持)ほか。
773
+
774
+ ## イベント
775
+
776
+ 入力・貼付・フィル・Delete の **すべての値変更が同じ `changes` 形式で同じフックを通る**ので、検証や整形を **一度書けば全経路に効きます**。
777
+
778
+ ```js
779
+ new TssGrid(el, {
780
+ headers, data,
781
+
782
+ // 確定の“直前”。検証・自動整形のフック。changes = [{ r, c, oldValue, newValue, source }]
783
+ onBeforeChange(changes, source) {
784
+ // ・return false → 変更を全部取り消す
785
+ // ・change.newValue を書換 → 整形して確定(例: 全角→半角、trim、大文字化…)
786
+ // ・1セルだけ拒否したい → その change.newValue を oldValue に戻す
787
+ changes.forEach(c => { c.newValue = c.newValue.trim(); });
788
+ if (changes.some(c => c.c === 0 && c.newValue === '')) return false; // 例: A列必須
789
+ },
790
+
791
+ // 確定の“直後”。保存・再計算など。source: 'edit'|'paste'|'fill'|'delete'|'undo'|'redo'
792
+ onAfterChange(changes, source) { /* save(grid.getData()) など */ },
793
+
794
+ // 編集の開始 / 終了(IME確定後に発火。Esc キャンセルは canceled:true)
795
+ onEditStart({ r, c, value }) {},
796
+ onEditEnd({ r, c, oldValue, newValue, canceled }) {},
797
+
798
+ // 選択範囲の変更。before は false で“選択させない”ことも可(特定セルへの移動を禁止 等)
799
+ onBeforeSelectionChange({ range, active, extent }) { /* return false で選択中止 */ },
800
+ onSelectionChange({ range, active, extent }) {}, // range = { r0, c0, r1, c1 }
801
+
802
+ // 貼り付けの前後。before は 2次元文字列配列を加工/取消、after は確定後の通知
803
+ onBeforePaste(data) { /* return false で取消 / 加工した string[][] を返すと置換 */ },
804
+ onAfterPaste(data, { range }) { /* 貼り付け確定後。range = 落ちた範囲 */ },
805
+
806
+ // コピー / カットの前後。before は false で取消・配列で差替え可
807
+ onBeforeCopy(data, { range }) {},
808
+ onBeforeCut(data, { range }) {},
809
+ onAfterCopy(data, { range, cut }) {},
810
+
811
+ // 行/列の挿入・削除の前後。before は false で中止できる
812
+ onBeforeStructureChange(info) { /* info.type = 'insertRow'|'deleteRows'|'insertCol'|'deleteCols'。return false で中止 */ },
813
+ onStructureChange(info) {}, // 後。undo 時は info.undo = true
814
+ });
815
+ ```
816
+
817
+ `onBeforeChange` で取り消した変更は **履歴(Undo)にも積まれません**。`undo`/`redo` では `onBeforeChange` は呼ばれず、`onAfterChange` のみ `source:'undo'|'redo'` で発火します。`onBefore*` 系(`Paste`/`Copy`/`Cut`/`StructureChange`/`SelectionChange`)は **`false` を返すとその操作を中止**します(履歴にも積まれません)。挿入/削除・コピー/カットの `onBefore*` は **redo では再発火しません**(確定済み操作の再実行のため)。
818
+
819
+ ## セルタイプ / バリデータ
820
+
821
+ `columns` で列ごとに型と検証を指定できます。検証は `onBeforeChange` の **手前** に効くので、編集・貼付・フィルのどの経路でも同じルールが適用されます。
822
+
823
+ ```js
824
+ new TssGrid(el, {
825
+ headers: ['担当', '状態', '完了', '期限', '数量'],
826
+ columns: [
827
+ {}, // text(既定)
828
+ { type: 'dropdown', options: ['未着手','進行中','完了'] },
829
+ { type: 'checkbox' }, // 既定 ON='1' / OFF=''
830
+ { type: 'date' }, // ISO(YYYY-MM-DD)。不正値は拒否
831
+ { validator: v => v === '' || /^\d+$/.test(v) ? true : '数値で入力' },
832
+ ],
833
+ onInvalid(rej) { alert(rej.map(x => x.message).filter(Boolean).join('\n')); },
834
+ });
835
+ ```
836
+
837
+ | `type` | 編集 UI | 保存値 / 検証 |
838
+ |---|---|---|
839
+ | `'text'`(既定) | テキスト入力 | そのまま |
840
+ | `'dropdown'` | `<select>`(`options` 必須)。セルに ▾ を表示、**1クリックで一覧が開き**、選ぶと即確定 | 選択肢 or 空のみ許可。範囲外は拒否。**クリア用の空オプションは自動で先頭に付く**ので `options` に `''` を入れない(入れると空が二重)。`allowEmpty:false` で**空オプションを出さず空値も弾く**=ラジオ的な必須選択。**`options` は `string[]` または `{value,label}[]`=保存値(value=内部コード)と表示(label)を分離**(`getData`/`getRows` は value で返る・検証も value で照合) |
841
+ | `'checkbox'` | Space / クリックでトグル | `checked`(既定 `'1'`) / `unchecked`(既定 `''`)。貼付値も正規化 |
842
+ | `'date'` | `<input type="date">` | `YYYY-MM-DD` のみ。実在しない日付(例 `2026-02-30`)は拒否 |
843
+ | `'time'` | `<input type="time">` / テキスト | `HH:MM[:SS]`(24時間制)で保存。`hour12:true` で AM/PM 表示・入力 |
844
+ | `'number'` | テキスト(編集時は素の数値) | 数値のみ。`decimals`/`thousands`/`prefix`/`suffix` で表示書式(→ [CellFormat](#cellformat宣言的な表示書式数値--日付))。**全角→半角を既定で正規化**(`123`→`123`、`zenkaku:false`で無効化)。右寄せ |
845
+
846
+ - **`validator(value, { r, c })`**: `true`/`undefined` で許可、`false` で拒否、**文字列を返すとエラーメッセージ付きで拒否**。型と併用可(型 → validator の順で評価)。
847
+ - 検証 NG 時の挙動は **`invalidMode`** で選べます:
848
+ - `'revert'`(既定): 元の値に巻き戻す。
849
+ - `'keep'`: **入力値を残してセルを赤く**警告(`td.tg-invalid`)。値を直せば赤は自動で消えます。フォームのように「後でまとめて直す」運用向き。`getData()` には不正値も入るので、保存前に再検証してください。
850
+ - `keep` 時は**赤セルにホバーで理由(`title`=validator のメッセージ)**が出ます。`invalidTitle:false` でOFF。
851
+ - どちらのモードでも確定時に `onInvalid(rejections, source)` が呼ばれます(`rejections=[{r,c,value,message}]`)。
852
+ - **全エラーをまとめて取得**: `grid.getInvalidCells()` → `[{r,c,key,value,code,level,message}]`(今の値を全セル検証)。保存前チェックや**エラー一覧パネル**の表示に → 動く例: [`examples/validation.html`](https://tssgrid.threestarsoftware.co.jp/examples/validation.html)(赤+ホバー+一覧パネルがライブ更新)。
853
+
854
+ #### エラーを コード / レベル / 共通メッセージ で管理(messages カタログ)
855
+
856
+ 業務システム向けに、**エラー本文を外部の共通メッセージ(設定系)で一元管理**できます(トーン統一・差し替え・多言語)。`validator` は**コードを返すだけ**にし、本文は `messages` カタログから解決します。
857
+
858
+ ```js
859
+ new TssGrid(el, {
860
+ messages: { // システム共通メッセージ(外部JSON/CSV/XML から)
861
+ E_RANGE: { level: 'error', text: '{label}は {min}〜{max} の範囲で入力してください' },
862
+ W_LONG: { level: 'warn', text: '{label}がやや長めです({value})' },
863
+ },
864
+ columns: [{ data:'age', title:'年齢',
865
+ validator: v => v==='' || (+v>=0 && +v<=120) || { code:'E_RANGE', params:{min:0,max:120} } }],
866
+ });
867
+ ```
868
+ - `validator` の戻り: `true/undefined`=OK / `false`=拒否 / **文字列**=その本文 / **`{ code, params?, level?, message? }`**=コード管理。
869
+ - `messages` は `{ code: 'text' | {level,text} }` か関数 `(code, ctx)=>text`。本文は **`{label}`(列タイトル)・`{value}`・任意 `params`** を差し込み。
870
+ - 各エラーは **`code` / `level` / 解決済み `message`** を持ち、`getInvalidCells()`・`onInvalid` で取得。**`keep` モードでは `level` で色分け**(赤セルに `data-err-level`=error/warn、CSS でトーン指定)。保存可否は `getInvalidCells()` を level でフィルタしてプログラム側で判断。
871
+ - カタログを**CSVから作る**ヘルパー: `plugins/tss-messages.js` の `TssMessages.fromCSV(text)` / `fromRows(rows)`(`code,level,text` の設定ファイルを読込)。動く例: [`examples/messages.html`](https://tssgrid.threestarsoftware.co.jp/examples/messages.html)。
872
+ - checkbox の保存値は `checked` / `unchecked` で変更可(例: `{ type:'checkbox', checked:'TRUE', unchecked:'FALSE' }`)。
873
+ - **checkbox 列はヘッダに全選択チェックボックス**が出ます(クリックで全 ON/全 OFF、部分選択は indeterminate)。`columns[c].headerCheckbox: false` で非表示に。プログラムからは **`setColumnChecked(c, true/false)`**(readOnly セルはスキップ・**Undo 一発**)。右クリックメニューに出したいときは `contextMenu` に組込キー **`'check_column'` / `'uncheck_column'`** を入れます。見た目は外部CSS `.tssgrid .tg-head-cb { … }` で変更可。
874
+
875
+ ### 入力の正規化 / 表示の整形(`parse` / `format`)
876
+
877
+ **保存値**(`getData()` が返す内部値)と、**入力**・**表示**を分離できます。外部ライブラリは同梱せず、必要なら列のフックに差し込む方針です。
878
+
879
+ | フック | 向き | 役割 |
880
+ |---|---|---|
881
+ | `parse(input, { r, c })` | 入力 → 保存値 | 入力文字列を保存値に正規化。`false`/`null` を返すと不正扱い。編集・貼付・フィルの全経路に効く |
882
+ | `format(stored, { r, c })` | 保存値 → 表示 | セルに出す文字列を整形(保存値そのものは変えない) |
883
+
884
+ ```js
885
+ columns: [
886
+ // 日付を yyyymmdd で打ちたい: テキスト入力にして、保存は ISO・表示は yyyy/mm/dd
887
+ { type: 'date', picker: false, format: v => v ? v.replace(/-/g, '/') : '' },
888
+ // 任意フォーマット(外部ライブラリも可): 入力 "500円" → 保存 "500" → 表示 "500円"
889
+ { parse: s => (s.match(/^(\d+)円$/) || [])[1] ?? false, format: v => v ? v + '円' : '' },
890
+ ]
891
+ ```
892
+
893
+ - **`type:'date'` の既定 `parse`**: `yyyymmdd` / `yyyy-mm-dd` / `yyyy/mm/dd` / `yyyy.mm.dd` を `yyyy-mm-dd` に正規化(実在日チェック付き)。`time` は `HH:MM[:SS]` を 24時間制に正規化(`1:30 PM` などの AM/PM 入力も受理)。
894
+ - **編集 UI**: `date`/`time` は既定でネイティブのピッカー(`<input type=date/time>`)。**`picker:false`** か `parse`/`format` を付けると**テキスト入力**になり、入力の並び順を自分で決められます(ネイティブの表示順はブラウザのロケール依存なので、yyyymmdd 固定で打ちたい時はこちら)。
895
+ - 評価順は **`parse` → 型チェック → `maxLength` → `validator`**。`format` は表示時のみ。`invalidMode:'keep'` なら `parse` 失敗値もそのまま赤く残ります。
896
+ - **`columns[c].maxLength`**: 桁数(文字数)上限。**編集中はそれ以上打てない**(`<input maxlength>`)+**貼り付け・`setValue` は超過分を切り詰め**。社員番号・電話・郵便番号など「無制限ではない」項目に。例: `{ data:'code', maxLength: 5 }`。さらに厳密な書式は `validator` 併用(`{ maxLength:8, validator: v => /^[A-Z0-9]+$/.test(v) || '英数字のみ' }`)。
897
+
898
+ ### CellFormat(宣言的な表示書式:数値 / 日付)
899
+
900
+ `parse`/`format` 関数を書かずに、**列定義のオプションだけ**で数値・日付の表示書式を指定できます。**保存値は正規のまま**(`getData()` は素の値)、**表示だけ**整形します。
901
+
902
+ ```js
903
+ columns: [
904
+ // 数値: type:'number' + 書式オプション
905
+ { type: 'number', thousands: true }, // 1234567 → 1,234,567
906
+ { type: 'number', thousands: true, decimals: 2, prefix: '¥' },// 1234.5 → ¥1,234.50
907
+ { type: 'number', decimals: 1, suffix: '%' }, // 33.33 → 33.3%
908
+ // 日付: type:'date' + 文字列パターン
909
+ { type: 'date', format: 'yyyy年m月d日' }, // 2026-06-30 → 2026年6月30日
910
+ ]
911
+ ```
912
+
913
+ | オプション(`type:'number'`) | 役割 |
914
+ |---|---|
915
+ | `decimals` | 小数桁(四捨五入して表示)。省略時は保存値のまま |
916
+ | `thousands` | `true` で 3桁カンマ区切り |
917
+ | `prefix` / `suffix` | 数値の前後に付ける文字(`'¥'` `'%'` `' 円'` など) |
918
+
919
+ - **入力**: グルーピングや前後綴り混じり(`'¥1,234'` `'50%'`)でも `parse` が正規化(`'1,234.50' → '1234.5'`)。非数値は拒否(検証経路に乗る)。
920
+ - **全角→半角**: 全角数字・符号を**既定で半角化**(`'123' → '123'`、`'1,234.5' → '1234.5'`、`'¥1,000' → '1000'`)。IME 誤入力に強い。`zenkaku:false` で無効化。
921
+ - **非数値の扱いは `invalidMode`**: `'revert'`(既定=黙って戻す)/ `'keep'`(**セルを赤くエラー**+`onInvalid` でメッセージ)。両モードとも全角→半角は効く(→ 動く例: [`examples/number-input.html`](https://tssgrid.threestarsoftware.co.jp/examples/number-input.html))。
922
+ - **編集時は素の数値**を表示(Excel 流。`¥1,234` のセルを編集すると `1234`)。
923
+ - 数値列は**右寄せ**(`td.tg-num`)。
924
+ - **日付パターン**(`type:'date'` に**文字列**の `format`): トークン `yyyy` `yy` `mm` `m` `dd` `d`。`parse` は組込のまま(`20260630` 等で入力可)。
925
+ - これらは内部で `parse`/`format` の上に薄く乗っているだけなので、関数版の `format`/`parse` を渡せば従来どおり完全カスタムにできます(関数が優先)。
926
+
927
+ ### 条件付き書式(cellClass)
928
+
929
+ **値・行・列を見てセルにCSSクラスを付ける**フックです。**ルールエンジンは持たず**、付けるのはクラスだけ=見た目は業務側CSSで自由(リーンコア方針)。**編集すると即座に再評価**されます(→ 動く例: [`examples/conditional-format.html`](https://tssgrid.threestarsoftware.co.jp/examples/conditional-format.html))。
930
+
931
+ ```js
932
+ columns: [
933
+ { data: '差分', type: 'number',
934
+ cellClass: (value, { r, c, row }) => +value < 0 ? 'tg-neg' : '' }, // 列版(値で判定)
935
+ ],
936
+ cellClass: (r, c, value, row, src) => src.holiday_kbn === '1' ? 'sat' : '', // 全体版(src=元レコード=非表示フィールドも見える)
937
+ // CSS は業務側で: .tssgrid td.tg-neg { color:#c00 } .tssgrid td.tg-row-ng { background:#fdecea }
938
+ ```
939
+ - 列版 `columns[c].cellClass(value, {r,c,row,src})` / 全体版 `cellClass(r,c,value,row,src)`。返り値は文字列/配列/空(クラス無し)。
940
+ - `src` は**元レコード**(オブジェクトデータ時)。**画面に出していない隠しフィールド基準の色付け**(勤怠の `holiday_kbn` で土日祝を色分け等)に使えます → 実例: [`examples/attendance.html`](https://tssgrid.threestarsoftware.co.jp/examples/attendance.html)。
941
+ - **他列に依存する行ルール**(状態で行を色分け等)は、依存先の編集時に `grid.redraw()` で行全体を再評価(`onAfterChange: () => grid.redraw()`)。
942
+ - 同じセルに行ルールとセルルールが両方付く場合は **CSSの記述順(後勝ち)**で優先を決めます(セル個別を後に書く)。
943
+
944
+ ### セル単位インラインスタイル(cellStyle)— 連続色・データバー
945
+
946
+ `cellClass` が**クラス(離散)**なのに対し、`cellStyle` は**セルに直接インラインCSS/CSS変数を差す**フック。**値に応じた連続色(ヒートマップ)や背景データバー**など、クラスでは出せない表現に(**編集できる数値セルのまま**)。返り値は CSSプロパティのオブジェクト or 文字列。
947
+
948
+ ```js
949
+ // 全体版 cellStyle(r,c,value,row,src) / 列版 columns[c].cellStyle(value,{r,c,row,src})
950
+ columns: [
951
+ { data: '売上', type: 'number',
952
+ cellStyle: (v) => ({ background: heat(+v), color: '#fff' }) }, // 連続色(オブジェクト)
953
+ { data: '進捗', type: 'number',
954
+ cellStyle: (v) => `--pct:${v}%` }, // CSS変数も('k:v;…' 文字列も可)
955
+ ]
956
+ ```
957
+ - 返り値プロパティ名は **CSS標準名/kebab/`--変数`**(`background`・`background-color`・`--pct` 等)。`{r,c,row,src}` は cellClass と同じ。
958
+ - **編集で即再評価**(`_renderCell` 経由)。前回分は自動でクリアして付け直し。連続色/バーの既製ヘルパーは下記 `TssHeatmap`/`TssDataBar`。
959
+
960
+ ### セルに画像 / SVG / HTML(html:true)
961
+
962
+ 列に `html: true` を付けると、その列は**表示だけ生HTML**になります(値 or `format` の戻りを innerHTML)。**値はテキスト保存のまま**=`getData()`・コピペ・CSV は無傷で、**見た目だけ**HTML化。画像・SVGアイコン・状態バッジ・進捗バー・★評価などに(→ 動く例: [`examples/html-cells.html`](https://tssgrid.threestarsoftware.co.jp/examples/html-cells.html))。
963
+
964
+ ```js
965
+ columns: [
966
+ { data:'状態', html:true, readOnly:true, format:(v)=> badgeHtml(v) }, // SVGドット+ラベル
967
+ { data:'写真', html:true, readOnly:true, format:(url)=> `<img src="${url}" height="22">` },
968
+ { data:'アイコン', html:true, readOnly:true }, // 値に inline SVG をそのまま
969
+ ]
970
+ ```
971
+ - ⚠️ **エスケープを外す=XSSの責任は `format`/値の提供側**。外部由来の文字列をそのまま埋めないこと。**表示用途なので `readOnly` 推奨**。
972
+ - 非 `html` 列は従来どおりエスケープされ安全(`<img>` 等はテキスト表示)。
973
+ - 画像は行高(既定28px)を超えると `overflow:hidden` で切れるので `rowHeights` で調整。
974
+
975
+ ### 読み取り専用(readOnly)
976
+
977
+ セル・列・グリッド全体を編集不可にできます。**編集モードに入れず**(クリック・F2・ダブルクリック・文字入力)、**貼り付け・フィル・Delete などの全変更経路でも拒否**して元の値に戻します(履歴にも積みません)。読み取り専用セルも**選択・コピーは可能**です。
978
+
979
+ ```js
980
+ new TssGrid(el, {
981
+ readOnly: false, // ← グリッド全体を読み取り専用にするならここで
982
+ columns: [
983
+ { readOnly: true }, // 列まるごと固定(ID・計算結果など)
984
+ {}, // 編集可
985
+ // セル単位の判定: 「完了」の行だけロック
986
+ { type: 'dropdown', options: ['受付','出荷','完了'], readOnly: v => v === '完了' },
987
+ ],
988
+ onReadOnly: (blocked) => console.log('変更を拒否:', blocked), // [{ r, c, value }]
989
+ });
990
+ ```
991
+
992
+ - 列の `readOnly` は **`true`** または **`(value, { r, c }) => boolean`**(セル単位の判定)。
993
+ - 読み取り専用セルは淡色表示(`td.tg-readonly`)。checkbox は `disabled`、dropdown は ▾ を出しません。
994
+ - 検証(`validator`)とは独立。`invalidMode` に関係なく常に元の値へ戻します。
995
+
996
+ ### 右クリックメニュー(行/列の挿入・削除)
997
+
998
+ セルやヘッダを**右クリック**すると、行/列の挿入・削除メニューが出ます。複数行/列を選択しておけば**まとめて削除**できます。挿入・削除は **Undo/Redo に1操作で積まれ**(`Ctrl+Z` / `Ctrl+Y`)、列の挿入・削除では `frozenCols` も自動で追従します。
999
+
1000
+ ```js
1001
+ new TssGrid(el, {
1002
+ contextMenu: true, // 既定 true。false でブラウザ既定メニューに戻す
1003
+ allowInsertRows: true, // 各操作は個別に ON/OFF(メニュー項目の表示も連動)
1004
+ allowDeleteRows: true,
1005
+ allowInsertCols: true,
1006
+ allowDeleteCols: true,
1007
+ onStructureChange: (info) => console.log(info.type), // 'insertRow' | 'deleteRows' | 'insertCol' | 'deleteCols'
1008
+ });
1009
+ ```
1010
+
1011
+ プログラムからも操作できます: **`insertRow(ri, 'above'|'below')`** / **`deleteRows(r0, r1?)`** / **`insertCol(ci, 'left'|'right')`** / **`deleteCols(c0, c1?)`**。最低1行・1列は残します。
1012
+
1013
+ 既定メニューには行/列の挿入・削除に加え **コピー / 切り取り / 貼り付け / 内容をクリア** も入ります(クリップボードは `copyPaste:false` で消えます)。
1014
+
1015
+ #### メニューを自分で構成する
1016
+
1017
+ `contextMenu` に**配列**を渡すと、出す項目を選び、独自項目を足せます。
1018
+
1019
+ ```js
1020
+ new TssGrid(el, {
1021
+ contextMenu: [
1022
+ 'row_above', 'row_below', 'remove_row',
1023
+ '---------', // 区切り線(ダッシュのみの文字列)
1024
+ 'copy', 'cut', 'paste', 'clear',
1025
+ '---------',
1026
+ // カスタム項目
1027
+ { name: '行を複製', key: 'dup', callback({ range }) { /* range = {r0,c0,r1,c1} */ } },
1028
+ { name: '完了にする', disabled: ({ r0 }) => r0 === 0, callback() {} }, // disabled/hidden は関数も可
1029
+ // サブメニュー
1030
+ { name: '一括操作', submenu: [
1031
+ { name: 'エクスポート', callback() {} },
1032
+ 'clear',
1033
+ ] },
1034
+ ],
1035
+ });
1036
+ ```
1037
+
1038
+ - **組込キー**: `row_above` / `row_below` / `remove_row` / `col_left` / `col_right` / `remove_col` / `copy` / `cut` / `paste` / `clear` / `undo` / `redo`、区切りは `'---------'`(ダッシュのみ)。
1039
+ - **カスタム項目**: `{ name, callback, disabled?, hidden?, key?, submenu? }`。`name`/`disabled`/`hidden` は**関数**も可(引数は選択範囲 `{r0,c0,r1,c1}`)。`callback` の `this` はグリッド、引数は `{ range, key }`。
1040
+ - `submenu` に配列を渡すとフライアウトの**サブメニュー**になります(同じ組込キー/カスタム項目が使えます)。
1041
+
1042
+ #### 時刻の 12時間制(AM/PM)
1043
+
1044
+ ```js
1045
+ columns: [ { type: 'time', hour12: true } ] // 表示・入力が 1:30 PM 形式。保存値は常に '13:30'(24h)
1046
+ ```
1047
+
1048
+ - 保存値は常に 24時間制 `HH:MM` なので、`hour12` を切り替えても**データは曖昧になりません**。
1049
+ - 入力は `1:30 PM` / `1:30pm` / `13:30` のいずれも受理(24時間制に正規化)。
1050
+ - 実行時に表示を切り替えるには、列の `hour12` を変更して **`grid.redraw()`**(データと選択は保持)。
1051
+
1052
+ ```js
1053
+ grid.columns[4].hour12 = !grid.columns[4].hour12;
1054
+ grid.redraw();
1055
+ ```
1056
+
1057
+ ### `HistoryManager`
1058
+
1059
+ `push(cmd)` / `undo()` / `redo()` / `canUndo()` / `canRedo()` / `clear()` / `onChange`。
1060
+ command は `{ apply(), revert(), label }`。
1061
+
1062
+ ## キーボード / マウス
1063
+
1064
+ | 操作 | 動作 |
1065
+ |---|---|
1066
+ | 矢印 | セル移動 |
1067
+ | Shift+矢印 / ドラッグ / Shift+クリック | 範囲選択 |
1068
+ | 行/列ヘッダをクリック(ドラッグ・Shift+クリックで複数) | 行・列の選択。左上の角で全選択 |
1069
+ | 列ヘッダ右端 / 行ヘッダ下端をドラッグ | 列幅 / 行高のリサイズ(既定はドラッグ中に線でプレビュー→離して確定) |
1070
+ | `frozenCols` で左 N 列を固定 | 横スクロールしても残る(`width` で枠幅を制約してスクロール可能に) |
1071
+ | 文字入力 / F2 / ダブルクリック | 編集 |
1072
+ | Enter / Tab | 確定して移動(既定 下 / 右。`enterMoves`・`tabMoves` で変更可、Shift で逆方向) |
1073
+ | Delete | 選択範囲クリア |
1074
+ | Ctrl+C / V / X | コピー / 貼付 / 切取(TSVでExcel相互) |
1075
+ | Ctrl+Z / Ctrl+Y(Ctrl+Shift+Z) | Undo / Redo |
1076
+ | フィルハンドル(右下)をドラッグ | コピー。数値2つ以上=連番 / 単一+Ctrl=+1 / Alt=2D矩形 / Ctrlで連番↔コピー反転 |
1077
+
1078
+ ## 設計メモ
1079
+
1080
+ - **なぜ `<input>` で contenteditable ではないか**: IMEのcomposition が contenteditable だとブラウザ間で不安定。変換中の `beforeinput` キャンセルも効かないため「入力抑制」目的でも結局確定時バリデーションが必要。`<input>` の方が IME が安定し、値がクリーンな文字列で、ペーストもプレーン、キャレット操作も簡単。
1081
+ - **入力制限**は「打たせて確定時に検証/変換」が基本(IME中はキーを弾けないため)。`.tg-editor.invalid`(赤)のCSSフックを用意済み。
1082
+ - **XSS**: セル/見出しは `&<>` をエスケープしてから描画。値はプレーン文字列のみ。
1083
+
1084
+ ## 制限
1085
+
1086
+ - 外部(Excel等)→グリッドの貼り付けはブラウザのクリップボード読取許可が必要。`file://` で読めない場合は http(ローカルサーバ)で開く。グリッド間・グリッド内のコピペは内部バッファで常に動く。
1087
+ - 仮想スクロール未対応(大量行が必要なら今後)。日付の連番フィルはコピー扱い(数値のみ連番)。
1088
+
1089
+ ## License
1090
+
1091
+ MIT