@threestarsoftware/tssgrid 0.0.4 → 0.0.5

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 CHANGED
@@ -238,7 +238,8 @@ grid.moveRows(r0, r1, to); // 連続する複数行をま
238
238
 
239
239
  ### 非破壊フィルタ(`filter` / `clearFilter`)
240
240
 
241
- **コア内蔵**の非破壊フィルタ(行インデックスマッパ)。全行は内部の `_allRows`(マスタ)に保持したまま、**可視部分集合だけ描画**します。**10万行でも絞り込んだ数百行だけ描く**ので、仮想スクロール無しで軽い → 動く例: [`examples/filter-live.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-live.html)。
241
+ **コア内蔵**の非破壊フィルタ(行インデックスマッパ)。全行は内部の `_allRows`(マスタ)に保持したまま、**絞り込んだ可視部分集合だけを描画**します。要点は**絞り込み後の描画・ライブ検索が軽い**こと(自社調べ:10万行を保持して約200行に絞り込み → 初回描画 ≈ 0.5 秒、絞り込み済みのライブ検索 ≈ 数 ms)→ 動く例: [`examples/filter-live.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-live.html)。
242
+ なお**全件をそのままスクロール表示**したい大量行は、仮想スクロール `virtual: true` を使います(下記)。
242
243
 
243
244
  ```js
244
245
  grid.filter(row => row[2] >= 100); // 述語: (row, masterIndex, src) => bool
@@ -254,9 +255,21 @@ grid.getAllRows(); // マスタ全行(フィルタ中で
254
255
  - 制限(v1): フィルタ中は **列の挿入削除・行移動・セル結合**はロック(解除してから)。`filter`/`clearFilter` は**Undo履歴をクリア**(ビューindex基準の履歴が境界を跨ぐと不整合になるため)。
255
256
  - **フィルタUI**(ヘッダの絞り込み等)はコアに入れず**プラグイン** `tss-filter`(下記)で。素朴な `setData` 差し替え方式の旧サンプルは [`examples/filter.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter.html)。
256
257
 
258
+ ### 仮想スクロール(`virtual: true`)
259
+
260
+ 大量行を**全件そのままスクロール表示**したい時に。全行は `data` に保持したまま、**画面に見える窓ぶん+バッファだけを描画**します(固定行高ウィンドウイング)。全件を一度に DOM 化しないので**初期描画が一瞬・スクロールも軽い**。有効化は**オプション一行**だけ。
261
+
262
+ ```js
263
+ new TssGrid(el, { virtual: true, data, /* … */ });
264
+ new TssGrid(el, { virtual: { buffer: 10 }, data }); // 窓の上下バッファ行数(既定6)
265
+ ```
266
+ - **自社調べ**(headless Chrome / Win11): 10 万行で**初回描画 ≈ 46 ms**(全件描画 ≈ 10.5 秒に対して)。スクロール 1 コマの窓差し替え ≈ 3 ms。動く例: [`examples/virtual.html`](https://tssgrid.threestarsoftware.co.jp/examples/virtual.html)。
267
+ - 制約(v1): **行高一定**/**固定行列・折り返し・セル結合は非対応**(指定時は自動で無効化+警告)。行番号の桁数が増える場合は `rowHeaderWidth` で行ヘッダ幅を広げられます。
268
+ - フィルタとの関係: `filter()` は「絞り込んだ部分集合だけ描く」軽さ、`virtual` は「絞らず全件をスクロールする」軽さ。用途で使い分け(v1 では併用は未対応)。
269
+
257
270
  ### 同梱プラグイン: ヘッダ オートフィルタ(filter UI)
258
271
 
259
- `plugins/tss-filter.js`+`.css`= 各見出しの**ロート(漏斗)アイコン**をクリックで**値チェックリスト+検索**のポップアップ(Excel風。ソートの▲▼と区別できるアイコン)。複数列の条件は **AND**。中身は上の**コア `filter()` を呼ぶだけ**。動く例: [`examples/filter-ui.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-ui.html)。
272
+ `plugins/tss-filter.js`+`.css`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= 各見出しの**ロート(漏斗)アイコン**をクリックで**値チェックリスト+検索**のポップアップ(Excel風。ソートの▲▼と区別できるアイコン)。複数列の条件は **AND**。中身は上の**コア `filter()` を呼ぶだけ**。動く例: [`examples/filter-ui.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-ui.html)。
260
273
 
261
274
  ```html
262
275
  <link rel="stylesheet" href="plugins/tss-filter.css">
@@ -271,7 +284,7 @@ grid.getPlugin('filter').clearAll(); // 全条件クリア
271
284
 
272
285
  ### 同梱プラグイン: 共有マスタ(複数グリッドで1データ)
273
286
 
274
- `plugins/tss-shared.js`= **1つのマスタ(同じ行データ)を複数グリッドで共有**。左右を**別フィルタ/別ソート**で表示でき、**片方の編集がもう片方の同じ行へ即反映**(行参照を共有)。A系/B系を並べて突き合わせ編集する用途。動く例: [`examples/shared-master.html`](https://tssgrid.threestarsoftware.co.jp/examples/shared-master.html)。
287
+ `plugins/tss-shared.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= **1つのマスタ(同じ行データ)を複数グリッドで共有**。左右を**別フィルタ/別ソート**で表示でき、**片方の編集がもう片方の同じ行へ即反映**(行参照を共有)。A系/B系を並べて突き合わせ編集する用途。動く例: [`examples/shared-master.html`](https://tssgrid.threestarsoftware.co.jp/examples/shared-master.html)。
275
288
 
276
289
  ```html
277
290
  <script src="plugins/tss-shared.js"></script>
@@ -322,7 +335,7 @@ TssGrid.registerPlugin('autosave', autoSavePlugin); // 名前登録 → plugin
322
335
 
323
336
  ## カスタムエディタ
324
337
 
325
- 組込エディタ(text/dropdown/checkbox/date/time/number)以外に、**独自の編集UI**を列に差せます。`columns[c].editor` に**薄いオブジェクト契約**を渡すだけ。
338
+ 組込エディタ(text/dropdown/checkbox/date/time/number)以外に、**独自の編集UI**を列に差せます。`columns[c].editor` に**薄いオブジェクト契約**を渡すだけ<!--DEV-->(無依存)<!--/DEV-->。
326
339
 
327
340
  ```js
328
341
  const myEditor = {
@@ -349,7 +362,7 @@ new TssGrid(el, { columns: [{ editor: myEditor }] });
349
362
 
350
363
  ### 同梱プラグイン: 休日カレンダー対応の日付ピッカー
351
364
 
352
- `plugins/tss-calendar.js`= ネイティブ `<input type=date>` では出来ない**会社の休日カレンダー(JSON)対応**の日付ピッカー。勤怠・シフト・予約などで効きます(動く例: [`examples/calendar.html`](https://tssgrid.threestarsoftware.co.jp/examples/calendar.html))。
365
+ `plugins/tss-calendar.js`<!--DEV-->(**無依存**)<!--/DEV-->= ネイティブ `<input type=date>` では出来ない**会社の休日カレンダー(JSON)対応**の日付ピッカー。勤怠・シフト・予約などで効きます(動く例: [`examples/calendar.html`](https://tssgrid.threestarsoftware.co.jp/examples/calendar.html))。
353
366
 
354
367
  ```html
355
368
  <link rel="stylesheet" href="plugins/tss-calendar.css">
@@ -370,7 +383,7 @@ new TssGrid(el, { columns: [{ type: 'date', editor: cal, format: 'yyyy/mm/dd' }]
370
383
 
371
384
  ### 同梱プラグイン: 時刻ピッカー(時/分プルダウン)
372
385
 
373
- `plugins/tss-time.js`= 業務の時刻入力向けの**時/分プルダウン**ピッカー(クロックフェイスのようなリッチさは省き、任意時刻を素早く)。保存値は常に **24h `'HH:MM'`**(組込 `type:'time'` と互換)。動く例: [`examples/time.html`](https://tssgrid.threestarsoftware.co.jp/examples/time.html)。
386
+ `plugins/tss-time.js`<!--DEV-->(**無依存**)<!--/DEV-->= 業務の時刻入力向けの**時/分プルダウン**ピッカー(クロックフェイスのようなリッチさは省き、任意時刻を素早く)。保存値は常に **24h `'HH:MM'`**(組込 `type:'time'` と互換)。動く例: [`examples/time.html`](https://tssgrid.threestarsoftware.co.jp/examples/time.html)。
374
387
 
375
388
  ```html
376
389
  <link rel="stylesheet" href="plugins/tss-time.css">
@@ -385,7 +398,7 @@ new TssGrid(el, { columns: [{ type: 'time', editor: t }] });
385
398
 
386
399
  ### 同梱プラグイン: 派生列(formula / computed column)
387
400
 
388
- `plugins/tss-formula.js`=「その行の値だけから計算する列」を `columns[c].formula` 関数で書けるようにする。金額=数量×単価、税込=金額×1.1 のような**派生列**を宣言的に。動く例: [`examples/formula.html`](https://tssgrid.threestarsoftware.co.jp/examples/formula.html)。
401
+ `plugins/tss-formula.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->=「その行の値だけから計算する列」を `columns[c].formula` 関数で書けるようにする。金額=数量×単価、税込=金額×1.1 のような**派生列**を宣言的に。動く例: [`examples/formula.html`](https://tssgrid.threestarsoftware.co.jp/examples/formula.html)。
389
402
 
390
403
  ```html
391
404
  <script src="plugins/tss-formula.js"></script>
@@ -408,7 +421,7 @@ new TssGrid(el, {
408
421
 
409
422
  ### 同梱プラグイン: 列の集計(totals / 縦の SUM・平均…)
410
423
 
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)。
424
+ `plugins/tss-totals.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= 列方向の集計(`sum`/`avg`/`count`/`min`/`max`/独自関数)を担う。**計算だけで描画はしない**=結果を `onTotals` で受けて好きな場所へ(フッタ・ラベル・**別の TssGrid の合計表**でも)。`formula` と併用すると派生列の後に集計が連動。動く例: [`examples/formula.html`](https://tssgrid.threestarsoftware.co.jp/examples/formula.html)(明細=formula/合計=totals)。
412
425
 
413
426
  ```html
414
427
  <script src="plugins/tss-totals.js"></script>
@@ -429,7 +442,7 @@ new TssGrid(el, {
429
442
 
430
443
  ### 同梱プラグイン: 条件付き合計(tss-sum / ラベル付き合計行・整列表示)
431
444
 
432
- `plugins/tss-sum.js`= **ラベル付きの"合計行"**(小計・消費税10%・消費税8%・合計 など)を、**条件(`when`)+対象(`of`)を宣言**で定義して作る。`tss-totals` が「列ごと1値」なのに対し、こちらは**同じ列を条件別に何本でも集計**でき(消費税10%と8%は両方 amount 由来=per-column では衝突する)、**ソースと同一列構成の readonly 集計グリッドを生成**するので**金額が縦にピッタリ揃う**(画面でも印刷でも)。請求書・見積などの合計欄に。動く例: [`examples/invoice.html`](https://tssgrid.threestarsoftware.co.jp/examples/invoice.html)(インボイス請求書の合計欄)。
445
+ `plugins/tss-sum.js`<!--DEV-->(**無依存・コア無改変**/ラベル横結合に TssMerge を任意利用)<!--/DEV-->= **ラベル付きの"合計行"**(小計・消費税10%・消費税8%・合計 など)を、**条件(`when`)+対象(`of`)を宣言**で定義して作る。`tss-totals` が「列ごと1値」なのに対し、こちらは**同じ列を条件別に何本でも集計**でき(消費税10%と8%は両方 amount 由来=per-column では衝突する)、**ソースと同一列構成の readonly 集計グリッドを生成**するので**金額が縦にピッタリ揃う**(画面でも印刷でも)。請求書・見積などの合計欄に。動く例: [`examples/invoice.html`](https://tssgrid.threestarsoftware.co.jp/examples/invoice.html)(インボイス請求書の合計欄)。
433
446
 
434
447
  ```html
435
448
  <script src="plugins/tss-merge.js"></script> <!-- ラベルの横結合に利用 -->
@@ -453,7 +466,7 @@ const sum = TssSum.attach(grid, document.getElementById('sumgrid'), {
453
466
 
454
467
  ### 同梱プラグイン: 残高/累計(running total・縦の連鎖)
455
468
 
456
- `plugins/tss-running.js`=「**前の行の結果 + この行の増減**」で縦に積む列。`formula`(行内)でも `totals`(全体合計)でも出せない**縦方向の連鎖**を担う。出納帳/通帳の**残高**、在庫受払の**在庫数**、経費・ポイントの**累計**など。動く例: [`examples/running.html`](https://tssgrid.threestarsoftware.co.jp/examples/running.html)(現金出納帳=入金・出金→残高)。
469
+ `plugins/tss-running.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->=「**前の行の結果 + この行の増減**」で縦に積む列。`formula`(行内)でも `totals`(全体合計)でも出せない**縦方向の連鎖**を担う。出納帳/通帳の**残高**、在庫受払の**在庫数**、経費・ポイントの**累計**など。動く例: [`examples/running.html`](https://tssgrid.threestarsoftware.co.jp/examples/running.html)(現金出納帳=入金・出金→残高)。
457
470
 
458
471
  ```html
459
472
  <script src="plugins/tss-running.js"></script>
@@ -472,11 +485,11 @@ new TssGrid(el, {
472
485
  - `column`(残高列) / `initial`(前残) / `delta` か `step` / `skipEmpty`(全列空の行は連鎖を進めず空表示、既定true)。
473
486
  - **行ドラッグ移動(rowReorder)併用時**は順序が変わるので `onAfterRowMove` で `handle.recompute()` を呼ぶ。
474
487
 
475
- > 「**行=formula/列=totals/縦=running**」の3点が揃う。いずれも `setValueRaw`+`onAfterChange` ラップで動く**プラグイン**。
488
+ > 「**行=formula/列=totals/縦=running**」の3点が揃う。いずれも `setValueRaw`+`onAfterChange` ラップで動く**<!--DEV-->コア無改変<!--/DEV-->プラグイン**。
476
489
 
477
490
  ### 同梱プラグイン: 氏名→フリガナ 自動入力(furigana・辞書不要)
478
491
 
479
- `plugins/tss-furigana.js`(**辞書不要**)= 漢字を入力(IME変換)すると**読みが別列に自動で入る**(スマホ連絡先の よみがな 自動入力と同じ)。辞書引きではなく、**IME変換の composition イベントから「変換前のかな」を拾う**方式。TssGrid は**アクティブセルに本物の `<input>` を重ねる**設計なので composition が確実に取れる=「**IMEに強い**」を機能で体現。動く例: [`examples/furigana.html`](https://tssgrid.threestarsoftware.co.jp/examples/furigana.html)。
492
+ `plugins/tss-furigana.js`(**<!--DEV-->無依存・<!--/DEV-->辞書不要<!--DEV-->・コア無改変<!--/DEV-->**)= 漢字を入力(IME変換)すると**読みが別列に自動で入る**(スマホ連絡先の よみがな 自動入力と同じ)。辞書引きではなく、**IME変換の composition イベントから「変換前のかな」を拾う**方式。TssGrid は**アクティブセルに本物の `<input>` を重ねる**設計なので composition が確実に取れる=「**IMEに強い**」を機能で体現。動く例: [`examples/furigana.html`](https://tssgrid.threestarsoftware.co.jp/examples/furigana.html)。
480
493
 
481
494
  ```html
482
495
  <script src="plugins/tss-furigana.js"></script>
@@ -493,7 +506,7 @@ new TssGrid(el, {
493
506
 
494
507
  ### 同梱プラグイン: セル内ミニ計算式(calc・=12*3)
495
508
 
496
- `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)。
509
+ `plugins/tss-calc.js`(**<!--DEV-->無依存・<!--/DEV-->eval/Function 不使用<!--DEV-->・コア無改変<!--/DEV-->**)= セルに `=12*3+5` と打つと確定時に **41** に。四則 `+ - * /`・括弧・小数対応の**自前再帰下降パーサ**(CSP安全)。`columns[c].parse`(入力変換)seam に乗るだけ。動く例: [`examples/calc.html`](https://tssgrid.threestarsoftware.co.jp/examples/calc.html)。
497
510
 
498
511
  ```html
499
512
  <script src="plugins/tss-calc.js"></script>
@@ -509,7 +522,7 @@ plugins: [ TssCalc.plugin({ columns: ['quantity', 'price'] }) ] // columns 省
509
522
 
510
523
  ### 同梱プラグイン: オートコンプリート(autocomplete エディタ)
511
524
 
512
- `plugins/tss-autocomplete.js`= 自由入力+**候補の絞り込み**。固定 dropdown の上位版で「だいたい決まってるが新規もある」項目(取引先・商品名)向け。**IME変換中の Enter は奪わない**(変換確定が最優先)。動く例: [`examples/autocomplete.html`](https://tssgrid.threestarsoftware.co.jp/examples/autocomplete.html)。
525
+ `plugins/tss-autocomplete.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= 自由入力+**候補の絞り込み**。固定 dropdown の上位版で「だいたい決まってるが新規もある」項目(取引先・商品名)向け。**IME変換中の Enter は奪わない**(変換確定が最優先)。動く例: [`examples/autocomplete.html`](https://tssgrid.threestarsoftware.co.jp/examples/autocomplete.html)。
513
526
 
514
527
  ```html
515
528
  <link rel="stylesheet" href="plugins/tss-autocomplete.css">
@@ -527,7 +540,7 @@ columns: [
527
540
 
528
541
  ### 同梱プラグイン: セル内スパークライン(sparkline・ミニグラフ)
529
542
 
530
- `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)(月次売上→動向グラフ・編集でライブ更新)。
543
+ `plugins/tss-sparkline.js`(**<!--DEV-->無依存・<!--/DEV-->SVG自前描画<!--DEV-->・コア無改変<!--/DEV-->**)= セルの数列を**ミニ折れ線/棒**で描く。`columns[c].html:true` + `format`(format が返す SVG がセルに入る)seam に乗るだけ。Chart.js 等は積まない。動く例: [`examples/sparkline.html`](https://tssgrid.threestarsoftware.co.jp/examples/sparkline.html)(月次売上→動向グラフ・編集でライブ更新)。
531
544
 
532
545
  ```html
533
546
  <script src="plugins/tss-sparkline.js"></script>
@@ -541,11 +554,11 @@ columns: [
541
554
  formula: (row) => ['m4','m5','m6','m7','m8','m9'].map(k=>row[k]).filter(v=>v!=='').join(',') }
542
555
  ```
543
556
  - `type`('line'/'bar') / `width` / `height` / `color` / `strokeWidth` / `fill`(折れ線下の塗り) / `min`・`max`(既定は自動)。
544
- - 数値しか SVG に入れない=安全。`formula`/`html-cells` と同じ仕組みで、**有料グリッドの "セル内グラフ" を無料で**。
557
+ - 数値しか SVG に入れない=安全。`formula`/`html-cells` と同じ仕組みで、**有料グリッドの "セル内グラフ" を無料<!--DEV-->・無依存<!--/DEV-->で**。
545
558
 
546
559
  ### 同梱プラグイン: セル内 横棒グラフ(bar・データバー)
547
560
 
548
- `plugins/tss-bar.js`= セルの数値を**横棒**で描く(認知率・進捗%・スコア)。`html:true`+`format` seam。`type:'number'` 併用で**バーのまま編集**(値を打つと伸びる)。動く例: [`examples/bar.html`](https://tssgrid.threestarsoftware.co.jp/examples/bar.html)。
561
+ `plugins/tss-bar.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= セルの数値を**横棒**で描く(認知率・進捗%・スコア)。`html:true`+`format` seam。`type:'number'` 併用で**バーのまま編集**(値を打つと伸びる)。動く例: [`examples/bar.html`](https://tssgrid.threestarsoftware.co.jp/examples/bar.html)。
549
562
 
550
563
  ```html
551
564
  <script src="plugins/tss-bar.js"></script>
@@ -560,7 +573,7 @@ columns: [
560
573
 
561
574
  ### 同梱プラグイン: ヒートマップ / 背景データバー(cellStyle)
562
575
 
563
- `plugins/tss-heatmap.js`= コアの `cellStyle` フックに乗せる**連続色(ヒートマップ)**と**背景データバー**。`tss-bar.js`(前景の棒+ラベル)と違い、**セル背景**を塗る=**数値はそのまま見せて編集できる**。動く例: [`examples/heatmap.html`](https://tssgrid.threestarsoftware.co.jp/examples/heatmap.html)。
576
+ `plugins/tss-heatmap.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= コアの `cellStyle` フックに乗せる**連続色(ヒートマップ)**と**背景データバー**。`tss-bar.js`(前景の棒+ラベル)と違い、**セル背景**を塗る=**数値はそのまま見せて編集できる**。動く例: [`examples/heatmap.html`](https://tssgrid.threestarsoftware.co.jp/examples/heatmap.html)。
564
577
 
565
578
  ```html
566
579
  <script src="plugins/tss-heatmap.js"></script>
@@ -574,7 +587,7 @@ columns: [
574
587
  - `TssHeatmap`: `min`/`max`(明示。セル単位呼び出しで列範囲を知らないため)/ `colors`(2色以上のストップ) or `low`/`high` / `text`(自動文字色, 既定on)。
575
588
  - `TssDataBar`: `min`/`max` / `color` / `align`('left'/'right')。編集で即更新。
576
589
 
577
- > 「セル内グラフ」3点: **前景の折れ線/棒=`html:true`+`format`(tss-sparkline / tss-bar)**、**背景の連続色/バー=`cellStyle`(tss-heatmap)**。どれもノービルド。
590
+ > 「セル内グラフ」3点: **前景の折れ線/棒=`html:true`+`format`(tss-sparkline / tss-bar)**、**背景の連続色/バー=`cellStyle`(tss-heatmap)**。どれも<!--DEV-->無依存・<!--/DEV-->ノービルド。
578
591
 
579
592
  ### グラフ連携(Chart.js などと)— 編集でライブ更新
580
593
 
@@ -588,7 +601,7 @@ new TssGrid(el, { columns, data, onAfterChange: () => { chart.data = toChartData
588
601
 
589
602
  ### 同梱プラグイン: 2択トグル(toggle・有効|無効)
590
603
 
591
- `plugins/tss-toggle.js`+`.css`= ラジオの2択を**並んだセグメント(有効|無効)**で表示、クリックで切替。チェックボックスより**2ラベルが見える**。値は dropdown 同様 **value を保存**。動く例: [`examples/toggle.html`](https://tssgrid.threestarsoftware.co.jp/examples/toggle.html)。
604
+ `plugins/tss-toggle.js`+`.css`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= ラジオの2択を**並んだセグメント(有効|無効)**で表示、クリックで切替。チェックボックスより**2ラベルが見える**。値は dropdown 同様 **value を保存**。動く例: [`examples/toggle.html`](https://tssgrid.threestarsoftware.co.jp/examples/toggle.html)。
592
605
 
593
606
  ```html
594
607
  <link rel="stylesheet" href="plugins/tss-toggle.css">
@@ -605,7 +618,7 @@ plugins: [ TssToggle.plugin({ columns: {
605
618
 
606
619
  ### 同梱プラグイン: セル結合(merge・横/縦/矩形)
607
620
 
608
- `plugins/tss-merge.js`= データセルの**矩形結合**(`colspan`×`rowspan`)。左上の**アンカーが値・編集・選択の対象**で、覆われた従属セルは**描画スキップ+読み取り専用**(貼り付け・フィルは弾く)。動く例: [`examples/merge.html`](https://tssgrid.threestarsoftware.co.jp/examples/merge.html)。(見出しの結合は `nestedHeaders` を参照)
621
+ `plugins/tss-merge.js`<!--DEV-->(**無依存**)<!--/DEV-->= データセルの**矩形結合**(`colspan`×`rowspan`)。左上の**アンカーが値・編集・選択の対象**で、覆われた従属セルは**描画スキップ+読み取り専用**(貼り付け・フィルは弾く)。動く例: [`examples/merge.html`](https://tssgrid.threestarsoftware.co.jp/examples/merge.html)。(見出しの結合は `nestedHeaders` を参照)
609
622
 
610
623
  ```html
611
624
  <script src="plugins/tss-merge.js"></script>
@@ -631,7 +644,7 @@ plugins: [ TssMerge.plugin({ merges: [
631
644
 
632
645
  ### 同梱プラグイン: 同期スクロール(syncscroll・左右2表を並べて比較)
633
646
 
634
- `plugins/tss-syncscroll.js`= 並べた複数グリッドのスクロールを連動(Excel「並べて比較+同時にスクロール」相当)。A系/B系で別々に絞り込んだ表を**突き合わせながら編集**する用途に。動く例: [`examples/sync-scroll.html`](https://tssgrid.threestarsoftware.co.jp/examples/sync-scroll.html)。
647
+ `plugins/tss-syncscroll.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= 並べた複数グリッドのスクロールを連動(Excel「並べて比較+同時にスクロール」相当)。A系/B系で別々に絞り込んだ表を**突き合わせながら編集**する用途に。動く例: [`examples/sync-scroll.html`](https://tssgrid.threestarsoftware.co.jp/examples/sync-scroll.html)。
635
648
 
636
649
  ```html
637
650
  <script src="plugins/tss-syncscroll.js"></script>
@@ -898,7 +911,7 @@ new TssGrid(el, {
898
911
 
899
912
  ### 入力の正規化 / 表示の整形(`parse` / `format`)
900
913
 
901
- **保存値**(`getData()` が返す内部値)と、**入力**・**表示**を分離できます。外部ライブラリは同梱せず、必要なら列のフックに差し込む方針です。
914
+ **保存値**(`getData()` が返す内部値)と、**入力**・**表示**を分離できます。外部ライブラリは同梱せず、必要なら列のフックに差し込む方針です<!--DEV-->(コアは無依存のまま)<!--/DEV-->。
902
915
 
903
916
  | フック | 向き | 役割 |
904
917
  |---|---|---|
@@ -1108,7 +1121,7 @@ command は `{ apply(), revert(), label }`。
1108
1121
  ## 制限
1109
1122
 
1110
1123
  - 外部(Excel等)→グリッドの貼り付けはブラウザのクリップボード読取許可が必要。`file://` で読めない場合は http(ローカルサーバ)で開く。グリッド間・グリッド内のコピペは内部バッファで常に動く。
1111
- - 仮想スクロール未対応(大量行が必要なら今後)。日付の連番フィルはコピー扱い(数値のみ連番)。
1124
+ - 仮想スクロールは `virtual: true` で対応(固定行高・固定行列/折り返し/セル結合は非対応)。日付の連番フィルはコピー扱い(数値のみ連番)。
1112
1125
 
1113
1126
  ## License
1114
1127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threestarsoftware/tssgrid",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
5
5
  "license": "MIT",
6
6
  "author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
@@ -1,4 +1,5 @@
1
1
  /*! TssAutocomplete — 候補サジェスト付きテキスト入力(TssGrid カスタムエディタ)。
2
+ * DEV: 無依存・コア無改変(dev専用メモ・公開時strip)
2
3
  *
3
4
  * 自由入力+候補の絞り込み。固定 dropdown の上位版(取引先名・商品名など「だいたい決まってるが新規もある」項目向け)。
4
5
  * **IME変換中の Enter は奪わない**(変換確定が最優先=TssGrid の流儀)。
@@ -1,4 +1,5 @@
1
1
  /*! TssBar — セル内 横棒グラフ(データバー)。
2
+ * DEV: 無依存・コア無改変
2
3
  *
3
4
  * セルの数値を**横棒**で描く(アンケートの認知率バー、進捗%、スコアなど)。
4
5
  * `columns[c].html:true` + `format` 関数 seam に乗るだけ(format が返す <div> がセルに入る)。
@@ -1,4 +1,5 @@
1
1
  /*! TssCalc — セル内ミニ計算式(=12*3+5 → 41)。**eval/Function 不使用**。
2
+ * DEV: 無依存・コア無改変
2
3
  *
3
4
  * セルに数式を打つと確定時に計算結果へ。四則 + - * / と括弧 () と小数に対応。
4
5
  * パーサは再帰下降の自前実装=**eval を使わない**(CSP安全)。`columns[c].parse` seam に乗るだけ。
@@ -1,4 +1,5 @@
1
1
  /*! TssCalendar — 日付ピッカー(TssGrid のカスタムエディタ / 単体ウィジェット両用)
2
+ * DEV: 無依存
2
3
  * 休日カレンダーを JSON で渡せる(勤怠・シフト等向け)。CSS は tss-calendar.css をクラス/変数で自由に。
3
4
  * 使い方(グリッド): columns: [{ type:'date', editor: TssCalendar({ holidays }) }]
4
5
  * 使い方(単体) : TssCalendar({holidays}).openAt(anchorEl, isoValue, onPick)
@@ -1,4 +1,5 @@
1
1
  /*! TssDatetime — 日付+時刻を同時に設定するピッカー(TssGrid カスタムエディタ / 単体ウィジェット両用)
2
+ * DEV: 無依存。カレンダー部は tss-calendar.css、時刻部は tss-time.css のクラスを再利用+薄い tss-datetime.css。
2
3
  * 保存値は常に 'YYYY-MM-DDTHH:MM'(kintone DATETIME と相互変換しやすい正規形。表示は列 format で整形)。
3
4
  * 使い方(グリッド): columns: [{ type:'text', editor: TssDatetime({ step:5 }), format: TssDatetime.format }]
4
5
  * 使い方(単体) : TssDatetime({step:15}).openAt(anchorEl, 'YYYY-MM-DDTHH:MM', onPick)
@@ -1,4 +1,5 @@
1
1
  /*! TssFilter — ヘッダのオートフィルタUI(Excel風)。コアの非破壊 filter() を呼ぶだけ。
2
+ * DEV: 無依存・コア無改変
2
3
  *
3
4
  * 各リーフ見出しにロート(漏斗)アイコンを出し(ソートの▲▼と区別)、クリックで「値チェックリスト+絞り込み検索」ポップアップ。
4
5
  * 複数列の条件は AND。適用は grid.filter(pred)/全解除で grid.clearFilter()。フィルタ済み列は印が付く。
@@ -1,4 +1,5 @@
1
1
  /*! TssFormula — 行スコープの派生列(computed column)プラグイン。
2
+ * DEV: コア無改変
2
3
  *
3
4
  * 「その行の値だけから計算する列」を columns 定義に formula 関数で書けるようにする。
4
5
  * 例: 金額 = 数量 × 単価 / 税込 = 金額 × 1.1 / 勤怠の勤務時間 など。
@@ -1,4 +1,5 @@
1
1
  /*! TssFurigana — 氏名→フリガナ 自動入力プラグイン。**辞書不要**。
2
+ * DEV: コア無改変
2
3
  *
3
4
  * スマホの連絡先のように「漢字を入力すると よみ が別列に自動で入る」。仕組みは**辞書引きではなく**、
4
5
  * IME変換の **composition イベントから「変換前の読み(かな)」を拾う**だけ。だから辞書データを持たない。
@@ -1,4 +1,5 @@
1
1
  /*! TssHeatmap / TssDataBar — セル背景の連続ビジュアライズ(cellStyle フック用)。
2
+ * DEV: 無依存・コア無改変
2
3
  *
3
4
  * コアの `cellStyle`(セルにインラインCSSを差すフック)に乗せる。cellClass(クラス=離散色)では出せない
4
5
  * **値に応じた連続色(ヒートマップ)/背景データバー**を、**編集できる数値セルのまま**実現する。
@@ -1,4 +1,5 @@
1
1
  /*! TssMerge — 矩形セル結合(colspan×rowspan)の軽量プラグイン。
2
+ * DEV: コアの薄い継ぎ目を使う
2
3
  *
3
4
  * アンカー=(r,c)=左上が値・編集・選択の対象。覆われた従属セルは描画スキップ+読取専用(貼付/フィルは弾く)。
4
5
  * 行/列の挿入削除では結合座標がコア側で縦横対称に追従(undo 対応)。
@@ -1,4 +1,5 @@
1
1
  /*! TssMessages — システム共通メッセージのカタログ作成ヘルパー(CSV/行配列 → grid.messages 用マップ)。
2
+ * DEV: 無依存
2
3
  *
3
4
  * エラー本文をコードで一元管理(トーン統一・多言語・差し替え)。validator は { code, params } を返すだけにし、
4
5
  * 本文は外部の設定(CSV/JSON/XML→行配列)に置く。ここはその設定を grid.messages の形に変換する係。
@@ -1,4 +1,5 @@
1
1
  /*! TssRunningTotal — 残高/累計(running total)プラグイン。
2
+ * DEV: コア無改変
2
3
  *
3
4
  * 「前の行の結果 + この行の増減」で縦に積む列を作る。formula(行内) でも totals(全体合計) でも出せない
4
5
  * **縦方向の連鎖**を担当。出納帳/通帳の残高、在庫受払の在庫数、経費・ポイントの累計など。
@@ -1,4 +1,5 @@
1
1
  /*! TssShared — 1つのマスタ(行データ)を複数グリッドで共有(A系/B系を別フィルタ・別ソートで並べて同時編集)。
2
+ * DEV: 無依存・コア無改変
2
3
  * 各ビューは同じ「行参照」を持つ=片方の編集がもう片方へ即反映(該当行を再描画)。
3
4
  * **行の挿入/削除も伝播**(マスタ&他ビューに反映)。**配列データ/オブジェクト配列データ どちらも対応**。
4
5
  *
@@ -1,4 +1,5 @@
1
1
  /*! TssSparkline — セル内スパークライン(ミニ折れ線/棒)。**SVG自前描画**。
2
+ * DEV: 無依存・コア無改変
2
3
  *
3
4
  * セルの値(カンマ/空白区切りの数列 "388,329,220,348,323,165")を**小さなグラフ**として描く。
4
5
  * 仕組みは `columns[c].html:true` + `format` 関数=format が返す SVG 文字列がそのままセルに入る(html-cells と同じ seam)。
@@ -1,4 +1,5 @@
1
1
  /*! TssSum — 条件付き合計グリッドの軽量プラグイン。
2
+ * DEV: コア無改変・無依存(ラベルの横結合に TssMerge を任意利用)
2
3
  *
3
4
  * tss-totals との住み分け: tss-totals は「列ごとの集計(sum/avg/min/max/count)」を onTotals で返す。
4
5
  * tss-sum は「同じ列を条件別に複数集計=ラベル付きの"合計行"(小計/消費税10%/消費税8%/合計 など)」+
@@ -1,4 +1,5 @@
1
1
  /*! TssSyncScroll — 複数グリッドの同期スクロール(Excel「並べて比較+同時にスクロール」相当)。
2
+ * DEV: 無依存・コア無改変
2
3
  *
3
4
  * 左右(or 上下)に並べた 2枚以上のグリッドのスクロールを連動。A系/B系で別々に絞り込んだ表を
4
5
  * 突き合わせながら編集する用途に。`grid.wrap`(overflow:auto のスクロール枠) の scroll を相手へ転写。
@@ -1,4 +1,5 @@
1
1
  /*! TssTime — 時刻ピッカー(TssGrid のカスタムエディタ / 単体ウィジェット両用)
2
+ * DEV: 無依存
2
3
  * 時/分プルダウン方式(業務の任意時刻入力向き)。保存値は常に 24h 'HH:MM'(type:'time' と互換)。
3
4
  * 使い方(グリッド): columns: [{ type:'time', editor: TssTime({ step:15 }) }]
4
5
  * opts: step(分刻み,既定1) / hour12(AM/PM表示,既定false) / icon(既定 '🕐') / openOnClick(既定true) / className
@@ -1,4 +1,5 @@
1
1
  /*! TssToggle — 2択セグメント・トグル(有効|無効・公開|非公開・男|女 …)。
2
+ * DEV: 無依存・コア無改変
2
3
  *
3
4
  * ラジオの2択を「並んだピル(セグメント)」で表示し、クリックでどちらかに切替。値は dropdown と同じく
4
5
  * 選んだ option の value を保存(getData も value)。html:true+format で描画、クリックは委譲で拾う。
@@ -1,4 +1,5 @@
1
1
  /*! TssTotals — 列方向の集計(合計/平均/件数/最小/最大)プラグイン。**計算だけ=描画は利用者側**。
2
+ * DEV: コア無改変
2
3
  *
3
4
  * 「縦の合計」をコアに埋めると合計行の置き場が minSpareRows/ソート/空行処理と衝突して肥大化する。
4
5
  * そこで **集計ロジックだけ** を担い、結果は onTotals コールバック / getTotals() で返す。
package/src/tssgrid.css CHANGED
@@ -66,6 +66,8 @@
66
66
  .tssgrid .tg-table tr:last-child > th { border-bottom: none; }
67
67
  .tssgrid .tg-table tr > td:last-child,
68
68
  .tssgrid .tg-table tr > th:last-child { border-right: none; }
69
+ /* 仮想スクロールのスペーサ行(窓の上下を埋めて総高を保つだけの不可視行)。罫線/背景/余白を持たない。 */
70
+ .tssgrid .tg-table tr.tg-vspace > td { border: none; padding: 0; background: transparent; }
69
71
  .tssgrid .tg-table th { background: var(--tg-colheader-bg); color: var(--tg-header-fg); font-weight: 600; text-align: center; }
70
72
  /* z-index 階層(.tg-wrap 内): 通常セル 0 < 本体sticky(rowhead/frozen) 2 < 選択オーバーレイ 3
71
73
  < editor/select 4 < fillhandle 5 < ヘッダ thead 6 < 角/固定列ヘッダ 7 < リサイズ線 8。
package/src/tssgrid.d.ts CHANGED
@@ -126,6 +126,11 @@ export interface TssGridOptions {
126
126
  cursor?: boolean;
127
127
  stretchH?: 'none' | 'last' | 'all';
128
128
  rowHeaders?: boolean;
129
+ /** 行番号列の幅(px)。既定 40。大量行で桁数が増える時に広げる。 */
130
+ rowHeaderWidth?: number;
131
+ /** 仮想スクロール(固定行高ウィンドウイング)。大量行を可視窓だけ描画。true か { buffer:窓上下バッファ行数(既定6) }。
132
+ * 制約(v1): 行高一定/固定行列・折り返し・セル結合は非対応(指定時は自動無効化)。 */
133
+ virtual?: boolean | { buffer?: number };
129
134
  colHeaders?: boolean;
130
135
  hiddenColumns?: number[];
131
136
  wordWrap?: boolean;
package/src/tssgrid.js CHANGED
@@ -183,6 +183,7 @@
183
183
  this.resizeRows = opts.resizeRows !== false;
184
184
  // 行/列ヘッダの表示(false で行番号列・ヘッダ行を隠す)
185
185
  this.rowHeaders = opts.rowHeaders !== false;
186
+ this.rowHeaderW = (opts.rowHeaderWidth | 0) || 40; // 行番号列の幅(px)。大量行で桁数が増える時に広げる
186
187
  this.colHeaders = opts.colHeaders !== false;
187
188
  // 行ヘッダの「つまみ」をドラッグして行を並べ替える(行番号列が必要)
188
189
  this.rowReorder = !!opts.rowReorder;
@@ -212,6 +213,16 @@
212
213
  this.autoWrapRow = !!opts.autoWrapRow; // Tab で行末→次行頭に折り返し
213
214
  this.autoWrapCol = !!opts.autoWrapCol; // Enter で列末→次列頭に折り返し
214
215
  this.navSkipReadOnly = !!opts.navSkipReadOnly; // Tab/Enter 送りで readonly セルを飛ばす(伝票入力向け=編集する所だけ移動)
216
+ // 仮想スクロール(簡易版・固定行高ウィンドウイング): 大量行で「可視窓+バッファ」だけ描画。
217
+ // 全行は data に保持・行座標は絶対のまま、tbody は窓ぶん+上下スペーサ行のみ。幾何はモデル(一定行高)。
218
+ // 制約(v1): 行高一定/セル結合・固定行列・折り返し非対応。指定時は無効化して警告。OFF時は一切作動せず従来通り。
219
+ this.virtual = !!opts.virtual;
220
+ this._vBuf = (opts.virtual && typeof opts.virtual === 'object' && opts.virtual.buffer) || 6; // 窓の上下バッファ行数
221
+ this._vStart = 0; this._vEnd = 0; this._vRAF = 0; this._vRendered = false;
222
+ if (this.virtual && (this.frozenRows || this.frozenCols || this.wordWrap)) {
223
+ try { console.warn('[TssGrid] virtual:true は固定行列/折り返し非対応のため、それらを無効化しました'); } catch (_) {}
224
+ this.frozenRows = 0; this.frozenCols = 0; this.wordWrap = false;
225
+ }
215
226
  // 行/列数の下限・上限(挿入/削除の境界。min は初期パディングにも使う)
216
227
  this.minRows = opts.minRows | 0; this.maxRows = opts.maxRows | 0;
217
228
  this.minCols = opts.minCols | 0; this.maxCols = opts.maxCols | 0;
@@ -510,6 +521,7 @@
510
521
  destroy() {
511
522
  if (this._destroyed) return;
512
523
  this._destroyed = true;
524
+ if (this._vRAF) { try { cancelAnimationFrame(this._vRAF); } catch (_) {} this._vRAF = 0; } // 仮想スクロールの保留フレームを破棄
513
525
  for (const p of this._plugins) { if (p.handle && typeof p.handle.destroy === 'function') { try { p.handle.destroy(); } catch (_) {} } }
514
526
  this._plugins = [];
515
527
  if (this._ro) { try { this._ro.disconnect(); } catch (_) {} this._ro = null; }
@@ -970,7 +982,7 @@
970
982
 
971
983
  _colWidth(c) { return this.colW[c] != null ? this.colW[c] : this.defColW; }
972
984
  _rowHeight(r) { return this.rowH[r] != null ? this.rowH[r] : this.defRowH; }
973
- _rhW() { return this.rowHeaders ? 40 : 0; } // 行ヘッダ列の幅(非表示なら 0)
985
+ _rhW() { return this.rowHeaders ? this.rowHeaderW : 0; } // 行ヘッダ列の幅(非表示なら 0)
974
986
  // ---- 列の非表示 ----
975
987
  _isHidden(c) { return this.hiddenCols.has(c); }
976
988
  _visCols() { const a = []; for (let c = 0; c < this.COLS; c++) if (!this.hiddenCols.has(c)) a.push(c); return a; }
@@ -1096,10 +1108,101 @@
1096
1108
  top += tr.offsetHeight;
1097
1109
  });
1098
1110
  }
1111
+ // 1行ぶんの <tr>…</tr> を生成(buildTable の本体ループと、仮想スクロールの窓再描画 _renderWindow で共用)。
1112
+ _rowHTML(r) {
1113
+ const rh = this.rowHeaders;
1114
+ const rgrip = (rh && this.resizeRows) ? '<div class="tg-rowgrip"></div>' : '';
1115
+ const dgrip = (rh && this.rowReorder && !this._hasVMerge()) ? '<span class="tg-rowmove" title="ドラッグで行を移動">⠿</span>' : ''; // 横結合は行と一緒に移動可/縦結合中は不可
1116
+ const rcls = (r < this.frozenRows) ? (' tg-frozen-row' + (r === this.frozenRows - 1 ? ' tg-frozen-row-edge' : '')) : ''; // 行固定
1117
+ let html = '<tr data-r="' + r + '" class="tg-row' + rcls + '" style="height:' + this._rowHeight(r) + 'px">' + (rh ? '<th class="rowhead" data-r="' + r + '">' + dgrip + (r + 1) + rgrip + '</th>' : '');
1118
+ for (let c = 0; c < this.COLS; c++) {
1119
+ if (this._merges && this._isCovered(r, c)) continue; // 結合に覆われた従属セルは td を出さない(アンカーが colspan で覆う)
1120
+ const cs = this._merges ? this._colspanAt(r, c) : 1; // 結合アンカーは colspan で従属列ぶんを覆う
1121
+ const rs = this._merges ? this._rowspanAt(r, c) : 1; // 縦結合は rowspan で従属行ぶんを覆う
1122
+ const cls = [];
1123
+ if (cs > 1 || rs > 1) cls.push('tg-merged'); // 結合アンカー(CSS で中央寄せ等のフック)
1124
+ if (c < this.frozenCols) { cls.push('tg-frozen'); if (c === this.frozenCols - 1) cls.push('tg-frozen-edge'); }
1125
+ if (this._isHidden(c)) cls.push('tg-hidden'); // 非表示列
1126
+ if (this.colType(c) === 'dropdown') cls.push('tg-dropdown'); // ▾ キャレット表示用
1127
+ if (this.colType(c) === 'number') cls.push('tg-num'); // 数値は右寄せ
1128
+ if (this._isReadOnly(r, c)) cls.push('tg-readonly'); // 読み取り専用(淡色+カーソル既定)
1129
+ if (this.colCfg(c).wordWrap) cls.push('tg-wrap-cell'); // 列単位の折り返し
1130
+ if (this.data[r][c] === '' && this._placeholder(c) != null) cls.push('tg-placeholder'); // 空セルのヒント
1131
+ const al = this._cellAlign(r, c); // 整列(列既定+セル上書き)
1132
+ if (al.h) cls.push('tg-h-' + al.h); if (al.v) cls.push('tg-v-' + al.v);
1133
+ let titleAttr = '';
1134
+ if (this._marksInvalid) { const _iv = this._coerceCell(r, c, this.data[r][c]); if (!_iv.ok) { cls.push('tg-invalid'); titleAttr = ' data-err-level="' + TssGrid.esc(_iv.level || 'error') + '"'; if (this.invalidTitle) titleAttr += ' title="' + TssGrid.esc(_iv.message || '入力値が不正です') + '"'; } }
1135
+ const ed = this.colCfg(c).editor; // カスタムエディタが icon を申告していれば右端に表示
1136
+ const ico = (ed && ed.icon) ? ' data-ico="' + TssGrid.esc(ed.icon) + '"' : '';
1137
+ if (ico) cls.push('tg-haspicker');
1138
+ const ccls = (this.cellClass || this.colCfg(c).cellClass) ? this._cellClasses(r, c) : []; // 条件付き書式
1139
+ for (const k of ccls) cls.push(k);
1140
+ const dccls = ccls.length ? ' data-ccls="' + TssGrid.esc(ccls.join(' ')) + '"' : '';
1141
+ let sty = ''; // セル単位インラインスタイル(連続色・データバー幅などCSS変数も)
1142
+ if (this.cellStyle || this.colCfg(c).cellStyle) {
1143
+ const so = this._cellStyleObj(r, c), sk = Object.keys(so);
1144
+ if (sk.length) { let css = ''; for (const k of sk) css += k + ':' + so[k] + ';'; sty = ' style="' + TssGrid.esc(css) + '" data-cstyle="' + TssGrid.esc(sk.join(' ')) + '"'; }
1145
+ }
1146
+ const a = cls.length ? ' class="' + cls.join(' ') + '"' : '';
1147
+ const csAttr = (cs > 1 ? ' colspan="' + cs + '"' : '') + (rs > 1 ? ' rowspan="' + rs + '"' : '');
1148
+ html += '<td' + a + ico + dccls + sty + titleAttr + csAttr + ' data-r="' + r + '" data-c="' + c + '">' + this._cellHTML(r, c) + '</td>';
1149
+ }
1150
+ return html + '</tr>';
1151
+ }
1152
+ // 仮想スクロール: 現在の scrollTop から描画すべき窓 [start,end)(絶対行)を算出。バッファ込み。
1153
+ _vWindow() {
1154
+ const rowH = this.defRowH;
1155
+ const viewH = (this.wrap && this.wrap.clientHeight) || parseInt(this.height, 10) || 400;
1156
+ const visible = Math.ceil(viewH / rowH) + 1;
1157
+ const scrollTop = this.wrap ? this.wrap.scrollTop : 0;
1158
+ let start = Math.floor(scrollTop / rowH) - this._vBuf;
1159
+ if (start < 0) start = 0;
1160
+ let end = start + visible + this._vBuf * 2;
1161
+ if (end > this.ROWS) end = this.ROWS;
1162
+ return { start, end };
1163
+ }
1164
+ // 仮想スクロール: tbody を窓ぶん+上下スペーサ行に差し替え(buildTable せず軽量再描画)。
1165
+ _renderWindow(win, force) {
1166
+ if (!force && this._vRendered && win.start === this._vStart && win.end === this._vEnd) return false;
1167
+ const tbody = this.table && this.table.querySelector('tbody');
1168
+ if (!tbody) return false;
1169
+ const rh = this.rowHeaders, colsSpan = this.COLS + (rh ? 1 : 0);
1170
+ const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
1171
+ let html = '';
1172
+ if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
1173
+ for (let r = win.start; r < win.end; r++) html += this._rowHTML(r);
1174
+ if (botH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + botH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
1175
+ tbody.innerHTML = html;
1176
+ this._vStart = win.start; this._vEnd = win.end; this._vRendered = true;
1177
+ this._syncHeaderCheckboxes();
1178
+ this.updateSelectionUI(); // セルが作り直されたので窓内の選択シェード/オーバーレイを再適用
1179
+ return true;
1180
+ }
1181
+ // 仮想スクロール: スクロールで窓がずれたら再描画(rAF スロットル)。
1182
+ _vSync() {
1183
+ if (!this.virtual || this._vRAF) return;
1184
+ this._vRAF = requestAnimationFrame(() => {
1185
+ this._vRAF = 0;
1186
+ if (this._destroyed) return;
1187
+ this._renderWindow(this._vWindow(), false);
1188
+ });
1189
+ }
1190
+ // 仮想スクロール: 行 r を可視域に入れる(scrollTop 調整→窓を同期描画)。setActive/setExtent から。
1191
+ _vEnsureVisible(r) {
1192
+ if (!this.virtual || !this.wrap) return;
1193
+ const rowH = this.defRowH, head = this._theadH || 0;
1194
+ const rowTop = head + r * rowH;
1195
+ const cur = this.wrap.scrollTop, viewH = this.wrap.clientHeight || 400;
1196
+ let st = cur;
1197
+ if (rowTop < cur + head) st = rowTop - head; // 上のヘッダ下に隠れてる→上端へ
1198
+ else if (rowTop + rowH > cur + viewH) st = rowTop + rowH - viewH; // 下に隠れてる→下端へ
1199
+ if (st !== cur) this.wrap.scrollTop = st;
1200
+ this._renderWindow(this._vWindow(), false); // 窓を即同期(place 前に DOM を確定)
1201
+ }
1099
1202
  buildTable() {
1100
1203
  this._selhdrFg = null; // 選択ヘッダ文字色キャッシュ破棄(テーマ変更に追従)
1101
1204
  const rh = this.rowHeaders;
1102
- let html = '<colgroup>' + (rh ? '<col style="width:40px">' : '');
1205
+ let html = '<colgroup>' + (rh ? '<col style="width:' + this.rowHeaderW + 'px">' : '');
1103
1206
  for (let c = 0; c < this.COLS; c++) html += '<col data-c="' + c + '"' + (this._isHidden(c) ? ' class="tg-hidden"' : '') + ' style="width:' + (this._isHidden(c) ? 0 : this._colWidth(c)) + 'px">';
1104
1207
  html += '</colgroup>';
1105
1208
  if (this.colHeaders) {
@@ -1128,44 +1231,15 @@
1128
1231
  html += '</thead>';
1129
1232
  }
1130
1233
  html += '<tbody>';
1131
- for (let r = 0; r < this.ROWS; r++) {
1132
- const rgrip = (rh && this.resizeRows) ? '<div class="tg-rowgrip"></div>' : '';
1133
- const dgrip = (rh && this.rowReorder && !this._hasVMerge()) ? '<span class="tg-rowmove" title="ドラッグで行を移動">⠿</span>' : ''; // 横結合は行と一緒に移動可/縦結合中は不可
1134
- const rcls = (r < this.frozenRows) ? (' tg-frozen-row' + (r === this.frozenRows - 1 ? ' tg-frozen-row-edge' : '')) : ''; // 行固定
1135
- html += '<tr data-r="' + r + '" class="tg-row' + rcls + '" style="height:' + this._rowHeight(r) + 'px">' + (rh ? '<th class="rowhead" data-r="' + r + '">' + dgrip + (r + 1) + rgrip + '</th>' : '');
1136
- for (let c = 0; c < this.COLS; c++) {
1137
- if (this._merges && this._isCovered(r, c)) continue; // 結合に覆われた従属セルは td を出さない(アンカーが colspan で覆う)
1138
- const cs = this._merges ? this._colspanAt(r, c) : 1; // 結合アンカーは colspan で従属列ぶんを覆う
1139
- const rs = this._merges ? this._rowspanAt(r, c) : 1; // 縦結合は rowspan で従属行ぶんを覆う
1140
- const cls = [];
1141
- if (cs > 1 || rs > 1) cls.push('tg-merged'); // 結合アンカー(CSS で中央寄せ等のフック)
1142
- if (c < this.frozenCols) { cls.push('tg-frozen'); if (c === this.frozenCols - 1) cls.push('tg-frozen-edge'); }
1143
- if (this._isHidden(c)) cls.push('tg-hidden'); // 非表示列
1144
- if (this.colType(c) === 'dropdown') cls.push('tg-dropdown'); // ▾ キャレット表示用
1145
- if (this.colType(c) === 'number') cls.push('tg-num'); // 数値は右寄せ
1146
- if (this._isReadOnly(r, c)) cls.push('tg-readonly'); // 読み取り専用(淡色+カーソル既定)
1147
- if (this.colCfg(c).wordWrap) cls.push('tg-wrap-cell'); // 列単位の折り返し
1148
- if (this.data[r][c] === '' && this._placeholder(c) != null) cls.push('tg-placeholder'); // 空セルのヒント
1149
- const al = this._cellAlign(r, c); // 整列(列既定+セル上書き)
1150
- if (al.h) cls.push('tg-h-' + al.h); if (al.v) cls.push('tg-v-' + al.v);
1151
- let titleAttr = '';
1152
- if (this._marksInvalid) { const _iv = this._coerceCell(r, c, this.data[r][c]); if (!_iv.ok) { cls.push('tg-invalid'); titleAttr = ' data-err-level="' + TssGrid.esc(_iv.level || 'error') + '"'; if (this.invalidTitle) titleAttr += ' title="' + TssGrid.esc(_iv.message || '入力値が不正です') + '"'; } }
1153
- const ed = this.colCfg(c).editor; // カスタムエディタが icon を申告していれば右端に表示
1154
- const ico = (ed && ed.icon) ? ' data-ico="' + TssGrid.esc(ed.icon) + '"' : '';
1155
- if (ico) cls.push('tg-haspicker');
1156
- const ccls = (this.cellClass || this.colCfg(c).cellClass) ? this._cellClasses(r, c) : []; // 条件付き書式
1157
- for (const k of ccls) cls.push(k);
1158
- const dccls = ccls.length ? ' data-ccls="' + TssGrid.esc(ccls.join(' ')) + '"' : '';
1159
- let sty = ''; // セル単位インラインスタイル(連続色・データバー幅などCSS変数も)
1160
- if (this.cellStyle || this.colCfg(c).cellStyle) {
1161
- const so = this._cellStyleObj(r, c), sk = Object.keys(so);
1162
- if (sk.length) { let css = ''; for (const k of sk) css += k + ':' + so[k] + ';'; sty = ' style="' + TssGrid.esc(css) + '" data-cstyle="' + TssGrid.esc(sk.join(' ')) + '"'; }
1163
- }
1164
- const a = cls.length ? ' class="' + cls.join(' ') + '"' : '';
1165
- const csAttr = (cs > 1 ? ' colspan="' + cs + '"' : '') + (rs > 1 ? ' rowspan="' + rs + '"' : '');
1166
- html += '<td' + a + ico + dccls + sty + titleAttr + csAttr + ' data-r="' + r + '" data-c="' + c + '">' + this._cellHTML(r, c) + '</td>';
1167
- }
1168
- html += '</tr>';
1234
+ if (this.virtual) {
1235
+ const win = this._vWindow(); this._vStart = win.start; this._vEnd = win.end; this._vRendered = true;
1236
+ const colsSpan = this.COLS + (rh ? 1 : 0);
1237
+ const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
1238
+ if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
1239
+ for (let r = win.start; r < win.end; r++) html += this._rowHTML(r);
1240
+ if (botH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + botH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
1241
+ } else {
1242
+ for (let r = 0; r < this.ROWS; r++) html += this._rowHTML(r);
1169
1243
  }
1170
1244
  html += '</tbody>';
1171
1245
  this.table.innerHTML = html;
@@ -1262,7 +1336,8 @@
1262
1336
  // 通常は buildTable/_reflow(ResizeObserver が table/wrap を監視)で自動更新される)。
1263
1337
  refreshGeometry() { this._buildGeomCache(); this.updateSelectionUI(); }
1264
1338
  place(elem, td) {
1265
- const r = +td.dataset.r, c = +td.dataset.c, R = this._cellRectAuto(r, c);
1339
+ // 仮想スクロールで active が窓外だと td=null。その場合は active 座標+モデル幾何で配置(窓外なら overflow で隠れる)。
1340
+ const r = td ? +td.dataset.r : this.active.r, c = td ? +td.dataset.c : this.active.c, R = this._cellRectAuto(r, c);
1266
1341
  elem.style.left = R.left + 'px'; elem.style.top = R.top + 'px';
1267
1342
  elem.style.width = R.width + 'px'; elem.style.height = R.height + 'px';
1268
1343
  this._clipOverlay(elem, R.left, R.top, c, r);
@@ -1290,9 +1365,15 @@
1290
1365
  // 行高は wordWrap で、列幅は stretch の DOM 改変で可変なので、モデル計算でなく実測が要る。
1291
1366
  _buildGeomCache() {
1292
1367
  if (!this.wrap || !this.table) { this._rowTops = this._colLefts = null; return; }
1293
- const trs = this.table.querySelectorAll('tbody tr');
1294
- const RT = new Array(this.ROWS + 1); RT[0] = this._theadH || 0;
1295
- for (let r = 0; r < this.ROWS; r++) { const tr = trs[r]; RT[r + 1] = RT[r] + (tr ? tr.getBoundingClientRect().height : this._rowHeight(r)); }
1368
+ const RT = new Array(this.ROWS + 1);
1369
+ if (this.virtual) {
1370
+ const base = this._theadH || 0, rowH = this.defRowH; // 仮想モードは一定行高=モデルで全行(窓外も算出可)
1371
+ for (let r = 0; r <= this.ROWS; r++) RT[r] = base + r * rowH;
1372
+ } else {
1373
+ const trs = this.table.querySelectorAll('tbody tr');
1374
+ RT[0] = this._theadH || 0;
1375
+ for (let r = 0; r < this.ROWS; r++) { const tr = trs[r]; RT[r + 1] = RT[r] + (tr ? tr.getBoundingClientRect().height : this._rowHeight(r)); }
1376
+ }
1296
1377
  this._rowTops = RT;
1297
1378
  // 列幅: リーフヘッダ th(マージ無しで確実)→ 無ければ colgroup col。stretch 後の実幅を拾う。
1298
1379
  const colEl = c => (this.colHeaders ? this.table.querySelector('thead th[data-c="' + c + '"]') : null) || this.table.querySelector('colgroup col[data-c="' + c + '"]');
@@ -1347,18 +1428,21 @@
1347
1428
  highlightRange() {
1348
1429
  this.table.querySelectorAll('td.sel, th.selhdr').forEach(el => el.classList.remove('sel', 'selhdr'));
1349
1430
  const { r0, r1, c0, c1 } = this.rectRange();
1350
- // 選択範囲が及ぶ行/列ヘッダをハイライト(Excel 風のアフォーダンス)
1431
+ // 選択範囲が及ぶ行/列ヘッダをハイライト(Excel 風のアフォーダンス)。仮想時は行ヘッダを窓内にクランプ(大量行で querySelector を無駄打ちしない)。
1432
+ const hlo = this.virtual ? Math.max(r0, this._vStart) : r0;
1433
+ const hhi = this.virtual ? Math.min(r1, this._vEnd - 1) : r1;
1351
1434
  for (let c = c0; c <= c1; c++) { const th = this.table.querySelector('thead th[data-c="' + c + '"]'); if (th) th.classList.add('selhdr'); }
1352
- for (let r = r0; r <= r1; r++) { const th = this.table.querySelector('th.rowhead[data-r="' + r + '"]'); if (th) th.classList.add('selhdr'); }
1435
+ for (let r = hlo; r <= hhi; r++) { const th = this.table.querySelector('th.rowhead[data-r="' + r + '"]'); if (th) th.classList.add('selhdr'); }
1353
1436
  this.wrap.style.setProperty('--tg-selhdr-fg', this._selhdrTextColor()); // 選択ヘッダ文字色を背景輝度から自動(暗背景→白)
1354
1437
  if (r0 === r1 && c0 === c1) return;
1355
1438
  const clampMerge = this._headerSel && this._merges; // 列/行選択では「範囲に収まらない結合」は塗らない(帯を塗り広げない)
1356
- for (let r = r0; r <= r1; r++) for (let c = c0; c <= c1; c++) {
1439
+ // 仮想スクロール: 窓内の行だけシェード(範囲枠 selrange は placeRect がモデル幾何で全域を描く)。
1440
+ for (let r = hlo; r <= hhi; r++) for (let c = c0; c <= c1; c++) {
1357
1441
  if (clampMerge) {
1358
1442
  const a = this._anchorOf(r, c), mc1 = a.c + this._colspanAt(a.r, a.c) - 1, mr1 = a.r + this._rowspanAt(a.r, a.c) - 1;
1359
1443
  if (a.r < r0 || mr1 > r1 || a.c < c0 || mc1 > c1) continue; // 結合が選択範囲に収まらない=塗らない
1360
1444
  }
1361
- this.cellEl(r, c).classList.add('sel');
1445
+ const el = this.cellEl(r, c); if (el) el.classList.add('sel');
1362
1446
  }
1363
1447
  }
1364
1448
  // 選択ヘッダの文字色を --tg-selhdr-bg の輝度から自動決定(暗背景→白/明背景→濃紺)。
@@ -1437,15 +1521,18 @@
1437
1521
  if (!this._beforeSel(a, a)) return;
1438
1522
  this._headerSel = null;
1439
1523
  this.active = a; this.extent = { ...a };
1524
+ if (this.virtual) this._vEnsureVisible(a.r); // 窓に入れてから placement(窓外なら activeTd=null になるため)
1440
1525
  this.place(this.editor, this.activeTd()); this.toNav(); this.editor.focus(); this.updateSelectionUI();
1441
- this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
1526
+ if (!this.virtual) this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' }); // 仮想は _vEnsureVisible が可視化済み
1442
1527
  this._fireSelection();
1443
1528
  }
1444
1529
  setExtent(r, c) {
1445
1530
  const e = { r: this.clampR(r), c: this.clampC(c) };
1446
1531
  if (!this._beforeSel(this.active, e)) return;
1447
- this.extent = e; this.updateSelectionUI();
1448
- this.cellEl(this.extent.r, this.extent.c).scrollIntoView({ block: 'nearest', inline: 'nearest' });
1532
+ this.extent = e;
1533
+ if (this.virtual) this._vEnsureVisible(e.r);
1534
+ this.updateSelectionUI();
1535
+ if (!this.virtual) this.cellEl(this.extent.r, this.extent.c).scrollIntoView({ block: 'nearest', inline: 'nearest' });
1449
1536
  this._fireSelection();
1450
1537
  }
1451
1538
  selectRect(r0, c0, r1, c1) {
@@ -1458,8 +1545,9 @@
1458
1545
  }
1459
1546
  focusAndSelect(sel) {
1460
1547
  this.active = { ...sel.active }; this.extent = { ...sel.extent };
1548
+ if (this.virtual) this._vEnsureVisible(this.active.r);
1461
1549
  this.place(this.editor, this.activeTd()); this.toNav(); this.editor.focus(); this.updateSelectionUI();
1462
- this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
1550
+ if (!this.virtual) this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
1463
1551
  this._fireSelection();
1464
1552
  }
1465
1553
 
@@ -2114,6 +2202,7 @@
2114
2202
  _reflow() {
2115
2203
  if (this.stretchH !== 'none') this._applyStretch(); // 枠幅が変わったら余白を再配分
2116
2204
  this._buildGeomCache(); // 幅/枠が変わったので実測幾何を取り直す(リサイズ/ResizeObserver/window resize)
2205
+ if (this.virtual) this._renderWindow(this._vWindow(), false); // 枠高が変われば窓行数も変わる=窓を同期
2117
2206
  const td = this.activeTd(); if (!td) return;
2118
2207
  this.place(this.editor, td);
2119
2208
  if (this.select && this.select.style.display !== 'none') this.place(this.select, td);
@@ -2121,6 +2210,7 @@
2121
2210
  }
2122
2211
 
2123
2212
  _bind() {
2213
+ if (this.virtual) this.wrap.addEventListener('scroll', () => this._vSync(), { passive: true }); // 仮想スクロール: 窓再描画
2124
2214
  this.editor.addEventListener('keydown', (e) => this._onKey(e));
2125
2215
  this.editor.addEventListener('compositionstart', () => {
2126
2216
  if (this.mode !== 'nav') return;