@threestarsoftware/tssgrid 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -0
- package/README.md +15 -2
- package/package.json +2 -1
- package/src/tssgrid.css +7 -0
- package/src/tssgrid.d.ts +11 -0
- package/src/tssgrid.js +241 -63
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# 変更履歴 (Changelog)
|
|
2
|
+
|
|
3
|
+
TssGrid の各リリースの変更点。日付は JST。形式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) 準拠(ゆるめ)。
|
|
4
|
+
数値は特記なき限り**自社調べ**(headless Chrome / Windows 11・環境差あり)。
|
|
5
|
+
|
|
6
|
+
## [0.0.6] - 2026-06-30
|
|
7
|
+
|
|
8
|
+
### 追加
|
|
9
|
+
- ドラッグ選択・フィルハンドルが**表示領域の外(上下左右)まで自動スクロールしながら伸びる**ように。ビューも一緒にスクロールし、速度は端からのはみ出し量に比例(少し超え=ゆっくり/大きく超え=速く)。仮想スクロール時・非仮想時の両方で動作。
|
|
10
|
+
- `rowHeaderLabel` オプション: 行ヘッダの表示を指定(`false`=数字なしの空ヘッダ/関数 `(r)=>文字列`=カスタム表示/未指定=従来の行番号)。空にしても溝は残り、クリックでの行選択は効く。
|
|
11
|
+
- `rowReorder: 'header'`: 行ヘッダーセル(左端の行番号の列)**全体**を行移動の掴み手に(クリック=行選択/ドラッグ=移動)。従来の `rowReorder: true` は ⠿ アイコンのみ掴める(不変)。
|
|
12
|
+
- `rowReorderMark` オプション: 行ヘッダーの ⠿ マークの位置を `'before'`(番号の前・既定)/ `'after'`(番号の後)/ `false`(出さない)から選べる。
|
|
13
|
+
|
|
14
|
+
### 修正
|
|
15
|
+
- 仮想スクロールでドラッグ選択/フィルが窓の端で止まっていた問題。
|
|
16
|
+
- フィルが端を1行超えただけで大量に伸びてしまう暴走を修正(伸ばす先を常にポインタ位置のセルに)。
|
|
17
|
+
- `examples/virtual.html`: 部署ドロップダウンの選択肢が出ない不具合(`options` 指定漏れ)を修正/表示中インジケータの更新でツールバーがずれる問題を修正/パネルを縦リサイズ可能に。
|
|
18
|
+
|
|
19
|
+
### ドキュメント
|
|
20
|
+
- 比較表(`examples/compare.html`): 仮想スクロールの行を「対応(`virtual:true`・opt-in)」に更新し、デモリンクに `virtual.html` を追加。
|
|
21
|
+
|
|
22
|
+
## [0.0.5] - 2026-06-25
|
|
23
|
+
|
|
24
|
+
### 追加
|
|
25
|
+
- **仮想スクロール `virtual: true`**(コア)— 大量行を「画面に見える窓ぶん+バッファだけ」描画。全件を一度に DOM 化しないので、初期描画が一瞬・スクロールも軽い。有効化はオプション一行。
|
|
26
|
+
- 自社調べ: 10万行で初回描画 ≈ 46ms(全件描画 ≈ 10.5秒に対して)。デモ: `examples/virtual.html`。
|
|
27
|
+
- 制約(v1): 行高一定/固定行列・折り返し・セル結合は非対応(指定時は自動無効化)。
|
|
28
|
+
- **`rowHeaderWidth` オプション**(コア)— 大量行で行番号の桁数が増える時に行ヘッダ幅を調整。
|
|
29
|
+
|
|
30
|
+
### ドキュメント
|
|
31
|
+
- README: 非破壊フィルタの説明を実測値に正確化(「絞り込み後が軽い」を自社調べ値で)+仮想スクロール節を追加。
|
|
32
|
+
- 比較表(`examples/compare.html`)に Tabulator を追加(公式デモ準拠)。
|
|
33
|
+
|
|
34
|
+
## [0.0.4] - 2026-06-24
|
|
35
|
+
|
|
36
|
+
### 追加
|
|
37
|
+
- 見積書デモ(`examples/quote.html`)— 印刷/日付ピッカー/列幅固定/Enter 横送り(`nextCell`)。
|
|
38
|
+
- `navSkipReadOnly` オプション(Tab/Enter 送りで readonly セルを飛ばす=伝票入力向け)。
|
|
39
|
+
- プラグイン `tss-datetime`(日付+時刻の同時ピッカー・曜日つき・別フィールド結合・時刻のみ対応)。
|
|
40
|
+
|
|
41
|
+
### 修正
|
|
42
|
+
- 編集中の `Ctrl+Z` で元の値に戻せない不具合(input 編集を Esc 同等でキャンセル)。
|
|
43
|
+
- グリッド外周の右・下が二重線になる不具合(最終行/列のセル罫線を枠線に一本化)。
|
|
44
|
+
- 全幅見出し帯の結合セルで列/行選択が他列へ広がる不具合(Excel 流にクランプ)。
|
|
45
|
+
- 結合解除で見出しラベルが消える不具合。
|
|
46
|
+
|
|
47
|
+
## [0.0.3] - 2026-06-23
|
|
48
|
+
|
|
49
|
+
### 追加
|
|
50
|
+
- プラグイン `tss-sum`(条件付き合計)+請求書デモで採用。
|
|
51
|
+
|
|
52
|
+
### 修正
|
|
53
|
+
- 請求書デモの合計欄を別グリッド化して金額列に完全整列/No. 列見出しが選択色になる不具合/印刷で選択枠を非表示。
|
|
54
|
+
|
|
55
|
+
## [0.0.2] - 2026-06-22
|
|
56
|
+
|
|
57
|
+
### 追加
|
|
58
|
+
- **npm パッケージ化**(`@threestarsoftware/tssgrid`)+公開 API の型定義 `tssgrid.d.ts`。
|
|
59
|
+
- 請求書(インボイス対応)デモ。
|
|
60
|
+
- サイト: `/docs` ドキュメント・`/api` リファレンス(`.d.ts` から自動生成)・会社名フッター。
|
|
61
|
+
|
|
62
|
+
### 修正
|
|
63
|
+
- 行/列ヘッダ選択中の右クリックで複数選択が解除される不具合。
|
|
64
|
+
|
|
65
|
+
## [0.0.1] - 2026-06-19
|
|
66
|
+
|
|
67
|
+
- 初回公開。コア(編集グリッド/日本語 IME 堅牢・全角半角・セル結合・固定行列・非破壊フィルタ・条件付き書式・コピー/貼付・Undo/Redo ほか)+同梱プラグイン+サンプル一式。
|
|
68
|
+
- 目玉デモ(方眼ガント/在庫管理)をサイトでライブ化。
|
|
69
|
+
|
|
70
|
+
[0.0.6]: https://github.com/threestarsoftware/TssGrid/releases/tag/v0.0.6
|
|
71
|
+
[0.0.5]: https://github.com/threestarsoftware/TssGrid/releases/tag/v0.0.5
|
|
72
|
+
[0.0.4]: https://github.com/threestarsoftware/TssGrid/releases/tag/v0.0.4
|
|
73
|
+
[0.0.3]: https://github.com/threestarsoftware/TssGrid/releases/tag/v0.0.3
|
|
74
|
+
[0.0.2]: https://github.com/threestarsoftware/TssGrid/releases/tag/v0.0.2
|
|
75
|
+
[0.0.1]: https://github.com/threestarsoftware/TssGrid/releases/tag/v0.0.1
|
package/README.md
CHANGED
|
@@ -238,7 +238,8 @@ grid.moveRows(r0, r1, to); // 連続する複数行をま
|
|
|
238
238
|
|
|
239
239
|
### 非破壊フィルタ(`filter` / `clearFilter`)
|
|
240
240
|
|
|
241
|
-
**コア内蔵**の非破壊フィルタ(行インデックスマッパ)。全行は内部の `_allRows
|
|
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,6 +255,18 @@ 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
272
|
`plugins/tss-filter.js`+`.css`= 各見出しの**ロート(漏斗)アイコン**をクリックで**値チェックリスト+検索**のポップアップ(Excel風。ソートの▲▼と区別できるアイコン)。複数列の条件は **AND**。中身は上の**コア `filter()` を呼ぶだけ**。動く例: [`examples/filter-ui.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-ui.html)。
|
|
@@ -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.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"src/tssgrid.d.ts",
|
|
53
53
|
"plugins/",
|
|
54
54
|
"README.md",
|
|
55
|
+
"CHANGELOG.md",
|
|
55
56
|
"LICENSE"
|
|
56
57
|
]
|
|
57
58
|
}
|
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。
|
|
@@ -132,6 +134,11 @@
|
|
|
132
134
|
.tssgrid .tg-rowmove { cursor: grab; color: #b3b6c8; margin-right: 2px; font-size: 11px; line-height: 1; user-select: none; vertical-align: 1px; }
|
|
133
135
|
.tssgrid .tg-rowmove:hover { color: var(--tg-accent); }
|
|
134
136
|
.tssgrid .tg-rowmove:active { cursor: grabbing; }
|
|
137
|
+
.tssgrid .tg-rowmove.tg-mark-after { margin-right: 0; margin-left: 4px; } /* ⠿ を番号の後ろに置く時の間隔 */
|
|
138
|
+
.tssgrid .tg-rowmove.tg-mark-only { margin: 0; } /* 番号なし=⠿だけ=前後の区別をやめて中央に */
|
|
139
|
+
/* rowReorder:'header' = 行ヘッダーセル全体が移動ハンドル(クリック=選択/ドラッグ=移動) */
|
|
140
|
+
.tssgrid .tg-table th.rowhead.tg-rowmove-cell { cursor: grab; }
|
|
141
|
+
.tssgrid .tg-table th.rowhead.tg-rowmove-cell:active { cursor: grabbing; }
|
|
135
142
|
.tssgrid .tg-colgrip:hover { background: var(--tg-edit-accent); opacity: .5; }
|
|
136
143
|
.tssgrid .tg-rowgrip:hover { background: var(--tg-edit-accent); opacity: .5; }
|
|
137
144
|
/* リサイズのプレビュー線(ドラッグ中だけ表示、離して確定) */
|
package/src/tssgrid.d.ts
CHANGED
|
@@ -126,6 +126,17 @@ export interface TssGridOptions {
|
|
|
126
126
|
cursor?: boolean;
|
|
127
127
|
stretchH?: 'none' | 'last' | 'all';
|
|
128
128
|
rowHeaders?: boolean;
|
|
129
|
+
/** 行番号列の幅(px)。既定 40。大量行で桁数が増える時に広げる。 */
|
|
130
|
+
rowHeaderWidth?: number;
|
|
131
|
+
/** 行ヘッダの表示内容。未指定=行番号(r+1) / false=数字なし(空) / (r)=>文字列=カスタム表示。 */
|
|
132
|
+
rowHeaderLabel?: false | ((r: number) => string | number);
|
|
133
|
+
/** 行のドラッグ並べ替え。true=行ヘッダーセルの ⠿ アイコンを掴んで移動 / 'header'=行ヘッダーセル全体が移動ハンドル(クリック=選択/ドラッグ=移動)。 */
|
|
134
|
+
rowReorder?: boolean | 'header';
|
|
135
|
+
/** 行ヘッダーの ⠿ マークの位置。'before'=番号の前(既定)/ 'after'=番号の後 / false=マークを出さない。 */
|
|
136
|
+
rowReorderMark?: 'before' | 'after' | false;
|
|
137
|
+
/** 仮想スクロール(固定行高ウィンドウイング)。大量行を可視窓だけ描画。true か { buffer:窓上下バッファ行数(既定6) }。
|
|
138
|
+
* 制約(v1): 行高一定/固定行列・折り返し・セル結合は非対応(指定時は自動無効化)。 */
|
|
139
|
+
virtual?: boolean | { buffer?: number };
|
|
129
140
|
colHeaders?: boolean;
|
|
130
141
|
hiddenColumns?: number[];
|
|
131
142
|
wordWrap?: boolean;
|
package/src/tssgrid.js
CHANGED
|
@@ -146,6 +146,7 @@
|
|
|
146
146
|
|
|
147
147
|
this.active = { r: 0, c: 0 }; this.extent = { r: 0, c: 0 };
|
|
148
148
|
this.mode = 'nav'; this.dragging = false; this.filling = null;
|
|
149
|
+
this._asRAF = 0; this._dragDir = 0; this._fillRow = null; // ドラッグ/フィルの端自動スクロール状態
|
|
149
150
|
this.pendingCut = null;
|
|
150
151
|
this.headerDrag = null; this._colAnchor = 0; this._rowAnchor = 0; // 行/列ヘッダ選択
|
|
151
152
|
this._headerSel = null; // 'col'/'row'/'all'=列/行/全選択モード(結合に拡張せず実列/行にクランプ=Excel流)。null=通常セル選択
|
|
@@ -183,9 +184,13 @@
|
|
|
183
184
|
this.resizeRows = opts.resizeRows !== false;
|
|
184
185
|
// 行/列ヘッダの表示(false で行番号列・ヘッダ行を隠す)
|
|
185
186
|
this.rowHeaders = opts.rowHeaders !== false;
|
|
187
|
+
this.rowHeaderW = (opts.rowHeaderWidth | 0) || 40; // 行番号列の幅(px)。大量行で桁数が増える時に広げる
|
|
188
|
+
this.rowHeaderLabel = opts.rowHeaderLabel; // 行ヘッダ表示: 未指定=行番号(r+1) / false=数字なし(空) / (r)=>文字列=カスタム
|
|
186
189
|
this.colHeaders = opts.colHeaders !== false;
|
|
187
190
|
// 行ヘッダの「つまみ」をドラッグして行を並べ替える(行番号列が必要)
|
|
188
191
|
this.rowReorder = !!opts.rowReorder;
|
|
192
|
+
this.rowReorderWhole = opts.rowReorder === 'header'; // 'header'=行ヘッダーセル全体が移動ハンドル(クリック=選択/ドラッグ=移動)/true=⠿アイコンのみ
|
|
193
|
+
this.rowReorderMark = opts.rowReorderMark === undefined ? 'before' : opts.rowReorderMark; // ⠿の位置: 'before'(番号の前/既定) | 'after'(番号の後) | false(出さない)
|
|
189
194
|
// 選択カーソル(緑の選択枠・範囲・セルハイライト・フィルハンドル)を表示するか。
|
|
190
195
|
// false で表示専用グリッド向けに非表示(内部の選択は保持=コピーは可)。
|
|
191
196
|
this.cursor = opts.cursor !== false;
|
|
@@ -212,6 +217,16 @@
|
|
|
212
217
|
this.autoWrapRow = !!opts.autoWrapRow; // Tab で行末→次行頭に折り返し
|
|
213
218
|
this.autoWrapCol = !!opts.autoWrapCol; // Enter で列末→次列頭に折り返し
|
|
214
219
|
this.navSkipReadOnly = !!opts.navSkipReadOnly; // Tab/Enter 送りで readonly セルを飛ばす(伝票入力向け=編集する所だけ移動)
|
|
220
|
+
// 仮想スクロール(簡易版・固定行高ウィンドウイング): 大量行で「可視窓+バッファ」だけ描画。
|
|
221
|
+
// 全行は data に保持・行座標は絶対のまま、tbody は窓ぶん+上下スペーサ行のみ。幾何はモデル(一定行高)。
|
|
222
|
+
// 制約(v1): 行高一定/セル結合・固定行列・折り返し非対応。指定時は無効化して警告。OFF時は一切作動せず従来通り。
|
|
223
|
+
this.virtual = !!opts.virtual;
|
|
224
|
+
this._vBuf = (opts.virtual && typeof opts.virtual === 'object' && opts.virtual.buffer) || 6; // 窓の上下バッファ行数
|
|
225
|
+
this._vStart = 0; this._vEnd = 0; this._vRAF = 0; this._vRendered = false;
|
|
226
|
+
if (this.virtual && (this.frozenRows || this.frozenCols || this.wordWrap)) {
|
|
227
|
+
try { console.warn('[TssGrid] virtual:true は固定行列/折り返し非対応のため、それらを無効化しました'); } catch (_) {}
|
|
228
|
+
this.frozenRows = 0; this.frozenCols = 0; this.wordWrap = false;
|
|
229
|
+
}
|
|
215
230
|
// 行/列数の下限・上限(挿入/削除の境界。min は初期パディングにも使う)
|
|
216
231
|
this.minRows = opts.minRows | 0; this.maxRows = opts.maxRows | 0;
|
|
217
232
|
this.minCols = opts.minCols | 0; this.maxCols = opts.maxCols | 0;
|
|
@@ -510,6 +525,8 @@
|
|
|
510
525
|
destroy() {
|
|
511
526
|
if (this._destroyed) return;
|
|
512
527
|
this._destroyed = true;
|
|
528
|
+
if (this._vRAF) { try { cancelAnimationFrame(this._vRAF); } catch (_) {} this._vRAF = 0; } // 仮想スクロールの保留フレームを破棄
|
|
529
|
+
this._stopAutoScroll(); // ドラッグ自動スクロールの保留フレームを破棄
|
|
513
530
|
for (const p of this._plugins) { if (p.handle && typeof p.handle.destroy === 'function') { try { p.handle.destroy(); } catch (_) {} } }
|
|
514
531
|
this._plugins = [];
|
|
515
532
|
if (this._ro) { try { this._ro.disconnect(); } catch (_) {} this._ro = null; }
|
|
@@ -970,7 +987,14 @@
|
|
|
970
987
|
|
|
971
988
|
_colWidth(c) { return this.colW[c] != null ? this.colW[c] : this.defColW; }
|
|
972
989
|
_rowHeight(r) { return this.rowH[r] != null ? this.rowH[r] : this.defRowH; }
|
|
973
|
-
_rhW() { return this.rowHeaders ?
|
|
990
|
+
_rhW() { return this.rowHeaders ? this.rowHeaderW : 0; } // 行ヘッダ列の幅(非表示なら 0)
|
|
991
|
+
// 行ヘッダの表示内容: false=空(数字なし)/ 関数=カスタム(エスケープ)/ 既定=行番号(r+1)。
|
|
992
|
+
_rowHeadLabel(r) {
|
|
993
|
+
const L = this.rowHeaderLabel;
|
|
994
|
+
if (L === false) return '';
|
|
995
|
+
if (typeof L === 'function') { let v; try { v = L(r); } catch (_) {} return TssGrid.esc(v == null ? '' : v); }
|
|
996
|
+
return (r + 1);
|
|
997
|
+
}
|
|
974
998
|
// ---- 列の非表示 ----
|
|
975
999
|
_isHidden(c) { return this.hiddenCols.has(c); }
|
|
976
1000
|
_visCols() { const a = []; for (let c = 0; c < this.COLS; c++) if (!this.hiddenCols.has(c)) a.push(c); return a; }
|
|
@@ -1096,10 +1120,107 @@
|
|
|
1096
1120
|
top += tr.offsetHeight;
|
|
1097
1121
|
});
|
|
1098
1122
|
}
|
|
1123
|
+
// 1行ぶんの <tr>…</tr> を生成(buildTable の本体ループと、仮想スクロールの窓再描画 _renderWindow で共用)。
|
|
1124
|
+
_rowHTML(r) {
|
|
1125
|
+
const rh = this.rowHeaders;
|
|
1126
|
+
const rgrip = (rh && this.resizeRows) ? '<div class="tg-rowgrip"></div>' : '';
|
|
1127
|
+
const canMove = rh && this.rowReorder && !this._hasVMerge();
|
|
1128
|
+
const lbl = this._rowHeadLabel(r);
|
|
1129
|
+
const after = this.rowReorderMark === 'after' && lbl !== ''; // 番号が無いなら「前/後」は無意味=中央に固定(マージンも消す)
|
|
1130
|
+
const markCls = 'tg-rowmove' + (lbl === '' ? ' tg-mark-only' : (after ? ' tg-mark-after' : ''));
|
|
1131
|
+
const mark = (canMove && this.rowReorderMark !== false) ? '<span class="' + markCls + '" title="ドラッグで行を移動">⠿</span>' : '';
|
|
1132
|
+
const inner = after ? (lbl + mark) : (mark + lbl); // ⠿ を番号の前/後に(rowReorderMark)。番号なしは前後どちらでも中央
|
|
1133
|
+
const rhCls = (canMove && this.rowReorderWhole) ? ' tg-rowmove-cell' : ''; // 'header'=行ヘッダーセル全体を掴み手に(grab カーソル)。番号/⠿の表示は独立
|
|
1134
|
+
const rcls = (r < this.frozenRows) ? (' tg-frozen-row' + (r === this.frozenRows - 1 ? ' tg-frozen-row-edge' : '')) : ''; // 行固定
|
|
1135
|
+
let html = '<tr data-r="' + r + '" class="tg-row' + rcls + '" style="height:' + this._rowHeight(r) + 'px">' + (rh ? '<th class="rowhead' + rhCls + '" data-r="' + r + '">' + inner + 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
|
+
return html + '</tr>';
|
|
1169
|
+
}
|
|
1170
|
+
// 仮想スクロール: 現在の scrollTop から描画すべき窓 [start,end)(絶対行)を算出。バッファ込み。
|
|
1171
|
+
_vWindow() {
|
|
1172
|
+
const rowH = this.defRowH;
|
|
1173
|
+
const viewH = (this.wrap && this.wrap.clientHeight) || parseInt(this.height, 10) || 400;
|
|
1174
|
+
const visible = Math.ceil(viewH / rowH) + 1;
|
|
1175
|
+
const scrollTop = this.wrap ? this.wrap.scrollTop : 0;
|
|
1176
|
+
let start = Math.floor(scrollTop / rowH) - this._vBuf;
|
|
1177
|
+
if (start < 0) start = 0;
|
|
1178
|
+
let end = start + visible + this._vBuf * 2;
|
|
1179
|
+
if (end > this.ROWS) end = this.ROWS;
|
|
1180
|
+
return { start, end };
|
|
1181
|
+
}
|
|
1182
|
+
// 仮想スクロール: tbody を窓ぶん+上下スペーサ行に差し替え(buildTable せず軽量再描画)。
|
|
1183
|
+
_renderWindow(win, force) {
|
|
1184
|
+
if (!force && this._vRendered && win.start === this._vStart && win.end === this._vEnd) return false;
|
|
1185
|
+
const tbody = this.table && this.table.querySelector('tbody');
|
|
1186
|
+
if (!tbody) return false;
|
|
1187
|
+
const rh = this.rowHeaders, colsSpan = this.COLS + (rh ? 1 : 0);
|
|
1188
|
+
const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
|
|
1189
|
+
let html = '';
|
|
1190
|
+
if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
1191
|
+
for (let r = win.start; r < win.end; r++) html += this._rowHTML(r);
|
|
1192
|
+
if (botH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + botH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
1193
|
+
tbody.innerHTML = html;
|
|
1194
|
+
this._vStart = win.start; this._vEnd = win.end; this._vRendered = true;
|
|
1195
|
+
this._syncHeaderCheckboxes();
|
|
1196
|
+
this.updateSelectionUI(); // セルが作り直されたので窓内の選択シェード/オーバーレイを再適用
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
// 仮想スクロール: スクロールで窓がずれたら再描画(rAF スロットル)。
|
|
1200
|
+
_vSync() {
|
|
1201
|
+
if (!this.virtual || this._vRAF) return;
|
|
1202
|
+
this._vRAF = requestAnimationFrame(() => {
|
|
1203
|
+
this._vRAF = 0;
|
|
1204
|
+
if (this._destroyed) return;
|
|
1205
|
+
this._renderWindow(this._vWindow(), false);
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
// 仮想スクロール: 行 r を可視域に入れる(scrollTop 調整→窓を同期描画)。setActive/setExtent から。
|
|
1209
|
+
_vEnsureVisible(r) {
|
|
1210
|
+
if (!this.virtual || !this.wrap) return;
|
|
1211
|
+
const rowH = this.defRowH, head = this._theadH || 0;
|
|
1212
|
+
const rowTop = head + r * rowH;
|
|
1213
|
+
const cur = this.wrap.scrollTop, viewH = this.wrap.clientHeight || 400;
|
|
1214
|
+
let st = cur;
|
|
1215
|
+
if (rowTop < cur + head) st = rowTop - head; // 上のヘッダ下に隠れてる→上端へ
|
|
1216
|
+
else if (rowTop + rowH > cur + viewH) st = rowTop + rowH - viewH; // 下に隠れてる→下端へ
|
|
1217
|
+
if (st !== cur) this.wrap.scrollTop = st;
|
|
1218
|
+
this._renderWindow(this._vWindow(), false); // 窓を即同期(place 前に DOM を確定)
|
|
1219
|
+
}
|
|
1099
1220
|
buildTable() {
|
|
1100
1221
|
this._selhdrFg = null; // 選択ヘッダ文字色キャッシュ破棄(テーマ変更に追従)
|
|
1101
1222
|
const rh = this.rowHeaders;
|
|
1102
|
-
let html = '<colgroup>' + (rh ? '<col style="width:
|
|
1223
|
+
let html = '<colgroup>' + (rh ? '<col style="width:' + this.rowHeaderW + 'px">' : '');
|
|
1103
1224
|
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
1225
|
html += '</colgroup>';
|
|
1105
1226
|
if (this.colHeaders) {
|
|
@@ -1128,44 +1249,15 @@
|
|
|
1128
1249
|
html += '</thead>';
|
|
1129
1250
|
}
|
|
1130
1251
|
html += '<tbody>';
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
const
|
|
1134
|
-
const
|
|
1135
|
-
html += '<tr
|
|
1136
|
-
for (let
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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>';
|
|
1252
|
+
if (this.virtual) {
|
|
1253
|
+
const win = this._vWindow(); this._vStart = win.start; this._vEnd = win.end; this._vRendered = true;
|
|
1254
|
+
const colsSpan = this.COLS + (rh ? 1 : 0);
|
|
1255
|
+
const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
|
|
1256
|
+
if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
1257
|
+
for (let r = win.start; r < win.end; r++) html += this._rowHTML(r);
|
|
1258
|
+
if (botH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + botH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
1259
|
+
} else {
|
|
1260
|
+
for (let r = 0; r < this.ROWS; r++) html += this._rowHTML(r);
|
|
1169
1261
|
}
|
|
1170
1262
|
html += '</tbody>';
|
|
1171
1263
|
this.table.innerHTML = html;
|
|
@@ -1262,7 +1354,8 @@
|
|
|
1262
1354
|
// 通常は buildTable/_reflow(ResizeObserver が table/wrap を監視)で自動更新される)。
|
|
1263
1355
|
refreshGeometry() { this._buildGeomCache(); this.updateSelectionUI(); }
|
|
1264
1356
|
place(elem, td) {
|
|
1265
|
-
|
|
1357
|
+
// 仮想スクロールで active が窓外だと td=null。その場合は active 座標+モデル幾何で配置(窓外なら overflow で隠れる)。
|
|
1358
|
+
const r = td ? +td.dataset.r : this.active.r, c = td ? +td.dataset.c : this.active.c, R = this._cellRectAuto(r, c);
|
|
1266
1359
|
elem.style.left = R.left + 'px'; elem.style.top = R.top + 'px';
|
|
1267
1360
|
elem.style.width = R.width + 'px'; elem.style.height = R.height + 'px';
|
|
1268
1361
|
this._clipOverlay(elem, R.left, R.top, c, r);
|
|
@@ -1290,9 +1383,15 @@
|
|
|
1290
1383
|
// 行高は wordWrap で、列幅は stretch の DOM 改変で可変なので、モデル計算でなく実測が要る。
|
|
1291
1384
|
_buildGeomCache() {
|
|
1292
1385
|
if (!this.wrap || !this.table) { this._rowTops = this._colLefts = null; return; }
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1386
|
+
const RT = new Array(this.ROWS + 1);
|
|
1387
|
+
if (this.virtual) {
|
|
1388
|
+
const base = this._theadH || 0, rowH = this.defRowH; // 仮想モードは一定行高=モデルで全行(窓外も算出可)
|
|
1389
|
+
for (let r = 0; r <= this.ROWS; r++) RT[r] = base + r * rowH;
|
|
1390
|
+
} else {
|
|
1391
|
+
const trs = this.table.querySelectorAll('tbody tr');
|
|
1392
|
+
RT[0] = this._theadH || 0;
|
|
1393
|
+
for (let r = 0; r < this.ROWS; r++) { const tr = trs[r]; RT[r + 1] = RT[r] + (tr ? tr.getBoundingClientRect().height : this._rowHeight(r)); }
|
|
1394
|
+
}
|
|
1296
1395
|
this._rowTops = RT;
|
|
1297
1396
|
// 列幅: リーフヘッダ th(マージ無しで確実)→ 無ければ colgroup col。stretch 後の実幅を拾う。
|
|
1298
1397
|
const colEl = c => (this.colHeaders ? this.table.querySelector('thead th[data-c="' + c + '"]') : null) || this.table.querySelector('colgroup col[data-c="' + c + '"]');
|
|
@@ -1347,18 +1446,21 @@
|
|
|
1347
1446
|
highlightRange() {
|
|
1348
1447
|
this.table.querySelectorAll('td.sel, th.selhdr').forEach(el => el.classList.remove('sel', 'selhdr'));
|
|
1349
1448
|
const { r0, r1, c0, c1 } = this.rectRange();
|
|
1350
|
-
// 選択範囲が及ぶ行/列ヘッダをハイライト(Excel
|
|
1449
|
+
// 選択範囲が及ぶ行/列ヘッダをハイライト(Excel 風のアフォーダンス)。仮想時は行ヘッダを窓内にクランプ(大量行で querySelector を無駄打ちしない)。
|
|
1450
|
+
const hlo = this.virtual ? Math.max(r0, this._vStart) : r0;
|
|
1451
|
+
const hhi = this.virtual ? Math.min(r1, this._vEnd - 1) : r1;
|
|
1351
1452
|
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 =
|
|
1453
|
+
for (let r = hlo; r <= hhi; r++) { const th = this.table.querySelector('th.rowhead[data-r="' + r + '"]'); if (th) th.classList.add('selhdr'); }
|
|
1353
1454
|
this.wrap.style.setProperty('--tg-selhdr-fg', this._selhdrTextColor()); // 選択ヘッダ文字色を背景輝度から自動(暗背景→白)
|
|
1354
1455
|
if (r0 === r1 && c0 === c1) return;
|
|
1355
1456
|
const clampMerge = this._headerSel && this._merges; // 列/行選択では「範囲に収まらない結合」は塗らない(帯を塗り広げない)
|
|
1356
|
-
|
|
1457
|
+
// 仮想スクロール: 窓内の行だけシェード(範囲枠 selrange は placeRect がモデル幾何で全域を描く)。
|
|
1458
|
+
for (let r = hlo; r <= hhi; r++) for (let c = c0; c <= c1; c++) {
|
|
1357
1459
|
if (clampMerge) {
|
|
1358
1460
|
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
1461
|
if (a.r < r0 || mr1 > r1 || a.c < c0 || mc1 > c1) continue; // 結合が選択範囲に収まらない=塗らない
|
|
1360
1462
|
}
|
|
1361
|
-
this.cellEl(r, c).classList.add('sel');
|
|
1463
|
+
const el = this.cellEl(r, c); if (el) el.classList.add('sel');
|
|
1362
1464
|
}
|
|
1363
1465
|
}
|
|
1364
1466
|
// 選択ヘッダの文字色を --tg-selhdr-bg の輝度から自動決定(暗背景→白/明背景→濃紺)。
|
|
@@ -1437,15 +1539,18 @@
|
|
|
1437
1539
|
if (!this._beforeSel(a, a)) return;
|
|
1438
1540
|
this._headerSel = null;
|
|
1439
1541
|
this.active = a; this.extent = { ...a };
|
|
1542
|
+
if (this.virtual) this._vEnsureVisible(a.r); // 窓に入れてから placement(窓外なら activeTd=null になるため)
|
|
1440
1543
|
this.place(this.editor, this.activeTd()); this.toNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1441
|
-
this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1544
|
+
if (!this.virtual) this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' }); // 仮想は _vEnsureVisible が可視化済み
|
|
1442
1545
|
this._fireSelection();
|
|
1443
1546
|
}
|
|
1444
1547
|
setExtent(r, c) {
|
|
1445
1548
|
const e = { r: this.clampR(r), c: this.clampC(c) };
|
|
1446
1549
|
if (!this._beforeSel(this.active, e)) return;
|
|
1447
|
-
this.extent = e;
|
|
1448
|
-
|
|
1550
|
+
this.extent = e;
|
|
1551
|
+
if (this.virtual) this._vEnsureVisible(e.r);
|
|
1552
|
+
this.updateSelectionUI();
|
|
1553
|
+
if (!this.virtual) this.cellEl(this.extent.r, this.extent.c).scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1449
1554
|
this._fireSelection();
|
|
1450
1555
|
}
|
|
1451
1556
|
selectRect(r0, c0, r1, c1) {
|
|
@@ -1458,8 +1563,9 @@
|
|
|
1458
1563
|
}
|
|
1459
1564
|
focusAndSelect(sel) {
|
|
1460
1565
|
this.active = { ...sel.active }; this.extent = { ...sel.extent };
|
|
1566
|
+
if (this.virtual) this._vEnsureVisible(this.active.r);
|
|
1461
1567
|
this.place(this.editor, this.activeTd()); this.toNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1462
|
-
this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1568
|
+
if (!this.virtual) this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1463
1569
|
this._fireSelection();
|
|
1464
1570
|
}
|
|
1465
1571
|
|
|
@@ -1610,7 +1716,10 @@
|
|
|
1610
1716
|
} else if (th.dataset.r != null) { // 行ヘッダ
|
|
1611
1717
|
const r = +th.dataset.r;
|
|
1612
1718
|
if (e.shiftKey) this._extendRow(r); else this.selectRow(r);
|
|
1613
|
-
this.editor.focus();
|
|
1719
|
+
this.editor.focus();
|
|
1720
|
+
// 'header'モード: 行ヘッダーセル全体が移動ハンドル=行を選択した上で移動を armする(クリック=選択のまま/ドラッグで移動)。縦結合/フィルタ中は不可。
|
|
1721
|
+
if (this.rowReorderWhole && !e.shiftKey && !this._hasVMerge() && !this._allRows) this._startRowMove(e);
|
|
1722
|
+
else this.headerDrag = 'row'; // 通常: ドラッグで複数行選択
|
|
1614
1723
|
} else { // 左上の角 → 全選択
|
|
1615
1724
|
this.selectAll(); this.editor.focus();
|
|
1616
1725
|
}
|
|
@@ -2109,11 +2218,72 @@
|
|
|
2109
2218
|
clearCopyMarquee() { this.copybox.style.display = 'none'; this.pendingCut = null; }
|
|
2110
2219
|
|
|
2111
2220
|
_tdAtPoint(e) { const el = document.elementFromPoint(e.clientX, e.clientY); return el && el.closest ? el.closest('td[data-r]') : null; }
|
|
2221
|
+
// ポインタ Y から絶対行を算出(td が無い窓外/スペーサ上でも使える=仮想スクロールのドラッグ用。一定行高モデル)。
|
|
2222
|
+
_rowFromPointerY(clientY) {
|
|
2223
|
+
const wr = this.wrap.getBoundingClientRect();
|
|
2224
|
+
const contentY = clientY - wr.top + this.wrap.scrollTop - (this.colHeaders ? (this._theadH || 0) : 0);
|
|
2225
|
+
return Math.max(0, Math.min(this.ROWS - 1, Math.floor(contentY / this.defRowH)));
|
|
2226
|
+
}
|
|
2227
|
+
// ポインタ X から列を算出(_colLefts 累積から。隠し列は飛ばす)。
|
|
2228
|
+
_colFromPointerX(clientX) {
|
|
2229
|
+
const wr = this.wrap.getBoundingClientRect(), x = clientX - wr.left + this.wrap.scrollLeft, L = this._colLefts;
|
|
2230
|
+
if (L) for (let c = this.COLS - 1; c >= 0; c--) if (!this._isHidden(c) && x >= L[c]) return c;
|
|
2231
|
+
return this.extent.c;
|
|
2232
|
+
}
|
|
2233
|
+
_stopAutoScroll() { if (this._asRAF) { try { cancelAnimationFrame(this._asRAF); } catch (_) {} this._asRAF = 0; } this._dragDir = 0; }
|
|
2234
|
+
_updateFillPreview(r, c, alt) {
|
|
2235
|
+
this._fillRow = r;
|
|
2236
|
+
const fr = this.fillRectFromPointer(r, c, this.filling.src, alt);
|
|
2237
|
+
if (fr) { this.placeRect(this.fillpreview, fr.r0, fr.c0, fr.r1, fr.c1); this.fillpreview.style.display = 'block'; }
|
|
2238
|
+
else this.fillpreview.style.display = 'none';
|
|
2239
|
+
}
|
|
2240
|
+
// ドラッグ/フィル中に上下端を越えたら自動スクロールしながら選択/プレビューを伸ばす。
|
|
2241
|
+
// 重要: 伸ばす先は「カウンタ加算」でなく常に【ポインタ位置の行】=端を1pxだけ超えたら1行しか伸びない(暴走しない)。
|
|
2242
|
+
// 自動スクロール速度は端からのはみ出し量に比例(少し超えた=ゆっくり / 大きく超えた=速く)。
|
|
2243
|
+
_edgeAutoScroll(e, mode) {
|
|
2244
|
+
this._dragY = e.clientY; this._dragX = e.clientX; this._dragAlt = !!e.altKey; this._dragMode = mode;
|
|
2245
|
+
const r = this._rowFromPointerY(e.clientY), c = this._colFromPointerX(e.clientX);
|
|
2246
|
+
if (mode === 'drag') this._extendTo(r, c); else this._updateFillPreview(r, c, this._dragAlt); // まずポインタ位置のセルへ
|
|
2247
|
+
const wr = this.wrap.getBoundingClientRect();
|
|
2248
|
+
const headB = wr.top + (this.colHeaders ? (this._theadH || 0) : 0); // 上端=ヘッダ下
|
|
2249
|
+
const leftB = wr.left + this._frozenPaneW(); // 左端=固定ペイン(行番号+固定列)の右
|
|
2250
|
+
const past = e.clientY > wr.bottom || e.clientY < headB || e.clientX > wr.right || e.clientX < leftB;
|
|
2251
|
+
if (past && !this._asRAF) this._asRAF = requestAnimationFrame(() => this._autoScrollTick());
|
|
2252
|
+
}
|
|
2253
|
+
_autoScrollTick() {
|
|
2254
|
+
this._asRAF = 0;
|
|
2255
|
+
if (!this.dragging && !this.filling) return;
|
|
2256
|
+
const wr = this.wrap.getBoundingClientRect();
|
|
2257
|
+
const headB = wr.top + (this.colHeaders ? (this._theadH || 0) : 0), leftB = wr.left + this._frozenPaneW();
|
|
2258
|
+
const amp = d => Math.min(24, 2 + d * 0.2); // px/フレーム=はみ出し量に比例(上限24・暴走しない)
|
|
2259
|
+
let v = 0, vh = 0;
|
|
2260
|
+
if (this._dragY > wr.bottom) v = amp(this._dragY - wr.bottom); else if (this._dragY < headB) v = -amp(headB - this._dragY);
|
|
2261
|
+
if (this._dragX > wr.right) vh = amp(this._dragX - wr.right); else if (this._dragX < leftB) vh = -amp(leftB - this._dragX);
|
|
2262
|
+
if (v !== 0) {
|
|
2263
|
+
const before = this.wrap.scrollTop;
|
|
2264
|
+
this.wrap.scrollTop = Math.max(0, before + v);
|
|
2265
|
+
if (this.wrap.scrollTop !== before && this.virtual) this._renderWindow(this._vWindow(), false); // 縦は仮想時のみ再窓(非仮想で呼ぶと tbody を壊す)
|
|
2266
|
+
}
|
|
2267
|
+
if (vh !== 0) this.wrap.scrollLeft = Math.max(0, this.wrap.scrollLeft + vh); // 横は全列描画なので scrollLeft だけ
|
|
2268
|
+
const r = this._rowFromPointerY(this._dragY), c = this._colFromPointerX(this._dragX); // 伸ばす先は常にポインタのセル
|
|
2269
|
+
if (this._dragMode === 'drag') this._extendTo(r, c); else this._updateFillPreview(r, c, this._dragAlt);
|
|
2270
|
+
const maxTop = this.wrap.scrollHeight - this.wrap.clientHeight, maxLeft = this.wrap.scrollWidth - this.wrap.clientWidth;
|
|
2271
|
+
const vStuck = (v > 0 && this.wrap.scrollTop >= maxTop) || (v < 0 && this.wrap.scrollTop <= 0);
|
|
2272
|
+
const hStuck = (vh > 0 && this.wrap.scrollLeft >= maxLeft) || (vh < 0 && this.wrap.scrollLeft <= 0);
|
|
2273
|
+
if ((v !== 0 && !vStuck) || (vh !== 0 && !hStuck)) this._asRAF = requestAnimationFrame(() => this._autoScrollTick());
|
|
2274
|
+
}
|
|
2275
|
+
// setExtent の自動可視化(_vEnsureVisible)を伴わない版=自動スクロールは _autoScrollTick が手動制御するため、二重スクロールを避ける。
|
|
2276
|
+
_extendTo(r, c) {
|
|
2277
|
+
const e = { r: this.clampR(r), c: this.clampC(c) };
|
|
2278
|
+
if (!this._beforeSel(this.active, e)) return;
|
|
2279
|
+
this.extent = e; this.updateSelectionUI(); this._fireSelection();
|
|
2280
|
+
}
|
|
2112
2281
|
|
|
2113
2282
|
// スクロール / リサイズでセル位置が動いた時、エディタと選択枠を貼り直す
|
|
2114
2283
|
_reflow() {
|
|
2115
2284
|
if (this.stretchH !== 'none') this._applyStretch(); // 枠幅が変わったら余白を再配分
|
|
2116
2285
|
this._buildGeomCache(); // 幅/枠が変わったので実測幾何を取り直す(リサイズ/ResizeObserver/window resize)
|
|
2286
|
+
if (this.virtual) this._renderWindow(this._vWindow(), false); // 枠高が変われば窓行数も変わる=窓を同期
|
|
2117
2287
|
const td = this.activeTd(); if (!td) return;
|
|
2118
2288
|
this.place(this.editor, td);
|
|
2119
2289
|
if (this.select && this.select.style.display !== 'none') this.place(this.select, td);
|
|
@@ -2121,6 +2291,7 @@
|
|
|
2121
2291
|
}
|
|
2122
2292
|
|
|
2123
2293
|
_bind() {
|
|
2294
|
+
if (this.virtual) this.wrap.addEventListener('scroll', () => this._vSync(), { passive: true }); // 仮想スクロール: 窓再描画
|
|
2124
2295
|
this.editor.addEventListener('keydown', (e) => this._onKey(e));
|
|
2125
2296
|
this.editor.addEventListener('compositionstart', () => {
|
|
2126
2297
|
if (this.mode !== 'nav') return;
|
|
@@ -2158,7 +2329,7 @@
|
|
|
2158
2329
|
const cls = e.target.classList;
|
|
2159
2330
|
if (cls && cls.contains('tg-colgrip')) { this._startColResize(e); return; } // 列幅リサイズ
|
|
2160
2331
|
if (cls && cls.contains('tg-rowgrip')) { this._startRowResize(e); return; } // 行高リサイズ
|
|
2161
|
-
if (cls && cls.contains('tg-rowmove')) { this._startRowMove(e); return; } //
|
|
2332
|
+
if (cls && cls.contains('tg-rowmove') && !this.rowReorderWhole) { this._startRowMove(e); return; } // ⠿つまみドラッグ移動('header'モードはセル全体扱い=_onHeaderMouseDown 経由で選択も伴う)
|
|
2162
2333
|
const th = e.target.closest('th');
|
|
2163
2334
|
if (th) { this._onHeaderMouseDown(e, th); return; } // 行/列/全体 選択
|
|
2164
2335
|
const td = e.target.closest('td[data-r]'); if (!td) return;
|
|
@@ -2188,7 +2359,7 @@
|
|
|
2188
2359
|
const td = e.target.closest('td[data-r]'); if (!td) return;
|
|
2189
2360
|
this.setActive(+td.dataset.r, +td.dataset.c); this._beginEdit(this.active.r, this.active.c);
|
|
2190
2361
|
});
|
|
2191
|
-
this.fillhandle.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); this.filling = { src: this.rectRange() }; });
|
|
2362
|
+
this.fillhandle.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); this.filling = { src: this.rectRange() }; this._fillRow = null; });
|
|
2192
2363
|
// 右クリックメニュー(行/列の挿入・削除)
|
|
2193
2364
|
this.table.addEventListener('contextmenu', (e) => this._onContextMenu(e));
|
|
2194
2365
|
this.menuEl.addEventListener('mousedown', (e) => {
|
|
@@ -2256,12 +2427,15 @@
|
|
|
2256
2427
|
}
|
|
2257
2428
|
return;
|
|
2258
2429
|
}
|
|
2259
|
-
if (this.dragging) {
|
|
2430
|
+
if (this.dragging) {
|
|
2431
|
+
const td = this._tdAtPoint(e);
|
|
2432
|
+
if (td) { this._stopAutoScroll(); this.setExtent(+td.dataset.r, +td.dataset.c); }
|
|
2433
|
+
else this._edgeAutoScroll(e, 'drag'); // 窓外/端=自動スクロール+モデル幾何で伸ばす
|
|
2434
|
+
}
|
|
2260
2435
|
else if (this.filling) {
|
|
2261
|
-
const td = this._tdAtPoint(e);
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
else this.fillpreview.style.display = 'none';
|
|
2436
|
+
const td = this._tdAtPoint(e);
|
|
2437
|
+
if (td) { this._stopAutoScroll(); this._updateFillPreview(+td.dataset.r, +td.dataset.c, e.altKey); }
|
|
2438
|
+
else this._edgeAutoScroll(e, 'fill');
|
|
2265
2439
|
}
|
|
2266
2440
|
});
|
|
2267
2441
|
this._on(document, 'mouseup', (e) => {
|
|
@@ -2280,8 +2454,9 @@
|
|
|
2280
2454
|
return;
|
|
2281
2455
|
}
|
|
2282
2456
|
if (this.headerDrag) { this.headerDrag = null; return; }
|
|
2283
|
-
if (this.dragging) { this.dragging = false; return; }
|
|
2457
|
+
if (this.dragging) { this.dragging = false; this._stopAutoScroll(); return; }
|
|
2284
2458
|
if (this.filling) {
|
|
2459
|
+
this._stopAutoScroll();
|
|
2285
2460
|
// autoInsertRow: フィルハンドルをテーブル下端より下へドロップしたら、足りない行を追加(縦フィル時)
|
|
2286
2461
|
if (this.fillAutoInsertRow && this.fillDirection !== 'horizontal' && !this._tdAtPoint(e)) {
|
|
2287
2462
|
const tb = this.table.getBoundingClientRect();
|
|
@@ -2291,8 +2466,11 @@
|
|
|
2291
2466
|
this.buildTable();
|
|
2292
2467
|
}
|
|
2293
2468
|
}
|
|
2294
|
-
|
|
2295
|
-
const
|
|
2469
|
+
// 確定の対象セル: td があればそれ、無ければ(窓外)モデル幾何=直近のフィル行/ポインタ位置から。
|
|
2470
|
+
const td = this._tdAtPoint(e);
|
|
2471
|
+
const tr = td ? +td.dataset.r : (this._fillRow != null ? this._fillRow : this._rowFromPointerY(e.clientY));
|
|
2472
|
+
const tc = td ? +td.dataset.c : this._colFromPointerX(e.clientX);
|
|
2473
|
+
const fr = this.fillRectFromPointer(tr, tc, this.filling.src, e.altKey);
|
|
2296
2474
|
this.fillpreview.style.display = 'none';
|
|
2297
2475
|
const src = this.filling.src; this.filling = null;
|
|
2298
2476
|
if (fr) { this.doFill(src, fr, e.ctrlKey || e.metaKey); this.selectRect(fr.r0, fr.c0, fr.r1, fr.c1); }
|