@threestarsoftware/tssgrid 0.1.3 → 0.1.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/CHANGELOG.md +29 -0
- package/README.md +37 -4
- package/package.json +1 -1
- package/plugins/tss-filter.css +9 -0
- package/plugins/tss-filter.js +48 -9
- package/src/tssgrid.css +22 -0
- package/src/tssgrid.d.ts +464 -419
- package/src/tssgrid.js +345 -45
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;
|
|
@@ -197,6 +202,10 @@
|
|
|
197
202
|
this.rowReorder = !!opts.rowReorder;
|
|
198
203
|
this.rowReorderWhole = opts.rowReorder === 'header'; // 'header'=行ヘッダーセル全体が移動ハンドル(クリック=選択/ドラッグ=移動)/true=⠿アイコンのみ
|
|
199
204
|
this.rowReorderMark = opts.rowReorderMark === undefined ? 'before' : opts.rowReorderMark; // ⠿の位置: 'before'(番号の前/既定) | 'after'(番号の後) | false(出さない)
|
|
205
|
+
// 右端に非データの「削除列」(🗑)を1列出す。クリックで deleteRows(既存の remove_row と同一経路・1 history・undo で戻せる)。
|
|
206
|
+
// 非データ・非編集・非選択・幅固定・並べ替え/リサイズ対象外=getRows/toCSV/列インデックスに現れない。allowDeleteRows:false なら出さない。
|
|
207
|
+
this.rowDelete = !!opts.rowDelete;
|
|
208
|
+
this.rowDeleteW = (opts.rowDeleteWidth | 0) || 36; // 削除列の幅(px)
|
|
200
209
|
// 選択カーソル(緑の選択枠・範囲・セルハイライト・フィルハンドル)を表示するか。
|
|
201
210
|
// false で表示専用グリッド向けに非表示(内部の選択は保持=コピーは可)。
|
|
202
211
|
this.cursor = opts.cursor !== false;
|
|
@@ -222,6 +231,11 @@
|
|
|
222
231
|
this.onBeforeAutofill = opts.onBeforeAutofill || null; // (src,target)=>false で取消
|
|
223
232
|
this.onAfterAutofill = opts.onAfterAutofill || null; // (src,target)
|
|
224
233
|
this.copyPaste = opts.copyPaste !== false; // false でコピー/カット/ペースト無効
|
|
234
|
+
// 共有エディタへのブラウザ自動入力(Chrome のポイントカード/住所/支払い等)を強めに抑止する。
|
|
235
|
+
// 既定 false(name+autocomplete=off の通常抑止のみ)。true で autocomplete='new-password' に切替=
|
|
236
|
+
// Chrome の loyalty/payment autofill を確実に止める('off' は無視される)。副作用として環境により鍵アイコンや
|
|
237
|
+
// 生成PW提案が出ることがあるため opt-in。周囲に自前のラベル付き input(フォーム塊)を並べる埋め込みで有効。
|
|
238
|
+
this.suppressAutofill = !!opts.suppressAutofill;
|
|
225
239
|
// 列幅オートフィット(内容に合わせて自動調整)。境界ダブルクリック / autoSizeColumn でも。
|
|
226
240
|
this.autoColumnSize = !!opts.autoColumnSize;
|
|
227
241
|
// データ入力支援
|
|
@@ -235,6 +249,9 @@
|
|
|
235
249
|
this.virtual = !!opts.virtual;
|
|
236
250
|
this._vBuf = (opts.virtual && typeof opts.virtual === 'object' && opts.virtual.buffer) || 6; // 窓の上下バッファ行数
|
|
237
251
|
this._vStart = 0; this._vEnd = 0; this._vRAF = 0; this._vRendered = false;
|
|
252
|
+
this._vpStart = -1; this._vpEnd = -1; // onViewportChange の最後に通知した範囲(同一範囲の再発火を抑止)
|
|
253
|
+
this.dataComplete = true; // 遅延ロード(push): false のとき data に PENDING 行が混じる(setRowCount)。sort/filter/spare の判定に使う
|
|
254
|
+
this._pendingCount = 0; // PENDING 行数(fillRows で減らし 0 で dataComplete=true へ)
|
|
238
255
|
if (this.virtual && (this.frozenRows || this.frozenCols || this.wordWrap)) {
|
|
239
256
|
try { console.warn('[TssGrid] virtual:true は固定行列/折り返し非対応のため、それらを無効化しました'); } catch (_) {}
|
|
240
257
|
this.frozenRows = 0; this.frozenCols = 0; this.wordWrap = false;
|
|
@@ -316,25 +333,109 @@
|
|
|
316
333
|
}
|
|
317
334
|
|
|
318
335
|
// ---- 取得用 API ----
|
|
319
|
-
|
|
320
|
-
//
|
|
336
|
+
// 常に 2次元配列(後方互換)。遅延ロード中は「ロード済みのみ」返す(未取得 PENDING 行は除外=AG Grid の as-rendered ポリシー。
|
|
337
|
+
// 全件が要る export はプラグインが全件 fillRows してから呼ぶ=dataComplete で通常経路)。
|
|
338
|
+
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()); }
|
|
339
|
+
// オブジェクト配列で取り出す。_src(元の非表示フィールド)にキー列の現在値を上書きして返す。遅延ロード中はロード済みのみ。
|
|
321
340
|
getRows() {
|
|
322
|
-
|
|
341
|
+
const P = TssGrid.PENDING, out = [];
|
|
342
|
+
for (let r = 0; r < this.ROWS; r++) {
|
|
343
|
+
const row = this.data[r]; if (row === P) continue; // 未取得行は除外
|
|
323
344
|
const o = this._src ? Object.assign({}, this._src[r]) : {};
|
|
324
345
|
for (let c = 0; c < this.COLS; c++) { const k = this._key(c); if (k != null) this._setPathCOW(o, k, row[c]); }
|
|
325
|
-
|
|
326
|
-
}
|
|
346
|
+
out.push(o);
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
327
349
|
}
|
|
328
350
|
getRow(r) { return this.getRows()[r]; }
|
|
329
351
|
// getRow の対(行→列)。1列分の値を全行ぶん配列で。列は index か data キー。
|
|
330
|
-
getColumn(c) { const ci = this._resolveCol(c);
|
|
331
|
-
// 全列を「列ごとの値配列」で。見出しキーは data キー > headers。getRows
|
|
352
|
+
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]); }
|
|
353
|
+
// 全列を「列ごとの値配列」で。見出しキーは data キー > headers。getRows の列方向版。遅延ロード中はロード済みのみ。
|
|
332
354
|
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]] =
|
|
355
|
+
const out = {}, rows = this.dataComplete ? this.data : this.data.filter(row => row !== TssGrid.PENDING);
|
|
356
|
+
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
357
|
return out;
|
|
336
358
|
}
|
|
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(); }
|
|
359
|
+
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(); }
|
|
360
|
+
// 背景で"育てる"追記(progressive): data 末尾へ rows を足し ROWS を更新、可視窓+上下スペーサだけ再描画する。
|
|
361
|
+
// buildTable 全再描画をしない=スクロール位置・アクティブセル・フォーカス・選択範囲・編集中のエディタと値を保つ。
|
|
362
|
+
// history には積まない(背景成長は undo 対象外)。既存の setData/insertRows 経路は不変=完全 opt-in。
|
|
363
|
+
// 用途: プラグインが「先頭ページを描く→残りを idle で appendRows」して first-paint を早める土台。
|
|
364
|
+
// rows は現在のデータと同形(オブジェクトモードならオブジェクト配列/配列モードなら行配列)。opts は将来用(keepScroll/noHistory は
|
|
365
|
+
// 実装が tbody 差し替えのみ=スクロール保持・履歴不変を常に満たすため現状は指定不要)。
|
|
366
|
+
// 注意: フィルタ中(_allRows)はマスタにも積んで desync を防ぐ(部分ロード中のフィルタは想定外だが壊さない)。
|
|
367
|
+
// 非virtual+minSpareRows では追記行が末尾スペア行の後ろに入る(育成は主に virtual 用途)。
|
|
368
|
+
appendRows(rows, opts = {}) {
|
|
369
|
+
if (!Array.isArray(rows) || !rows.length) return;
|
|
370
|
+
const n0 = this.ROWS;
|
|
371
|
+
for (const src of rows) {
|
|
372
|
+
const srcIsObj = src && !Array.isArray(src) && typeof src === 'object';
|
|
373
|
+
const srcObj = srcIsObj ? Object.assign({}, src) : {};
|
|
374
|
+
let rowArr;
|
|
375
|
+
if (srcIsObj) rowArr = this._objToRow(src); // オブジェクト→キー列で行配列化
|
|
376
|
+
else { rowArr = Array.isArray(src) ? src.slice(0, this.COLS) : []; while (rowArr.length < this.COLS) rowArr.push(''); } // 配列は COLS 長に整える
|
|
377
|
+
this.data.push(rowArr);
|
|
378
|
+
this.rowH.push(undefined);
|
|
379
|
+
if (this._src) this._src.push(srcObj);
|
|
380
|
+
if (this._meta) this._meta.set(rowArr, { a: new Map(), ro: new Set(), h: undefined }); // フィルタが張った per-row メタも用意
|
|
381
|
+
if (this._allRows) { this._allRows.push(rowArr); if (this._allSrc) this._allSrc.push(srcObj); } // フィルタ中はマスタも同期
|
|
382
|
+
}
|
|
383
|
+
this.ROWS = this.data.length;
|
|
384
|
+
if (!this.table) return; // 未マウントなら次の buildTable が拾う
|
|
385
|
+
if (this.virtual) {
|
|
386
|
+
// 窓+上下スペーサを作り直す(force)。下スペーサ高が ROWS で再計算され新行までスクロール可能に。
|
|
387
|
+
// 窓が末尾に無い(新行が可視外)時も安全=可視 ~33 行の innerHTML 差し替えのみ・scrollTop 不変・
|
|
388
|
+
// エディタは tbody 外のオーバーレイなので値も維持。
|
|
389
|
+
this._renderWindow(this._vWindow(), true);
|
|
390
|
+
} else {
|
|
391
|
+
const tbody = this.table.querySelector('tbody');
|
|
392
|
+
if (tbody) { let html = ''; for (let r = n0; r < this.ROWS; r++) html += this._rowHTML(r); tbody.insertAdjacentHTML('beforeend', html); }
|
|
393
|
+
}
|
|
394
|
+
// 追記前が floor 以下(最後の1行=クリア/minRows>1 ロック)だった時だけ、既存行の🗑 状態を再描画せず更新
|
|
395
|
+
// (appendRows は既存行を描き直さないため title/disabled が古いまま残るのを消す。floor 超過後は常態=更新不要)。
|
|
396
|
+
if (this._hasDelCol() && n0 <= Math.max(1, this.minRows)) this._refreshRowDel();
|
|
397
|
+
}
|
|
398
|
+
// 遅延ロード(push・Phase B): 総行数を total に確保する。現行の行はそのまま・不足分を PENDING 行で埋め、超過は切る。
|
|
399
|
+
// dataComplete=false になり、この間 minSpareRows は自動無効(末尾スペアと PENDING tail は両立不可)。
|
|
400
|
+
// virtual 前提(PENDING 領域はスペーサに畳まれ描画されない=軽い)。プラグインは onViewportChange→fillRows で実データを流し込む。
|
|
401
|
+
// setData で解除(実データに差し替わり dataComplete=true)。
|
|
402
|
+
setRowCount(total) {
|
|
403
|
+
total = Math.max(0, total | 0);
|
|
404
|
+
const cur = this.ROWS;
|
|
405
|
+
if (total > cur) { for (let r = cur; r < total; r++) { this.data.push(TssGrid.PENDING); this.rowH.push(undefined); } }
|
|
406
|
+
else if (total < cur) { this.data.length = total; this.rowH.length = total; }
|
|
407
|
+
this.ROWS = total;
|
|
408
|
+
let pend = 0; for (let r = 0; r < this.ROWS; r++) if (this.data[r] === TssGrid.PENDING) pend++; // 確保時の一度きり O(N)。以降 fillRows が差分で維持
|
|
409
|
+
this._pendingCount = pend; this.dataComplete = pend === 0;
|
|
410
|
+
if (this.active.r >= this.ROWS) this.active = { r: Math.max(0, this.ROWS - 1), c: this.active.c }; // アクティブが範囲外なら詰める
|
|
411
|
+
if (this.table) { if (this.virtual) this._renderWindow(this._vWindow(), true); else this.buildTable(); }
|
|
412
|
+
}
|
|
413
|
+
// 遅延ロード(push・Phase B): PENDING 行 [start, start+rows.length) を実データに差し替える(onViewportChange への応答)。
|
|
414
|
+
// 塗り替え範囲が可視窓に重なる時だけ窓を再描画(buildTable しない)=スクロール/選択/アクティブは不変。窓外は data 差し替えのみ。
|
|
415
|
+
// rows は現在のデータと同形(オブジェクトモードはオブジェクト配列/配列モードは行配列)。範囲外(>=ROWS)は無視。
|
|
416
|
+
fillRows(start, rows) {
|
|
417
|
+
if (!Array.isArray(rows) || !rows.length) return;
|
|
418
|
+
start = Math.max(0, start | 0);
|
|
419
|
+
for (let i = 0; i < rows.length; i++) {
|
|
420
|
+
const r = start + i; if (r >= this.ROWS) break;
|
|
421
|
+
const src = rows[i];
|
|
422
|
+
const srcIsObj = src && !Array.isArray(src) && typeof src === 'object';
|
|
423
|
+
const srcObj = srcIsObj ? Object.assign({}, src) : {};
|
|
424
|
+
let rowArr;
|
|
425
|
+
if (srcIsObj) rowArr = this._objToRow(src);
|
|
426
|
+
else { rowArr = Array.isArray(src) ? src.slice(0, this.COLS) : []; while (rowArr.length < this.COLS) rowArr.push(''); }
|
|
427
|
+
if (this.data[r] === TssGrid.PENDING) this._pendingCount--; // PENDING→実データ(既に実データの再fill は count 不変)
|
|
428
|
+
this.data[r] = rowArr;
|
|
429
|
+
if (this._src) this._src[r] = srcObj;
|
|
430
|
+
if (this._meta) this._meta.set(rowArr, { a: new Map(), ro: new Set(), h: undefined });
|
|
431
|
+
}
|
|
432
|
+
if (this._pendingCount < 0) this._pendingCount = 0;
|
|
433
|
+
this.dataComplete = this._pendingCount === 0;
|
|
434
|
+
if (!this.table) return;
|
|
435
|
+
if (this.virtual) {
|
|
436
|
+
if (start < this._vEnd && start + rows.length > this._vStart) this._renderWindow(this._vWindow(), true); // 可視窓に重なる時だけ再描画
|
|
437
|
+
} else this.buildTable();
|
|
438
|
+
}
|
|
338
439
|
// 初期カーソル位置を [r,c] に解決(c は index か data キー)。範囲内へクランプ。
|
|
339
440
|
_resolveInitialCell() {
|
|
340
441
|
const ic = this.initialCell; if (!ic) return [0, 0];
|
|
@@ -371,6 +472,7 @@
|
|
|
371
472
|
const lines = [];
|
|
372
473
|
if (o.headers && this.headers) lines.push(this.headers.slice(0, this.COLS).map(esc).join(','));
|
|
373
474
|
for (let r = 0; r < this.ROWS; r++) {
|
|
475
|
+
if (this.data[r] === TssGrid.PENDING) continue; // 遅延ロード: 未取得行は出さない(loaded-only)
|
|
374
476
|
if (o.skipEmpty && this._rowEmpty(r)) continue;
|
|
375
477
|
const cells = [];
|
|
376
478
|
for (let c = 0; c < this.COLS; c++) cells.push(esc(o.formatted ? this._displayValue(r, c) : this.data[r][c]));
|
|
@@ -434,8 +536,27 @@
|
|
|
434
536
|
const na = TssGrid.toNum(va), nb = TssGrid.toNum(vb);
|
|
435
537
|
return (na !== null && nb !== null) ? na - nb : String(va).localeCompare(String(vb));
|
|
436
538
|
}
|
|
539
|
+
// 遅延ロード(push): sort/filter は「グリッドが計算しない・サーバ再フェッチ」(AG Grid ポリシー)。
|
|
540
|
+
// 状態を onSortFilterChange で通知し、ロード済みを PENDING へ戻す(パージ)=プラグインが新順で取り直す。
|
|
541
|
+
// 通知先が未設定なら破壊せず no-op(パージで画面を空にしない安全側)。呼び側は !dataComplete で本メソッドを使う。
|
|
542
|
+
_lazySortFilter(state) {
|
|
543
|
+
if (this.onSortFilterChange) {
|
|
544
|
+
try { this.onSortFilterChange(state); } catch (_) {}
|
|
545
|
+
this._purgeToPending(); // 窓再描画で onViewportChange も発火=プラグインが可視域を新順で取り直す
|
|
546
|
+
} else {
|
|
547
|
+
try { console.warn('[TssGrid] 遅延ロード中の sort/filter はサーバ側で解決してください(onSortFilterChange 未設定のため no-op)'); } catch (_) {}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// ロード済み行を全て PENDING に戻す(sort/filter 変更でブロックが陳腐化=AG Grid の cache purge 相当)。
|
|
551
|
+
_purgeToPending() {
|
|
552
|
+
for (let r = 0; r < this.ROWS; r++) { this.data[r] = TssGrid.PENDING; if (this._src) this._src[r] = {}; }
|
|
553
|
+
this._pendingCount = this.ROWS; this.dataComplete = false;
|
|
554
|
+
this._vpStart = -1; this._vpEnd = -1; // 次の窓通知を必ず出す(新順の取り直しを促す)
|
|
555
|
+
if (this.table) { if (this.virtual) this._renderWindow(this._vWindow(), true); else this.buildTable(); }
|
|
556
|
+
}
|
|
437
557
|
// 列 c(index か data キー)で並べ替え。dir='asc'(既定)|'desc'。数値っぽければ数値比較、でなければ文字列比較。
|
|
438
558
|
sortBy(c, dir) {
|
|
559
|
+
if (!this.dataComplete) return this._lazySortFilter({ type: 'sort', sort: [{ col: c, dir: dir || 'asc' }] }); // 遅延ロード中はサーバ委譲
|
|
439
560
|
if (this._sortLocked()) return; // 結合中はソート禁止(値による組み替えで結合帯が散らばる)
|
|
440
561
|
const ci = this._resolveCol(c); if (ci < 0) return;
|
|
441
562
|
const sgn = dir === 'desc' ? -1 : 1;
|
|
@@ -447,6 +568,7 @@
|
|
|
447
568
|
// 多段(複数列)ソート。specs=[{col, dir}] を優先順(先頭=第1キー)に。col=index か data キー、dir='asc'(既定)|'desc'。
|
|
448
569
|
// 各段は sortBy と同じセル比較。前段が同値のときだけ次段で決める(安定ソート=残った同値は現在の並びを保つ)。空行は最下部固定。
|
|
449
570
|
sortByCols(specs) {
|
|
571
|
+
if (!this.dataComplete) return this._lazySortFilter({ type: 'sort', sort: specs }); // 遅延ロード中はサーバ委譲
|
|
450
572
|
if (this._sortLocked()) return; // 結合中はソート禁止
|
|
451
573
|
const keys = (specs || [])
|
|
452
574
|
.map(s => ({ ci: this._resolveCol(s && s.col), sgn: (s && s.dir === 'desc') ? -1 : 1 }))
|
|
@@ -462,6 +584,7 @@
|
|
|
462
584
|
}
|
|
463
585
|
// 任意比較で並べ替え。cmp(rowA, rowB) は行(配列 or オブジェクト)を受ける。空行は最下部固定。
|
|
464
586
|
sortRows(cmp) {
|
|
587
|
+
if (!this.dataComplete) return this._lazySortFilter({ type: 'sort', sort: 'custom' }); // 遅延ロード中はサーバ委譲(JS比較子はサーバへ渡せない=custom 通知のみ)
|
|
465
588
|
if (this._sortLocked()) return; // 結合中はソート禁止
|
|
466
589
|
this._rememberOrder();
|
|
467
590
|
const rows = this._src ? this.getRows() : this.data;
|
|
@@ -519,6 +642,7 @@
|
|
|
519
642
|
}
|
|
520
643
|
// 絞り込み。pred=(row, masterIndex, src)=>bool / {col:value|fn} / falsy=解除。非破壊(編集は参照でマスタへ反映)。
|
|
521
644
|
filter(pred) {
|
|
645
|
+
if (!this.dataComplete) { this._lazySortFilter({ type: 'filter', filter: pred }); return false; } // 遅延ロード中はサーバ委譲(JS述語はサーバへ渡せない=通知のみ)
|
|
522
646
|
if (this._merges && this._merges.length) return false; // v1: 結合中は不可
|
|
523
647
|
const fn = this._normFilter(pred);
|
|
524
648
|
if (!fn) return this.clearFilter();
|
|
@@ -646,6 +770,23 @@
|
|
|
646
770
|
this.copybox = q('.tg-copybox'); this.fillhandle = q('.tg-fillhandle'); this.fillpreview = q('.tg-fillpreview');
|
|
647
771
|
this.resizeGuide = q('.tg-resizeguide');
|
|
648
772
|
this._edInput = q('input.tg-editor'); this._edMulti = q('.tg-editor-multi'); this.editor = this._edInput;
|
|
773
|
+
// ブラウザ/拡張の自動入力(Chrome の「ポイントカード/住所/支払い」等)抑止。
|
|
774
|
+
// name 無しだとブラウザが周囲のラベルからフィールド種別をML推論して誤爆する(会員番号扱い等)ため、
|
|
775
|
+
// 非意味的で一意な name を付けて種別マッチと入力記憶を避ける。autocomplete=off に加え各PWマネージャの無視属性も。
|
|
776
|
+
// ※Chrome のポイントカード/支払い系は autocomplete=off を無視することがあり100%ではない(確実に切るならブラウザ設定側)。
|
|
777
|
+
// suppressAutofill:true は autocomplete を 'new-password' に(Chrome の loyalty/payment は 'off' を無視するため)。
|
|
778
|
+
// 公式なフィールド無効化 API が将来出たら、この1行の値を差し替えれば全 TssGrid に波及する。
|
|
779
|
+
const _acVal = this.suppressAutofill ? 'new-password' : 'off';
|
|
780
|
+
const _eduid = 'tg-ed-' + (TssGrid._edSeq = (TssGrid._edSeq | 0) + 1);
|
|
781
|
+
[this._edInput, this._edMulti].forEach((el, i) => {
|
|
782
|
+
el.name = _eduid + '-' + i;
|
|
783
|
+
el.setAttribute('autocomplete', _acVal);
|
|
784
|
+
el.setAttribute('autocapitalize', 'off');
|
|
785
|
+
el.setAttribute('data-lpignore', 'true'); // LastPass
|
|
786
|
+
el.setAttribute('data-1p-ignore', 'true'); // 1Password
|
|
787
|
+
el.setAttribute('data-bwignore', 'true'); // Bitwarden
|
|
788
|
+
el.setAttribute('data-form-type', 'other'); // 一部ヒューリスティック向けヒント
|
|
789
|
+
});
|
|
649
790
|
this.select = q('.tg-select'); this.dumpEl = q('.tg-dump');
|
|
650
791
|
this.menuEl = q('.tg-menu');
|
|
651
792
|
this.root = q('.tssgrid');
|
|
@@ -665,13 +806,14 @@
|
|
|
665
806
|
activeTd() { return this.cellEl(this.active.r, this.active.c); }
|
|
666
807
|
clampR(r) { return Math.max(0, Math.min(this.ROWS - 1, r)); }
|
|
667
808
|
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); }
|
|
809
|
+
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
810
|
|
|
670
811
|
// ---- 列定義(セルタイプ / バリデータ) ----
|
|
671
812
|
colCfg(c) { return this.columns[c] || {}; }
|
|
672
813
|
colType(c) { return this.colCfg(c).type || 'text'; }
|
|
673
814
|
// 読み取り専用判定(グリッド全体 / 列 readOnly: bool/関数 / セル単位 setCellReadOnly)。
|
|
674
815
|
_isReadOnly(r, c) {
|
|
816
|
+
if (this.data[r] === TssGrid.PENDING) return true; // 遅延ロード: 未取得セルは「読込中」=編集不可(fillRows で実データが来たら普通に編集可)
|
|
675
817
|
if (this.readOnly) return true;
|
|
676
818
|
if (this._merges && this._isCovered(r, c)) return true; // 結合の従属セルは編集不可(貼付/フィルは既存の readOnly 経路で巻戻し)
|
|
677
819
|
if (this._cellRO.has(r + ',' + c)) return true; // セル単位の実行時トグル
|
|
@@ -734,6 +876,7 @@
|
|
|
734
876
|
_structRemapFn(info) {
|
|
735
877
|
switch (info.type) {
|
|
736
878
|
case 'insertRow': { const at = info.at; return (r, c) => [r >= at ? r + 1 : r, c]; }
|
|
879
|
+
case 'insertRows': { const { at, count } = info; return (r, c) => [r >= at ? r + count : r, c]; }
|
|
737
880
|
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
881
|
case 'insertCol': { const at = info.at; return (r, c) => [r, c >= at ? c + 1 : c]; }
|
|
739
882
|
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 +892,7 @@
|
|
|
749
892
|
for (const m of this._merges) {
|
|
750
893
|
let { r, c, colspan } = m, rowspan = m.rowspan || 1;
|
|
751
894
|
if (info.type === 'insertRow') { const at = info.at; if (at <= r) r++; else if (at < r + rowspan) rowspan++; }
|
|
895
|
+
else if (info.type === 'insertRows') { const { at, count } = info; if (at <= r) r += count; else if (at < r + rowspan) rowspan += count; }
|
|
752
896
|
else if (info.type === 'deleteRows') {
|
|
753
897
|
const { r0, r1 } = info, n = r1 - r0 + 1, top = r, bot = r + rowspan - 1;
|
|
754
898
|
if (r0 <= top && r1 >= bot) continue; // 縦方向すべて消える
|
|
@@ -1025,6 +1169,7 @@
|
|
|
1025
1169
|
if (keys.length) td.dataset.cstyle = keys.join(' '); else delete td.dataset.cstyle;
|
|
1026
1170
|
}
|
|
1027
1171
|
_renderCell(r, c) {
|
|
1172
|
+
if (this._renderPaused) { this._renderDirty = true; return; } // バッチ描画中は最後の buildTable が全セルを一括で描く
|
|
1028
1173
|
if (this._merges && this._isCovered(r, c)) return; // 従属セルは描画対象なし(cellEl はアンカーへ解決=誤って上書きしない)
|
|
1029
1174
|
const td = this.cellEl(r, c); if (!td) return;
|
|
1030
1175
|
const ph = (this.data[r][c] === '' && this.colType(c) !== 'checkbox') ? this._placeholder(c) : null;
|
|
@@ -1044,6 +1189,27 @@
|
|
|
1044
1189
|
_colWidth(c) { return this.colW[c] != null ? this.colW[c] : this.defColW; }
|
|
1045
1190
|
_rowHeight(r) { return this.rowH[r] != null ? this.rowH[r] : this.defRowH; }
|
|
1046
1191
|
_rhW() { return this.rowHeaders ? this.rowHeaderW : 0; } // 行ヘッダ列の幅(非表示なら 0)
|
|
1192
|
+
_hasDelCol() { return this.rowDelete && this.allowDeleteRows; } // 右端削除列を出すか(allowDeleteRows:false なら出さない)
|
|
1193
|
+
_delW() { return this._hasDelCol() ? this.rowDeleteW : 0; } // 削除列の幅(非表示なら 0)
|
|
1194
|
+
// 行末の削除セル(🗑)。data-r/data-c を持たない=各種 td[data-r] 探索に引っかからない(行番号は closest('tr[data-r]') から取る)。
|
|
1195
|
+
_delCellHTML(r) {
|
|
1196
|
+
if (!this._hasDelCol()) return '';
|
|
1197
|
+
const floor = Math.max(1, this.minRows);
|
|
1198
|
+
const canDelete = this.ROWS > floor;
|
|
1199
|
+
const soleClear = !canDelete && this.ROWS === 1; // 最後の1行=削除でなく「中身クリア」(行は残す)=🗑 は有効のまま
|
|
1200
|
+
const dis = (!canDelete && !soleClear) ? ' tg-disabled' : ''; // minRows>1 のロック時だけ無効(クリアもしない)
|
|
1201
|
+
const title = soleClear ? '内容をクリア' : '行を削除'; // 最後の1行は動作が「クリア」なのでツールチップも変える
|
|
1202
|
+
return '<td class="tg-rowdel-cell"><span class="tg-rowdel' + dis + '" title="' + title + '">🗑</span></td>';
|
|
1203
|
+
}
|
|
1204
|
+
// 🗑 の title/disabled は ROWS 依存(最後の1行=「内容をクリア」/minRows>1 ロック=無効)で全行一律。
|
|
1205
|
+
// 行数だけ変えて既存行を再描画しない経路(appendRows の非virtual)用に、描画済み🗑を現在の ROWS へ揃える。
|
|
1206
|
+
_refreshRowDel() {
|
|
1207
|
+
if (!this._hasDelCol() || !this.table) return;
|
|
1208
|
+
const canDelete = this.ROWS > Math.max(1, this.minRows);
|
|
1209
|
+
const soleClear = !canDelete && this.ROWS === 1;
|
|
1210
|
+
const dis = !canDelete && !soleClear, title = soleClear ? '内容をクリア' : '行を削除';
|
|
1211
|
+
this.table.querySelectorAll('tbody .tg-rowdel').forEach(el => { el.classList.toggle('tg-disabled', dis); el.setAttribute('title', title); });
|
|
1212
|
+
}
|
|
1047
1213
|
// 行ヘッダの表示内容: false=空(数字なし)/ 関数=カスタム(エスケープ)/ 既定=行番号(r+1)。
|
|
1048
1214
|
_rowHeadLabel(r) {
|
|
1049
1215
|
const L = this.rowHeaderLabel;
|
|
@@ -1112,7 +1278,7 @@
|
|
|
1112
1278
|
}
|
|
1113
1279
|
toggleColumn(c) { this.hiddenCols.has(this.clampC(c)) ? this.showColumn(c) : this.hideColumn(c); }
|
|
1114
1280
|
_placeholder(c) { const p = this.colCfg(c).placeholder; return p != null ? String(p) : this.placeholder; }
|
|
1115
|
-
_totalWidth() { let w = this._rhW(); for (let c = 0; c < this.COLS; c++) if (!this._isHidden(c)) w += this._colWidth(c); return w; }
|
|
1281
|
+
_totalWidth() { let w = this._rhW(); for (let c = 0; c < this.COLS; c++) if (!this._isHidden(c)) w += this._colWidth(c); return w + this._delW(); }
|
|
1116
1282
|
// テーブルに合計幅を明示。これが無いと狭い枠で fixed レイアウトが列を比例圧縮し、
|
|
1117
1283
|
// 保存値(colW)と実描画幅がズレてリサイズのプレビュー線が外れる。
|
|
1118
1284
|
_applyTableWidth() { this.table.style.width = this._totalWidth() + 'px'; }
|
|
@@ -1152,7 +1318,7 @@
|
|
|
1152
1318
|
// 上から n 行を固定(実行時変更可)
|
|
1153
1319
|
freezeRows(n) { this.frozenRows = Math.max(0, Math.min(this.ROWS, n | 0)); this.buildTable(); this.setActive(this.active.r, this.active.c); }
|
|
1154
1320
|
// リーフ(実列に対応する)ヘッダ行を生成。labels 指定時はその文字列/ {label} を見出しに使う。
|
|
1155
|
-
_leafHeaderTr(withCorner, labels) {
|
|
1321
|
+
_leafHeaderTr(withCorner, labels, delHdr) {
|
|
1156
1322
|
let html = '<tr>' + (withCorner ? '<th class="rowhead corner"></th>' : '');
|
|
1157
1323
|
for (let c = 0; c < this.COLS; c++) {
|
|
1158
1324
|
const cls = [];
|
|
@@ -1165,6 +1331,7 @@
|
|
|
1165
1331
|
if (lbl && typeof lbl === 'object') lbl = lbl.label != null ? lbl.label : '';
|
|
1166
1332
|
html += '<th data-c="' + c + '"' + a + '>' + headCb + TssGrid.esc(lbl != null ? lbl : '') + grip + '</th>';
|
|
1167
1333
|
}
|
|
1334
|
+
if (delHdr) html += '<th class="tg-rowdel-hdr"></th>'; // 右端削除列の見出し(無地・非データ)。ネストヘッダ時は角を rowspan で出すのでここでは出さない
|
|
1168
1335
|
return html + '</tr>';
|
|
1169
1336
|
}
|
|
1170
1337
|
// ネストヘッダ: 各ヘッダ行を異なる top で sticky(段が重ならないように積む)。
|
|
@@ -1177,7 +1344,16 @@
|
|
|
1177
1344
|
});
|
|
1178
1345
|
}
|
|
1179
1346
|
// 1行ぶんの <tr>…</tr> を生成(buildTable の本体ループと、仮想スクロールの窓再描画 _renderWindow で共用)。
|
|
1347
|
+
// 遅延ロード: 未取得(PENDING)行は data[r][c] を一切読まず「読込中」プレースホルダ行を描く。
|
|
1348
|
+
_pendingRowHTML(r) {
|
|
1349
|
+
const rh = this.rowHeaders;
|
|
1350
|
+
const head = rh ? '<th class="rowhead" data-r="' + r + '">' + this._rowHeadLabel(r) + '</th>' : '';
|
|
1351
|
+
const cell = '<td class="tg-pending-cell" colspan="' + this.COLS + '" data-r="' + r + '" data-c="0"><span class="tg-pending-dots"></span></td>';
|
|
1352
|
+
const del = this._hasDelCol() ? '<td class="tg-rowdel-cell"></td>' : ''; // 読込中行は🗑を出さない(値未取得)=列合わせの空セルのみ
|
|
1353
|
+
return '<tr data-r="' + r + '" class="tg-row tg-pending-row" style="height:' + this._rowHeight(r) + 'px">' + head + cell + del + '</tr>';
|
|
1354
|
+
}
|
|
1180
1355
|
_rowHTML(r) {
|
|
1356
|
+
if (this.data[r] === TssGrid.PENDING) return this._pendingRowHTML(r); // 未取得行=読込中プレースホルダ(値は読まない)
|
|
1181
1357
|
const rh = this.rowHeaders;
|
|
1182
1358
|
const rgrip = (rh && this.resizeRows) ? '<div class="tg-rowgrip"></div>' : '';
|
|
1183
1359
|
const canMove = rh && this.rowReorder && !this._hasVMerge();
|
|
@@ -1222,7 +1398,7 @@
|
|
|
1222
1398
|
const csAttr = (cs > 1 ? ' colspan="' + cs + '"' : '') + (rs > 1 ? ' rowspan="' + rs + '"' : '');
|
|
1223
1399
|
html += '<td' + a + ico + dccls + sty + titleAttr + csAttr + ' data-r="' + r + '" data-c="' + c + '">' + this._cellHTML(r, c) + '</td>';
|
|
1224
1400
|
}
|
|
1225
|
-
return html + '</tr>';
|
|
1401
|
+
return html + this._delCellHTML(r) + '</tr>';
|
|
1226
1402
|
}
|
|
1227
1403
|
// 仮想スクロール: 現在の scrollTop から描画すべき窓 [start,end)(絶対行)を算出。バッファ込み。
|
|
1228
1404
|
_vWindow() {
|
|
@@ -1241,7 +1417,7 @@
|
|
|
1241
1417
|
if (!force && this._vRendered && win.start === this._vStart && win.end === this._vEnd) return false;
|
|
1242
1418
|
const tbody = this.table && this.table.querySelector('tbody');
|
|
1243
1419
|
if (!tbody) return false;
|
|
1244
|
-
const rh = this.rowHeaders, colsSpan = this.COLS + (rh ? 1 : 0);
|
|
1420
|
+
const rh = this.rowHeaders, colsSpan = this.COLS + (rh ? 1 : 0) + (this._hasDelCol() ? 1 : 0);
|
|
1245
1421
|
const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
|
|
1246
1422
|
let html = '';
|
|
1247
1423
|
if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
@@ -1251,8 +1427,15 @@
|
|
|
1251
1427
|
this._vStart = win.start; this._vEnd = win.end; this._vRendered = true;
|
|
1252
1428
|
this._syncHeaderCheckboxes();
|
|
1253
1429
|
this.updateSelectionUI(); // セルが作り直されたので窓内の選択シェード/オーバーレイを再適用
|
|
1430
|
+
this._fireViewport(win.start, win.end); // 遅延ロード: 窓が変わったらプラグインへ通知(PENDING を fillRows で埋めさせる)
|
|
1254
1431
|
return true;
|
|
1255
1432
|
}
|
|
1433
|
+
// 遅延ロード: 描画窓が変わった時だけ onViewportChange(start,end) を発火(同一範囲は抑止=fillRows→再描画の再帰も止まる)。
|
|
1434
|
+
_fireViewport(start, end) {
|
|
1435
|
+
if (!this.onViewportChange || (start === this._vpStart && end === this._vpEnd)) return;
|
|
1436
|
+
this._vpStart = start; this._vpEnd = end;
|
|
1437
|
+
try { this.onViewportChange(start, end); } catch (_) {}
|
|
1438
|
+
}
|
|
1256
1439
|
// 仮想スクロール: スクロールで窓がずれたら再描画(rAF スロットル)。
|
|
1257
1440
|
_vSync() {
|
|
1258
1441
|
if (!this.virtual || this._vRAF) return;
|
|
@@ -1275,11 +1458,13 @@
|
|
|
1275
1458
|
this._renderWindow(this._vWindow(), false); // 窓を即同期(place 前に DOM を確定)
|
|
1276
1459
|
}
|
|
1277
1460
|
buildTable() {
|
|
1461
|
+
if (this._renderPaused) { this._renderDirty = true; return; } // バッチ描画中は最後にまとめて1回(_withBatchedRender)
|
|
1278
1462
|
if (this._customEditor && this._customCancel) this._customCancel(); // 開いているカスタムエディタを閉じる(body直下ポップアップの取り残し防止・_commitActive と同じ扱い)
|
|
1279
1463
|
this._selhdrFg = null; // 選択ヘッダ文字色キャッシュ破棄(テーマ変更に追従)
|
|
1280
1464
|
const rh = this.rowHeaders;
|
|
1281
1465
|
let html = '<colgroup>' + (rh ? '<col style="width:' + this.rowHeaderW + 'px">' : '');
|
|
1282
1466
|
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">';
|
|
1467
|
+
if (this._hasDelCol()) html += '<col class="tg-rowdel-col" style="width:' + this.rowDeleteW + 'px">'; // 右端削除列(固定幅・システム列)
|
|
1283
1468
|
html += '</colgroup>';
|
|
1284
1469
|
if (this.colHeaders) {
|
|
1285
1470
|
html += '<thead>';
|
|
@@ -1298,18 +1483,19 @@
|
|
|
1298
1483
|
html += '<th class="tg-grouphdr' + froz + '" colspan="' + span + '" data-c0="' + c + '" data-c1="' + c1 + '">' + TssGrid.esc(label) + '</th>';
|
|
1299
1484
|
c += span;
|
|
1300
1485
|
}
|
|
1486
|
+
if (this._hasDelCol() && ri === 0) html += '<th class="tg-rowdel-hdr corner" rowspan="' + nRows + '"></th>'; // 右端削除列の角(全ヘッダ段を rowspan で覆う)
|
|
1301
1487
|
html += '</tr>';
|
|
1302
1488
|
});
|
|
1303
|
-
html += this._leafHeaderTr(false, leaf); //
|
|
1489
|
+
html += this._leafHeaderTr(false, leaf, false); // リーフ段(左角も右削除角も上段で rowspan 済み)
|
|
1304
1490
|
} else {
|
|
1305
|
-
html += this._leafHeaderTr(rh, null);
|
|
1491
|
+
html += this._leafHeaderTr(rh, null, this._hasDelCol());
|
|
1306
1492
|
}
|
|
1307
1493
|
html += '</thead>';
|
|
1308
1494
|
}
|
|
1309
1495
|
html += '<tbody>';
|
|
1310
1496
|
if (this.virtual) {
|
|
1311
1497
|
const win = this._vWindow(); this._vStart = win.start; this._vEnd = win.end; this._vRendered = true;
|
|
1312
|
-
const colsSpan = this.COLS + (rh ? 1 : 0);
|
|
1498
|
+
const colsSpan = this.COLS + (rh ? 1 : 0) + (this._hasDelCol() ? 1 : 0);
|
|
1313
1499
|
const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
|
|
1314
1500
|
if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
1315
1501
|
for (let r = win.start; r < win.end; r++) html += this._rowHTML(r);
|
|
@@ -1331,6 +1517,7 @@
|
|
|
1331
1517
|
this._syncHeaderCheckboxes();
|
|
1332
1518
|
this._buildGeomCache(); // 実測幾何キャッシュ(全 _apply 後=幅高が確定してから)
|
|
1333
1519
|
this.renderDump();
|
|
1520
|
+
if (this.virtual) this._fireViewport(this._vStart, this._vEnd); // 遅延ロード: 初期/再構築後の窓もプラグインへ通知(table 確定後に発火)
|
|
1334
1521
|
}
|
|
1335
1522
|
// 各リーフ列ヘッダth(data-c 付き)を生成後に onHeaderRender へ渡す。ソートアイコン等の差し込み用。
|
|
1336
1523
|
_applyHeaderRender() {
|
|
@@ -1384,7 +1571,7 @@
|
|
|
1384
1571
|
if (this.stretchH === 'none' || !this.stretchH) return;
|
|
1385
1572
|
const avail = this.wrap ? this.wrap.clientWidth : 0;
|
|
1386
1573
|
if (!avail) return;
|
|
1387
|
-
const base = this._rhW(), widths = [];
|
|
1574
|
+
const base = this._rhW() + this._delW(), widths = []; // 削除列は固定幅=ストレッチ対象外(余白計算から差し引く)
|
|
1388
1575
|
let sum = 0;
|
|
1389
1576
|
for (let c = 0; c < this.COLS; c++) { const wd = this._colWidth(c); widths.push(wd); sum += wd; }
|
|
1390
1577
|
const extra = avail - base - sum;
|
|
@@ -1519,13 +1706,17 @@
|
|
|
1519
1706
|
// 選択範囲が及ぶ行/列ヘッダをハイライト(Excel 風のアフォーダンス)。仮想時は行ヘッダを窓内にクランプ(大量行で querySelector を無駄打ちしない)。
|
|
1520
1707
|
const hlo = this.virtual ? Math.max(r0, this._vStart) : r0;
|
|
1521
1708
|
const hhi = this.virtual ? Math.min(r1, this._vEnd - 1) : r1;
|
|
1709
|
+
// 巨大選択(大量貼付/全選択など)は per-cell の背景シェードと行ヘッダ塗りを省き、selrange アウトライン(O(1)・
|
|
1710
|
+
// スクロール追従)に任せる=O(行×列) の DOM 変異を回避(1000行貼付で updateSelectionUI ~175ms→~1ms)。
|
|
1711
|
+
// 画面に収まる通常選択(~2000セル未満)は従来どおり全セルをシェード=見た目不変。
|
|
1712
|
+
const rows = hhi - hlo + 1, bigSel = rows > 300 || rows * (c1 - c0 + 1) > 2000;
|
|
1522
1713
|
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'); }
|
|
1714
|
+
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
1715
|
this.wrap.style.setProperty('--tg-selhdr-fg', this._selhdrTextColor()); // 選択ヘッダ文字色を背景輝度から自動(暗背景→白)
|
|
1525
1716
|
if (r0 === r1 && c0 === c1) return;
|
|
1526
1717
|
const clampMerge = this._headerSel && this._merges; // 列/行選択では「範囲に収まらない結合」は塗らない(帯を塗り広げない)
|
|
1527
1718
|
// 仮想スクロール: 窓内の行だけシェード(範囲枠 selrange は placeRect がモデル幾何で全域を描く)。
|
|
1528
|
-
for (let r = hlo; r <= hhi; r++) for (let c = c0; c <= c1; c++) {
|
|
1719
|
+
if (!bigSel) for (let r = hlo; r <= hhi; r++) for (let c = c0; c <= c1; c++) {
|
|
1529
1720
|
if (clampMerge) {
|
|
1530
1721
|
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
1722
|
if (a.r < r0 || mr1 > r1 || a.c < c0 || mc1 > c1) continue; // 結合が選択範囲に収まらない=塗らない
|
|
@@ -1561,6 +1752,7 @@
|
|
|
1561
1752
|
return (this._selhdrFg = L < 0.55 ? '#fff' : '#1a3a6b');
|
|
1562
1753
|
}
|
|
1563
1754
|
updateSelectionUI() {
|
|
1755
|
+
if (this._renderPaused) return; // バッチ描画中は選択枠の位置決めを保留(resume 後に1回)
|
|
1564
1756
|
if (!this.cursor) { // 表示専用: 選択枠・範囲・フィルハンドルを出さない(選択自体は内部に保持)
|
|
1565
1757
|
this.selbox.style.display = 'none'; this.selrange.style.display = 'none'; this.fillhandle.style.display = 'none';
|
|
1566
1758
|
return;
|
|
@@ -1730,6 +1922,7 @@
|
|
|
1730
1922
|
}
|
|
1731
1923
|
focusAndSelect(sel) {
|
|
1732
1924
|
this.active = { ...sel.active }; this.extent = { ...sel.extent };
|
|
1925
|
+
if (this._renderPaused) { this._pendingSel = sel; return; } // バッチ描画中は選択を記録だけ→resume で1回反映
|
|
1733
1926
|
if (this.virtual) this._vEnsureVisible(this.active.r);
|
|
1734
1927
|
this._syncEditorNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1735
1928
|
if (!this.virtual) this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
@@ -2011,14 +2204,16 @@
|
|
|
2011
2204
|
}
|
|
2012
2205
|
this.renderDump();
|
|
2013
2206
|
const grid = this;
|
|
2207
|
+
// minSpareRows: この編集で末尾スペア行が自動追加された分を、履歴コマンドに畳み込む
|
|
2208
|
+
// (undo で編集値と一緒に追加行も外す/redo で戻す)=「自動行追加が undo で消えない」不具合の解消。
|
|
2209
|
+
const spareAdded = this._ensureSpare();
|
|
2014
2210
|
this.history.push({
|
|
2015
2211
|
label: this.name,
|
|
2016
|
-
apply() { grid._applyCells(changes, 'neu', 'redo'); grid.focusAndSelect(selAfter); },
|
|
2017
|
-
revert() { grid._applyCells(changes, 'old', 'undo'); grid.focusAndSelect(selBefore); },
|
|
2212
|
+
apply() { grid._applyCells(changes, 'neu', 'redo'); grid._addSpareRows(spareAdded); grid.focusAndSelect(selAfter); },
|
|
2213
|
+
revert() { grid._applyCells(changes, 'old', 'undo'); grid._removeSpareRows(spareAdded); grid.focusAndSelect(selBefore); },
|
|
2018
2214
|
});
|
|
2019
2215
|
if (this.onAfterChange) { try { this.onAfterChange(this._pubChanges(changes, source), source); } catch (_) {} }
|
|
2020
2216
|
this._syncHeaderCheckboxes(); // ヘッダ全選択チェックの見た目を同期
|
|
2021
|
-
this._ensureSpare(); // 末尾の空行確保(minSpareRows)
|
|
2022
2217
|
}
|
|
2023
2218
|
commit() {
|
|
2024
2219
|
const wasEditing = this.mode === 'edit';
|
|
@@ -2056,13 +2251,28 @@
|
|
|
2056
2251
|
}
|
|
2057
2252
|
}
|
|
2058
2253
|
// 末尾に常に minSpareRows 行の空行を確保(足りなければ追加。履歴には積まない)。
|
|
2254
|
+
// 追加した空行数を返す(pushCmd が履歴コマンドに畳み込んで undo/redo で戻す/足すため)。
|
|
2059
2255
|
_ensureSpare() {
|
|
2060
|
-
if (
|
|
2256
|
+
if (this._renderPaused) { this._renderDirty = true; return 0; } // バッチ描画中は保留→resume でまとめて確保
|
|
2257
|
+
if (!this.minSpareRows || this._allRows || !this.dataComplete) return 0; // フィルタ中/遅延ロード中(PENDING tail)は末尾の自動空行を足さない
|
|
2061
2258
|
let empties = 0;
|
|
2062
2259
|
for (let r = this.ROWS - 1; r >= 0; r--) { if (this._rowEmpty(r)) empties++; else break; }
|
|
2063
2260
|
let added = 0;
|
|
2064
2261
|
while (empties < this.minSpareRows && (!this.maxRows || this.ROWS < this.maxRows)) { this.data.push(new Array(this.COLS).fill('')); this.rowH.push(undefined); if (this._src) this._src.push({}); this.ROWS++; empties++; added++; } // maxRows で天井打ち(スペア行も超えない)
|
|
2065
2262
|
if (added && this.table) { this.buildTable(); this.setActive(this.active.r, this.active.c); }
|
|
2263
|
+
return added;
|
|
2264
|
+
}
|
|
2265
|
+
// pushCmd の undo/redo 用: minSpareRows で自動追加された末尾空行 n 行を、編集の取消/やり直しと一緒に外す/戻す。
|
|
2266
|
+
_addSpareRows(n) {
|
|
2267
|
+
if (!n) return;
|
|
2268
|
+
for (let k = 0; k < n; k++) { this.data.push(new Array(this.COLS).fill('')); this.rowH.push(undefined); if (this._src) this._src.push({}); }
|
|
2269
|
+
this.ROWS += n; this.buildTable();
|
|
2270
|
+
}
|
|
2271
|
+
_removeSpareRows(n) {
|
|
2272
|
+
if (!n) return;
|
|
2273
|
+
n = Math.min(n, this.ROWS - 1); // 最低1行は残す(防御)
|
|
2274
|
+
this.data.length -= n; this.rowH.length -= n; if (this._src) this._src.length -= n;
|
|
2275
|
+
this.ROWS -= n; this.buildTable();
|
|
2066
2276
|
}
|
|
2067
2277
|
// 構造変更を実行+履歴に積む共通処理。doIt/undoIt は冪等(redo で再実行される)。
|
|
2068
2278
|
_structCmd(doIt, undoIt, selAfterFn, selBefore, info) {
|
|
@@ -2089,26 +2299,40 @@
|
|
|
2089
2299
|
if (at - 1 >= 0) { const i = this._allRows.indexOf(this.data[at - 1]); if (i >= 0) return i + 1; }
|
|
2090
2300
|
return this._allRows.length;
|
|
2091
2301
|
}
|
|
2092
|
-
insertRow(ri, where = 'above') {
|
|
2302
|
+
insertRow(ri, where = 'above') { this.insertRows(ri, 1, where); }
|
|
2303
|
+
// 一括挿入プリミティブ: count 行を1回の buildTable・1つの history コマンドで挿入(Undo/Redo が1手で count 行まとめて戻る)。
|
|
2304
|
+
// 大量貼付など「まとめて足す」を O(N²)(1行ずつ insertRow)から O(N) に。既定 count=1 は insertRow と挙動等価。
|
|
2305
|
+
insertRows(ri, count = 1, where = 'above') {
|
|
2093
2306
|
if (!this.allowInsertRows) return;
|
|
2094
|
-
|
|
2307
|
+
count = Math.max(0, count | 0);
|
|
2308
|
+
if (this.maxRows) count = Math.min(count, this.maxRows - this.ROWS); // 上限までにクランプ(超過分は足さない)
|
|
2309
|
+
if (count <= 0) return;
|
|
2095
2310
|
if (this.mode === 'edit') this._commitActive();
|
|
2096
2311
|
this._normalizeArrays();
|
|
2097
2312
|
const at = Math.max(0, Math.min(this.ROWS, where === 'below' ? ri + 1 : ri));
|
|
2098
2313
|
const before = this.snapSel(), grid = this, c = this.active.c, frozenBump = at < this.frozenRows;
|
|
2099
|
-
let
|
|
2314
|
+
let refs; // 挿入した行参照の配列(フィルタ中の _allRows 追従・undo 用に保持)
|
|
2100
2315
|
const doIt = () => {
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2316
|
+
refs = []; const hs = [], srcs = grid._src ? [] : null;
|
|
2317
|
+
for (let k = 0; k < count; k++) { refs.push(new Array(grid.COLS).fill('')); hs.push(undefined); if (srcs) srcs.push({}); }
|
|
2318
|
+
// マスタ挿入位置は data を splice する前の位置関係から決める(_masterInsertIndex と同じ規則をバッチ用に)
|
|
2319
|
+
let mi = -1;
|
|
2320
|
+
if (grid._allRows) {
|
|
2321
|
+
mi = grid._allRows.length;
|
|
2322
|
+
if (at < grid.data.length) { const i = grid._allRows.indexOf(grid.data[at]); if (i >= 0) mi = i; }
|
|
2323
|
+
else if (at - 1 >= 0) { const i = grid._allRows.indexOf(grid.data[at - 1]); if (i >= 0) mi = i + 1; }
|
|
2324
|
+
}
|
|
2325
|
+
grid.data.splice(at, 0, ...refs); grid.rowH.splice(at, 0, ...hs); if (grid._src) grid._src.splice(at, 0, ...srcs); grid.ROWS += count;
|
|
2326
|
+
if (grid._allRows) { grid._allRows.splice(mi, 0, ...refs); if (grid._allSrc) grid._allSrc.splice(mi, 0, ...refs.map(() => ({}))); } // マスタにも一括挿入
|
|
2327
|
+
if (frozenBump) grid.frozenRows += count;
|
|
2105
2328
|
};
|
|
2106
2329
|
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
|
|
2330
|
+
grid.data.splice(at, count); grid.rowH.splice(at, count); if (grid._src) grid._src.splice(at, count); grid.ROWS -= count;
|
|
2331
|
+
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); } } }
|
|
2332
|
+
if (frozenBump) grid.frozenRows -= count;
|
|
2110
2333
|
};
|
|
2111
|
-
|
|
2334
|
+
const type = count === 1 ? 'insertRow' : 'insertRows'; // 単行は従来どおり type:'insertRow'(後方互換)/複数は 'insertRows'
|
|
2335
|
+
this._structCmd(doIt, undoIt, () => ({ active: { r: at, c }, extent: { r: at + count - 1, c } }), before, { type, at, count });
|
|
2112
2336
|
}
|
|
2113
2337
|
deleteRows(r0, r1) {
|
|
2114
2338
|
if (!this.allowDeleteRows) return;
|
|
@@ -2135,6 +2359,15 @@
|
|
|
2135
2359
|
};
|
|
2136
2360
|
this._structCmd(doIt, undoIt, () => { const rr = Math.min(r0, grid.ROWS - 1); return { active: { r: rr, c }, extent: { r: rr, c } }; }, before, { type: 'deleteRows', r0, r1 });
|
|
2137
2361
|
}
|
|
2362
|
+
// 右端🗑の「最後の1行」用: 0行にはできない(削除ガード)ので、行は残して中身だけ空にする
|
|
2363
|
+
// (「消してから空行を足す」のと正味同じ結果を、0行に一瞬も落とさず 1 undo で)。readOnly セルはスキップ。
|
|
2364
|
+
clearRow(r) {
|
|
2365
|
+
if (r < 0 || r >= this.ROWS) return;
|
|
2366
|
+
if (this.mode === 'edit') this._commitActive();
|
|
2367
|
+
const before = this.snapSel(), changes = [];
|
|
2368
|
+
for (let c = 0; c < this.COLS; c++) { if (this._isReadOnly(r, c)) continue; this.setCell(r, c, '', changes); }
|
|
2369
|
+
this.pushCmd(changes, before, this.snapSel(), 'delete'); // changes 空(既に空/全 readOnly)なら pushCmd 側で no-op
|
|
2370
|
+
}
|
|
2138
2371
|
insertCol(ci, where = 'left') {
|
|
2139
2372
|
if (!this.allowInsertCols) return;
|
|
2140
2373
|
if (this._allRows) return; // v1: フィルタ中は構造変更ロック
|
|
@@ -2207,6 +2440,26 @@
|
|
|
2207
2440
|
revert() { for (let i = batch.length - 1; i >= 0; i--) batch[i].revert(); },
|
|
2208
2441
|
});
|
|
2209
2442
|
}
|
|
2443
|
+
// 大量セル更新を「最後に1回 buildTable」へ畳む。中の buildTable/_renderCell/renderDump/選択UI は保留され、
|
|
2444
|
+
// resume 時に buildTable 1回+選択反映+renderDump 1回だけ走る(1行ずつ描く N 回の DOM 変異を消す)。
|
|
2445
|
+
// 例: 1000×5 の貼付が per-cell 描画 ~500ms → 一括 ~80ms。入れ子は外側が最後にまとめて描く。
|
|
2446
|
+
_withBatchedRender(fn) {
|
|
2447
|
+
if (this._renderPaused) return fn(); // 入れ子: 外側の resume に任せる
|
|
2448
|
+
this._renderPaused = true; this._renderDirty = false; this._pendingSel = null;
|
|
2449
|
+
let ret;
|
|
2450
|
+
try { ret = fn(); }
|
|
2451
|
+
finally {
|
|
2452
|
+
this._renderPaused = false;
|
|
2453
|
+
if (this._renderDirty) {
|
|
2454
|
+
this.buildTable(); // 全セルを1回のHTML生成で描く(末尾で renderDump=dumpEl/onChange も1回発火)
|
|
2455
|
+
this._ensureSpare(); // 保留していた末尾スペア行の確保
|
|
2456
|
+
const s = this._pendingSel; this._pendingSel = null;
|
|
2457
|
+
if (s) this.focusAndSelect(s); // 選択枠・スクロールを1回だけ
|
|
2458
|
+
}
|
|
2459
|
+
this._renderDirty = false;
|
|
2460
|
+
}
|
|
2461
|
+
return ret;
|
|
2462
|
+
}
|
|
2210
2463
|
|
|
2211
2464
|
// ---- 右クリックメニュー ----
|
|
2212
2465
|
_onContextMenu(e) {
|
|
@@ -2466,6 +2719,21 @@
|
|
|
2466
2719
|
if (Array.isArray(ret)) g = ret;
|
|
2467
2720
|
if (!g || !g.length) return;
|
|
2468
2721
|
}
|
|
2722
|
+
// pasteAutoGrow=true では「行の一括生成」と「値の貼付」を1つの history にまとめる(1回の undo で両方戻る)。
|
|
2723
|
+
// 非オートグロー時は history.push が1回だけ=_batchHistory は素通し(従来とバイト等価)。
|
|
2724
|
+
let after;
|
|
2725
|
+
const body = () => { this._batchHistory(() => { after = this._pasteGridBody(g); }); };
|
|
2726
|
+
// 大量貼付だけ _withBatchedRender で「最後に buildTable 1回」へ畳む(per-cell 描画 N 回を消す=1000行 ~500ms→~80ms)。
|
|
2727
|
+
// 小さな貼付を大きなグリッドに落とす時は従来どおり per-cell(全再描画の方が高くつくため回避)。
|
|
2728
|
+
const willGrow = this.pasteAutoGrow && (this.active.r + g.length > this.ROWS);
|
|
2729
|
+
const heavy = willGrow || (g.length >= 8 && g.length * 5 >= this.ROWS); // 貼付が表の ~20% 以上を占める or 行が増える時だけ畳む
|
|
2730
|
+
if (heavy) this._withBatchedRender(body); else body();
|
|
2731
|
+
if (after && this.onAfterPaste) { // onAfterPaste は batch の外で呼ぶ(コールバック内の履歴操作を貼付コマンドに混ぜない)
|
|
2732
|
+
const range = { r0: after.active.r, c0: after.active.c, r1: after.extent.r, c1: after.extent.c };
|
|
2733
|
+
try { this.onAfterPaste(g, { range }); } catch (_) {}
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
_pasteGridBody(g) {
|
|
2469
2737
|
const before = this.snapSel(), changes = [], sel = this.rectRange();
|
|
2470
2738
|
const visAll = this._visCols();
|
|
2471
2739
|
let after;
|
|
@@ -2474,21 +2742,31 @@
|
|
|
2474
2742
|
for (let r = sel.r0; r <= sel.r1; r++) for (let c = sel.c0; c <= sel.c1; c++) if (!this._isHidden(c)) this.setCell(r, c, v, changes);
|
|
2475
2743
|
after = { active: { r: sel.r0, c: sel.c0 }, extent: { r: sel.r1, c: sel.c1 } };
|
|
2476
2744
|
} else {
|
|
2477
|
-
|
|
2745
|
+
// アクティブ列が隠し列だと(例: selectRow は active.c=0 を置く=先頭列が hiddenColumns の時)以下の indexOf が -1 になり、
|
|
2746
|
+
// 貼り付けが1列ずれて先頭値が落ちる。可視列へスナップしてから起点を取る(setActive と同じ「隠し列に置かない」規則)。
|
|
2747
|
+
const r0 = this.active.r, c0 = this._snapVisCol(this.active.c);
|
|
2478
2748
|
let maxw = 0; for (let i = 0; i < g.length; i++) maxw = Math.max(maxw, g[i].length);
|
|
2479
2749
|
const p0 = visAll.indexOf(c0), availCols = visAll.length - p0; // 貼り付けは可視列へ(隠し列をスキップ)
|
|
2480
|
-
//
|
|
2481
|
-
|
|
2750
|
+
// オートグロー時に伸ばせる上限行数(maxRows があればそこまで/無ければ無制限)。非オートグローは現状の ROWS。
|
|
2751
|
+
const capRows = this.pasteAutoGrow ? (this.maxRows || Infinity) : this.ROWS;
|
|
2752
|
+
// 範囲超過チェック(オプトイン): 伸ばしても収まらない or 列がはみ出すなら貼り付けを中止してエラー通知(行生成の前に判定=原子性)
|
|
2753
|
+
if (this.pasteOverflow === 'error' && (r0 + g.length > capRows || maxw > availCols)) {
|
|
2482
2754
|
if (this.onPasteOverflow) {
|
|
2483
2755
|
try {
|
|
2484
2756
|
this.onPasteOverflow({
|
|
2485
2757
|
anchor: { r: r0, c: c0 }, height: g.length, width: maxw, rows: this.ROWS, cols: this.COLS,
|
|
2486
|
-
overRows: Math.max(0, r0 + g.length -
|
|
2758
|
+
overRows: Math.max(0, r0 + g.length - capRows), overCols: Math.max(0, maxw - availCols),
|
|
2487
2759
|
});
|
|
2488
2760
|
} catch (_) {}
|
|
2489
2761
|
}
|
|
2490
2762
|
return; // 何も変更しない
|
|
2491
2763
|
}
|
|
2764
|
+
// オートグロー: 不足分を先に一括生成(buildTable 1回・history 1コマンド)。超過は maxRows でクランプ、残りは pasteOverflow に従う。
|
|
2765
|
+
if (this.pasteAutoGrow && r0 + g.length > this.ROWS) {
|
|
2766
|
+
const target = this.maxRows ? Math.min(this.maxRows, r0 + g.length) : r0 + g.length;
|
|
2767
|
+
const grow = target - this.ROWS;
|
|
2768
|
+
if (grow > 0) this.insertRows(this.ROWS - 1, grow, 'below');
|
|
2769
|
+
}
|
|
2492
2770
|
let lastC = c0;
|
|
2493
2771
|
for (let i = 0; i < g.length; i++) for (let j = 0; j < g[i].length; j++) {
|
|
2494
2772
|
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 +2780,7 @@
|
|
|
2502
2780
|
this.pendingCut = null; this.copybox.style.display = 'none'; }
|
|
2503
2781
|
this.pushCmd(changes, before, after, 'paste');
|
|
2504
2782
|
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
|
-
}
|
|
2783
|
+
return after;
|
|
2509
2784
|
}
|
|
2510
2785
|
clearCopyMarquee() { this.copybox.style.display = 'none'; this.pendingCut = null; }
|
|
2511
2786
|
|
|
@@ -2587,6 +2862,20 @@
|
|
|
2587
2862
|
// input と textarea(multiline) の両方に同じ編集入口リスナを張る(this.editor はアクティブ列で張り替わる)。
|
|
2588
2863
|
[this._edInput, this._edMulti].forEach((ed) => {
|
|
2589
2864
|
ed.addEventListener('keydown', (e) => this._onKey(e));
|
|
2865
|
+
// 貼り付けの主経路=ネイティブ paste イベント(clipboardData.getData)。Ctrl+V のジェスチャー内で
|
|
2866
|
+
// 同期に読める=許可プロンプト不要+堅牢(readText の権限/非セキュア/非フォーカスで落ちる問題を回避)。
|
|
2867
|
+
// nav 中の editor は focus 代理なので native 挿入は常に止める(copyPaste=false でも挿入させない)。
|
|
2868
|
+
// 編集中は横取りせず native 貼付(1セルへの通常ペースト)に任せる。paste は beforeinput より先に来るため、
|
|
2869
|
+
// ここで preventDefault すれば beforeinput(insertFromPaste) の誤発火=エディタ誤オープンも起きない。
|
|
2870
|
+
// 右クリックメニューの「貼り付け」/プログラム呼び出しは paste イベントを伴わないので paste()=readText 経路を温存。
|
|
2871
|
+
ed.addEventListener('paste', (e) => {
|
|
2872
|
+
if (this.mode !== 'nav') return;
|
|
2873
|
+
e.preventDefault();
|
|
2874
|
+
if (!this.copyPaste) return;
|
|
2875
|
+
const text = (e.clipboardData && e.clipboardData.getData('text')) || '';
|
|
2876
|
+
const g = (text && text.length) ? TssGrid.parseTSV(text) : sharedClip; // clipboard 空/非テキストは内部コピー buffer へフォールバック
|
|
2877
|
+
if (g && g.length) this.pasteGrid(g);
|
|
2878
|
+
});
|
|
2590
2879
|
// inline エディタへ入力を通知(打った/変換確定した内容で候補を絞る用)
|
|
2591
2880
|
ed.addEventListener('input', () => {
|
|
2592
2881
|
if (this._inlineEd && this._inlineEd.onInput) { try { this._inlineEd.onInput(this.editor.value, this._inlineCtx); } catch (_) {} }
|
|
@@ -2627,6 +2916,7 @@
|
|
|
2627
2916
|
this.table.addEventListener('mousedown', (e) => {
|
|
2628
2917
|
if (e.button !== 0) return; // 右/中クリックは選択ドラッグを開始しない(右は contextmenu で処理)
|
|
2629
2918
|
const cls = e.target.classList;
|
|
2919
|
+
if (e.target.closest('.tg-rowdel-cell')) { e.preventDefault(); return; } // 削除列: 選択ドラッグを始めない(実削除は click で)。preventDefault で🗑のテキスト選択も抑止
|
|
2630
2920
|
if (cls && cls.contains('tg-colgrip')) { this._startColResize(e); return; } // 列幅リサイズ
|
|
2631
2921
|
if (cls && cls.contains('tg-rowgrip')) { this._startRowResize(e); return; } // 行高リサイズ
|
|
2632
2922
|
if (cls && cls.contains('tg-rowmove') && !this.rowReorderWhole) { this._startRowMove(e); return; } // ⠿つまみドラッグ移動('header'モードはセル全体扱い=_onHeaderMouseDown 経由で選択も伴う)
|
|
@@ -2650,6 +2940,13 @@
|
|
|
2650
2940
|
// ヘッダ全選択チェックは mousedown で処理済み。click のネイティブ・トグルを止めて二重反転を防ぐ
|
|
2651
2941
|
this.table.addEventListener('click', (e) => {
|
|
2652
2942
|
if (e.target.classList && e.target.classList.contains('tg-head-cb')) e.preventDefault();
|
|
2943
|
+
// 右端削除列の🗑クリック。通常は当該行を削除(remove_row と同一経路・1 undo で復活)。
|
|
2944
|
+
// 最後の1行(0行にはできない)は削除でなく「中身クリア(行は残す)」。minRows>1 のロック時は no-op(🗑 も無効表示)。
|
|
2945
|
+
const del = e.target.closest('.tg-rowdel-cell');
|
|
2946
|
+
if (del) {
|
|
2947
|
+
const tr = del.closest('tr[data-r]');
|
|
2948
|
+
if (tr) { const r = +tr.dataset.r; if (this.ROWS > Math.max(1, this.minRows)) this.deleteRows(r, r); else if (this.ROWS === 1) this.clearRow(r); }
|
|
2949
|
+
}
|
|
2653
2950
|
});
|
|
2654
2951
|
this.table.addEventListener('dblclick', (e) => {
|
|
2655
2952
|
// 列境界グリップのダブルクリック=オートフィット(Excel風)
|
|
@@ -2931,7 +3228,8 @@
|
|
|
2931
3228
|
if (ctrl && (e.key.toLowerCase() === 'y' || (e.shiftKey && e.key.toLowerCase() === 'z'))) { e.preventDefault(); this.history.redo(); return; }
|
|
2932
3229
|
if (ctrl && e.key.toLowerCase() === 'c') { e.preventDefault(); this.copy(false); return; }
|
|
2933
3230
|
if (ctrl && e.key.toLowerCase() === 'x') { e.preventDefault(); this.copy(true); return; }
|
|
2934
|
-
|
|
3231
|
+
// Ctrl+V は keydown で拾わず native paste イベントへ委譲(editor の paste リスナが処理)。
|
|
3232
|
+
// ここで preventDefault すると paste イベント自体を止めかねないため分岐を置かない。
|
|
2935
3233
|
if (e.shiftKey && e.key.startsWith('Arrow')) {
|
|
2936
3234
|
e.preventDefault();
|
|
2937
3235
|
let { r, c } = this.extent;
|
|
@@ -2958,6 +3256,8 @@
|
|
|
2958
3256
|
}
|
|
2959
3257
|
}
|
|
2960
3258
|
|
|
3259
|
+
TssGrid.PENDING = Symbol('tg-pending'); // 遅延ロード(push): 未取得行を表す sentinel。data[r] に入れると _rowHTML が「読込中」行を描く
|
|
3260
|
+
|
|
2961
3261
|
// ---- UMD export ----
|
|
2962
3262
|
const api = { TssGrid, HistoryManager };
|
|
2963
3263
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|