@threestarsoftware/tssgrid 0.1.4 → 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/src/tssgrid.js CHANGED
@@ -202,6 +202,10 @@
202
202
  this.rowReorder = !!opts.rowReorder;
203
203
  this.rowReorderWhole = opts.rowReorder === 'header'; // 'header'=行ヘッダーセル全体が移動ハンドル(クリック=選択/ドラッグ=移動)/true=⠿アイコンのみ
204
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)
205
209
  // 選択カーソル(緑の選択枠・範囲・セルハイライト・フィルハンドル)を表示するか。
206
210
  // false で表示専用グリッド向けに非表示(内部の選択は保持=コピーは可)。
207
211
  this.cursor = opts.cursor !== false;
@@ -227,6 +231,11 @@
227
231
  this.onBeforeAutofill = opts.onBeforeAutofill || null; // (src,target)=>false で取消
228
232
  this.onAfterAutofill = opts.onAfterAutofill || null; // (src,target)
229
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;
230
239
  // 列幅オートフィット(内容に合わせて自動調整)。境界ダブルクリック / autoSizeColumn でも。
231
240
  this.autoColumnSize = !!opts.autoColumnSize;
232
241
  // データ入力支援
@@ -382,6 +391,9 @@
382
391
  const tbody = this.table.querySelector('tbody');
383
392
  if (tbody) { let html = ''; for (let r = n0; r < this.ROWS; r++) html += this._rowHTML(r); tbody.insertAdjacentHTML('beforeend', html); }
384
393
  }
394
+ // 追記前が floor 以下(最後の1行=クリア/minRows>1 ロック)だった時だけ、既存行の🗑 状態を再描画せず更新
395
+ // (appendRows は既存行を描き直さないため title/disabled が古いまま残るのを消す。floor 超過後は常態=更新不要)。
396
+ if (this._hasDelCol() && n0 <= Math.max(1, this.minRows)) this._refreshRowDel();
385
397
  }
386
398
  // 遅延ロード(push・Phase B): 総行数を total に確保する。現行の行はそのまま・不足分を PENDING 行で埋め、超過は切る。
387
399
  // dataComplete=false になり、この間 minSpareRows は自動無効(末尾スペアと PENDING tail は両立不可)。
@@ -758,6 +770,23 @@
758
770
  this.copybox = q('.tg-copybox'); this.fillhandle = q('.tg-fillhandle'); this.fillpreview = q('.tg-fillpreview');
759
771
  this.resizeGuide = q('.tg-resizeguide');
760
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
+ });
761
790
  this.select = q('.tg-select'); this.dumpEl = q('.tg-dump');
762
791
  this.menuEl = q('.tg-menu');
763
792
  this.root = q('.tssgrid');
@@ -1160,6 +1189,27 @@
1160
1189
  _colWidth(c) { return this.colW[c] != null ? this.colW[c] : this.defColW; }
1161
1190
  _rowHeight(r) { return this.rowH[r] != null ? this.rowH[r] : this.defRowH; }
1162
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
+ }
1163
1213
  // 行ヘッダの表示内容: false=空(数字なし)/ 関数=カスタム(エスケープ)/ 既定=行番号(r+1)。
1164
1214
  _rowHeadLabel(r) {
1165
1215
  const L = this.rowHeaderLabel;
@@ -1228,7 +1278,7 @@
1228
1278
  }
1229
1279
  toggleColumn(c) { this.hiddenCols.has(this.clampC(c)) ? this.showColumn(c) : this.hideColumn(c); }
1230
1280
  _placeholder(c) { const p = this.colCfg(c).placeholder; return p != null ? String(p) : this.placeholder; }
1231
- _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(); }
1232
1282
  // テーブルに合計幅を明示。これが無いと狭い枠で fixed レイアウトが列を比例圧縮し、
1233
1283
  // 保存値(colW)と実描画幅がズレてリサイズのプレビュー線が外れる。
1234
1284
  _applyTableWidth() { this.table.style.width = this._totalWidth() + 'px'; }
@@ -1268,7 +1318,7 @@
1268
1318
  // 上から n 行を固定(実行時変更可)
1269
1319
  freezeRows(n) { this.frozenRows = Math.max(0, Math.min(this.ROWS, n | 0)); this.buildTable(); this.setActive(this.active.r, this.active.c); }
1270
1320
  // リーフ(実列に対応する)ヘッダ行を生成。labels 指定時はその文字列/ {label} を見出しに使う。
1271
- _leafHeaderTr(withCorner, labels) {
1321
+ _leafHeaderTr(withCorner, labels, delHdr) {
1272
1322
  let html = '<tr>' + (withCorner ? '<th class="rowhead corner"></th>' : '');
1273
1323
  for (let c = 0; c < this.COLS; c++) {
1274
1324
  const cls = [];
@@ -1281,6 +1331,7 @@
1281
1331
  if (lbl && typeof lbl === 'object') lbl = lbl.label != null ? lbl.label : '';
1282
1332
  html += '<th data-c="' + c + '"' + a + '>' + headCb + TssGrid.esc(lbl != null ? lbl : '') + grip + '</th>';
1283
1333
  }
1334
+ if (delHdr) html += '<th class="tg-rowdel-hdr"></th>'; // 右端削除列の見出し(無地・非データ)。ネストヘッダ時は角を rowspan で出すのでここでは出さない
1284
1335
  return html + '</tr>';
1285
1336
  }
1286
1337
  // ネストヘッダ: 各ヘッダ行を異なる top で sticky(段が重ならないように積む)。
@@ -1298,7 +1349,8 @@
1298
1349
  const rh = this.rowHeaders;
1299
1350
  const head = rh ? '<th class="rowhead" data-r="' + r + '">' + this._rowHeadLabel(r) + '</th>' : '';
1300
1351
  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>';
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>';
1302
1354
  }
1303
1355
  _rowHTML(r) {
1304
1356
  if (this.data[r] === TssGrid.PENDING) return this._pendingRowHTML(r); // 未取得行=読込中プレースホルダ(値は読まない)
@@ -1346,7 +1398,7 @@
1346
1398
  const csAttr = (cs > 1 ? ' colspan="' + cs + '"' : '') + (rs > 1 ? ' rowspan="' + rs + '"' : '');
1347
1399
  html += '<td' + a + ico + dccls + sty + titleAttr + csAttr + ' data-r="' + r + '" data-c="' + c + '">' + this._cellHTML(r, c) + '</td>';
1348
1400
  }
1349
- return html + '</tr>';
1401
+ return html + this._delCellHTML(r) + '</tr>';
1350
1402
  }
1351
1403
  // 仮想スクロール: 現在の scrollTop から描画すべき窓 [start,end)(絶対行)を算出。バッファ込み。
1352
1404
  _vWindow() {
@@ -1365,7 +1417,7 @@
1365
1417
  if (!force && this._vRendered && win.start === this._vStart && win.end === this._vEnd) return false;
1366
1418
  const tbody = this.table && this.table.querySelector('tbody');
1367
1419
  if (!tbody) return false;
1368
- 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);
1369
1421
  const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
1370
1422
  let html = '';
1371
1423
  if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
@@ -1412,6 +1464,7 @@
1412
1464
  const rh = this.rowHeaders;
1413
1465
  let html = '<colgroup>' + (rh ? '<col style="width:' + this.rowHeaderW + 'px">' : '');
1414
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">'; // 右端削除列(固定幅・システム列)
1415
1468
  html += '</colgroup>';
1416
1469
  if (this.colHeaders) {
1417
1470
  html += '<thead>';
@@ -1430,18 +1483,19 @@
1430
1483
  html += '<th class="tg-grouphdr' + froz + '" colspan="' + span + '" data-c0="' + c + '" data-c1="' + c1 + '">' + TssGrid.esc(label) + '</th>';
1431
1484
  c += span;
1432
1485
  }
1486
+ if (this._hasDelCol() && ri === 0) html += '<th class="tg-rowdel-hdr corner" rowspan="' + nRows + '"></th>'; // 右端削除列の角(全ヘッダ段を rowspan で覆う)
1433
1487
  html += '</tr>';
1434
1488
  });
1435
- html += this._leafHeaderTr(false, leaf); // リーフ段(角は上で rowspan 済み)
1489
+ html += this._leafHeaderTr(false, leaf, false); // リーフ段(左角も右削除角も上段で rowspan 済み)
1436
1490
  } else {
1437
- html += this._leafHeaderTr(rh, null);
1491
+ html += this._leafHeaderTr(rh, null, this._hasDelCol());
1438
1492
  }
1439
1493
  html += '</thead>';
1440
1494
  }
1441
1495
  html += '<tbody>';
1442
1496
  if (this.virtual) {
1443
1497
  const win = this._vWindow(); this._vStart = win.start; this._vEnd = win.end; this._vRendered = true;
1444
- const colsSpan = this.COLS + (rh ? 1 : 0);
1498
+ const colsSpan = this.COLS + (rh ? 1 : 0) + (this._hasDelCol() ? 1 : 0);
1445
1499
  const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
1446
1500
  if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
1447
1501
  for (let r = win.start; r < win.end; r++) html += this._rowHTML(r);
@@ -1517,7 +1571,7 @@
1517
1571
  if (this.stretchH === 'none' || !this.stretchH) return;
1518
1572
  const avail = this.wrap ? this.wrap.clientWidth : 0;
1519
1573
  if (!avail) return;
1520
- const base = this._rhW(), widths = [];
1574
+ const base = this._rhW() + this._delW(), widths = []; // 削除列は固定幅=ストレッチ対象外(余白計算から差し引く)
1521
1575
  let sum = 0;
1522
1576
  for (let c = 0; c < this.COLS; c++) { const wd = this._colWidth(c); widths.push(wd); sum += wd; }
1523
1577
  const extra = avail - base - sum;
@@ -2150,14 +2204,16 @@
2150
2204
  }
2151
2205
  this.renderDump();
2152
2206
  const grid = this;
2207
+ // minSpareRows: この編集で末尾スペア行が自動追加された分を、履歴コマンドに畳み込む
2208
+ // (undo で編集値と一緒に追加行も外す/redo で戻す)=「自動行追加が undo で消えない」不具合の解消。
2209
+ const spareAdded = this._ensureSpare();
2153
2210
  this.history.push({
2154
2211
  label: this.name,
2155
- apply() { grid._applyCells(changes, 'neu', 'redo'); grid.focusAndSelect(selAfter); },
2156
- 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); },
2157
2214
  });
2158
2215
  if (this.onAfterChange) { try { this.onAfterChange(this._pubChanges(changes, source), source); } catch (_) {} }
2159
2216
  this._syncHeaderCheckboxes(); // ヘッダ全選択チェックの見た目を同期
2160
- this._ensureSpare(); // 末尾の空行確保(minSpareRows)
2161
2217
  }
2162
2218
  commit() {
2163
2219
  const wasEditing = this.mode === 'edit';
@@ -2195,14 +2251,28 @@
2195
2251
  }
2196
2252
  }
2197
2253
  // 末尾に常に minSpareRows 行の空行を確保(足りなければ追加。履歴には積まない)。
2254
+ // 追加した空行数を返す(pushCmd が履歴コマンドに畳み込んで undo/redo で戻す/足すため)。
2198
2255
  _ensureSpare() {
2199
- if (this._renderPaused) { this._renderDirty = true; return; } // バッチ描画中は保留→resume でまとめて確保
2200
- if (!this.minSpareRows || this._allRows || !this.dataComplete) return; // フィルタ中/遅延ロード中(PENDING tail)は末尾の自動空行を足さない
2256
+ if (this._renderPaused) { this._renderDirty = true; return 0; } // バッチ描画中は保留→resume でまとめて確保
2257
+ if (!this.minSpareRows || this._allRows || !this.dataComplete) return 0; // フィルタ中/遅延ロード中(PENDING tail)は末尾の自動空行を足さない
2201
2258
  let empties = 0;
2202
2259
  for (let r = this.ROWS - 1; r >= 0; r--) { if (this._rowEmpty(r)) empties++; else break; }
2203
2260
  let added = 0;
2204
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 で天井打ち(スペア行も超えない)
2205
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();
2206
2276
  }
2207
2277
  // 構造変更を実行+履歴に積む共通処理。doIt/undoIt は冪等(redo で再実行される)。
2208
2278
  _structCmd(doIt, undoIt, selAfterFn, selBefore, info) {
@@ -2289,6 +2359,15 @@
2289
2359
  };
2290
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 });
2291
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
+ }
2292
2371
  insertCol(ci, where = 'left') {
2293
2372
  if (!this.allowInsertCols) return;
2294
2373
  if (this._allRows) return; // v1: フィルタ中は構造変更ロック
@@ -2663,7 +2742,9 @@
2663
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);
2664
2743
  after = { active: { r: sel.r0, c: sel.c0 }, extent: { r: sel.r1, c: sel.c1 } };
2665
2744
  } else {
2666
- const r0 = this.active.r, c0 = this.active.c;
2745
+ // アクティブ列が隠し列だと(例: selectRow は active.c=0 を置く=先頭列が hiddenColumns の時)以下の indexOf が -1 になり、
2746
+ // 貼り付けが1列ずれて先頭値が落ちる。可視列へスナップしてから起点を取る(setActive と同じ「隠し列に置かない」規則)。
2747
+ const r0 = this.active.r, c0 = this._snapVisCol(this.active.c);
2667
2748
  let maxw = 0; for (let i = 0; i < g.length; i++) maxw = Math.max(maxw, g[i].length);
2668
2749
  const p0 = visAll.indexOf(c0), availCols = visAll.length - p0; // 貼り付けは可視列へ(隠し列をスキップ)
2669
2750
  // オートグロー時に伸ばせる上限行数(maxRows があればそこまで/無ければ無制限)。非オートグローは現状の ROWS。
@@ -2835,6 +2916,7 @@
2835
2916
  this.table.addEventListener('mousedown', (e) => {
2836
2917
  if (e.button !== 0) return; // 右/中クリックは選択ドラッグを開始しない(右は contextmenu で処理)
2837
2918
  const cls = e.target.classList;
2919
+ if (e.target.closest('.tg-rowdel-cell')) { e.preventDefault(); return; } // 削除列: 選択ドラッグを始めない(実削除は click で)。preventDefault で🗑のテキスト選択も抑止
2838
2920
  if (cls && cls.contains('tg-colgrip')) { this._startColResize(e); return; } // 列幅リサイズ
2839
2921
  if (cls && cls.contains('tg-rowgrip')) { this._startRowResize(e); return; } // 行高リサイズ
2840
2922
  if (cls && cls.contains('tg-rowmove') && !this.rowReorderWhole) { this._startRowMove(e); return; } // ⠿つまみドラッグ移動('header'モードはセル全体扱い=_onHeaderMouseDown 経由で選択も伴う)
@@ -2858,6 +2940,13 @@
2858
2940
  // ヘッダ全選択チェックは mousedown で処理済み。click のネイティブ・トグルを止めて二重反転を防ぐ
2859
2941
  this.table.addEventListener('click', (e) => {
2860
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
+ }
2861
2950
  });
2862
2951
  this.table.addEventListener('dblclick', (e) => {
2863
2952
  // 列境界グリップのダブルクリック=オートフィット(Excel風)