@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 CHANGED
@@ -3,6 +3,35 @@
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.5] - 2026-07-25
7
+
8
+ ### 追加
9
+ - **`rowDelete`(右端の🗑削除列・opt-in・既定 `false`)**(コア)— 右端に非データの「削除列」を1列出し、各行の 🗑 を1クリックで削除(右クリック「行を削除」と同一経路=1 undo で復活)。列は `getData`/`getRows`/`toCSV`・列インデックス・列選択/リサイズ/並べ替えの**対象外**でデータ列と混ざらない。**最後の1行(0行にはできない)は削除でなく「中身クリア(行は残す)」**=🗑 は無効化せず内容だけ空に(`readOnly` はスキップ・1 undo・新メソッド `clearRow(r)`)。`allowDeleteRows:false` で列ごと非表示、`minRows≥2` の下限では無効表示。仮想スクロール/ネストヘッダ/固定行に対応。幅は `rowDeleteWidth`(既定 36px)。
10
+ - **フィルタ UI に「部分一致 / 前方一致」モード**(プラグイン `tss-filter`)— ポップアップ上部のモード切替で「値を選択(Excel風チェックリスト・完全一致)/部分一致/前方一致」を列ごとに選べる。テキスト一致は既定で大小無視(`caseSensitive` で切替)。コアの `filter()` は元から関数述語対応=プラグインの UI 追加のみ(コア無改変)。
11
+ - **`suppressAutofill`(オプション・既定 `false`)**(コア)— 共有エディタへのブラウザ自動入力(Chrome の「ポイントカード/住所/支払い」等)を強めに抑止。`true` で `autocomplete='new-password'` に切替(Chrome は loyalty/payment 系で `'off'` を無視するため)。周囲に自前のラベル付き `<input>`(フォーム塊)を並べる埋め込み画面で有効。既定でも一意 `name`+各PWマネージャの無視属性を付与。
12
+
13
+ ### 修正
14
+ - **`minSpareRows` の自動追加行が undo/redo で戻らない不具合**(コア)— 末尾スペア行の自動確保(`_ensureSpare`)が履歴の外で行われていたため、最後のスペア行に入力→新スペアが増える→その編集を undo しても追加行が残っていた。`pushCmd`(編集/貼付/フィル/クリア共通)で追加行数を履歴コマンドに畳み込み、値の取消/やり直しと一緒に外す/戻すよう修正。
15
+ - **先頭列が `hiddenColumns` の時、行選択→貼り付けが1列ずれて先頭値が落ちる不具合**(コア)— `selectRow` がアクティブを隠し列(`c=0`)に置くと貼付起点の `indexOf` が -1 になりずれていた。貼付起点を可視列へスナップして修正(`hiddenColumns` を使う全グリッドに効く)。
16
+ - **`rowDelete` 列の罫線**(コア/CSS)— 枠がグリッドより広い時に最終列の右罫線が消えて“開いて”見える/隣セルと重なり左罫線が 2px に太る、を修正(右端を閉じ、左は隣セルの右罫線に一任)。
17
+
18
+ ### その他
19
+ - **改行コードを `.gitattributes`(`* text=auto eol=lf`)で LF に固定**(Windows の autocrlf 変換を上書き)。
20
+
21
+ ## [0.1.4] - 2026-07-21
22
+
23
+ ### 追加
24
+ - **`appendRows(rows, opts?)`=背景で"育てる"progressive 追記**(コア)— `data` 末尾へ足し、可視窓+スペーサだけ再描画する。`buildTable` 全再描画をせず、**スクロール位置・アクティブセル・選択・編集中エディタと値を保ち、履歴にも積まない**。「先頭ページを描く→残りを idle で `appendRows`」で first-paint を早める土台。既存の `setData`/`insertRows` 経路は不変=opt-in。
25
+ - **遅延ロード(on-demand)=`setRowCount(total)` / `fillRows(start, rows)` / `onViewportChange(start, end)` / `onSortFilterChange(state)` / `TssGrid.PENDING`**(コア・`virtual:true` 前提)— 総件数を確保して未取得ぶんは「読込中」行で持ち、スクロールで `onViewportChange` が必要な範囲を通知→アプリが取得→`fillRows` で流し込む。**sort/filter は遅延中ローカル計算せず `onSortFilterChange` でサーバへ委譲**、**未取得セルは編集不可**(カーソルは乗れる)、**export(`getData`/`getRows`/`toCSV`)はロード済みのみ**返す。=AG Grid の Infinite Row Model 相当を、最小コアの push プリミティブで実装(datasource/ブロックキャッシュはコアに持たない=将来アドオンに委ねる)。大量一覧の first-paint を大きく縮められる。
26
+ - **`insertRows(ri, count?, where?)`=一括行挿入プリミティブ**(コア)— `count` 行を1回の再描画・1 undo で挿入。1行ずつの `insertRow` を繰り返す O(N²) を O(N) に。`insertRow` は `insertRows(…, 1, …)` へ委譲=後方互換。
27
+ - **`pasteAutoGrow`(オプション・既定 `false`)**(コア)— 貼付元の行数がグリッド行数を超える時、不足分を先に `insertRows` で一括生成してから貼る(Excel から貼れば貼った行数だけ行が増える)。行生成+値貼付は1回の undo で戻る。`maxRows` 上限内・超過は `pasteOverflow` に従う。
28
+
29
+ ### 変更
30
+ - **貼り付けを paste イベント主経路化**(コア)— Ctrl+V を `navigator.clipboard.readText()`(初回に**許可プロンプト**が出る・Chrome 仕様)から、ネイティブ paste イベント(`clipboardData.getData`)へ移した。ジェスチャー内で同期に読める=**許可プロンプト不要・より堅牢**(権限/非セキュアコンテキスト/非フォーカスで落ちない)。右クリックメニューの「貼り付け」とプログラム呼び出しは従来どおり `readText`。セル編集中の1セル貼付は native 挙動に委ねる(回帰なし)。
31
+
32
+ ### 改善(性能)
33
+ - **大量貼付を O(N) 化**(コア)— 1000行規模の貼付で per-cell 描画 N 回を「最後に `buildTable` 1回」へ畳み、巨大選択の選択塗り(従来 O(行×列))をアウトライン矩形へ委譲。`insertRows`/`pasteAutoGrow` と併せ、大量貼付が O(N²)→O(N)(自社調べ・実機で大幅短縮)。
34
+
6
35
  ## [0.1.3] - 2026-07-17
7
36
 
8
37
  ### 追加
package/README.md CHANGED
@@ -241,6 +241,34 @@ 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
+
244
272
  ### 非破壊フィルタ(`filter` / `clearFilter`)
245
273
 
246
274
  **コア内蔵**の非破壊フィルタ(行インデックスマッパ)。全行は内部の `_allRows`(マスタ)に保持したまま、**絞り込んだ可視部分集合だけを描画**します。要点は**絞り込み後の描画・ライブ検索が軽い**こと(自社調べ:10万行を保持して約200行に絞り込み → 初回描画 ≈ 0.5 秒、絞り込み済みのライブ検索 ≈ 数 ms)→ 動く例: [`examples/filter-live.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-live.html)。
@@ -784,6 +812,7 @@ new TssGrid(el, { hiddenColumns: [1] }); // 初期で隠す
784
812
  | `invalidTitle` | `boolean` | `true` | `keep` 時、赤セルに validator メッセージを `title=`(ホバーで理由)。`false` でOFF。全件は `getInvalidCells()` |
785
813
  | `messages` | `{code:…}\|fn` | — | エラーの**共通メッセージカタログ**。`validator` が `{code,params,level}` を返すと本文を解決(`{label}`/`{value}`/params 差込)。本文を外部JSON/CSV/XMLで一元管理 |
786
814
  | `pasteOverflow` | `'clip'\|'error'` | `'clip'` | 貼り付けがグリッド外に超過した時。`clip`=はみ出し分を切り捨て / `error`=貼り付け中止 |
815
+ | `pasteAutoGrow` | `boolean` | `false` | 貼付元の行数がグリッド行数を超える時、不足分を **`insertRows` で先に一括生成**してから貼る(Excel から貼れば貼った行数だけ行が増える)。**行生成+値貼付は1回の undo で両方戻る**。`maxRows` 上限内・超過分は `pasteOverflow` に従う。既定 `false`=従来どおりはみ出しは切り捨て |
787
816
  | `colWidths` / `rowHeights` | `number[]` \| `{[key]:number}` | `[]` | 列幅 / 行高の初期値(px)。未指定の要素は既定値。`colWidths` は**配列(位置指定)でも、`{ dataキー: px }` のオブジェクト(列ID指定)でも可**=後者は列を入れ替えても幅が追従する。列定義側に `columns[c].width` で同居指定も可(並べ替え・コピペで幅が一緒に動く=最も壊れにくい)。優先度: 実行時リサイズ/明示 `colWidths` > `columns[c].width` > 既定 |
788
817
  | `defaultColWidth` / `defaultRowHeight` | `number` | `120` / `28` | 既定の列幅 / 行高(px) |
789
818
  | `minColWidth` / `minRowHeight` | `number` | `30` / `18` | リサイズの下限(px) |
@@ -850,7 +879,7 @@ new TssGrid(el, {
850
879
  });
851
880
  ```
852
881
 
853
- メソッド: `getData()` / `getRows()` / `getRow(r)` / `getColumn(cOrKey)` / `getColumns()` / `setData(rows)` / `getValue(r,cOrKey)` / `setValue(r,cOrKey,val,force?)` / `setValueRaw(r,cOrKey,val)`(履歴に積まない派生値書き込み・計算列用) / `hideColumn(c)` / `showColumn(c)` / `toggleColumn(c)` / `isColumnHidden(c)` / `getHiddenColumns()` / `setAlignment(align,range?)` / `getAlignment(r,c)` / `setCellReadOnly(flag,range?)` / `autoSizeColumn(c)` / `autoSizeAllColumns()` / `sortBy(cOrKey, dir)` / `sortRows(cmp)` / `addShortcut(s)` / `removeShortcut(name)` / `usePlugin(fn)` / `getPlugin(name)` / `destroy()` / `setActive(r,c)` / `selectRow(r)` / `selectCol(c)` / `selectAll()` / `setColWidth(c,px)` / `setRowHeight(r,px)` / `freezeCols(n)` / `freezeRows(n)` / `moveRow(from,to)` / `moveRows(r0,r1,to)` / `clearSort()` / `toCSV(opts)` / `downloadCSV(name,opts)` / `insertRow(ri,'above'|'below')` / `deleteRows(r0,r1?)` / `insertCol(ci,'left'|'right')` / `deleteCols(c0,c1?)` / `redraw()`(列定義変更後の再描画, データ・選択は保持)ほか。
882
+ メソッド: `getData()` / `getRows()` / `getRow(r)` / `getColumn(cOrKey)` / `getColumns()` / `setData(rows)` / `appendRows(rows,opts?)`(**背景で"育てる"追記**=data末尾に足し可視窓だけ再描画。buildTable全再描画せずスクロール位置・選択・編集中エディタを保ち履歴にも積まない。「先頭ページを描く→残りをidleでappendRows」でfirst-paintを早める土台。既存経路は不変のopt-in) / `setRowCount(total)` + `fillRows(start,rows)`(**遅延ロード=on-demand**=virtual前提。総件数を確保し未取得ぶんは「読込中」行、`onViewportChange(start,end)` で要る範囲を受け `fillRows` で実データを流し込む。sort/filterは遅延中ローカル計算せず `onSortFilterChange` でサーバ委譲、未取得セルは編集不可、export(getData/toCSV)はロード済みのみ=AG Grid の Infinite Row Model 相当を push で) / `getValue(r,cOrKey)` / `setValue(r,cOrKey,val,force?)` / `setValueRaw(r,cOrKey,val)`(履歴に積まない派生値書き込み・計算列用) / `hideColumn(c)` / `showColumn(c)` / `toggleColumn(c)` / `isColumnHidden(c)` / `getHiddenColumns()` / `setAlignment(align,range?)` / `getAlignment(r,c)` / `setCellReadOnly(flag,range?)` / `autoSizeColumn(c)` / `autoSizeAllColumns()` / `sortBy(cOrKey, dir)` / `sortRows(cmp)` / `addShortcut(s)` / `removeShortcut(name)` / `usePlugin(fn)` / `getPlugin(name)` / `destroy()` / `setActive(r,c)` / `selectRow(r)` / `selectCol(c)` / `selectAll()` / `setColWidth(c,px)` / `setRowHeight(r,px)` / `freezeCols(n)` / `freezeRows(n)` / `moveRow(from,to)` / `moveRows(r0,r1,to)` / `clearSort()` / `toCSV(opts)` / `downloadCSV(name,opts)` / `insertRow(ri,'above'|'below')` / `insertRows(ri,count?,'above'|'below')`(count 行を1回の再描画・1 undo で一括挿入=大量挿入が O(N)) / `deleteRows(r0,r1?)` / `insertCol(ci,'left'|'right')` / `deleteCols(c0,c1?)` / `redraw()`(列定義変更後の再描画, データ・選択は保持)ほか。
854
883
 
855
884
  ## イベント
856
885
 
@@ -890,8 +919,12 @@ new TssGrid(el, {
890
919
  onAfterCopy(data, { range, cut }) {},
891
920
 
892
921
  // 行/列の挿入・削除の前後。before は false で中止できる
893
- onBeforeStructureChange(info) { /* info.type = 'insertRow'|'deleteRows'|'insertCol'|'deleteCols'。return false で中止 */ },
922
+ onBeforeStructureChange(info) { /* info.type = 'insertRow'|'insertRows'|'deleteRows'|'insertCol'|'deleteCols'。return false で中止 */ },
894
923
  onStructureChange(info) {}, // 後。undo 時は info.undo = true
924
+
925
+ // 遅延ロード(push・on-demand)。setRowCount/fillRows と組で使う
926
+ onViewportChange(start, end) {}, // 描画窓が変わった=「今この範囲が要る」。ここで fillRows(start, 取得した行) する
927
+ onSortFilterChange(state) {}, // 遅延中の sort/filter 要求。ローカル計算せずサーバで並替/絞込→setRowCount+fillRows し直す(state.type='sort'|'filter')
895
928
  });
896
929
  ```
897
930
 
@@ -1109,11 +1142,11 @@ new TssGrid(el, {
1109
1142
  allowDeleteRows: true,
1110
1143
  allowInsertCols: true,
1111
1144
  allowDeleteCols: true,
1112
- onStructureChange: (info) => console.log(info.type), // 'insertRow' | 'deleteRows' | 'insertCol' | 'deleteCols'
1145
+ onStructureChange: (info) => console.log(info.type), // 'insertRow' | 'insertRows' | 'deleteRows' | 'insertCol' | 'deleteCols'
1113
1146
  });
1114
1147
  ```
1115
1148
 
1116
- プログラムからも操作できます: **`insertRow(ri, 'above'|'below')`** / **`deleteRows(r0, r1?)`** / **`insertCol(ci, 'left'|'right')`** / **`deleteCols(c0, c1?)`**。最低1行・1列は残します。
1149
+ プログラムからも操作できます: **`insertRow(ri, 'above'|'below')`** / **`insertRows(ri, count?, 'above'|'below')`**(`count` 行を **1回の再描画・1つの undo** で一括挿入=大量行の追加が O(N²)→O(N)。CSV取込・テンプレ行のまとめ足しに) / **`deleteRows(r0, r1?)`** / **`insertCol(ci, 'left'|'right')`** / **`deleteCols(c0, c1?)`**。最低1行・1列は残します。大量貼付で行を増やしたい場合は `pasteAutoGrow: true` も参照。
1117
1150
 
1118
1151
  既定メニューには行/列の挿入・削除に加え **コピー / 切り取り / 貼り付け / 内容をクリア** も入ります(クリップボードは `copyPaste:false` で消えます)。
1119
1152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threestarsoftware/tssgrid",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
5
5
  "license": "MIT",
6
6
  "author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
@@ -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
  }
@@ -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
- return row => cols.every(c => active[c].has(String(row[c])));
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 allowed = active[c] || new Set(vals); // 未設定なら全許可
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
- search.focus();
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
- pop.querySelector('[data-act=apply]').onclick = () => {
104
- const checked = [...list.querySelectorAll('input:checked')].map(cb => cb.value);
105
- if (checked.length === vals.length) delete active[c]; // 全選択=この列は無条件
106
- else active[c] = new Set(checked);
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
  /* リサイズのプレビュー線(ドラッグ中だけ表示、離して確定) */
@@ -230,3 +246,9 @@
230
246
  .tssgrid .tg-menu .tg-mi.tg-has-sub::after { content: '▶'; position: absolute; right: 10px; top: 0; bottom: 0; display: flex; align-items: center; font-size: 9px; color: #7d8a99; }
231
247
  .tssgrid .tg-menu .tg-submenu { display: none; position: absolute; left: 100%; top: -5px; background: #fff; border: 1px solid var(--tg-wrap-border); border-radius: var(--tg-radius); box-shadow: 0 4px 16px rgba(0,0,0,.18); padding: 4px 0; min-width: 150px; }
232
248
  .tssgrid .tg-menu .tg-mi.tg-has-sub:hover > .tg-submenu { display: block; }
249
+
250
+ /* 遅延ロード(push): 未取得(PENDING)行の「読込中」プレースホルダ。値は描かず淡いシマーの点線を出す */
251
+ .tssgrid .tg-table tr.tg-pending-row td.tg-pending-cell { color: var(--tg-muted); text-align: center; cursor: default; }
252
+ .tssgrid .tg-table tr.tg-pending-row .tg-pending-dots { display: inline-block; width: 40%; max-width: 220px; height: 8px; border-radius: 4px;
253
+ background: linear-gradient(90deg, #eef2f6 25%, #e2e8f0 37%, #eef2f6 63%); background-size: 400% 100%; animation: tg-pending-shimmer 1.2s ease-in-out infinite; }
254
+ @keyframes tg-pending-shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }