@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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
TssGrid の各リリースの変更点。日付は JST。形式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) 準拠(ゆるめ)。
|
|
4
4
|
数値は特記なき限り**自社調べ**(headless Chrome / Windows 11・環境差あり)。
|
|
5
5
|
|
|
6
|
+
## [0.1.6] - 2026-07-30
|
|
7
|
+
|
|
8
|
+
### 追加
|
|
9
|
+
- **依存ドロップダウン=`columns[c].options` に関数 `(r, row) => [...]`**(コア)— options を関数にすると**行ごとに違う候補**になり、「A 列で選んだ値に応じて B 列のリストが変わる」依存ドロップダウン(都道府県→市区町村 等)が作れる。`row` はその行の値配列(`grid.data[r]`)。開くたびに options を読み直すので、**列共有の静的配列では両立できない「行ごとに別リスト」が同時に成立**(`_optList(c,r)`/`_optLabel(r,c,v)` に r を通し、表示ラベル・保存値の検証もその行の候補で解決)。静的配列は従来どおり(後方互換)。動く例 `examples/dependent-dropdown.html`。
|
|
10
|
+
- **`html:true` 列は `format` を空セルでも実行**(コア)— 従来は空セルだと `_displayValue` が値を素通しして `format` が走らず、**ボタン/アイコン等の"飾り"を空セルに描けなかった**。`html:true` かつ `format` が関数のときは空でも走らせ、**全行にボタン列などを描ける**ように(素テキスト列・非空 html セルは不変)。セル内ボタンは専用 API を持たず `html:true`+`format`+`grid.table` への click 委譲で作る(動く例 `examples/cell-button.html`)。
|
|
11
|
+
|
|
12
|
+
## [0.1.5] - 2026-07-25
|
|
13
|
+
|
|
14
|
+
### 追加
|
|
15
|
+
- **`rowDelete`(右端の🗑削除列・opt-in・既定 `false`)**(コア)— 右端に非データの「削除列」を1列出し、各行の 🗑 を1クリックで削除(右クリック「行を削除」と同一経路=1 undo で復活)。列は `getData`/`getRows`/`toCSV`・列インデックス・列選択/リサイズ/並べ替えの**対象外**でデータ列と混ざらない。**最後の1行(0行にはできない)は削除でなく「中身クリア(行は残す)」**=🗑 は無効化せず内容だけ空に(`readOnly` はスキップ・1 undo・新メソッド `clearRow(r)`)。`allowDeleteRows:false` で列ごと非表示、`minRows≥2` の下限では無効表示。仮想スクロール/ネストヘッダ/固定行に対応。幅は `rowDeleteWidth`(既定 36px)。
|
|
16
|
+
- **フィルタ UI に「部分一致 / 前方一致」モード**(プラグイン `tss-filter`)— ポップアップ上部のモード切替で「値を選択(Excel風チェックリスト・完全一致)/部分一致/前方一致」を列ごとに選べる。テキスト一致は既定で大小無視(`caseSensitive` で切替)。コアの `filter()` は元から関数述語対応=プラグインの UI 追加のみ(コア無改変)。
|
|
17
|
+
- **`suppressAutofill`(オプション・既定 `false`)**(コア)— 共有エディタへのブラウザ自動入力(Chrome の「ポイントカード/住所/支払い」等)を強めに抑止。`true` で `autocomplete='new-password'` に切替(Chrome は loyalty/payment 系で `'off'` を無視するため)。周囲に自前のラベル付き `<input>`(フォーム塊)を並べる埋め込み画面で有効。既定でも一意 `name`+各PWマネージャの無視属性を付与。
|
|
18
|
+
|
|
19
|
+
### 修正
|
|
20
|
+
- **`minSpareRows` の自動追加行が undo/redo で戻らない不具合**(コア)— 末尾スペア行の自動確保(`_ensureSpare`)が履歴の外で行われていたため、最後のスペア行に入力→新スペアが増える→その編集を undo しても追加行が残っていた。`pushCmd`(編集/貼付/フィル/クリア共通)で追加行数を履歴コマンドに畳み込み、値の取消/やり直しと一緒に外す/戻すよう修正。
|
|
21
|
+
- **先頭列が `hiddenColumns` の時、行選択→貼り付けが1列ずれて先頭値が落ちる不具合**(コア)— `selectRow` がアクティブを隠し列(`c=0`)に置くと貼付起点の `indexOf` が -1 になりずれていた。貼付起点を可視列へスナップして修正(`hiddenColumns` を使う全グリッドに効く)。
|
|
22
|
+
- **`rowDelete` 列の罫線**(コア/CSS)— 枠がグリッドより広い時に最終列の右罫線が消えて“開いて”見える/隣セルと重なり左罫線が 2px に太る、を修正(右端を閉じ、左は隣セルの右罫線に一任)。
|
|
23
|
+
|
|
24
|
+
### その他
|
|
25
|
+
- **改行コードを `.gitattributes`(`* text=auto eol=lf`)で LF に固定**(Windows の autocrlf 変換を上書き)。
|
|
26
|
+
|
|
6
27
|
## [0.1.4] - 2026-07-21
|
|
7
28
|
|
|
8
29
|
### 追加
|
package/README.md
CHANGED
|
@@ -241,6 +241,64 @@ grid.moveRows(r0, r1, to); // 連続する複数行をま
|
|
|
241
241
|
- 行移動は**Undo/Redo 対応**(`Ctrl+Z`/`Ctrl+Y`。意図的な編集なので戻せる。※`sortBy` の並べ替えは履歴非対象でこちらと別)。
|
|
242
242
|
- `minSpareRows` と併用すると追加用の空行が末尾以外に動くことがあるので、入力途中の表では `rowReorder` を切るか並べ替え確定後に使うのが無難。
|
|
243
243
|
|
|
244
|
+
### 行削除アイコン(rowDelete)
|
|
245
|
+
|
|
246
|
+
`rowDelete: true` で**右端に専用の「削除列」**が出て、各行の **🗑 を1クリックで当該行を削除**できます(右クリック→「行を削除」と同一経路)。削除は**Undo/Redo 対応**(`Ctrl+Z` で復活)なので確認ダイアログは出しません。非技術者にも分かる削除アフォーダンスとして、右クリックに気づかなくても消せます(→ 動く例: [`examples/row-delete.html`](https://tssgrid.threestarsoftware.co.jp/examples/row-delete.html))。
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
new TssGrid(el, {
|
|
250
|
+
rowDelete: true, // 右端に🗑削除列(既定 false)
|
|
251
|
+
rowDeleteWidth: 36, // 削除列の幅(px)・任意(既定 36)
|
|
252
|
+
rowReorder: 'header', // 併用可: 左=⠿で移動 / 右=🗑で削除(役割が左右で分かれる)
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
- 削除列は**非データのシステム列**です。`getData`/`getRows`/`toCSV`・列インデックス・列選択・リサイズ・並べ替えの**対象外**で、データ列とは混ざりません。データ列の右隣に並ぶ末尾列で、横幅がはみ出す表では横スクロールで表示されます(表がはみ出す時に🗑を貼り付けると、選択枠やデータ列と重なるため貼り付けません)。
|
|
256
|
+
- **最後の1行**(0行にはできない)で🗑を押すと、**削除ではなく「中身クリア(行は残す)」**になります=🗑は無効にならず、行の内容だけ空に(`readOnly` セルは保持・1 undo で復活。ツールチップも「内容をクリア」に変わります)。「消してから空行を足す」のと同じ結果を、0行に一瞬も落とさず1手で。
|
|
257
|
+
- **`allowDeleteRows: false` なら削除列そのものを出しません**。`minRows` を **2 以上**に設定した場合は下限で🗑を**淡色の無効表示**にします(そのロック行は削除もクリアもしません)。
|
|
258
|
+
- 込み入った削除(一括・条件付きなど)は、削除列に頼らず**チェックボックス列+「選択行を削除」ボタン**で組むのが素直です(🗑は「1行1クリック」の単機能に留めています)。
|
|
259
|
+
|
|
260
|
+
### ブラウザ自動入力の抑止(`suppressAutofill`)
|
|
261
|
+
|
|
262
|
+
グリッドは編集用に本物の `<input>` を重ねる方式なので、稀に **Chrome の自動入力(「ポイントカードを管理…」等)** がセル編集時に被さることがあります。既定で `name` + `autocomplete="off"` +各パスワードマネージャの無視属性を付けて抑止していますが、**Chrome はポイントカード/住所/支払い系で `autocomplete="off"` を無視する**ことがあり、特に**周囲に自前のラベル付き `<input>`(フォーム塊)を並べた画面**(モーダルのヘッダ入力欄など)だと Chrome がそれをフォームと認識して誘発します。
|
|
263
|
+
|
|
264
|
+
その場合は `suppressAutofill: true` を渡すと、共有エディタの `autocomplete` を **`new-password`** に切り替えて確実に止めます(Chrome が `off` を無視する系にも効く唯一の実用手)。
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
new TssGrid(el, { suppressAutofill: true }); // 既定 false
|
|
268
|
+
```
|
|
269
|
+
- 副作用として環境により**鍵アイコン**や**生成パスワードの提案**が出ることがあるため **opt-in**(既定は付けません)。値はマスクされません(`type` は `text` のまま)。
|
|
270
|
+
- **あわせて、あなたのページ側の素の `<input>`(テーブル名欄など)にも同じ `autocomplete="new-password"` を付けてください**。誘発の主因はグリッドではなく周囲のフォーム塊のことが多いためです。
|
|
271
|
+
|
|
272
|
+
### 依存ドロップダウン(`options` を関数に=行ごとにリスト差替え)
|
|
273
|
+
|
|
274
|
+
`columns[c].options` は配列(列共通)のほか、**関数 `(r, row) => [...]`** を渡せます。すると**行ごとに違う候補**になり、「A 列で選んだ値に応じて B 列のリストが変わる」**依存ドロップダウン**(都道府県→市区町村 等)が作れます。`row` はその行の値配列(`grid.data[r]`)。開くたびに `options` を読み直すので、**列共有の静的配列では両立できない「行ごとに別リスト」が同時に成立**します(→ 動く例: [`examples/dependent-dropdown.html`](https://tssgrid.threestarsoftware.co.jp/examples/dependent-dropdown.html))。
|
|
275
|
+
|
|
276
|
+
```js
|
|
277
|
+
const CITY = { 東京都:['渋谷区','新宿区'], 大阪府:['大阪市','堺市'] };
|
|
278
|
+
columns: [
|
|
279
|
+
{ type:'dropdown', options: Object.keys(CITY) }, // 親(静的)
|
|
280
|
+
{ type:'dropdown', options: (r, row) => CITY[row[0]] || [] }, // 子(行依存)
|
|
281
|
+
]
|
|
282
|
+
// 親が変わったら子をクリア(古い値が残らないように)
|
|
283
|
+
onAfterChange: (chs) => chs.forEach(ch => { if (ch.c === 0 && ch.oldValue !== ch.newValue) grid.setValue(ch.r, 1, ''); });
|
|
284
|
+
```
|
|
285
|
+
- 行依存でも**表示ラベル・検証(保存値が候補にあるか)はその行の候補で解決**します。静的配列は従来どおり(後方互換)。
|
|
286
|
+
|
|
287
|
+
### セル内ボタン(`html:true` + クリック委譲)
|
|
288
|
+
|
|
289
|
+
専用のボタン列 API はありません。**`columns[c].html:true` + `format` が返す HTML** でセルにボタンを描き、**`grid.table` に click 委譲**して `data-*` で拾うのが定石です(→ 動く例: [`examples/cell-button.html`](https://tssgrid.threestarsoftware.co.jp/examples/cell-button.html))。
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
columns: [ /* … */,
|
|
293
|
+
{ html:true, readOnly:true, format: () => '<button class="mybtn" data-act="del">削除</button>' } ]
|
|
294
|
+
grid.table.addEventListener('click', e => {
|
|
295
|
+
const b = e.target.closest('.mybtn'); if (!b) return;
|
|
296
|
+
const r = +b.closest('tr[data-r]').dataset.r;
|
|
297
|
+
if (b.dataset.act === 'del') grid.deleteRows(r, r);
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
- ボタン列は `readOnly:true` にして編集モードに入らないように。行番号は `closest('tr[data-r]')` から。**`html:true` はエスケープを外す**ので、ラベルにユーザー入力を入れる時は自分でエスケープを(XSS 責任は HTML 提供側)。空セルでも `format` は走ります(飾り列を全行に描ける)。
|
|
301
|
+
|
|
244
302
|
### 非破壊フィルタ(`filter` / `clearFilter`)
|
|
245
303
|
|
|
246
304
|
**コア内蔵**の非破壊フィルタ(行インデックスマッパ)。全行は内部の `_allRows`(マスタ)に保持したまま、**絞り込んだ可視部分集合だけを描画**します。要点は**絞り込み後の描画・ライブ検索が軽い**こと(自社調べ:10万行を保持して約200行に絞り込み → 初回描画 ≈ 0.5 秒、絞り込み済みのライブ検索 ≈ 数 ms)→ 動く例: [`examples/filter-live.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-live.html)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threestarsoftware/tssgrid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
|
package/plugins/tss-filter.css
CHANGED
|
@@ -12,6 +12,15 @@
|
|
|
12
12
|
border: 1px solid #c4ccd6; border-radius: 8px; box-shadow: 0 6px 22px rgba(20,40,80,.22);
|
|
13
13
|
font: 12.5px "Meiryo UI", "Segoe UI", sans-serif; padding: 8px; box-sizing: border-box;
|
|
14
14
|
}
|
|
15
|
+
/* モード切替(値を選択/部分一致/前方一致)。セグメント風に3分割。 */
|
|
16
|
+
.tg-filter-pop .tg-filter-mode { display: flex; gap: 2px; margin-bottom: 7px; border: 1px solid #dbe1ea; border-radius: 6px; padding: 2px; background: #f4f6fa; }
|
|
17
|
+
.tg-filter-pop .tg-filter-mode a {
|
|
18
|
+
flex: 1; text-align: center; padding: 4px 0; border-radius: 4px; cursor: pointer; color: #5b6470;
|
|
19
|
+
font-size: 11.5px; white-space: nowrap; user-select: none;
|
|
20
|
+
}
|
|
21
|
+
.tg-filter-pop .tg-filter-mode a:hover { background: #e7edf6; }
|
|
22
|
+
.tg-filter-pop .tg-filter-mode a.on { background: var(--tg-accent, #2f6fed); color: #fff; }
|
|
23
|
+
.tg-filter-pop .tg-filter-text input,
|
|
15
24
|
.tg-filter-pop .tg-filter-search input {
|
|
16
25
|
width: 100%; box-sizing: border-box; padding: 4px 7px; border: 1px solid #cbd2dc; border-radius: 5px; font: inherit;
|
|
17
26
|
}
|
package/plugins/tss-filter.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/*! TssFilter — ヘッダのオートフィルタUI(Excel風)。コアの非破壊 filter() を呼ぶだけ。
|
|
2
2
|
*
|
|
3
|
-
* 各リーフ見出しにロート(漏斗)
|
|
3
|
+
* 各リーフ見出しにロート(漏斗)アイコンを出し(ソートの▲▼と区別)、クリックでポップアップ。
|
|
4
|
+
* モード切替で「値を選択(Excel風チェックリスト・完全一致)/部分一致/前方一致」を選べる(列ごと独立)。
|
|
4
5
|
* 複数列の条件は AND。適用は grid.filter(pred)/全解除で grid.clearFilter()。フィルタ済み列は印が付く。
|
|
6
|
+
* ※部分/前方一致は caseSensitive:false(既定)で大小無視。「値を選択」は文字列の完全一致(大小区別)。
|
|
5
7
|
*
|
|
6
8
|
* 使い方:
|
|
7
9
|
* const grid = new TssGrid(el, { plugins: [ TssFilter.plugin() ] }); // 全列にフィルタUI
|
|
@@ -39,10 +41,18 @@
|
|
|
39
41
|
return out;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
// active[c] は「Set(値を選択=完全一致メンバーシップ)」か「{mode:'contains'|'prefix', q}(テキスト一致)」のどちらか。
|
|
42
45
|
function buildPredicate() {
|
|
43
46
|
const cols = Object.keys(active).map(Number).filter(c => active[c]);
|
|
44
47
|
if (!cols.length) return null;
|
|
45
|
-
|
|
48
|
+
const cs = !!opts.caseSensitive;
|
|
49
|
+
const norm = s => cs ? String(s) : String(s).toLowerCase();
|
|
50
|
+
return row => cols.every(c => {
|
|
51
|
+
const a = active[c], v = String(row[c]);
|
|
52
|
+
if (a instanceof Set) return a.has(v); // 値を選択(大小区別の完全一致)
|
|
53
|
+
const hay = norm(v), q = norm(a.q); // テキスト一致(既定 大小無視・caseSensitive で区別)
|
|
54
|
+
return a.mode === 'prefix' ? hay.startsWith(q) : hay.indexOf(q) >= 0;
|
|
55
|
+
});
|
|
46
56
|
}
|
|
47
57
|
function apply() {
|
|
48
58
|
const p = buildPredicate();
|
|
@@ -72,10 +82,14 @@
|
|
|
72
82
|
closePop();
|
|
73
83
|
popCol = c;
|
|
74
84
|
const vals = distinctValues(c);
|
|
75
|
-
const
|
|
85
|
+
const cur = active[c];
|
|
86
|
+
const curMode = (cur && !(cur instanceof Set)) ? cur.mode : 'set'; // 現在の条件からモードを復元
|
|
87
|
+
const allowed = (cur instanceof Set) ? cur : new Set(vals); // 「値を選択」モードのチェック初期状態(未設定=全許可)
|
|
76
88
|
pop = document.createElement('div');
|
|
77
89
|
pop.className = 'tg-filter-pop';
|
|
78
90
|
pop.innerHTML =
|
|
91
|
+
'<div class="tg-filter-mode"><a data-mode="set">値を選択</a><a data-mode="contains">部分一致</a><a data-mode="prefix">前方一致</a></div>' +
|
|
92
|
+
'<div class="tg-filter-text"><input type="text" placeholder="文字を入力…"></div>' +
|
|
79
93
|
'<div class="tg-filter-search"><input type="text" placeholder="値を検索…"></div>' +
|
|
80
94
|
'<div class="tg-filter-tools"><a data-act="all">すべて</a> / <a data-act="none">解除</a></div>' +
|
|
81
95
|
'<div class="tg-filter-list">' +
|
|
@@ -93,19 +107,44 @@
|
|
|
93
107
|
|
|
94
108
|
const search = pop.querySelector('.tg-filter-search input');
|
|
95
109
|
const list = pop.querySelector('.tg-filter-list');
|
|
96
|
-
|
|
110
|
+
const textBox = pop.querySelector('.tg-filter-text');
|
|
111
|
+
const textInput = textBox.querySelector('input');
|
|
112
|
+
|
|
113
|
+
// モード切替: 'set'=値チェックリスト/'contains'|'prefix'=テキスト入力。表示を出し分ける。
|
|
114
|
+
let mode = curMode;
|
|
115
|
+
function showMode(m) {
|
|
116
|
+
mode = m;
|
|
117
|
+
pop.querySelectorAll('.tg-filter-mode a').forEach(a => a.classList.toggle('on', a.dataset.mode === m));
|
|
118
|
+
const isSet = m === 'set';
|
|
119
|
+
textBox.style.display = isSet ? 'none' : '';
|
|
120
|
+
pop.querySelector('.tg-filter-search').style.display = isSet ? '' : 'none';
|
|
121
|
+
pop.querySelector('.tg-filter-tools').style.display = isSet ? '' : 'none';
|
|
122
|
+
list.style.display = isSet ? '' : 'none';
|
|
123
|
+
(isSet ? search : textInput).focus();
|
|
124
|
+
}
|
|
125
|
+
pop.querySelectorAll('.tg-filter-mode a').forEach(a => a.onclick = () => showMode(a.dataset.mode));
|
|
126
|
+
if (curMode !== 'set') textInput.value = cur.q;
|
|
127
|
+
showMode(curMode);
|
|
128
|
+
|
|
97
129
|
search.oninput = () => {
|
|
98
130
|
const q = search.value.trim().toLowerCase();
|
|
99
131
|
list.querySelectorAll('label').forEach(l => { l.style.display = (!q || l.textContent.toLowerCase().indexOf(q) >= 0) ? '' : 'none'; });
|
|
100
132
|
};
|
|
101
133
|
pop.querySelector('[data-act=all]').onclick = () => list.querySelectorAll('label:not([style*="none"]) input').forEach(cb => cb.checked = true);
|
|
102
134
|
pop.querySelector('[data-act=none]').onclick = () => list.querySelectorAll('label:not([style*="none"]) input').forEach(cb => cb.checked = false);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
135
|
+
function doApply() {
|
|
136
|
+
if (mode === 'set') {
|
|
137
|
+
const checked = [...list.querySelectorAll('input:checked')].map(cb => cb.value);
|
|
138
|
+
if (checked.length === vals.length) delete active[c]; // 全選択=この列は無条件
|
|
139
|
+
else active[c] = new Set(checked);
|
|
140
|
+
} else {
|
|
141
|
+
const q = textInput.value.trim();
|
|
142
|
+
if (!q) delete active[c]; else active[c] = { mode: mode, q: q }; // 空欄=無条件
|
|
143
|
+
}
|
|
107
144
|
closePop(); apply();
|
|
108
|
-
}
|
|
145
|
+
}
|
|
146
|
+
pop.querySelector('[data-act=apply]').onclick = doApply;
|
|
147
|
+
textInput.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); doApply(); } });
|
|
109
148
|
pop.querySelector('[data-act=clear]').onclick = () => { delete active[c]; closePop(); apply(); };
|
|
110
149
|
}
|
|
111
150
|
|
package/src/tssgrid.css
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
--tg-invalid-fg: #d61f2b; /* 検証NG(keep) */
|
|
28
28
|
--tg-invalid-bg: #ffe3e6;
|
|
29
29
|
--tg-muted: #b6bdc6; /* 無効メニュー項目など */
|
|
30
|
+
--tg-danger: var(--tg-invalid-fg); /* 破壊的操作の色(削除列🗑ホバー等) */
|
|
30
31
|
--tg-radius: 6px; /* メニュー/ダンプの角丸 */
|
|
31
32
|
--tg-box-color: var(--tg-accent); /* セル囲み .tg-box の色 */
|
|
32
33
|
--tg-box-width: 2px; /* セル囲み .tg-box の太さ */
|
|
@@ -139,6 +140,21 @@
|
|
|
139
140
|
/* rowReorder:'header' = 行ヘッダーセル全体が移動ハンドル(クリック=選択/ドラッグ=移動) */
|
|
140
141
|
.tssgrid .tg-table th.rowhead.tg-rowmove-cell { cursor: grab; }
|
|
141
142
|
.tssgrid .tg-table th.rowhead.tg-rowmove-cell:active { cursor: grabbing; }
|
|
143
|
+
/* 右端の専用削除列(rowDelete)。非データ・非選択のシステム列=データ列の右隣に普通に並ぶ末尾列。
|
|
144
|
+
横スクロールで貼り付けない(sticky にすると横溢れ時に🗑がデータ列へ重なり・選択枠と衝突するため。見出しの縦 sticky は
|
|
145
|
+
thead th 既存ルール、固定行の縦 sticky は tr.tg-frozen-row 既存ルールがそのまま担う)。 */
|
|
146
|
+
.tssgrid .tg-table td.tg-rowdel-cell, .tssgrid .tg-table th.tg-rowdel-hdr {
|
|
147
|
+
text-align: center; padding: 0; cursor: default; background: var(--tg-rowhead-bg); }
|
|
148
|
+
/* 左の区切りは隣セルの border-right に任せる(自前の border-left を足すと隣の右罫線と重なって 2px になる)。 */
|
|
149
|
+
.tssgrid .tg-table th.tg-rowdel-hdr { background: var(--tg-colheader-bg); } /* 見出しは無地(縦 sticky は thead th 既存ルール) */
|
|
150
|
+
/* 削除列は最終列でも右罫線を残す(tr>td:last-child の border-right:none を上書き)。
|
|
151
|
+
枠がグリッドより広い時にグレーのシステム列が右端で“開いて”見えるのを防ぐ=右端を線で閉じる。 */
|
|
152
|
+
.tssgrid .tg-table tr > td.tg-rowdel-cell, .tssgrid .tg-table tr > th.tg-rowdel-hdr { border-right: 1px solid var(--tg-border); }
|
|
153
|
+
.tssgrid .tg-rowdel { cursor: pointer; opacity: .55; font-size: 13px; line-height: 1; user-select: none;
|
|
154
|
+
transition: opacity .1s, color .1s; }
|
|
155
|
+
.tssgrid .tg-table tr:hover .tg-rowdel { opacity: .9; } /* 行ホバーで浮き出る */
|
|
156
|
+
.tssgrid .tg-rowdel:hover { opacity: 1; color: var(--tg-danger); }
|
|
157
|
+
.tssgrid .tg-rowdel.tg-disabled { opacity: .22; pointer-events: none; } /* minRows 到達=削除不可 */
|
|
142
158
|
.tssgrid .tg-colgrip:hover { background: var(--tg-edit-accent); opacity: .5; }
|
|
143
159
|
.tssgrid .tg-rowgrip:hover { background: var(--tg-edit-accent); opacity: .5; }
|
|
144
160
|
/* リサイズのプレビュー線(ドラッグ中だけ表示、離して確定) */
|