@threestarsoftware/tssgrid 0.1.3 → 0.1.4
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 +14 -0
- package/README.md +9 -4
- package/package.json +1 -1
- package/src/tssgrid.css +6 -0
- package/src/tssgrid.d.ts +32 -1
- package/src/tssgrid.js +244 -33
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
TssGrid の各リリースの変更点。日付は JST。形式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) 準拠(ゆるめ)。
|
|
4
4
|
数値は特記なき限り**自社調べ**(headless Chrome / Windows 11・環境差あり)。
|
|
5
5
|
|
|
6
|
+
## [0.1.4] - 2026-07-21
|
|
7
|
+
|
|
8
|
+
### 追加
|
|
9
|
+
- **`appendRows(rows, opts?)`=背景で"育てる"progressive 追記**(コア)— `data` 末尾へ足し、可視窓+スペーサだけ再描画する。`buildTable` 全再描画をせず、**スクロール位置・アクティブセル・選択・編集中エディタと値を保ち、履歴にも積まない**。「先頭ページを描く→残りを idle で `appendRows`」で first-paint を早める土台。既存の `setData`/`insertRows` 経路は不変=opt-in。
|
|
10
|
+
- **遅延ロード(on-demand)=`setRowCount(total)` / `fillRows(start, rows)` / `onViewportChange(start, end)` / `onSortFilterChange(state)` / `TssGrid.PENDING`**(コア・`virtual:true` 前提)— 総件数を確保して未取得ぶんは「読込中」行で持ち、スクロールで `onViewportChange` が必要な範囲を通知→アプリが取得→`fillRows` で流し込む。**sort/filter は遅延中ローカル計算せず `onSortFilterChange` でサーバへ委譲**、**未取得セルは編集不可**(カーソルは乗れる)、**export(`getData`/`getRows`/`toCSV`)はロード済みのみ**返す。=AG Grid の Infinite Row Model 相当を、最小コアの push プリミティブで実装(datasource/ブロックキャッシュはコアに持たない=将来アドオンに委ねる)。大量一覧の first-paint を大きく縮められる。
|
|
11
|
+
- **`insertRows(ri, count?, where?)`=一括行挿入プリミティブ**(コア)— `count` 行を1回の再描画・1 undo で挿入。1行ずつの `insertRow` を繰り返す O(N²) を O(N) に。`insertRow` は `insertRows(…, 1, …)` へ委譲=後方互換。
|
|
12
|
+
- **`pasteAutoGrow`(オプション・既定 `false`)**(コア)— 貼付元の行数がグリッド行数を超える時、不足分を先に `insertRows` で一括生成してから貼る(Excel から貼れば貼った行数だけ行が増える)。行生成+値貼付は1回の undo で戻る。`maxRows` 上限内・超過は `pasteOverflow` に従う。
|
|
13
|
+
|
|
14
|
+
### 変更
|
|
15
|
+
- **貼り付けを paste イベント主経路化**(コア)— Ctrl+V を `navigator.clipboard.readText()`(初回に**許可プロンプト**が出る・Chrome 仕様)から、ネイティブ paste イベント(`clipboardData.getData`)へ移した。ジェスチャー内で同期に読める=**許可プロンプト不要・より堅牢**(権限/非セキュアコンテキスト/非フォーカスで落ちない)。右クリックメニューの「貼り付け」とプログラム呼び出しは従来どおり `readText`。セル編集中の1セル貼付は native 挙動に委ねる(回帰なし)。
|
|
16
|
+
|
|
17
|
+
### 改善(性能)
|
|
18
|
+
- **大量貼付を O(N) 化**(コア)— 1000行規模の貼付で per-cell 描画 N 回を「最後に `buildTable` 1回」へ畳み、巨大選択の選択塗り(従来 O(行×列))をアウトライン矩形へ委譲。`insertRows`/`pasteAutoGrow` と併せ、大量貼付が O(N²)→O(N)(自社調べ・実機で大幅短縮)。
|
|
19
|
+
|
|
6
20
|
## [0.1.3] - 2026-07-17
|
|
7
21
|
|
|
8
22
|
### 追加
|
package/README.md
CHANGED
|
@@ -784,6 +784,7 @@ new TssGrid(el, { hiddenColumns: [1] }); // 初期で隠す
|
|
|
784
784
|
| `invalidTitle` | `boolean` | `true` | `keep` 時、赤セルに validator メッセージを `title=`(ホバーで理由)。`false` でOFF。全件は `getInvalidCells()` |
|
|
785
785
|
| `messages` | `{code:…}\|fn` | — | エラーの**共通メッセージカタログ**。`validator` が `{code,params,level}` を返すと本文を解決(`{label}`/`{value}`/params 差込)。本文を外部JSON/CSV/XMLで一元管理 |
|
|
786
786
|
| `pasteOverflow` | `'clip'\|'error'` | `'clip'` | 貼り付けがグリッド外に超過した時。`clip`=はみ出し分を切り捨て / `error`=貼り付け中止 |
|
|
787
|
+
| `pasteAutoGrow` | `boolean` | `false` | 貼付元の行数がグリッド行数を超える時、不足分を **`insertRows` で先に一括生成**してから貼る(Excel から貼れば貼った行数だけ行が増える)。**行生成+値貼付は1回の undo で両方戻る**。`maxRows` 上限内・超過分は `pasteOverflow` に従う。既定 `false`=従来どおりはみ出しは切り捨て |
|
|
787
788
|
| `colWidths` / `rowHeights` | `number[]` \| `{[key]:number}` | `[]` | 列幅 / 行高の初期値(px)。未指定の要素は既定値。`colWidths` は**配列(位置指定)でも、`{ dataキー: px }` のオブジェクト(列ID指定)でも可**=後者は列を入れ替えても幅が追従する。列定義側に `columns[c].width` で同居指定も可(並べ替え・コピペで幅が一緒に動く=最も壊れにくい)。優先度: 実行時リサイズ/明示 `colWidths` > `columns[c].width` > 既定 |
|
|
788
789
|
| `defaultColWidth` / `defaultRowHeight` | `number` | `120` / `28` | 既定の列幅 / 行高(px) |
|
|
789
790
|
| `minColWidth` / `minRowHeight` | `number` | `30` / `18` | リサイズの下限(px) |
|
|
@@ -850,7 +851,7 @@ new TssGrid(el, {
|
|
|
850
851
|
});
|
|
851
852
|
```
|
|
852
853
|
|
|
853
|
-
メソッド: `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()`(列定義変更後の再描画, データ・選択は保持)ほか。
|
|
854
|
+
メソッド: `getData()` / `getRows()` / `getRow(r)` / `getColumn(cOrKey)` / `getColumns()` / `setData(rows)` / `appendRows(rows,opts?)`(**背景で"育てる"追記**=data末尾に足し可視窓だけ再描画。buildTable全再描画せずスクロール位置・選択・編集中エディタを保ち履歴にも積まない。「先頭ページを描く→残りをidleでappendRows」でfirst-paintを早める土台。既存経路は不変のopt-in) / `setRowCount(total)` + `fillRows(start,rows)`(**遅延ロード=on-demand**=virtual前提。総件数を確保し未取得ぶんは「読込中」行、`onViewportChange(start,end)` で要る範囲を受け `fillRows` で実データを流し込む。sort/filterは遅延中ローカル計算せず `onSortFilterChange` でサーバ委譲、未取得セルは編集不可、export(getData/toCSV)はロード済みのみ=AG Grid の Infinite Row Model 相当を push で) / `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')` / `insertRows(ri,count?,'above'|'below')`(count 行を1回の再描画・1 undo で一括挿入=大量挿入が O(N)) / `deleteRows(r0,r1?)` / `insertCol(ci,'left'|'right')` / `deleteCols(c0,c1?)` / `redraw()`(列定義変更後の再描画, データ・選択は保持)ほか。
|
|
854
855
|
|
|
855
856
|
## イベント
|
|
856
857
|
|
|
@@ -890,8 +891,12 @@ new TssGrid(el, {
|
|
|
890
891
|
onAfterCopy(data, { range, cut }) {},
|
|
891
892
|
|
|
892
893
|
// 行/列の挿入・削除の前後。before は false で中止できる
|
|
893
|
-
onBeforeStructureChange(info) { /* info.type = 'insertRow'|'deleteRows'|'insertCol'|'deleteCols'。return false で中止 */ },
|
|
894
|
+
onBeforeStructureChange(info) { /* info.type = 'insertRow'|'insertRows'|'deleteRows'|'insertCol'|'deleteCols'。return false で中止 */ },
|
|
894
895
|
onStructureChange(info) {}, // 後。undo 時は info.undo = true
|
|
896
|
+
|
|
897
|
+
// 遅延ロード(push・on-demand)。setRowCount/fillRows と組で使う
|
|
898
|
+
onViewportChange(start, end) {}, // 描画窓が変わった=「今この範囲が要る」。ここで fillRows(start, 取得した行) する
|
|
899
|
+
onSortFilterChange(state) {}, // 遅延中の sort/filter 要求。ローカル計算せずサーバで並替/絞込→setRowCount+fillRows し直す(state.type='sort'|'filter')
|
|
895
900
|
});
|
|
896
901
|
```
|
|
897
902
|
|
|
@@ -1109,11 +1114,11 @@ new TssGrid(el, {
|
|
|
1109
1114
|
allowDeleteRows: true,
|
|
1110
1115
|
allowInsertCols: true,
|
|
1111
1116
|
allowDeleteCols: true,
|
|
1112
|
-
onStructureChange: (info) => console.log(info.type), // 'insertRow' | 'deleteRows' | 'insertCol' | 'deleteCols'
|
|
1117
|
+
onStructureChange: (info) => console.log(info.type), // 'insertRow' | 'insertRows' | 'deleteRows' | 'insertCol' | 'deleteCols'
|
|
1113
1118
|
});
|
|
1114
1119
|
```
|
|
1115
1120
|
|
|
1116
|
-
プログラムからも操作できます: **`insertRow(ri, 'above'|'below')`** / **`deleteRows(r0, r1?)`** / **`insertCol(ci, 'left'|'right')`** / **`deleteCols(c0, c1?)`**。最低1行・1
|
|
1121
|
+
プログラムからも操作できます: **`insertRow(ri, 'above'|'below')`** / **`insertRows(ri, count?, 'above'|'below')`**(`count` 行を **1回の再描画・1つの undo** で一括挿入=大量行の追加が O(N²)→O(N)。CSV取込・テンプレ行のまとめ足しに) / **`deleteRows(r0, r1?)`** / **`insertCol(ci, 'left'|'right')`** / **`deleteCols(c0, c1?)`**。最低1行・1列は残します。大量貼付で行を増やしたい場合は `pasteAutoGrow: true` も参照。
|
|
1117
1122
|
|
|
1118
1123
|
既定メニューには行/列の挿入・削除に加え **コピー / 切り取り / 貼り付け / 内容をクリア** も入ります(クリップボードは `copyPaste:false` で消えます)。
|
|
1119
1124
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threestarsoftware/tssgrid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
|
package/src/tssgrid.css
CHANGED
|
@@ -230,3 +230,9 @@
|
|
|
230
230
|
.tssgrid .tg-menu .tg-mi.tg-has-sub::after { content: '▶'; position: absolute; right: 10px; top: 0; bottom: 0; display: flex; align-items: center; font-size: 9px; color: #7d8a99; }
|
|
231
231
|
.tssgrid .tg-menu .tg-submenu { display: none; position: absolute; left: 100%; top: -5px; background: #fff; border: 1px solid var(--tg-wrap-border); border-radius: var(--tg-radius); box-shadow: 0 4px 16px rgba(0,0,0,.18); padding: 4px 0; min-width: 150px; }
|
|
232
232
|
.tssgrid .tg-menu .tg-mi.tg-has-sub:hover > .tg-submenu { display: block; }
|
|
233
|
+
|
|
234
|
+
/* 遅延ロード(push): 未取得(PENDING)行の「読込中」プレースホルダ。値は描かず淡いシマーの点線を出す */
|
|
235
|
+
.tssgrid .tg-table tr.tg-pending-row td.tg-pending-cell { color: var(--tg-muted); text-align: center; cursor: default; }
|
|
236
|
+
.tssgrid .tg-table tr.tg-pending-row .tg-pending-dots { display: inline-block; width: 40%; max-width: 220px; height: 8px; border-radius: 4px;
|
|
237
|
+
background: linear-gradient(90deg, #eef2f6 25%, #e2e8f0 37%, #eef2f6 63%); background-size: 400% 100%; animation: tg-pending-shimmer 1.2s ease-in-out infinite; }
|
|
238
|
+
@keyframes tg-pending-shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
|
package/src/tssgrid.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface Rejection { r: number; c: number; value: any; code?: string; le
|
|
|
33
33
|
export interface ReadOnlyBlock { r: number; c: number; value: any; }
|
|
34
34
|
|
|
35
35
|
/** 行/列の挿入・削除の情報(onStructureChange / onBeforeStructureChange)。 */
|
|
36
|
-
export interface StructureInfo { type: 'insertRow' | 'deleteRows' | 'insertCol' | 'deleteCols'; undo?: boolean; [k: string]: any; }
|
|
36
|
+
export interface StructureInfo { type: 'insertRow' | 'insertRows' | 'deleteRows' | 'insertCol' | 'deleteCols'; undo?: boolean; [k: string]: any; }
|
|
37
37
|
|
|
38
38
|
/** validator の戻り値: true=OK / 文字列=エラー文 / オブジェクト=メッセージカタログ解決用。 */
|
|
39
39
|
export type ValidatorResult = true | string | { code: string; params?: Record<string, any>; level?: string };
|
|
@@ -107,6 +107,8 @@ export interface TssGridOptions {
|
|
|
107
107
|
/** エラーの共通メッセージカタログ。 */
|
|
108
108
|
messages?: Record<string, any> | ((info: { code: string; params?: any; level?: string }) => string);
|
|
109
109
|
pasteOverflow?: 'clip' | 'error';
|
|
110
|
+
/** 貼付元の行数がグリッド行数を超える時、不足分を insertRows で一括生成してから貼る(既定 false=従来のクランプ)。行生成+値貼付は1回の undo で戻る。maxRows 上限内・超過分は pasteOverflow に従う。 */
|
|
111
|
+
pasteAutoGrow?: boolean;
|
|
110
112
|
colWidths?: number[] | Record<string, number>;
|
|
111
113
|
rowHeights?: number[] | Record<string, number>;
|
|
112
114
|
defaultColWidth?: number;
|
|
@@ -191,6 +193,10 @@ export interface TssGridOptions {
|
|
|
191
193
|
onAfterCopy?: (data: string[][], info: { range: SelectionRange; cut: boolean }) => void;
|
|
192
194
|
onBeforeStructureChange?: (info: StructureInfo) => boolean | void;
|
|
193
195
|
onStructureChange?: (info: StructureInfo) => void;
|
|
196
|
+
/** 遅延ロード(push): 描画窓が変わった時に呼ぶ (start,end)。未取得(PENDING)行を fillRows で埋めるトリガ。 */
|
|
197
|
+
onViewportChange?: (start: number, end: number) => void;
|
|
198
|
+
/** 遅延ロード(push): dataComplete=false で sort/filter 要求が来た時に呼ぶ。エンジンはローカル計算せず、これで通知+パージ=プラグインがサーバ側で並替/絞込して setRowCount+fillRows し直す。 */
|
|
199
|
+
onSortFilterChange?: (state: { type: 'sort' | 'filter'; sort?: any; filter?: any }) => void;
|
|
194
200
|
onBeforeAutofill?: (src: SelectionRange, target: SelectionRange) => boolean | void;
|
|
195
201
|
onAfterAutofill?: (src: SelectionRange, target: SelectionRange) => void;
|
|
196
202
|
onBeforeKeyDown?: (e: KeyboardEvent) => boolean | void;
|
|
@@ -312,6 +318,26 @@ export declare class TssGrid {
|
|
|
312
318
|
getValue(r: number, cOrKey: number | string): any;
|
|
313
319
|
/** データを差し替え(履歴・フィルタ・整列状態はリセット)。 */
|
|
314
320
|
setData(rows: any[][] | Record<string, any>[]): void;
|
|
321
|
+
/**
|
|
322
|
+
* 背景で"育てる"追記(progressive)。data 末尾へ rows を足し、可視窓+スペーサだけ再描画する。
|
|
323
|
+
* buildTable 全再描画をせず、スクロール位置・アクティブセル・フォーカス・選択・編集中のエディタと値を保ち、履歴にも積まない。
|
|
324
|
+
* 「先頭ページを描く→残りを idle で appendRows」して first-paint を早める土台(既存の setData/insertRows 経路は不変)。
|
|
325
|
+
* rows は現在のデータと同形(オブジェクトモードはオブジェクト配列/配列モードは行配列)。
|
|
326
|
+
*/
|
|
327
|
+
appendRows(rows: any[][] | Record<string, any>[], opts?: { keepScroll?: boolean; noHistory?: boolean }): void;
|
|
328
|
+
/**
|
|
329
|
+
* 遅延ロード(push・on-demand)。総行数を total に確保し、未取得ぶんを PENDING 行で埋める(virtual 前提)。
|
|
330
|
+
* dataComplete=false になり、この間 minSpareRows は自動無効。onViewportChange で「今要る範囲」を受け fillRows で埋める流れ。
|
|
331
|
+
* setData で解除。AG Grid の Infinite Row Model 相当を push プリミティブで。
|
|
332
|
+
*/
|
|
333
|
+
setRowCount(total: number): void;
|
|
334
|
+
/**
|
|
335
|
+
* 遅延ロード(push): PENDING 行 [start, start+rows.length) を実データに差し替える(onViewportChange への応答)。
|
|
336
|
+
* 塗り替え範囲が可視窓に重なる時だけ窓を再描画=スクロール/選択/アクティブは不変。全 PENDING が埋まると dataComplete=true。
|
|
337
|
+
*/
|
|
338
|
+
fillRows(start: number, rows: any[][] | Record<string, any>[]): void;
|
|
339
|
+
/** 遅延ロード: 全行がロード済みなら true。setRowCount で false、全 fillRows 完了で true に戻る。 */
|
|
340
|
+
readonly dataComplete: boolean;
|
|
315
341
|
/** 1セルに書き込み(履歴あり・検証/onBeforeChange 経由)。force=true で readOnly 貫通。 */
|
|
316
342
|
setValue(r: number, cOrKey: number | string, val: any, force?: boolean): void;
|
|
317
343
|
/** 履歴に積まず・検証も通さず直接書き込み(計算列など派生値用。readOnly 貫通)。 */
|
|
@@ -353,6 +379,8 @@ export declare class TssGrid {
|
|
|
353
379
|
|
|
354
380
|
// ---- 行 / 列 構造 -------------------------------------------------------
|
|
355
381
|
insertRow(ri: number, where?: 'above' | 'below'): void;
|
|
382
|
+
/** count 行を1回の再描画・1つの undo コマンドで挿入(大量挿入を O(N) に)。maxRows 上限でクランプ。既定 count=1 は insertRow と等価。 */
|
|
383
|
+
insertRows(ri: number, count?: number, where?: 'above' | 'below'): void;
|
|
356
384
|
deleteRows(r0: number, r1?: number): void;
|
|
357
385
|
insertCol(ci: number, where?: 'left' | 'right'): void;
|
|
358
386
|
deleteCols(c0: number, c1?: number): void;
|
|
@@ -407,6 +435,9 @@ export declare class TssGrid {
|
|
|
407
435
|
/** セルの DOM 要素を返す(低レベル・内部DOM構造に依存)。結合セルはアンカーに解決。 */
|
|
408
436
|
cellEl(r: number, c: number): HTMLTableCellElement | null;
|
|
409
437
|
|
|
438
|
+
/** 遅延ロード: 未取得行を表す sentinel。setRowCount が未取得ぶんを data[r] に入れ、fillRows で実データへ差し替える。 */
|
|
439
|
+
static readonly PENDING: unique symbol;
|
|
440
|
+
|
|
410
441
|
[k: string]: any;
|
|
411
442
|
}
|
|
412
443
|
|
package/src/tssgrid.js
CHANGED
|
@@ -127,6 +127,8 @@
|
|
|
127
127
|
this.allowDeleteCols = opts.allowDeleteCols !== false;
|
|
128
128
|
this.onStructureChange = opts.onStructureChange || null; // (info) 行/列の挿入・削除後
|
|
129
129
|
this.onFilter = opts.onFilter || null; // ({count,total,cleared?}) 非破壊フィルタ適用/解除後
|
|
130
|
+
this.onViewportChange = opts.onViewportChange || null; // (start,end) 遅延ロード: 描画窓が変わった=「今この範囲が要る」通知(fillRows で応答)
|
|
131
|
+
this.onSortFilterChange = opts.onSortFilterChange || null; // (state) 遅延ロード: sort/filter はローカル計算せずサーバへ委譲=この通知で再フェッチさせる
|
|
130
132
|
this.onBeforeStructureChange = opts.onBeforeStructureChange || null; // (info)=>false で挿入/削除を中止
|
|
131
133
|
this.onAfterPaste = opts.onAfterPaste || null; // (data, {range}) 貼り付け確定後
|
|
132
134
|
this.onBeforeCopy = opts.onBeforeCopy || null; // (data, {range})=>false 取消 / 配列で差替え
|
|
@@ -145,6 +147,9 @@
|
|
|
145
147
|
// 貼り付けがグリッド範囲を超えた時: 'clip'(既定, はみ出し分を切り捨て) | 'error'(貼り付け中止)
|
|
146
148
|
this.pasteOverflow = opts.pasteOverflow || 'clip';
|
|
147
149
|
this.onPasteOverflow = opts.onPasteOverflow || null; // (info) => void。error 時に呼ぶ
|
|
150
|
+
// 貼り付け元の行数がグリッド行数を超える時、先に不足分を insertRows で一括生成してから貼る(既定 false=従来のクランプ)。
|
|
151
|
+
// 行生成+値貼付を1つの history にまとめる(1回の undo で両方戻る)。maxRows 上限内・超過分は pasteOverflow に従う。
|
|
152
|
+
this.pasteAutoGrow = !!opts.pasteAutoGrow;
|
|
148
153
|
|
|
149
154
|
this.active = { r: 0, c: 0 }; this.extent = { r: 0, c: 0 };
|
|
150
155
|
this.mode = 'nav'; this.dragging = false; this.filling = null;
|
|
@@ -235,6 +240,9 @@
|
|
|
235
240
|
this.virtual = !!opts.virtual;
|
|
236
241
|
this._vBuf = (opts.virtual && typeof opts.virtual === 'object' && opts.virtual.buffer) || 6; // 窓の上下バッファ行数
|
|
237
242
|
this._vStart = 0; this._vEnd = 0; this._vRAF = 0; this._vRendered = false;
|
|
243
|
+
this._vpStart = -1; this._vpEnd = -1; // onViewportChange の最後に通知した範囲(同一範囲の再発火を抑止)
|
|
244
|
+
this.dataComplete = true; // 遅延ロード(push): false のとき data に PENDING 行が混じる(setRowCount)。sort/filter/spare の判定に使う
|
|
245
|
+
this._pendingCount = 0; // PENDING 行数(fillRows で減らし 0 で dataComplete=true へ)
|
|
238
246
|
if (this.virtual && (this.frozenRows || this.frozenCols || this.wordWrap)) {
|
|
239
247
|
try { console.warn('[TssGrid] virtual:true は固定行列/折り返し非対応のため、それらを無効化しました'); } catch (_) {}
|
|
240
248
|
this.frozenRows = 0; this.frozenCols = 0; this.wordWrap = false;
|
|
@@ -316,25 +324,106 @@
|
|
|
316
324
|
}
|
|
317
325
|
|
|
318
326
|
// ---- 取得用 API ----
|
|
319
|
-
|
|
320
|
-
//
|
|
327
|
+
// 常に 2次元配列(後方互換)。遅延ロード中は「ロード済みのみ」返す(未取得 PENDING 行は除外=AG Grid の as-rendered ポリシー。
|
|
328
|
+
// 全件が要る export はプラグインが全件 fillRows してから呼ぶ=dataComplete で通常経路)。
|
|
329
|
+
getData() { if (this.dataComplete) return this.data.map(row => row.slice()); const P = TssGrid.PENDING; return this.data.filter(row => row !== P).map(row => row.slice()); }
|
|
330
|
+
// オブジェクト配列で取り出す。_src(元の非表示フィールド)にキー列の現在値を上書きして返す。遅延ロード中はロード済みのみ。
|
|
321
331
|
getRows() {
|
|
322
|
-
|
|
332
|
+
const P = TssGrid.PENDING, out = [];
|
|
333
|
+
for (let r = 0; r < this.ROWS; r++) {
|
|
334
|
+
const row = this.data[r]; if (row === P) continue; // 未取得行は除外
|
|
323
335
|
const o = this._src ? Object.assign({}, this._src[r]) : {};
|
|
324
336
|
for (let c = 0; c < this.COLS; c++) { const k = this._key(c); if (k != null) this._setPathCOW(o, k, row[c]); }
|
|
325
|
-
|
|
326
|
-
}
|
|
337
|
+
out.push(o);
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
327
340
|
}
|
|
328
341
|
getRow(r) { return this.getRows()[r]; }
|
|
329
342
|
// getRow の対(行→列)。1列分の値を全行ぶん配列で。列は index か data キー。
|
|
330
|
-
getColumn(c) { const ci = this._resolveCol(c);
|
|
331
|
-
// 全列を「列ごとの値配列」で。見出しキーは data キー > headers。getRows
|
|
343
|
+
getColumn(c) { const ci = this._resolveCol(c); if (ci < 0) return undefined; const rows = this.dataComplete ? this.data : this.data.filter(row => row !== TssGrid.PENDING); return rows.map(row => row[ci]); }
|
|
344
|
+
// 全列を「列ごとの値配列」で。見出しキーは data キー > headers。getRows の列方向版。遅延ロード中はロード済みのみ。
|
|
332
345
|
getColumns() {
|
|
333
|
-
const out = {};
|
|
334
|
-
for (let c = 0; c < this.COLS; c++) { const k = this._key(c); out[k != null ? k : this.headers[c]] =
|
|
346
|
+
const out = {}, rows = this.dataComplete ? this.data : this.data.filter(row => row !== TssGrid.PENDING);
|
|
347
|
+
for (let c = 0; c < this.COLS; c++) { const k = this._key(c); out[k != null ? k : this.headers[c]] = rows.map(row => row[c]); }
|
|
335
348
|
return out;
|
|
336
349
|
}
|
|
337
|
-
setData(rows) { this._align.clear(); this._cellRO.clear(); this._unsortedData = null; this._allRows = this._allSrc = this._meta = this._filterFn = null; if (this.history) this.history.clear(); this._ingest(rows); this._padToMin(); this.buildTable(); const ic = this._resolveInitialCell(); this.setActive(ic[0], ic[1]); this._ensureSpare(); }
|
|
350
|
+
setData(rows) { this._align.clear(); this._cellRO.clear(); this._unsortedData = null; this._allRows = this._allSrc = this._meta = this._filterFn = null; this.dataComplete = true; this._pendingCount = 0; if (this.history) this.history.clear(); this._ingest(rows); this._padToMin(); this.buildTable(); const ic = this._resolveInitialCell(); this.setActive(ic[0], ic[1]); this._ensureSpare(); }
|
|
351
|
+
// 背景で"育てる"追記(progressive): data 末尾へ rows を足し ROWS を更新、可視窓+上下スペーサだけ再描画する。
|
|
352
|
+
// buildTable 全再描画をしない=スクロール位置・アクティブセル・フォーカス・選択範囲・編集中のエディタと値を保つ。
|
|
353
|
+
// history には積まない(背景成長は undo 対象外)。既存の setData/insertRows 経路は不変=完全 opt-in。
|
|
354
|
+
// 用途: プラグインが「先頭ページを描く→残りを idle で appendRows」して first-paint を早める土台。
|
|
355
|
+
// rows は現在のデータと同形(オブジェクトモードならオブジェクト配列/配列モードなら行配列)。opts は将来用(keepScroll/noHistory は
|
|
356
|
+
// 実装が tbody 差し替えのみ=スクロール保持・履歴不変を常に満たすため現状は指定不要)。
|
|
357
|
+
// 注意: フィルタ中(_allRows)はマスタにも積んで desync を防ぐ(部分ロード中のフィルタは想定外だが壊さない)。
|
|
358
|
+
// 非virtual+minSpareRows では追記行が末尾スペア行の後ろに入る(育成は主に virtual 用途)。
|
|
359
|
+
appendRows(rows, opts = {}) {
|
|
360
|
+
if (!Array.isArray(rows) || !rows.length) return;
|
|
361
|
+
const n0 = this.ROWS;
|
|
362
|
+
for (const src of rows) {
|
|
363
|
+
const srcIsObj = src && !Array.isArray(src) && typeof src === 'object';
|
|
364
|
+
const srcObj = srcIsObj ? Object.assign({}, src) : {};
|
|
365
|
+
let rowArr;
|
|
366
|
+
if (srcIsObj) rowArr = this._objToRow(src); // オブジェクト→キー列で行配列化
|
|
367
|
+
else { rowArr = Array.isArray(src) ? src.slice(0, this.COLS) : []; while (rowArr.length < this.COLS) rowArr.push(''); } // 配列は COLS 長に整える
|
|
368
|
+
this.data.push(rowArr);
|
|
369
|
+
this.rowH.push(undefined);
|
|
370
|
+
if (this._src) this._src.push(srcObj);
|
|
371
|
+
if (this._meta) this._meta.set(rowArr, { a: new Map(), ro: new Set(), h: undefined }); // フィルタが張った per-row メタも用意
|
|
372
|
+
if (this._allRows) { this._allRows.push(rowArr); if (this._allSrc) this._allSrc.push(srcObj); } // フィルタ中はマスタも同期
|
|
373
|
+
}
|
|
374
|
+
this.ROWS = this.data.length;
|
|
375
|
+
if (!this.table) return; // 未マウントなら次の buildTable が拾う
|
|
376
|
+
if (this.virtual) {
|
|
377
|
+
// 窓+上下スペーサを作り直す(force)。下スペーサ高が ROWS で再計算され新行までスクロール可能に。
|
|
378
|
+
// 窓が末尾に無い(新行が可視外)時も安全=可視 ~33 行の innerHTML 差し替えのみ・scrollTop 不変・
|
|
379
|
+
// エディタは tbody 外のオーバーレイなので値も維持。
|
|
380
|
+
this._renderWindow(this._vWindow(), true);
|
|
381
|
+
} else {
|
|
382
|
+
const tbody = this.table.querySelector('tbody');
|
|
383
|
+
if (tbody) { let html = ''; for (let r = n0; r < this.ROWS; r++) html += this._rowHTML(r); tbody.insertAdjacentHTML('beforeend', html); }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// 遅延ロード(push・Phase B): 総行数を total に確保する。現行の行はそのまま・不足分を PENDING 行で埋め、超過は切る。
|
|
387
|
+
// dataComplete=false になり、この間 minSpareRows は自動無効(末尾スペアと PENDING tail は両立不可)。
|
|
388
|
+
// virtual 前提(PENDING 領域はスペーサに畳まれ描画されない=軽い)。プラグインは onViewportChange→fillRows で実データを流し込む。
|
|
389
|
+
// setData で解除(実データに差し替わり dataComplete=true)。
|
|
390
|
+
setRowCount(total) {
|
|
391
|
+
total = Math.max(0, total | 0);
|
|
392
|
+
const cur = this.ROWS;
|
|
393
|
+
if (total > cur) { for (let r = cur; r < total; r++) { this.data.push(TssGrid.PENDING); this.rowH.push(undefined); } }
|
|
394
|
+
else if (total < cur) { this.data.length = total; this.rowH.length = total; }
|
|
395
|
+
this.ROWS = total;
|
|
396
|
+
let pend = 0; for (let r = 0; r < this.ROWS; r++) if (this.data[r] === TssGrid.PENDING) pend++; // 確保時の一度きり O(N)。以降 fillRows が差分で維持
|
|
397
|
+
this._pendingCount = pend; this.dataComplete = pend === 0;
|
|
398
|
+
if (this.active.r >= this.ROWS) this.active = { r: Math.max(0, this.ROWS - 1), c: this.active.c }; // アクティブが範囲外なら詰める
|
|
399
|
+
if (this.table) { if (this.virtual) this._renderWindow(this._vWindow(), true); else this.buildTable(); }
|
|
400
|
+
}
|
|
401
|
+
// 遅延ロード(push・Phase B): PENDING 行 [start, start+rows.length) を実データに差し替える(onViewportChange への応答)。
|
|
402
|
+
// 塗り替え範囲が可視窓に重なる時だけ窓を再描画(buildTable しない)=スクロール/選択/アクティブは不変。窓外は data 差し替えのみ。
|
|
403
|
+
// rows は現在のデータと同形(オブジェクトモードはオブジェクト配列/配列モードは行配列)。範囲外(>=ROWS)は無視。
|
|
404
|
+
fillRows(start, rows) {
|
|
405
|
+
if (!Array.isArray(rows) || !rows.length) return;
|
|
406
|
+
start = Math.max(0, start | 0);
|
|
407
|
+
for (let i = 0; i < rows.length; i++) {
|
|
408
|
+
const r = start + i; if (r >= this.ROWS) break;
|
|
409
|
+
const src = rows[i];
|
|
410
|
+
const srcIsObj = src && !Array.isArray(src) && typeof src === 'object';
|
|
411
|
+
const srcObj = srcIsObj ? Object.assign({}, src) : {};
|
|
412
|
+
let rowArr;
|
|
413
|
+
if (srcIsObj) rowArr = this._objToRow(src);
|
|
414
|
+
else { rowArr = Array.isArray(src) ? src.slice(0, this.COLS) : []; while (rowArr.length < this.COLS) rowArr.push(''); }
|
|
415
|
+
if (this.data[r] === TssGrid.PENDING) this._pendingCount--; // PENDING→実データ(既に実データの再fill は count 不変)
|
|
416
|
+
this.data[r] = rowArr;
|
|
417
|
+
if (this._src) this._src[r] = srcObj;
|
|
418
|
+
if (this._meta) this._meta.set(rowArr, { a: new Map(), ro: new Set(), h: undefined });
|
|
419
|
+
}
|
|
420
|
+
if (this._pendingCount < 0) this._pendingCount = 0;
|
|
421
|
+
this.dataComplete = this._pendingCount === 0;
|
|
422
|
+
if (!this.table) return;
|
|
423
|
+
if (this.virtual) {
|
|
424
|
+
if (start < this._vEnd && start + rows.length > this._vStart) this._renderWindow(this._vWindow(), true); // 可視窓に重なる時だけ再描画
|
|
425
|
+
} else this.buildTable();
|
|
426
|
+
}
|
|
338
427
|
// 初期カーソル位置を [r,c] に解決(c は index か data キー)。範囲内へクランプ。
|
|
339
428
|
_resolveInitialCell() {
|
|
340
429
|
const ic = this.initialCell; if (!ic) return [0, 0];
|
|
@@ -371,6 +460,7 @@
|
|
|
371
460
|
const lines = [];
|
|
372
461
|
if (o.headers && this.headers) lines.push(this.headers.slice(0, this.COLS).map(esc).join(','));
|
|
373
462
|
for (let r = 0; r < this.ROWS; r++) {
|
|
463
|
+
if (this.data[r] === TssGrid.PENDING) continue; // 遅延ロード: 未取得行は出さない(loaded-only)
|
|
374
464
|
if (o.skipEmpty && this._rowEmpty(r)) continue;
|
|
375
465
|
const cells = [];
|
|
376
466
|
for (let c = 0; c < this.COLS; c++) cells.push(esc(o.formatted ? this._displayValue(r, c) : this.data[r][c]));
|
|
@@ -434,8 +524,27 @@
|
|
|
434
524
|
const na = TssGrid.toNum(va), nb = TssGrid.toNum(vb);
|
|
435
525
|
return (na !== null && nb !== null) ? na - nb : String(va).localeCompare(String(vb));
|
|
436
526
|
}
|
|
527
|
+
// 遅延ロード(push): sort/filter は「グリッドが計算しない・サーバ再フェッチ」(AG Grid ポリシー)。
|
|
528
|
+
// 状態を onSortFilterChange で通知し、ロード済みを PENDING へ戻す(パージ)=プラグインが新順で取り直す。
|
|
529
|
+
// 通知先が未設定なら破壊せず no-op(パージで画面を空にしない安全側)。呼び側は !dataComplete で本メソッドを使う。
|
|
530
|
+
_lazySortFilter(state) {
|
|
531
|
+
if (this.onSortFilterChange) {
|
|
532
|
+
try { this.onSortFilterChange(state); } catch (_) {}
|
|
533
|
+
this._purgeToPending(); // 窓再描画で onViewportChange も発火=プラグインが可視域を新順で取り直す
|
|
534
|
+
} else {
|
|
535
|
+
try { console.warn('[TssGrid] 遅延ロード中の sort/filter はサーバ側で解決してください(onSortFilterChange 未設定のため no-op)'); } catch (_) {}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// ロード済み行を全て PENDING に戻す(sort/filter 変更でブロックが陳腐化=AG Grid の cache purge 相当)。
|
|
539
|
+
_purgeToPending() {
|
|
540
|
+
for (let r = 0; r < this.ROWS; r++) { this.data[r] = TssGrid.PENDING; if (this._src) this._src[r] = {}; }
|
|
541
|
+
this._pendingCount = this.ROWS; this.dataComplete = false;
|
|
542
|
+
this._vpStart = -1; this._vpEnd = -1; // 次の窓通知を必ず出す(新順の取り直しを促す)
|
|
543
|
+
if (this.table) { if (this.virtual) this._renderWindow(this._vWindow(), true); else this.buildTable(); }
|
|
544
|
+
}
|
|
437
545
|
// 列 c(index か data キー)で並べ替え。dir='asc'(既定)|'desc'。数値っぽければ数値比較、でなければ文字列比較。
|
|
438
546
|
sortBy(c, dir) {
|
|
547
|
+
if (!this.dataComplete) return this._lazySortFilter({ type: 'sort', sort: [{ col: c, dir: dir || 'asc' }] }); // 遅延ロード中はサーバ委譲
|
|
439
548
|
if (this._sortLocked()) return; // 結合中はソート禁止(値による組み替えで結合帯が散らばる)
|
|
440
549
|
const ci = this._resolveCol(c); if (ci < 0) return;
|
|
441
550
|
const sgn = dir === 'desc' ? -1 : 1;
|
|
@@ -447,6 +556,7 @@
|
|
|
447
556
|
// 多段(複数列)ソート。specs=[{col, dir}] を優先順(先頭=第1キー)に。col=index か data キー、dir='asc'(既定)|'desc'。
|
|
448
557
|
// 各段は sortBy と同じセル比較。前段が同値のときだけ次段で決める(安定ソート=残った同値は現在の並びを保つ)。空行は最下部固定。
|
|
449
558
|
sortByCols(specs) {
|
|
559
|
+
if (!this.dataComplete) return this._lazySortFilter({ type: 'sort', sort: specs }); // 遅延ロード中はサーバ委譲
|
|
450
560
|
if (this._sortLocked()) return; // 結合中はソート禁止
|
|
451
561
|
const keys = (specs || [])
|
|
452
562
|
.map(s => ({ ci: this._resolveCol(s && s.col), sgn: (s && s.dir === 'desc') ? -1 : 1 }))
|
|
@@ -462,6 +572,7 @@
|
|
|
462
572
|
}
|
|
463
573
|
// 任意比較で並べ替え。cmp(rowA, rowB) は行(配列 or オブジェクト)を受ける。空行は最下部固定。
|
|
464
574
|
sortRows(cmp) {
|
|
575
|
+
if (!this.dataComplete) return this._lazySortFilter({ type: 'sort', sort: 'custom' }); // 遅延ロード中はサーバ委譲(JS比較子はサーバへ渡せない=custom 通知のみ)
|
|
465
576
|
if (this._sortLocked()) return; // 結合中はソート禁止
|
|
466
577
|
this._rememberOrder();
|
|
467
578
|
const rows = this._src ? this.getRows() : this.data;
|
|
@@ -519,6 +630,7 @@
|
|
|
519
630
|
}
|
|
520
631
|
// 絞り込み。pred=(row, masterIndex, src)=>bool / {col:value|fn} / falsy=解除。非破壊(編集は参照でマスタへ反映)。
|
|
521
632
|
filter(pred) {
|
|
633
|
+
if (!this.dataComplete) { this._lazySortFilter({ type: 'filter', filter: pred }); return false; } // 遅延ロード中はサーバ委譲(JS述語はサーバへ渡せない=通知のみ)
|
|
522
634
|
if (this._merges && this._merges.length) return false; // v1: 結合中は不可
|
|
523
635
|
const fn = this._normFilter(pred);
|
|
524
636
|
if (!fn) return this.clearFilter();
|
|
@@ -665,13 +777,14 @@
|
|
|
665
777
|
activeTd() { return this.cellEl(this.active.r, this.active.c); }
|
|
666
778
|
clampR(r) { return Math.max(0, Math.min(this.ROWS - 1, r)); }
|
|
667
779
|
clampC(c) { return Math.max(0, Math.min(this.COLS - 1, c)); }
|
|
668
|
-
renderDump() { if (this.dumpEl) this.dumpEl.textContent = JSON.stringify(this.data); if (this.onChange) this.onChange(this); }
|
|
780
|
+
renderDump() { if (this._renderPaused) { this._renderDirty = true; return; } if (this.dumpEl) this.dumpEl.textContent = JSON.stringify(this.data); if (this.onChange) this.onChange(this); }
|
|
669
781
|
|
|
670
782
|
// ---- 列定義(セルタイプ / バリデータ) ----
|
|
671
783
|
colCfg(c) { return this.columns[c] || {}; }
|
|
672
784
|
colType(c) { return this.colCfg(c).type || 'text'; }
|
|
673
785
|
// 読み取り専用判定(グリッド全体 / 列 readOnly: bool/関数 / セル単位 setCellReadOnly)。
|
|
674
786
|
_isReadOnly(r, c) {
|
|
787
|
+
if (this.data[r] === TssGrid.PENDING) return true; // 遅延ロード: 未取得セルは「読込中」=編集不可(fillRows で実データが来たら普通に編集可)
|
|
675
788
|
if (this.readOnly) return true;
|
|
676
789
|
if (this._merges && this._isCovered(r, c)) return true; // 結合の従属セルは編集不可(貼付/フィルは既存の readOnly 経路で巻戻し)
|
|
677
790
|
if (this._cellRO.has(r + ',' + c)) return true; // セル単位の実行時トグル
|
|
@@ -734,6 +847,7 @@
|
|
|
734
847
|
_structRemapFn(info) {
|
|
735
848
|
switch (info.type) {
|
|
736
849
|
case 'insertRow': { const at = info.at; return (r, c) => [r >= at ? r + 1 : r, c]; }
|
|
850
|
+
case 'insertRows': { const { at, count } = info; return (r, c) => [r >= at ? r + count : r, c]; }
|
|
737
851
|
case 'deleteRows': { const { r0, r1 } = info, n = r1 - r0 + 1; return (r, c) => (r >= r0 && r <= r1) ? null : [r > r1 ? r - n : r, c]; }
|
|
738
852
|
case 'insertCol': { const at = info.at; return (r, c) => [r, c >= at ? c + 1 : c]; }
|
|
739
853
|
case 'deleteCols': { const { c0, c1 } = info, m = c1 - c0 + 1; return (r, c) => (c >= c0 && c <= c1) ? null : [r, c > c1 ? c - m : c]; }
|
|
@@ -749,6 +863,7 @@
|
|
|
749
863
|
for (const m of this._merges) {
|
|
750
864
|
let { r, c, colspan } = m, rowspan = m.rowspan || 1;
|
|
751
865
|
if (info.type === 'insertRow') { const at = info.at; if (at <= r) r++; else if (at < r + rowspan) rowspan++; }
|
|
866
|
+
else if (info.type === 'insertRows') { const { at, count } = info; if (at <= r) r += count; else if (at < r + rowspan) rowspan += count; }
|
|
752
867
|
else if (info.type === 'deleteRows') {
|
|
753
868
|
const { r0, r1 } = info, n = r1 - r0 + 1, top = r, bot = r + rowspan - 1;
|
|
754
869
|
if (r0 <= top && r1 >= bot) continue; // 縦方向すべて消える
|
|
@@ -1025,6 +1140,7 @@
|
|
|
1025
1140
|
if (keys.length) td.dataset.cstyle = keys.join(' '); else delete td.dataset.cstyle;
|
|
1026
1141
|
}
|
|
1027
1142
|
_renderCell(r, c) {
|
|
1143
|
+
if (this._renderPaused) { this._renderDirty = true; return; } // バッチ描画中は最後の buildTable が全セルを一括で描く
|
|
1028
1144
|
if (this._merges && this._isCovered(r, c)) return; // 従属セルは描画対象なし(cellEl はアンカーへ解決=誤って上書きしない)
|
|
1029
1145
|
const td = this.cellEl(r, c); if (!td) return;
|
|
1030
1146
|
const ph = (this.data[r][c] === '' && this.colType(c) !== 'checkbox') ? this._placeholder(c) : null;
|
|
@@ -1177,7 +1293,15 @@
|
|
|
1177
1293
|
});
|
|
1178
1294
|
}
|
|
1179
1295
|
// 1行ぶんの <tr>…</tr> を生成(buildTable の本体ループと、仮想スクロールの窓再描画 _renderWindow で共用)。
|
|
1296
|
+
// 遅延ロード: 未取得(PENDING)行は data[r][c] を一切読まず「読込中」プレースホルダ行を描く。
|
|
1297
|
+
_pendingRowHTML(r) {
|
|
1298
|
+
const rh = this.rowHeaders;
|
|
1299
|
+
const head = rh ? '<th class="rowhead" data-r="' + r + '">' + this._rowHeadLabel(r) + '</th>' : '';
|
|
1300
|
+
const cell = '<td class="tg-pending-cell" colspan="' + this.COLS + '" data-r="' + r + '" data-c="0"><span class="tg-pending-dots"></span></td>';
|
|
1301
|
+
return '<tr data-r="' + r + '" class="tg-row tg-pending-row" style="height:' + this._rowHeight(r) + 'px">' + head + cell + '</tr>';
|
|
1302
|
+
}
|
|
1180
1303
|
_rowHTML(r) {
|
|
1304
|
+
if (this.data[r] === TssGrid.PENDING) return this._pendingRowHTML(r); // 未取得行=読込中プレースホルダ(値は読まない)
|
|
1181
1305
|
const rh = this.rowHeaders;
|
|
1182
1306
|
const rgrip = (rh && this.resizeRows) ? '<div class="tg-rowgrip"></div>' : '';
|
|
1183
1307
|
const canMove = rh && this.rowReorder && !this._hasVMerge();
|
|
@@ -1251,8 +1375,15 @@
|
|
|
1251
1375
|
this._vStart = win.start; this._vEnd = win.end; this._vRendered = true;
|
|
1252
1376
|
this._syncHeaderCheckboxes();
|
|
1253
1377
|
this.updateSelectionUI(); // セルが作り直されたので窓内の選択シェード/オーバーレイを再適用
|
|
1378
|
+
this._fireViewport(win.start, win.end); // 遅延ロード: 窓が変わったらプラグインへ通知(PENDING を fillRows で埋めさせる)
|
|
1254
1379
|
return true;
|
|
1255
1380
|
}
|
|
1381
|
+
// 遅延ロード: 描画窓が変わった時だけ onViewportChange(start,end) を発火(同一範囲は抑止=fillRows→再描画の再帰も止まる)。
|
|
1382
|
+
_fireViewport(start, end) {
|
|
1383
|
+
if (!this.onViewportChange || (start === this._vpStart && end === this._vpEnd)) return;
|
|
1384
|
+
this._vpStart = start; this._vpEnd = end;
|
|
1385
|
+
try { this.onViewportChange(start, end); } catch (_) {}
|
|
1386
|
+
}
|
|
1256
1387
|
// 仮想スクロール: スクロールで窓がずれたら再描画(rAF スロットル)。
|
|
1257
1388
|
_vSync() {
|
|
1258
1389
|
if (!this.virtual || this._vRAF) return;
|
|
@@ -1275,6 +1406,7 @@
|
|
|
1275
1406
|
this._renderWindow(this._vWindow(), false); // 窓を即同期(place 前に DOM を確定)
|
|
1276
1407
|
}
|
|
1277
1408
|
buildTable() {
|
|
1409
|
+
if (this._renderPaused) { this._renderDirty = true; return; } // バッチ描画中は最後にまとめて1回(_withBatchedRender)
|
|
1278
1410
|
if (this._customEditor && this._customCancel) this._customCancel(); // 開いているカスタムエディタを閉じる(body直下ポップアップの取り残し防止・_commitActive と同じ扱い)
|
|
1279
1411
|
this._selhdrFg = null; // 選択ヘッダ文字色キャッシュ破棄(テーマ変更に追従)
|
|
1280
1412
|
const rh = this.rowHeaders;
|
|
@@ -1331,6 +1463,7 @@
|
|
|
1331
1463
|
this._syncHeaderCheckboxes();
|
|
1332
1464
|
this._buildGeomCache(); // 実測幾何キャッシュ(全 _apply 後=幅高が確定してから)
|
|
1333
1465
|
this.renderDump();
|
|
1466
|
+
if (this.virtual) this._fireViewport(this._vStart, this._vEnd); // 遅延ロード: 初期/再構築後の窓もプラグインへ通知(table 確定後に発火)
|
|
1334
1467
|
}
|
|
1335
1468
|
// 各リーフ列ヘッダth(data-c 付き)を生成後に onHeaderRender へ渡す。ソートアイコン等の差し込み用。
|
|
1336
1469
|
_applyHeaderRender() {
|
|
@@ -1519,13 +1652,17 @@
|
|
|
1519
1652
|
// 選択範囲が及ぶ行/列ヘッダをハイライト(Excel 風のアフォーダンス)。仮想時は行ヘッダを窓内にクランプ(大量行で querySelector を無駄打ちしない)。
|
|
1520
1653
|
const hlo = this.virtual ? Math.max(r0, this._vStart) : r0;
|
|
1521
1654
|
const hhi = this.virtual ? Math.min(r1, this._vEnd - 1) : r1;
|
|
1655
|
+
// 巨大選択(大量貼付/全選択など)は per-cell の背景シェードと行ヘッダ塗りを省き、selrange アウトライン(O(1)・
|
|
1656
|
+
// スクロール追従)に任せる=O(行×列) の DOM 変異を回避(1000行貼付で updateSelectionUI ~175ms→~1ms)。
|
|
1657
|
+
// 画面に収まる通常選択(~2000セル未満)は従来どおり全セルをシェード=見た目不変。
|
|
1658
|
+
const rows = hhi - hlo + 1, bigSel = rows > 300 || rows * (c1 - c0 + 1) > 2000;
|
|
1522
1659
|
for (let c = c0; c <= c1; c++) { const th = this.table.querySelector('thead th[data-c="' + c + '"]'); if (th) th.classList.add('selhdr'); }
|
|
1523
|
-
for (let r = hlo; r <= hhi; r++) { const th = this.table.querySelector('th.rowhead[data-r="' + r + '"]'); if (th) th.classList.add('selhdr'); }
|
|
1660
|
+
if (!bigSel) for (let r = hlo; r <= hhi; r++) { const th = this.table.querySelector('th.rowhead[data-r="' + r + '"]'); if (th) th.classList.add('selhdr'); }
|
|
1524
1661
|
this.wrap.style.setProperty('--tg-selhdr-fg', this._selhdrTextColor()); // 選択ヘッダ文字色を背景輝度から自動(暗背景→白)
|
|
1525
1662
|
if (r0 === r1 && c0 === c1) return;
|
|
1526
1663
|
const clampMerge = this._headerSel && this._merges; // 列/行選択では「範囲に収まらない結合」は塗らない(帯を塗り広げない)
|
|
1527
1664
|
// 仮想スクロール: 窓内の行だけシェード(範囲枠 selrange は placeRect がモデル幾何で全域を描く)。
|
|
1528
|
-
for (let r = hlo; r <= hhi; r++) for (let c = c0; c <= c1; c++) {
|
|
1665
|
+
if (!bigSel) for (let r = hlo; r <= hhi; r++) for (let c = c0; c <= c1; c++) {
|
|
1529
1666
|
if (clampMerge) {
|
|
1530
1667
|
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;
|
|
1531
1668
|
if (a.r < r0 || mr1 > r1 || a.c < c0 || mc1 > c1) continue; // 結合が選択範囲に収まらない=塗らない
|
|
@@ -1561,6 +1698,7 @@
|
|
|
1561
1698
|
return (this._selhdrFg = L < 0.55 ? '#fff' : '#1a3a6b');
|
|
1562
1699
|
}
|
|
1563
1700
|
updateSelectionUI() {
|
|
1701
|
+
if (this._renderPaused) return; // バッチ描画中は選択枠の位置決めを保留(resume 後に1回)
|
|
1564
1702
|
if (!this.cursor) { // 表示専用: 選択枠・範囲・フィルハンドルを出さない(選択自体は内部に保持)
|
|
1565
1703
|
this.selbox.style.display = 'none'; this.selrange.style.display = 'none'; this.fillhandle.style.display = 'none';
|
|
1566
1704
|
return;
|
|
@@ -1730,6 +1868,7 @@
|
|
|
1730
1868
|
}
|
|
1731
1869
|
focusAndSelect(sel) {
|
|
1732
1870
|
this.active = { ...sel.active }; this.extent = { ...sel.extent };
|
|
1871
|
+
if (this._renderPaused) { this._pendingSel = sel; return; } // バッチ描画中は選択を記録だけ→resume で1回反映
|
|
1733
1872
|
if (this.virtual) this._vEnsureVisible(this.active.r);
|
|
1734
1873
|
this._syncEditorNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1735
1874
|
if (!this.virtual) this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
@@ -2057,7 +2196,8 @@
|
|
|
2057
2196
|
}
|
|
2058
2197
|
// 末尾に常に minSpareRows 行の空行を確保(足りなければ追加。履歴には積まない)。
|
|
2059
2198
|
_ensureSpare() {
|
|
2060
|
-
if (
|
|
2199
|
+
if (this._renderPaused) { this._renderDirty = true; return; } // バッチ描画中は保留→resume でまとめて確保
|
|
2200
|
+
if (!this.minSpareRows || this._allRows || !this.dataComplete) return; // フィルタ中/遅延ロード中(PENDING tail)は末尾の自動空行を足さない
|
|
2061
2201
|
let empties = 0;
|
|
2062
2202
|
for (let r = this.ROWS - 1; r >= 0; r--) { if (this._rowEmpty(r)) empties++; else break; }
|
|
2063
2203
|
let added = 0;
|
|
@@ -2089,26 +2229,40 @@
|
|
|
2089
2229
|
if (at - 1 >= 0) { const i = this._allRows.indexOf(this.data[at - 1]); if (i >= 0) return i + 1; }
|
|
2090
2230
|
return this._allRows.length;
|
|
2091
2231
|
}
|
|
2092
|
-
insertRow(ri, where = 'above') {
|
|
2232
|
+
insertRow(ri, where = 'above') { this.insertRows(ri, 1, where); }
|
|
2233
|
+
// 一括挿入プリミティブ: count 行を1回の buildTable・1つの history コマンドで挿入(Undo/Redo が1手で count 行まとめて戻る)。
|
|
2234
|
+
// 大量貼付など「まとめて足す」を O(N²)(1行ずつ insertRow)から O(N) に。既定 count=1 は insertRow と挙動等価。
|
|
2235
|
+
insertRows(ri, count = 1, where = 'above') {
|
|
2093
2236
|
if (!this.allowInsertRows) return;
|
|
2094
|
-
|
|
2237
|
+
count = Math.max(0, count | 0);
|
|
2238
|
+
if (this.maxRows) count = Math.min(count, this.maxRows - this.ROWS); // 上限までにクランプ(超過分は足さない)
|
|
2239
|
+
if (count <= 0) return;
|
|
2095
2240
|
if (this.mode === 'edit') this._commitActive();
|
|
2096
2241
|
this._normalizeArrays();
|
|
2097
2242
|
const at = Math.max(0, Math.min(this.ROWS, where === 'below' ? ri + 1 : ri));
|
|
2098
2243
|
const before = this.snapSel(), grid = this, c = this.active.c, frozenBump = at < this.frozenRows;
|
|
2099
|
-
let
|
|
2244
|
+
let refs; // 挿入した行参照の配列(フィルタ中の _allRows 追従・undo 用に保持)
|
|
2100
2245
|
const doIt = () => {
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2246
|
+
refs = []; const hs = [], srcs = grid._src ? [] : null;
|
|
2247
|
+
for (let k = 0; k < count; k++) { refs.push(new Array(grid.COLS).fill('')); hs.push(undefined); if (srcs) srcs.push({}); }
|
|
2248
|
+
// マスタ挿入位置は data を splice する前の位置関係から決める(_masterInsertIndex と同じ規則をバッチ用に)
|
|
2249
|
+
let mi = -1;
|
|
2250
|
+
if (grid._allRows) {
|
|
2251
|
+
mi = grid._allRows.length;
|
|
2252
|
+
if (at < grid.data.length) { const i = grid._allRows.indexOf(grid.data[at]); if (i >= 0) mi = i; }
|
|
2253
|
+
else if (at - 1 >= 0) { const i = grid._allRows.indexOf(grid.data[at - 1]); if (i >= 0) mi = i + 1; }
|
|
2254
|
+
}
|
|
2255
|
+
grid.data.splice(at, 0, ...refs); grid.rowH.splice(at, 0, ...hs); if (grid._src) grid._src.splice(at, 0, ...srcs); grid.ROWS += count;
|
|
2256
|
+
if (grid._allRows) { grid._allRows.splice(mi, 0, ...refs); if (grid._allSrc) grid._allSrc.splice(mi, 0, ...refs.map(() => ({}))); } // マスタにも一括挿入
|
|
2257
|
+
if (frozenBump) grid.frozenRows += count;
|
|
2105
2258
|
};
|
|
2106
2259
|
const undoIt = () => {
|
|
2107
|
-
grid.data.splice(at,
|
|
2108
|
-
if (grid._allRows && ref) { const i = grid._allRows.indexOf(ref); if (i >= 0) { grid._allRows.splice(i, 1); if (grid._allSrc) grid._allSrc.splice(i, 1); } }
|
|
2109
|
-
if (frozenBump) grid.frozenRows
|
|
2260
|
+
grid.data.splice(at, count); grid.rowH.splice(at, count); if (grid._src) grid._src.splice(at, count); grid.ROWS -= count;
|
|
2261
|
+
if (grid._allRows && refs) { for (const ref of refs) { const i = grid._allRows.indexOf(ref); if (i >= 0) { grid._allRows.splice(i, 1); if (grid._allSrc) grid._allSrc.splice(i, 1); } } }
|
|
2262
|
+
if (frozenBump) grid.frozenRows -= count;
|
|
2110
2263
|
};
|
|
2111
|
-
|
|
2264
|
+
const type = count === 1 ? 'insertRow' : 'insertRows'; // 単行は従来どおり type:'insertRow'(後方互換)/複数は 'insertRows'
|
|
2265
|
+
this._structCmd(doIt, undoIt, () => ({ active: { r: at, c }, extent: { r: at + count - 1, c } }), before, { type, at, count });
|
|
2112
2266
|
}
|
|
2113
2267
|
deleteRows(r0, r1) {
|
|
2114
2268
|
if (!this.allowDeleteRows) return;
|
|
@@ -2207,6 +2361,26 @@
|
|
|
2207
2361
|
revert() { for (let i = batch.length - 1; i >= 0; i--) batch[i].revert(); },
|
|
2208
2362
|
});
|
|
2209
2363
|
}
|
|
2364
|
+
// 大量セル更新を「最後に1回 buildTable」へ畳む。中の buildTable/_renderCell/renderDump/選択UI は保留され、
|
|
2365
|
+
// resume 時に buildTable 1回+選択反映+renderDump 1回だけ走る(1行ずつ描く N 回の DOM 変異を消す)。
|
|
2366
|
+
// 例: 1000×5 の貼付が per-cell 描画 ~500ms → 一括 ~80ms。入れ子は外側が最後にまとめて描く。
|
|
2367
|
+
_withBatchedRender(fn) {
|
|
2368
|
+
if (this._renderPaused) return fn(); // 入れ子: 外側の resume に任せる
|
|
2369
|
+
this._renderPaused = true; this._renderDirty = false; this._pendingSel = null;
|
|
2370
|
+
let ret;
|
|
2371
|
+
try { ret = fn(); }
|
|
2372
|
+
finally {
|
|
2373
|
+
this._renderPaused = false;
|
|
2374
|
+
if (this._renderDirty) {
|
|
2375
|
+
this.buildTable(); // 全セルを1回のHTML生成で描く(末尾で renderDump=dumpEl/onChange も1回発火)
|
|
2376
|
+
this._ensureSpare(); // 保留していた末尾スペア行の確保
|
|
2377
|
+
const s = this._pendingSel; this._pendingSel = null;
|
|
2378
|
+
if (s) this.focusAndSelect(s); // 選択枠・スクロールを1回だけ
|
|
2379
|
+
}
|
|
2380
|
+
this._renderDirty = false;
|
|
2381
|
+
}
|
|
2382
|
+
return ret;
|
|
2383
|
+
}
|
|
2210
2384
|
|
|
2211
2385
|
// ---- 右クリックメニュー ----
|
|
2212
2386
|
_onContextMenu(e) {
|
|
@@ -2466,6 +2640,21 @@
|
|
|
2466
2640
|
if (Array.isArray(ret)) g = ret;
|
|
2467
2641
|
if (!g || !g.length) return;
|
|
2468
2642
|
}
|
|
2643
|
+
// pasteAutoGrow=true では「行の一括生成」と「値の貼付」を1つの history にまとめる(1回の undo で両方戻る)。
|
|
2644
|
+
// 非オートグロー時は history.push が1回だけ=_batchHistory は素通し(従来とバイト等価)。
|
|
2645
|
+
let after;
|
|
2646
|
+
const body = () => { this._batchHistory(() => { after = this._pasteGridBody(g); }); };
|
|
2647
|
+
// 大量貼付だけ _withBatchedRender で「最後に buildTable 1回」へ畳む(per-cell 描画 N 回を消す=1000行 ~500ms→~80ms)。
|
|
2648
|
+
// 小さな貼付を大きなグリッドに落とす時は従来どおり per-cell(全再描画の方が高くつくため回避)。
|
|
2649
|
+
const willGrow = this.pasteAutoGrow && (this.active.r + g.length > this.ROWS);
|
|
2650
|
+
const heavy = willGrow || (g.length >= 8 && g.length * 5 >= this.ROWS); // 貼付が表の ~20% 以上を占める or 行が増える時だけ畳む
|
|
2651
|
+
if (heavy) this._withBatchedRender(body); else body();
|
|
2652
|
+
if (after && this.onAfterPaste) { // onAfterPaste は batch の外で呼ぶ(コールバック内の履歴操作を貼付コマンドに混ぜない)
|
|
2653
|
+
const range = { r0: after.active.r, c0: after.active.c, r1: after.extent.r, c1: after.extent.c };
|
|
2654
|
+
try { this.onAfterPaste(g, { range }); } catch (_) {}
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
_pasteGridBody(g) {
|
|
2469
2658
|
const before = this.snapSel(), changes = [], sel = this.rectRange();
|
|
2470
2659
|
const visAll = this._visCols();
|
|
2471
2660
|
let after;
|
|
@@ -2477,18 +2666,26 @@
|
|
|
2477
2666
|
const r0 = this.active.r, c0 = this.active.c;
|
|
2478
2667
|
let maxw = 0; for (let i = 0; i < g.length; i++) maxw = Math.max(maxw, g[i].length);
|
|
2479
2668
|
const p0 = visAll.indexOf(c0), availCols = visAll.length - p0; // 貼り付けは可視列へ(隠し列をスキップ)
|
|
2480
|
-
//
|
|
2481
|
-
|
|
2669
|
+
// オートグロー時に伸ばせる上限行数(maxRows があればそこまで/無ければ無制限)。非オートグローは現状の ROWS。
|
|
2670
|
+
const capRows = this.pasteAutoGrow ? (this.maxRows || Infinity) : this.ROWS;
|
|
2671
|
+
// 範囲超過チェック(オプトイン): 伸ばしても収まらない or 列がはみ出すなら貼り付けを中止してエラー通知(行生成の前に判定=原子性)
|
|
2672
|
+
if (this.pasteOverflow === 'error' && (r0 + g.length > capRows || maxw > availCols)) {
|
|
2482
2673
|
if (this.onPasteOverflow) {
|
|
2483
2674
|
try {
|
|
2484
2675
|
this.onPasteOverflow({
|
|
2485
2676
|
anchor: { r: r0, c: c0 }, height: g.length, width: maxw, rows: this.ROWS, cols: this.COLS,
|
|
2486
|
-
overRows: Math.max(0, r0 + g.length -
|
|
2677
|
+
overRows: Math.max(0, r0 + g.length - capRows), overCols: Math.max(0, maxw - availCols),
|
|
2487
2678
|
});
|
|
2488
2679
|
} catch (_) {}
|
|
2489
2680
|
}
|
|
2490
2681
|
return; // 何も変更しない
|
|
2491
2682
|
}
|
|
2683
|
+
// オートグロー: 不足分を先に一括生成(buildTable 1回・history 1コマンド)。超過は maxRows でクランプ、残りは pasteOverflow に従う。
|
|
2684
|
+
if (this.pasteAutoGrow && r0 + g.length > this.ROWS) {
|
|
2685
|
+
const target = this.maxRows ? Math.min(this.maxRows, r0 + g.length) : r0 + g.length;
|
|
2686
|
+
const grow = target - this.ROWS;
|
|
2687
|
+
if (grow > 0) this.insertRows(this.ROWS - 1, grow, 'below');
|
|
2688
|
+
}
|
|
2492
2689
|
let lastC = c0;
|
|
2493
2690
|
for (let i = 0; i < g.length; i++) for (let j = 0; j < g[i].length; j++) {
|
|
2494
2691
|
const r = r0 + i, c = visAll[p0 + j]; if (r < this.ROWS && c != null) { this.setCell(r, c, g[i][j], changes); lastC = c; }
|
|
@@ -2502,10 +2699,7 @@
|
|
|
2502
2699
|
this.pendingCut = null; this.copybox.style.display = 'none'; }
|
|
2503
2700
|
this.pushCmd(changes, before, after, 'paste');
|
|
2504
2701
|
this.focusAndSelect(after);
|
|
2505
|
-
|
|
2506
|
-
const range = { r0: after.active.r, c0: after.active.c, r1: after.extent.r, c1: after.extent.c };
|
|
2507
|
-
try { this.onAfterPaste(g, { range }); } catch (_) {}
|
|
2508
|
-
}
|
|
2702
|
+
return after;
|
|
2509
2703
|
}
|
|
2510
2704
|
clearCopyMarquee() { this.copybox.style.display = 'none'; this.pendingCut = null; }
|
|
2511
2705
|
|
|
@@ -2587,6 +2781,20 @@
|
|
|
2587
2781
|
// input と textarea(multiline) の両方に同じ編集入口リスナを張る(this.editor はアクティブ列で張り替わる)。
|
|
2588
2782
|
[this._edInput, this._edMulti].forEach((ed) => {
|
|
2589
2783
|
ed.addEventListener('keydown', (e) => this._onKey(e));
|
|
2784
|
+
// 貼り付けの主経路=ネイティブ paste イベント(clipboardData.getData)。Ctrl+V のジェスチャー内で
|
|
2785
|
+
// 同期に読める=許可プロンプト不要+堅牢(readText の権限/非セキュア/非フォーカスで落ちる問題を回避)。
|
|
2786
|
+
// nav 中の editor は focus 代理なので native 挿入は常に止める(copyPaste=false でも挿入させない)。
|
|
2787
|
+
// 編集中は横取りせず native 貼付(1セルへの通常ペースト)に任せる。paste は beforeinput より先に来るため、
|
|
2788
|
+
// ここで preventDefault すれば beforeinput(insertFromPaste) の誤発火=エディタ誤オープンも起きない。
|
|
2789
|
+
// 右クリックメニューの「貼り付け」/プログラム呼び出しは paste イベントを伴わないので paste()=readText 経路を温存。
|
|
2790
|
+
ed.addEventListener('paste', (e) => {
|
|
2791
|
+
if (this.mode !== 'nav') return;
|
|
2792
|
+
e.preventDefault();
|
|
2793
|
+
if (!this.copyPaste) return;
|
|
2794
|
+
const text = (e.clipboardData && e.clipboardData.getData('text')) || '';
|
|
2795
|
+
const g = (text && text.length) ? TssGrid.parseTSV(text) : sharedClip; // clipboard 空/非テキストは内部コピー buffer へフォールバック
|
|
2796
|
+
if (g && g.length) this.pasteGrid(g);
|
|
2797
|
+
});
|
|
2590
2798
|
// inline エディタへ入力を通知(打った/変換確定した内容で候補を絞る用)
|
|
2591
2799
|
ed.addEventListener('input', () => {
|
|
2592
2800
|
if (this._inlineEd && this._inlineEd.onInput) { try { this._inlineEd.onInput(this.editor.value, this._inlineCtx); } catch (_) {} }
|
|
@@ -2931,7 +3139,8 @@
|
|
|
2931
3139
|
if (ctrl && (e.key.toLowerCase() === 'y' || (e.shiftKey && e.key.toLowerCase() === 'z'))) { e.preventDefault(); this.history.redo(); return; }
|
|
2932
3140
|
if (ctrl && e.key.toLowerCase() === 'c') { e.preventDefault(); this.copy(false); return; }
|
|
2933
3141
|
if (ctrl && e.key.toLowerCase() === 'x') { e.preventDefault(); this.copy(true); return; }
|
|
2934
|
-
|
|
3142
|
+
// Ctrl+V は keydown で拾わず native paste イベントへ委譲(editor の paste リスナが処理)。
|
|
3143
|
+
// ここで preventDefault すると paste イベント自体を止めかねないため分岐を置かない。
|
|
2935
3144
|
if (e.shiftKey && e.key.startsWith('Arrow')) {
|
|
2936
3145
|
e.preventDefault();
|
|
2937
3146
|
let { r, c } = this.extent;
|
|
@@ -2958,6 +3167,8 @@
|
|
|
2958
3167
|
}
|
|
2959
3168
|
}
|
|
2960
3169
|
|
|
3170
|
+
TssGrid.PENDING = Symbol('tg-pending'); // 遅延ロード(push): 未取得行を表す sentinel。data[r] に入れると _rowHTML が「読込中」行を描く
|
|
3171
|
+
|
|
2961
3172
|
// ---- UMD export ----
|
|
2962
3173
|
const api = { TssGrid, HistoryManager };
|
|
2963
3174
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|