@threestarsoftware/tssgrid 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +58 -0
- package/package.json +1 -1
- package/plugins/tss-filter.css +9 -0
- package/plugins/tss-filter.js +48 -9
- package/src/tssgrid.css +16 -0
- package/src/tssgrid.d.ts +466 -450
- package/src/tssgrid.js +120 -22
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');
|
|
@@ -1002,7 +1031,7 @@
|
|
|
1002
1031
|
}
|
|
1003
1032
|
if (t === 'dropdown') {
|
|
1004
1033
|
if (v === '') { if (cfg.allowEmpty === false) return { ok: false }; } // allowEmpty:false=必須選択(空は不可)
|
|
1005
|
-
else if (!this._optList(c).some(o => o.value === v)) return { ok: false }; // 保存値は option の value
|
|
1034
|
+
else if (!this._optList(c, r).some(o => o.value === v)) return { ok: false }; // 保存値は option の value と照合(行依存 options も r で解決)
|
|
1006
1035
|
} else if (t === 'checkbox') {
|
|
1007
1036
|
v = this._isCheckedVal(c, v) ? this._cbChecked(c) : this._cbUnchecked(c);
|
|
1008
1037
|
}
|
|
@@ -1069,16 +1098,20 @@
|
|
|
1069
1098
|
if (typeof cfg.format === 'string') return TssGrid._applyDatePattern(v, cfg.format); // 日付パターン表示
|
|
1070
1099
|
if (cfg.type === 'number') return TssGrid._formatNumber(v, cfg); // 数値書式(カンマ/小数/前後綴り)
|
|
1071
1100
|
if (cfg.type === 'time' && cfg.hour12) return TssGrid._fmtTime12(v); // 組込 12時間表示
|
|
1072
|
-
if (cfg.type === 'dropdown') return this._optLabel(c, v); // options が {value,label}
|
|
1101
|
+
if (cfg.type === 'dropdown') return this._optLabel(r, c, v); // options が {value,label} なら表示はラベル(行依存 options も r で解決)
|
|
1073
1102
|
return v;
|
|
1074
1103
|
}
|
|
1075
1104
|
// dropdown の options を [{value,label}] に正規化(文字列なら value=label)。値↔表示を分けられる。
|
|
1076
|
-
|
|
1077
|
-
|
|
1105
|
+
// options が関数なら (r, row)=>[…] で行依存(依存ドロップダウン)。row はその行の値配列(this.data[r])。
|
|
1106
|
+
// 静的配列はそのまま(r は無視)=後方互換。
|
|
1107
|
+
_optList(c, r) {
|
|
1108
|
+
let opts = this.colCfg(c).options;
|
|
1109
|
+
if (typeof opts === 'function') { try { opts = opts(r, r != null ? this.data[r] : undefined) || []; } catch (_) { opts = []; } }
|
|
1110
|
+
return (opts || []).map(o =>
|
|
1078
1111
|
(o && typeof o === 'object') ? { value: String(o.value), label: String(o.label != null ? o.label : o.value) }
|
|
1079
1112
|
: { value: String(o), label: String(o) });
|
|
1080
1113
|
}
|
|
1081
|
-
_optLabel(c, value) { const o = this._optList(c).find(x => x.value === value); return o ? o.label : value; }
|
|
1114
|
+
_optLabel(r, c, value) { const o = this._optList(c, r).find(x => x.value === value); return o ? o.label : value; }
|
|
1082
1115
|
// 編集開始時にエディタへ入れる文字列。number は Excel風に「素の数値」を出す(書式は表示専用)。
|
|
1083
1116
|
// ネイティブ(date/time picker)は保存値、それ以外のテキスト編集は表示形。
|
|
1084
1117
|
_editText(r, c) {
|
|
@@ -1092,7 +1125,12 @@
|
|
|
1092
1125
|
return '<input type="checkbox" class="tg-cb" tabindex="-1"' + (this._isReadOnly(r, c) ? ' disabled' : '') + (this._isCheckedVal(c, this.data[r][c]) ? ' checked' : '') + '>';
|
|
1093
1126
|
if (this.data[r][c] === '' && this._placeholder(c) != null) return TssGrid.esc(this._placeholder(c));
|
|
1094
1127
|
// html:true の列は表示だけ生HTML(値はテキスト保存のまま)。エスケープを外す=XSS責任は format/値の提供側。
|
|
1095
|
-
|
|
1128
|
+
// 空セルでも format を走らせる=ボタン/アイコン等の"飾り"を全行に描ける(_displayValue は空を素通しするので html+関数formatだけ特別扱い)。
|
|
1129
|
+
if (this.colCfg(c).html) {
|
|
1130
|
+
const fmt = this.colCfg(c).format;
|
|
1131
|
+
if (typeof fmt === 'function' && this.data[r][c] === '') { try { const o = fmt('', { r, c }); return o == null ? '' : String(o); } catch (_) { return ''; } }
|
|
1132
|
+
return this._displayValue(r, c);
|
|
1133
|
+
}
|
|
1096
1134
|
const _h = TssGrid.esc(this._displayValue(r, c));
|
|
1097
1135
|
// 複数行セルは絶対配置のクリップ箱で包む=行を押し広げない(行高一定・仮想スクロール維持/行高ぶんだけ表示)
|
|
1098
1136
|
if (this.colCfg(c).multiline) return '<div class="tg-ml-clip">' + _h + '</div>';
|
|
@@ -1160,6 +1198,27 @@
|
|
|
1160
1198
|
_colWidth(c) { return this.colW[c] != null ? this.colW[c] : this.defColW; }
|
|
1161
1199
|
_rowHeight(r) { return this.rowH[r] != null ? this.rowH[r] : this.defRowH; }
|
|
1162
1200
|
_rhW() { return this.rowHeaders ? this.rowHeaderW : 0; } // 行ヘッダ列の幅(非表示なら 0)
|
|
1201
|
+
_hasDelCol() { return this.rowDelete && this.allowDeleteRows; } // 右端削除列を出すか(allowDeleteRows:false なら出さない)
|
|
1202
|
+
_delW() { return this._hasDelCol() ? this.rowDeleteW : 0; } // 削除列の幅(非表示なら 0)
|
|
1203
|
+
// 行末の削除セル(🗑)。data-r/data-c を持たない=各種 td[data-r] 探索に引っかからない(行番号は closest('tr[data-r]') から取る)。
|
|
1204
|
+
_delCellHTML(r) {
|
|
1205
|
+
if (!this._hasDelCol()) return '';
|
|
1206
|
+
const floor = Math.max(1, this.minRows);
|
|
1207
|
+
const canDelete = this.ROWS > floor;
|
|
1208
|
+
const soleClear = !canDelete && this.ROWS === 1; // 最後の1行=削除でなく「中身クリア」(行は残す)=🗑 は有効のまま
|
|
1209
|
+
const dis = (!canDelete && !soleClear) ? ' tg-disabled' : ''; // minRows>1 のロック時だけ無効(クリアもしない)
|
|
1210
|
+
const title = soleClear ? '内容をクリア' : '行を削除'; // 最後の1行は動作が「クリア」なのでツールチップも変える
|
|
1211
|
+
return '<td class="tg-rowdel-cell"><span class="tg-rowdel' + dis + '" title="' + title + '">🗑</span></td>';
|
|
1212
|
+
}
|
|
1213
|
+
// 🗑 の title/disabled は ROWS 依存(最後の1行=「内容をクリア」/minRows>1 ロック=無効)で全行一律。
|
|
1214
|
+
// 行数だけ変えて既存行を再描画しない経路(appendRows の非virtual)用に、描画済み🗑を現在の ROWS へ揃える。
|
|
1215
|
+
_refreshRowDel() {
|
|
1216
|
+
if (!this._hasDelCol() || !this.table) return;
|
|
1217
|
+
const canDelete = this.ROWS > Math.max(1, this.minRows);
|
|
1218
|
+
const soleClear = !canDelete && this.ROWS === 1;
|
|
1219
|
+
const dis = !canDelete && !soleClear, title = soleClear ? '内容をクリア' : '行を削除';
|
|
1220
|
+
this.table.querySelectorAll('tbody .tg-rowdel').forEach(el => { el.classList.toggle('tg-disabled', dis); el.setAttribute('title', title); });
|
|
1221
|
+
}
|
|
1163
1222
|
// 行ヘッダの表示内容: false=空(数字なし)/ 関数=カスタム(エスケープ)/ 既定=行番号(r+1)。
|
|
1164
1223
|
_rowHeadLabel(r) {
|
|
1165
1224
|
const L = this.rowHeaderLabel;
|
|
@@ -1228,7 +1287,7 @@
|
|
|
1228
1287
|
}
|
|
1229
1288
|
toggleColumn(c) { this.hiddenCols.has(this.clampC(c)) ? this.showColumn(c) : this.hideColumn(c); }
|
|
1230
1289
|
_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; }
|
|
1290
|
+
_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
1291
|
// テーブルに合計幅を明示。これが無いと狭い枠で fixed レイアウトが列を比例圧縮し、
|
|
1233
1292
|
// 保存値(colW)と実描画幅がズレてリサイズのプレビュー線が外れる。
|
|
1234
1293
|
_applyTableWidth() { this.table.style.width = this._totalWidth() + 'px'; }
|
|
@@ -1268,7 +1327,7 @@
|
|
|
1268
1327
|
// 上から n 行を固定(実行時変更可)
|
|
1269
1328
|
freezeRows(n) { this.frozenRows = Math.max(0, Math.min(this.ROWS, n | 0)); this.buildTable(); this.setActive(this.active.r, this.active.c); }
|
|
1270
1329
|
// リーフ(実列に対応する)ヘッダ行を生成。labels 指定時はその文字列/ {label} を見出しに使う。
|
|
1271
|
-
_leafHeaderTr(withCorner, labels) {
|
|
1330
|
+
_leafHeaderTr(withCorner, labels, delHdr) {
|
|
1272
1331
|
let html = '<tr>' + (withCorner ? '<th class="rowhead corner"></th>' : '');
|
|
1273
1332
|
for (let c = 0; c < this.COLS; c++) {
|
|
1274
1333
|
const cls = [];
|
|
@@ -1281,6 +1340,7 @@
|
|
|
1281
1340
|
if (lbl && typeof lbl === 'object') lbl = lbl.label != null ? lbl.label : '';
|
|
1282
1341
|
html += '<th data-c="' + c + '"' + a + '>' + headCb + TssGrid.esc(lbl != null ? lbl : '') + grip + '</th>';
|
|
1283
1342
|
}
|
|
1343
|
+
if (delHdr) html += '<th class="tg-rowdel-hdr"></th>'; // 右端削除列の見出し(無地・非データ)。ネストヘッダ時は角を rowspan で出すのでここでは出さない
|
|
1284
1344
|
return html + '</tr>';
|
|
1285
1345
|
}
|
|
1286
1346
|
// ネストヘッダ: 各ヘッダ行を異なる top で sticky(段が重ならないように積む)。
|
|
@@ -1298,7 +1358,8 @@
|
|
|
1298
1358
|
const rh = this.rowHeaders;
|
|
1299
1359
|
const head = rh ? '<th class="rowhead" data-r="' + r + '">' + this._rowHeadLabel(r) + '</th>' : '';
|
|
1300
1360
|
const cell = '<td class="tg-pending-cell" colspan="' + this.COLS + '" data-r="' + r + '" data-c="0"><span class="tg-pending-dots"></span></td>';
|
|
1301
|
-
|
|
1361
|
+
const del = this._hasDelCol() ? '<td class="tg-rowdel-cell"></td>' : ''; // 読込中行は🗑を出さない(値未取得)=列合わせの空セルのみ
|
|
1362
|
+
return '<tr data-r="' + r + '" class="tg-row tg-pending-row" style="height:' + this._rowHeight(r) + 'px">' + head + cell + del + '</tr>';
|
|
1302
1363
|
}
|
|
1303
1364
|
_rowHTML(r) {
|
|
1304
1365
|
if (this.data[r] === TssGrid.PENDING) return this._pendingRowHTML(r); // 未取得行=読込中プレースホルダ(値は読まない)
|
|
@@ -1346,7 +1407,7 @@
|
|
|
1346
1407
|
const csAttr = (cs > 1 ? ' colspan="' + cs + '"' : '') + (rs > 1 ? ' rowspan="' + rs + '"' : '');
|
|
1347
1408
|
html += '<td' + a + ico + dccls + sty + titleAttr + csAttr + ' data-r="' + r + '" data-c="' + c + '">' + this._cellHTML(r, c) + '</td>';
|
|
1348
1409
|
}
|
|
1349
|
-
return html + '</tr>';
|
|
1410
|
+
return html + this._delCellHTML(r) + '</tr>';
|
|
1350
1411
|
}
|
|
1351
1412
|
// 仮想スクロール: 現在の scrollTop から描画すべき窓 [start,end)(絶対行)を算出。バッファ込み。
|
|
1352
1413
|
_vWindow() {
|
|
@@ -1365,7 +1426,7 @@
|
|
|
1365
1426
|
if (!force && this._vRendered && win.start === this._vStart && win.end === this._vEnd) return false;
|
|
1366
1427
|
const tbody = this.table && this.table.querySelector('tbody');
|
|
1367
1428
|
if (!tbody) return false;
|
|
1368
|
-
const rh = this.rowHeaders, colsSpan = this.COLS + (rh ? 1 : 0);
|
|
1429
|
+
const rh = this.rowHeaders, colsSpan = this.COLS + (rh ? 1 : 0) + (this._hasDelCol() ? 1 : 0);
|
|
1369
1430
|
const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
|
|
1370
1431
|
let html = '';
|
|
1371
1432
|
if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
@@ -1412,6 +1473,7 @@
|
|
|
1412
1473
|
const rh = this.rowHeaders;
|
|
1413
1474
|
let html = '<colgroup>' + (rh ? '<col style="width:' + this.rowHeaderW + 'px">' : '');
|
|
1414
1475
|
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">';
|
|
1476
|
+
if (this._hasDelCol()) html += '<col class="tg-rowdel-col" style="width:' + this.rowDeleteW + 'px">'; // 右端削除列(固定幅・システム列)
|
|
1415
1477
|
html += '</colgroup>';
|
|
1416
1478
|
if (this.colHeaders) {
|
|
1417
1479
|
html += '<thead>';
|
|
@@ -1430,18 +1492,19 @@
|
|
|
1430
1492
|
html += '<th class="tg-grouphdr' + froz + '" colspan="' + span + '" data-c0="' + c + '" data-c1="' + c1 + '">' + TssGrid.esc(label) + '</th>';
|
|
1431
1493
|
c += span;
|
|
1432
1494
|
}
|
|
1495
|
+
if (this._hasDelCol() && ri === 0) html += '<th class="tg-rowdel-hdr corner" rowspan="' + nRows + '"></th>'; // 右端削除列の角(全ヘッダ段を rowspan で覆う)
|
|
1433
1496
|
html += '</tr>';
|
|
1434
1497
|
});
|
|
1435
|
-
html += this._leafHeaderTr(false, leaf); //
|
|
1498
|
+
html += this._leafHeaderTr(false, leaf, false); // リーフ段(左角も右削除角も上段で rowspan 済み)
|
|
1436
1499
|
} else {
|
|
1437
|
-
html += this._leafHeaderTr(rh, null);
|
|
1500
|
+
html += this._leafHeaderTr(rh, null, this._hasDelCol());
|
|
1438
1501
|
}
|
|
1439
1502
|
html += '</thead>';
|
|
1440
1503
|
}
|
|
1441
1504
|
html += '<tbody>';
|
|
1442
1505
|
if (this.virtual) {
|
|
1443
1506
|
const win = this._vWindow(); this._vStart = win.start; this._vEnd = win.end; this._vRendered = true;
|
|
1444
|
-
const colsSpan = this.COLS + (rh ? 1 : 0);
|
|
1507
|
+
const colsSpan = this.COLS + (rh ? 1 : 0) + (this._hasDelCol() ? 1 : 0);
|
|
1445
1508
|
const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
|
|
1446
1509
|
if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
1447
1510
|
for (let r = win.start; r < win.end; r++) html += this._rowHTML(r);
|
|
@@ -1517,7 +1580,7 @@
|
|
|
1517
1580
|
if (this.stretchH === 'none' || !this.stretchH) return;
|
|
1518
1581
|
const avail = this.wrap ? this.wrap.clientWidth : 0;
|
|
1519
1582
|
if (!avail) return;
|
|
1520
|
-
const base = this._rhW(), widths = [];
|
|
1583
|
+
const base = this._rhW() + this._delW(), widths = []; // 削除列は固定幅=ストレッチ対象外(余白計算から差し引く)
|
|
1521
1584
|
let sum = 0;
|
|
1522
1585
|
for (let c = 0; c < this.COLS; c++) { const wd = this._colWidth(c); widths.push(wd); sum += wd; }
|
|
1523
1586
|
const extra = avail - base - sum;
|
|
@@ -2150,14 +2213,16 @@
|
|
|
2150
2213
|
}
|
|
2151
2214
|
this.renderDump();
|
|
2152
2215
|
const grid = this;
|
|
2216
|
+
// minSpareRows: この編集で末尾スペア行が自動追加された分を、履歴コマンドに畳み込む
|
|
2217
|
+
// (undo で編集値と一緒に追加行も外す/redo で戻す)=「自動行追加が undo で消えない」不具合の解消。
|
|
2218
|
+
const spareAdded = this._ensureSpare();
|
|
2153
2219
|
this.history.push({
|
|
2154
2220
|
label: this.name,
|
|
2155
|
-
apply() { grid._applyCells(changes, 'neu', 'redo'); grid.focusAndSelect(selAfter); },
|
|
2156
|
-
revert() { grid._applyCells(changes, 'old', 'undo'); grid.focusAndSelect(selBefore); },
|
|
2221
|
+
apply() { grid._applyCells(changes, 'neu', 'redo'); grid._addSpareRows(spareAdded); grid.focusAndSelect(selAfter); },
|
|
2222
|
+
revert() { grid._applyCells(changes, 'old', 'undo'); grid._removeSpareRows(spareAdded); grid.focusAndSelect(selBefore); },
|
|
2157
2223
|
});
|
|
2158
2224
|
if (this.onAfterChange) { try { this.onAfterChange(this._pubChanges(changes, source), source); } catch (_) {} }
|
|
2159
2225
|
this._syncHeaderCheckboxes(); // ヘッダ全選択チェックの見た目を同期
|
|
2160
|
-
this._ensureSpare(); // 末尾の空行確保(minSpareRows)
|
|
2161
2226
|
}
|
|
2162
2227
|
commit() {
|
|
2163
2228
|
const wasEditing = this.mode === 'edit';
|
|
@@ -2195,14 +2260,28 @@
|
|
|
2195
2260
|
}
|
|
2196
2261
|
}
|
|
2197
2262
|
// 末尾に常に minSpareRows 行の空行を確保(足りなければ追加。履歴には積まない)。
|
|
2263
|
+
// 追加した空行数を返す(pushCmd が履歴コマンドに畳み込んで undo/redo で戻す/足すため)。
|
|
2198
2264
|
_ensureSpare() {
|
|
2199
|
-
if (this._renderPaused) { this._renderDirty = true; return; } // バッチ描画中は保留→resume でまとめて確保
|
|
2200
|
-
if (!this.minSpareRows || this._allRows || !this.dataComplete) return; // フィルタ中/遅延ロード中(PENDING tail)は末尾の自動空行を足さない
|
|
2265
|
+
if (this._renderPaused) { this._renderDirty = true; return 0; } // バッチ描画中は保留→resume でまとめて確保
|
|
2266
|
+
if (!this.minSpareRows || this._allRows || !this.dataComplete) return 0; // フィルタ中/遅延ロード中(PENDING tail)は末尾の自動空行を足さない
|
|
2201
2267
|
let empties = 0;
|
|
2202
2268
|
for (let r = this.ROWS - 1; r >= 0; r--) { if (this._rowEmpty(r)) empties++; else break; }
|
|
2203
2269
|
let added = 0;
|
|
2204
2270
|
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
2271
|
if (added && this.table) { this.buildTable(); this.setActive(this.active.r, this.active.c); }
|
|
2272
|
+
return added;
|
|
2273
|
+
}
|
|
2274
|
+
// pushCmd の undo/redo 用: minSpareRows で自動追加された末尾空行 n 行を、編集の取消/やり直しと一緒に外す/戻す。
|
|
2275
|
+
_addSpareRows(n) {
|
|
2276
|
+
if (!n) return;
|
|
2277
|
+
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({}); }
|
|
2278
|
+
this.ROWS += n; this.buildTable();
|
|
2279
|
+
}
|
|
2280
|
+
_removeSpareRows(n) {
|
|
2281
|
+
if (!n) return;
|
|
2282
|
+
n = Math.min(n, this.ROWS - 1); // 最低1行は残す(防御)
|
|
2283
|
+
this.data.length -= n; this.rowH.length -= n; if (this._src) this._src.length -= n;
|
|
2284
|
+
this.ROWS -= n; this.buildTable();
|
|
2206
2285
|
}
|
|
2207
2286
|
// 構造変更を実行+履歴に積む共通処理。doIt/undoIt は冪等(redo で再実行される)。
|
|
2208
2287
|
_structCmd(doIt, undoIt, selAfterFn, selBefore, info) {
|
|
@@ -2289,6 +2368,15 @@
|
|
|
2289
2368
|
};
|
|
2290
2369
|
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
2370
|
}
|
|
2371
|
+
// 右端🗑の「最後の1行」用: 0行にはできない(削除ガード)ので、行は残して中身だけ空にする
|
|
2372
|
+
// (「消してから空行を足す」のと正味同じ結果を、0行に一瞬も落とさず 1 undo で)。readOnly セルはスキップ。
|
|
2373
|
+
clearRow(r) {
|
|
2374
|
+
if (r < 0 || r >= this.ROWS) return;
|
|
2375
|
+
if (this.mode === 'edit') this._commitActive();
|
|
2376
|
+
const before = this.snapSel(), changes = [];
|
|
2377
|
+
for (let c = 0; c < this.COLS; c++) { if (this._isReadOnly(r, c)) continue; this.setCell(r, c, '', changes); }
|
|
2378
|
+
this.pushCmd(changes, before, this.snapSel(), 'delete'); // changes 空(既に空/全 readOnly)なら pushCmd 側で no-op
|
|
2379
|
+
}
|
|
2292
2380
|
insertCol(ci, where = 'left') {
|
|
2293
2381
|
if (!this.allowInsertCols) return;
|
|
2294
2382
|
if (this._allRows) return; // v1: フィルタ中は構造変更ロック
|
|
@@ -2663,7 +2751,9 @@
|
|
|
2663
2751
|
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
2752
|
after = { active: { r: sel.r0, c: sel.c0 }, extent: { r: sel.r1, c: sel.c1 } };
|
|
2665
2753
|
} else {
|
|
2666
|
-
|
|
2754
|
+
// アクティブ列が隠し列だと(例: selectRow は active.c=0 を置く=先頭列が hiddenColumns の時)以下の indexOf が -1 になり、
|
|
2755
|
+
// 貼り付けが1列ずれて先頭値が落ちる。可視列へスナップしてから起点を取る(setActive と同じ「隠し列に置かない」規則)。
|
|
2756
|
+
const r0 = this.active.r, c0 = this._snapVisCol(this.active.c);
|
|
2667
2757
|
let maxw = 0; for (let i = 0; i < g.length; i++) maxw = Math.max(maxw, g[i].length);
|
|
2668
2758
|
const p0 = visAll.indexOf(c0), availCols = visAll.length - p0; // 貼り付けは可視列へ(隠し列をスキップ)
|
|
2669
2759
|
// オートグロー時に伸ばせる上限行数(maxRows があればそこまで/無ければ無制限)。非オートグローは現状の ROWS。
|
|
@@ -2835,6 +2925,7 @@
|
|
|
2835
2925
|
this.table.addEventListener('mousedown', (e) => {
|
|
2836
2926
|
if (e.button !== 0) return; // 右/中クリックは選択ドラッグを開始しない(右は contextmenu で処理)
|
|
2837
2927
|
const cls = e.target.classList;
|
|
2928
|
+
if (e.target.closest('.tg-rowdel-cell')) { e.preventDefault(); return; } // 削除列: 選択ドラッグを始めない(実削除は click で)。preventDefault で🗑のテキスト選択も抑止
|
|
2838
2929
|
if (cls && cls.contains('tg-colgrip')) { this._startColResize(e); return; } // 列幅リサイズ
|
|
2839
2930
|
if (cls && cls.contains('tg-rowgrip')) { this._startRowResize(e); return; } // 行高リサイズ
|
|
2840
2931
|
if (cls && cls.contains('tg-rowmove') && !this.rowReorderWhole) { this._startRowMove(e); return; } // ⠿つまみドラッグ移動('header'モードはセル全体扱い=_onHeaderMouseDown 経由で選択も伴う)
|
|
@@ -2858,6 +2949,13 @@
|
|
|
2858
2949
|
// ヘッダ全選択チェックは mousedown で処理済み。click のネイティブ・トグルを止めて二重反転を防ぐ
|
|
2859
2950
|
this.table.addEventListener('click', (e) => {
|
|
2860
2951
|
if (e.target.classList && e.target.classList.contains('tg-head-cb')) e.preventDefault();
|
|
2952
|
+
// 右端削除列の🗑クリック。通常は当該行を削除(remove_row と同一経路・1 undo で復活)。
|
|
2953
|
+
// 最後の1行(0行にはできない)は削除でなく「中身クリア(行は残す)」。minRows>1 のロック時は no-op(🗑 も無効表示)。
|
|
2954
|
+
const del = e.target.closest('.tg-rowdel-cell');
|
|
2955
|
+
if (del) {
|
|
2956
|
+
const tr = del.closest('tr[data-r]');
|
|
2957
|
+
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); }
|
|
2958
|
+
}
|
|
2861
2959
|
});
|
|
2862
2960
|
this.table.addEventListener('dblclick', (e) => {
|
|
2863
2961
|
// 列境界グリップのダブルクリック=オートフィット(Excel風)
|
|
@@ -3040,7 +3138,7 @@
|
|
|
3040
3138
|
_openSelect() {
|
|
3041
3139
|
const r = this.active.r, c = this.active.c, cfg = this.colCfg(c);
|
|
3042
3140
|
if (this._isReadOnly(r, c)) return; // 読み取り専用は <select> を開かない
|
|
3043
|
-
const opts = this._optList(c), values = opts.map(o => o.value), cur = this.data[r][c];
|
|
3141
|
+
const opts = this._optList(c, r), values = opts.map(o => o.value), cur = this.data[r][c];
|
|
3044
3142
|
const allowEmpty = cfg.allowEmpty !== false; // 既定: クリア用の空オプションを先頭に付ける
|
|
3045
3143
|
let html = allowEmpty ? '<option value=""></option>' : '';
|
|
3046
3144
|
for (const o of opts) html += '<option value="' + TssGrid.esc(o.value) + '">' + TssGrid.esc(o.label) + '</option>'; // value↔label 分離
|