@threestarsoftware/tssgrid 0.1.7 → 0.1.9

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,22 @@
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.9] - 2026-09-15
7
+
8
+ ### 追加
9
+ - **`rowClass(r, row, src)`(行ごとに `<tr>` へクラスを付与)**(コア)— 行の状態で**行まるごと色を付ける**のが素直に書けるオプション。`rowClass: (r, row) => row[3] === '欠品' ? 'row-ng' : ''` のように返したクラスが `<tr>` に付く(配列可)。CSS は `.tssgrid tr.row-ng > td { background:#fdecea }` の要領。従来は表全体版 `cellClass` で全 `<td>` に付けて代用していたが、**行単位はこちらが自然**。編集で行の状態が変わっても `_applyRowClass` で即追従。
10
+
11
+ ### 修正
12
+ - **`cellClass` / `cellStyle` の型定義(`.d.ts`)が実装と食い違っていたのを修正**(型のみ・実行時は元から正しい)— (1) **列版**(`columns[c].cellClass`)は第1引数がそのセルの値で `(value, { r, c, row, src }) => クラス`。旧型は `(r, c, value, row) => string` で**引数順が逆**だった。(2) **表全体版**(`TssGridOptions.cellClass` = `(r, c, value, row, src)`)は**そもそも型に無かった**ので追加。`cellStyle` も同様に両方を修正。返り値も `string | string[] | false | null` 等を許容する `ClassValue`/`StyleValue` に。**影響は TypeScript / エディタ補完のみ**(生の JS・README・examples は元から正しい)。
13
+
14
+ ## [0.1.8] - 2026-09-02
15
+
16
+ ### 追加
17
+ - **多段(フライアウト)プルダウン プラグイン `tss-cascade`**(プラグイン・無依存・コア無改変)— メニューのように「大分類 ▸ 中分類 ▸ 小分類」とフライアウトで開き、**葉を選ぶとセルに確定**するカスタムエディタ(`columns[c].editor` 契約に乗る)。`options` は入れ子配列 `[{ label, value?, children? }]`、保存は葉の `value`(未指定なら label)、表示は **`TssCascade.pathFormat(options)`** で「大 / 中 / 小」のパスに。クリック展開+キーボード(↑↓/→←/Enter/Esc)+画面端フリップ。動く例 `examples/cascade-dropdown.html`。
18
+
19
+ ### 修正
20
+ - **右クリックメニューのカスタム項目が `key`(組込キー)だけ・`callback` 無しで無反応だったのを修正**(コア)— カスタム項目 `{ name, key:'row_above' }` は `callback` を実行する仕様で、`key` だけ書いても組込アクションが起動せず**何も起きなかった**(位置に依らず/組込文字列 `'row_above'` は最下行でも正常)。**`callback` 無しで `key` が組込項目を指す時はその組込アクションを実行**するようにし、「**組込動作のまま `name`/`disabled` だけカスタムしたい**」を可能に。`callback` 付き・未知 `key`・組込文字列・`disabled` は不変(後方互換)。
21
+
6
22
  ## [0.1.7] - 2026-08-01
7
23
 
8
24
  ### 追加
package/README.md CHANGED
@@ -1089,7 +1089,8 @@ columns: [
1089
1089
  cellClass: (r, c, value, row, src) => src.holiday_kbn === '1' ? 'sat' : '', // 全体版(src=元レコード=非表示フィールドも見える)
1090
1090
  // CSS は業務側で: .tssgrid td.tg-neg { color:#c00 } .tssgrid td.tg-row-ng { background:#fdecea }
1091
1091
  ```
1092
- - 列版 `columns[c].cellClass(value, {r,c,row,src})` / 全体版 `cellClass(r,c,value,row,src)`。返り値は文字列/配列/空(クラス無し)。
1092
+ - 列版 `columns[c].cellClass(value, {r,c,row,src})` / 全体版 `cellClass(r,c,value,row,src)`。**引数順が違う**ので注意(列版は第1引数が値・全体版は先頭が r,c)。返り値は文字列/配列/空(クラス無し)。
1093
+ - **行に色を付けるなら `rowClass` が簡単**: `rowClass: (r, row, src) => 'row-ng'` で **`<tr>` に直接クラス**が付きます(全 td に付ける全体版 cellClass の代用より素直)。**その行のセルを編集すれば即追従**。CSS は `.tssgrid tr.row-ng > td { background:#fdecea }` のように行単位で当てます。
1093
1094
  - `src` は**元レコード**(オブジェクトデータ時)。**画面に出していない隠しフィールド基準の色付け**(勤怠の `holiday_kbn` で土日祝を色分け等)に使えます → 実例: [`examples/attendance.html`](https://tssgrid.threestarsoftware.co.jp/examples/attendance.html)。
1094
1095
  - **他列に依存する行ルール**(状態で行を色分け等)は、依存先の編集時に `grid.redraw()` で行全体を再評価(`onAfterChange: () => grid.redraw()`)。
1095
1096
  - 同じセルに行ルールとセルルールが両方付く場合は **CSSの記述順(後勝ち)**で優先を決めます(セル個別を後に書く)。
@@ -1214,6 +1215,7 @@ new TssGrid(el, {
1214
1215
 
1215
1216
  - **組込キー**: `row_above` / `row_below` / `remove_row` / `col_left` / `col_right` / `remove_col` / `copy` / `cut` / `paste` / `clear` / `undo` / `redo`、区切りは `'---------'`(ダッシュのみ)。
1216
1217
  - **カスタム項目**: `{ name, callback, disabled?, hidden?, key?, submenu? }`。`name`/`disabled`/`hidden` は**関数**も可=引数は `(range, ctx)`(`range`=選択範囲 `{r0,c0,r1,c1}`)。`callback` の `this` はグリッド、引数は `{ range, key, ctx }`。
1218
+ - **組込動作のまま name/disabled だけ変えたい**: カスタム項目に `callback` を**書かず** `key` に組込キー(`row_above` 等)を指定すると、**その組込アクションを実行**します(`disabled`/`name` はカスタム側を尊重)。例: `{ name: '行を上に追加', key: 'row_above', disabled: (s) => … }`=ラベルと活性条件だけ差し替え、挿入動作は組込のまま。
1217
1219
  - **`ctx`=右クリックの対象** `{ on, r, c }`。`on` は `'col'`(列ヘッダ)/`'row'`(行ヘッダ)/`'cell'`(データセル)/`'corner'`(隅)。例: **列ヘッダを右クリックした時だけ項目を出す**なら `hidden: (range, ctx) => ctx.on !== 'col'`。
1218
1220
  - `submenu` に配列を渡すとフライアウトの**サブメニュー**になります(同じ組込キー/カスタム項目が使えます)。
1219
1221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threestarsoftware/tssgrid",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
5
5
  "license": "MIT",
6
6
  "author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
@@ -0,0 +1,15 @@
1
+ /* TssCascade — 多段(フライアウト)プルダウン(plugins/tss-cascade.js)。色は --tg-accent を流用。 */
2
+ .tg-cascade {
3
+ position: fixed; z-index: 1000; min-width: 150px; max-height: 60vh; overflow: auto;
4
+ background: #fff; color: #1f2733; border: 1px solid #c4ccd6; border-radius: 8px;
5
+ box-shadow: 0 6px 22px rgba(20, 40, 80, .22);
6
+ font: 13px "Meiryo UI", "Segoe UI", sans-serif; padding: 4px; box-sizing: border-box;
7
+ }
8
+ .tg-cascade .tg-casc-item {
9
+ display: flex; align-items: center; gap: 10px; padding: 5px 10px; border-radius: 5px;
10
+ cursor: pointer; white-space: nowrap; outline: none;
11
+ }
12
+ .tg-cascade .tg-casc-item:hover,
13
+ .tg-cascade .tg-casc-item.active { background: var(--tg-accent, #1573c0); color: #fff; }
14
+ .tg-cascade .tg-casc-label { flex: 1; }
15
+ .tg-cascade .tg-casc-arrow { font-size: 10px; opacity: .7; margin-left: 6px; }
@@ -0,0 +1,139 @@
1
+ /*! TssCascade — 多段(フライアウト)プルダウン。TssGrid カスタムエディタ(ポップアップ型)。
2
+ *
3
+ * メニューのように「大分類 ▸ 中分類 ▸ 小分類」とフライアウトで開き、葉を選ぶとセルに確定。
4
+ * 保存はその葉の value(未指定なら label)。表示は TssCascade.pathFormat で「大 / 中 / 小」のパスにできる。
5
+ *
6
+ * 使い方:
7
+ * const OPTS = [
8
+ * { label:'営業', children:[
9
+ * { label:'国内', children:[ {label:'東日本', value:'sales.jp.east'}, {label:'西日本', value:'sales.jp.west'} ] },
10
+ * { label:'海外', value:'sales.global' },
11
+ * ] },
12
+ * { label:'開発', children:[ … ] },
13
+ * ];
14
+ * columns: [
15
+ * { data:'category', editor: TssCascade({ options: OPTS }),
16
+ * format: TssCascade.pathFormat(OPTS) }, // 表示を「営業 / 国内 / 東日本」に(任意)
17
+ * ]
18
+ *
19
+ * opts:
20
+ * options : 入れ子配列 [{ label, value?, children? }]。必須。value 未指定の葉は label を保存。
21
+ * openOnClick : 既定 true(プルダウン同様シングルクリックで開く)。
22
+ * className : ポップアップに付ける任意クラス。
23
+ * 静的:
24
+ * TssCascade.pathFormat(options, { separator }) => (value)=>string // 列 format 用
25
+ * TssCascade.pathOf(options, value, sep?) => string
26
+ */
27
+ (function (root) {
28
+ 'use strict';
29
+ function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
30
+ function leafVal(n) { return n.value !== undefined ? n.value : n.label; }
31
+
32
+ // 値 → ラベルのパス配列([大, 中, 小])。見つからなければ null。
33
+ function findPath(nodes, value, trail) {
34
+ trail = trail || [];
35
+ for (var i = 0; i < (nodes || []).length; i++) {
36
+ var n = nodes[i], t = trail.concat(n.label);
37
+ if (n.children && n.children.length) { var r = findPath(n.children, value, t); if (r) return r; }
38
+ else if (String(leafVal(n)) === String(value)) return t;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ function TssCascade(opts) {
44
+ opts = opts || {};
45
+ var roots = opts.options || [];
46
+ var panels = []; // 開いているパネル(div)。panels[0]=root, [1]=sub, …
47
+ var ctxRef = null, done = false, onDoc = null, onKey = null;
48
+
49
+ function teardown() {
50
+ panels.forEach(function (p) { if (p.parentNode) p.parentNode.removeChild(p); });
51
+ panels = [];
52
+ if (onDoc) document.removeEventListener('mousedown', onDoc, true);
53
+ if (onKey) document.removeEventListener('keydown', onKey, true);
54
+ onDoc = onKey = null;
55
+ }
56
+ function closeFrom(depth) { while (panels.length > depth) { var p = panels.pop(); if (p.parentNode) p.parentNode.removeChild(p); } }
57
+ function commitLeaf(n) { if (done) return; done = true; var c = ctxRef; teardown(); c.commit(leafVal(n)); }
58
+ function doCancel() { if (done) return; done = true; var c = ctxRef; teardown(); c.cancel(); }
59
+
60
+ function position(pop, anchor, depth) {
61
+ var pw = pop.offsetWidth, ph = pop.offsetHeight, vw = window.innerWidth, vh = window.innerHeight, left, top;
62
+ if (depth === 0) { left = anchor.left; top = anchor.bottom + 2; }
63
+ else { left = anchor.right - 2; top = anchor.top - 4; } // 親アイテムの右へフライアウト
64
+ if (left + pw > vw - 6) left = depth === 0 ? Math.max(6, vw - pw - 6) : Math.max(6, anchor.left - pw + 2); // 右端で左へフリップ
65
+ if (top + ph > vh - 6) top = Math.max(6, vh - ph - 6);
66
+ pop.style.left = Math.max(6, left) + 'px'; pop.style.top = top + 'px';
67
+ }
68
+
69
+ function openPanel(nodes, depth, anchor) {
70
+ closeFrom(depth);
71
+ var pop = document.createElement('div');
72
+ pop.className = 'tg-cascade' + (opts.className ? ' ' + opts.className : '');
73
+ pop.setAttribute('role', 'menu');
74
+ pop.innerHTML = nodes.map(function (n, i) {
75
+ var branch = n.children && n.children.length;
76
+ return '<div class="tg-casc-item' + (branch ? ' branch' : '') + '" data-i="' + i + '" role="menuitem" tabindex="-1">' +
77
+ '<span class="tg-casc-label">' + esc(n.label) + '</span>' + (branch ? '<span class="tg-casc-arrow">▸</span>' : '') + '</div>';
78
+ }).join('');
79
+ document.body.appendChild(pop);
80
+ panels[depth] = pop;
81
+ position(pop, anchor, depth);
82
+
83
+ var items = pop.querySelectorAll('.tg-casc-item');
84
+ pop._items = items; pop._active = null; pop._depth = depth;
85
+ pop._activate = function (idx) { Array.prototype.forEach.call(items, function (el, k) { el.classList.toggle('active', k === idx); }); pop._active = idx; };
86
+ pop._enter = function (idx, viaKey) { // 分岐なら submenu を開く(ホバー/→)。葉なら深いパネルを閉じる。
87
+ var n = nodes[idx]; if (!n) return; pop._activate(idx);
88
+ if (n.children && n.children.length) {
89
+ openPanel(n.children, depth + 1, items[idx].getBoundingClientRect());
90
+ if (viaKey) { var sub = panels[depth + 1], f = sub._items[0]; if (f) { sub._activate(0); f.focus(); } }
91
+ } else closeFrom(depth + 1);
92
+ };
93
+ pop._choose = function (idx) { // クリック/Enter:分岐→開く、葉→確定
94
+ var n = nodes[idx]; if (!n) return;
95
+ if (n.children && n.children.length) pop._enter(idx, true); else commitLeaf(n);
96
+ };
97
+ Array.prototype.forEach.call(items, function (el) {
98
+ var idx = +el.dataset.i;
99
+ el.addEventListener('mouseenter', function () { pop._enter(idx, false); });
100
+ el.addEventListener('click', function (e) { e.stopPropagation(); pop._choose(idx); });
101
+ });
102
+ return pop;
103
+ }
104
+
105
+ function handleKey(e) {
106
+ var pop = panels[panels.length - 1]; if (!pop) return;
107
+ var items = pop._items, idx = pop._active == null ? -1 : pop._active, n;
108
+ if (e.key === 'ArrowDown') { e.preventDefault(); n = (idx + 1) % items.length; pop._activate(n); items[n].focus(); }
109
+ else if (e.key === 'ArrowUp') { e.preventDefault(); n = (idx - 1 + items.length) % items.length; pop._activate(n); items[n].focus(); }
110
+ else if (e.key === 'ArrowRight') { e.preventDefault(); if (idx >= 0) pop._enter(idx, true); }
111
+ else if (e.key === 'ArrowLeft') { e.preventDefault(); if (panels.length > 1) { closeFrom(panels.length - 1); var pp = panels[panels.length - 1]; if (pp._active != null) pp._items[pp._active].focus(); } }
112
+ else if (e.key === 'Enter') { e.preventDefault(); if (idx >= 0) pop._choose(idx); }
113
+ else if (e.key === 'Escape') { e.preventDefault(); doCancel(); }
114
+ }
115
+
116
+ return {
117
+ openOnClick: opts.openOnClick !== false,
118
+ open: function (ctx) {
119
+ ctxRef = ctx; done = false;
120
+ openPanel(roots, 0, ctx.td.getBoundingClientRect());
121
+ onDoc = function (e) { for (var i = 0; i < panels.length; i++) if (panels[i].contains(e.target)) return; doCancel(); };
122
+ onKey = handleKey;
123
+ document.addEventListener('mousedown', onDoc, true);
124
+ document.addEventListener('keydown', onKey, true);
125
+ var f = panels[0]._items[0]; if (f) { panels[0]._activate(0); f.focus(); }
126
+ },
127
+ close: function () { teardown(); },
128
+ };
129
+ }
130
+
131
+ TssCascade.pathFormat = function (options, o) {
132
+ o = o || {}; var sep = o.separator || ' / ';
133
+ return function (value) { if (value === '' || value == null) return ''; var p = findPath(options || [], value); return p ? p.join(sep) : String(value); };
134
+ };
135
+ TssCascade.pathOf = function (options, value, sep) { var p = findPath(options || [], value); return p ? p.join(sep || ' / ') : (value == null ? '' : String(value)); };
136
+
137
+ if (typeof module !== 'undefined' && module.exports) module.exports = TssCascade;
138
+ else root.TssCascade = TssCascade;
139
+ })(typeof self !== 'undefined' ? self : this);
package/src/tssgrid.d.ts CHANGED
@@ -25,6 +25,12 @@ export interface ChangeItem {
25
25
  /** 選択範囲。r0/c0=開始、r1/c1=終了(昇順とは限らない)。 */
26
26
  export interface SelectionRange { r0: number; c0: number; r1: number; c1: number; }
27
27
  export interface CellCoord { r: number; c: number; }
28
+ /** cellClass/cellStyle(列版)が受け取る第2引数。row=その行の値配列, src=元レコード(object モード時・非表示フィールドも見える/配列モードは null)。 */
29
+ export interface CellClassCtx { r: number; c: number; row: any[]; src: Record<string, any> | null; }
30
+ /** cellClass/cellStyle の戻り値のクラス。文字列(空白区切り可)/配列/null 等(=クラス無し)。 */
31
+ export type ClassValue = string | string[] | null | undefined | false;
32
+ /** cellStyle の戻り値。CSS プロパティのオブジェクト(`{'background':'#fde','--pct':'42%'}`)か文字列(`'background:#fde;--pct:42%'`)。 */
33
+ export type StyleValue = string | Partial<CSSStyleDeclaration> | Record<string, string> | null | undefined;
28
34
  export interface SelectionInfo { range: SelectionRange; active: CellCoord; extent: CellCoord; }
29
35
 
30
36
  /** validator が弾いたセル(onInvalid / getInvalidCells)。 */
@@ -73,10 +79,11 @@ export interface ColumnDef {
73
79
  thousands?: boolean;
74
80
  /** 数値型: 接頭辞(例 '¥')。 */
75
81
  prefix?: string;
76
- /** 値/行を見てセルに付与する CSS クラス(条件付き書式)。 */
77
- cellClass?: (r: number, c: number, value: any, row: any) => string;
78
- /** セル単位のインラインスタイル(連続色・データバー等)。 */
79
- cellStyle?: (r: number, c: number, value: any, row: any) => Partial<CSSStyleDeclaration> | Record<string, string>;
82
+ /** 値/行を見てセルに付与する CSS クラス(条件付き書式)。**引数は `(value, { r, c, row, src })`**(第1引数がそのセルの値)。
83
+ * *表全体版は `TssGridOptions.cellClass`(引数順が違う: `(r, c, value, row, src)`)。 */
84
+ cellClass?: (value: any, cell: CellClassCtx) => ClassValue;
85
+ /** セル単位のインラインスタイル(連続色・データバー等)。**引数は `(value, { r, c, row, src })`**。 */
86
+ cellStyle?: (value: any, cell: CellClassCtx) => StyleValue;
80
87
  /** true で表示だけ生HTML(値はテキスト保存のまま)。 */
81
88
  html?: boolean;
82
89
  /** 列単位の折り返し。 */
@@ -105,6 +112,14 @@ export interface TssGridOptions {
105
112
  /** 入力フロー制御。Enter/Tab の移動先を上書き(null で既定方向)。入力セルだけ巡回できる。 */
106
113
  nextCell?: (a: { r: number; c: number; key: string; shift: boolean }) => { r: number; c: number } | null;
107
114
  columns?: ColumnDef[];
115
+ /** 表全体の条件付きクラス(全セルで呼ばれる)。**引数は `(r, c, value, row, src)`**(先頭が行/列番号・列版と順序が違う)。
116
+ * `row`=その行の値配列, `src`=元レコード(object モード時・非表示フィールドも見える)。行に色を付けたいなら `rowClass` の方が簡単。 */
117
+ cellClass?: (r: number, c: number, value: any, row: any[], src: Record<string, any> | null) => ClassValue;
118
+ /** 表全体のセル単位インラインスタイル。**引数は `(r, c, value, row, src)`**。返り値は CSS オブジェクト or 文字列。 */
119
+ cellStyle?: (r: number, c: number, value: any, row: any[], src: Record<string, any> | null) => StyleValue;
120
+ /** 行ごとに `<tr>` へ付与する CSS クラス(**行に色を付けるならこれ**)。`(r, row, src)` で呼ばれ、文字列/配列を返す。
121
+ * 編集でセルが再描画されると即追従。CSS は `.tssgrid tr.クラス > td { … }` のように行単位で当てる。 */
122
+ rowClass?: (r: number, row: any[], src: Record<string, any> | null) => ClassValue;
108
123
  /** 検証NG時。'revert'=元に戻す / 'keep'=赤く警告して残す。 */
109
124
  invalidMode?: 'revert' | 'keep';
110
125
  invalidTitle?: boolean;
package/src/tssgrid.js CHANGED
@@ -266,6 +266,9 @@
266
266
  // セル単位インラインスタイル: cellStyle(r,c,value,row,src)→{CSSプロパティ} or 'k:v;…'。列版 columns[c].cellStyle(value,{r,c,row,src})。
267
267
  // クラス(離散)では出せない連続色・データバー幅(CSS変数)向け。例: cellStyle:(r,c,v)=>({background:heat(v)})
268
268
  this.cellStyle = opts.cellStyle || null;
269
+ // 行ごとに <tr> へ付けるクラス(行の色付け向け)。rowClass(r, row, src)→クラス文字列/配列。
270
+ // 全体版 cellClass でも代用できるが(全 td に付く)、行単位はこちらが素直。編集で再描画時に追従(_applyRowClass)。
271
+ this.rowClass = opts.rowClass || null;
269
272
  // 列の非表示(描画で畳む+移動/幅/コピーは隠し列をスキップ)。実行時は hideColumn/showColumn。
270
273
  this.hiddenCols = new Set((opts.hiddenColumns || []).map(Number));
271
274
  // セル単位の可変状態(キー "r,c")。整列 / 実行時 readOnly。構造変更で _structCmd がキー追従。
@@ -1154,6 +1157,21 @@
1154
1157
  for (const k of next) td.classList.add(k);
1155
1158
  if (next.length) td.dataset.ccls = next.join(' '); else delete td.dataset.ccls;
1156
1159
  }
1160
+ // 行クラス: rowClass(r, row, src) → 空白区切り文字列(配列は join)。無効/例外は ''。
1161
+ _rowClassStr(r) {
1162
+ if (typeof this.rowClass !== 'function') return '';
1163
+ try { const x = this.rowClass(r, this.data[r], this._src ? this._src[r] : null); return x ? (Array.isArray(x) ? x.filter(Boolean).join(' ') : String(x).trim()) : ''; } catch (_) { return ''; }
1164
+ }
1165
+ // <tr> の行クラスを反映(前回分 data-rcls を外して付け直す=セル再描画で追従)。
1166
+ _applyRowClass(r) {
1167
+ const tr = this.table && this.table.querySelector('tbody tr[data-r="' + r + '"]');
1168
+ if (!tr) return;
1169
+ const prev = tr.dataset.rcls ? tr.dataset.rcls.split(/\s+/) : [];
1170
+ for (const k of prev) if (k) tr.classList.remove(k);
1171
+ const next = this._rowClassStr(r);
1172
+ if (next) { for (const k of next.split(/\s+/)) if (k) tr.classList.add(k); tr.dataset.rcls = next; }
1173
+ else delete tr.dataset.rcls;
1174
+ }
1157
1175
  // セル単位のインラインスタイル: 列版 columns[c].cellStyle(value,{r,c,row,src}) + 全体版 cellStyle(r,c,value,row,src)。
1158
1176
  // 返り値は CSSプロパティのオブジェクト {'background':'#fde','--pct':'42%'} か文字列 'background:#fde;--pct:42%'。
1159
1177
  // cellClass(クラス=離散)では出せない**連続値の背景色・データバー幅(CSS変数)**などに(プロパティ名は kebab/標準名 or --var)。
@@ -1193,6 +1211,7 @@
1193
1211
  }
1194
1212
  if (this.cellClass || this.colCfg(c).cellClass) this._applyCellClass(td, r, c); // 条件付き書式
1195
1213
  if (this.cellStyle || this.colCfg(c).cellStyle) this._applyCellStyle(td, r, c); // セル単位インラインスタイル
1214
+ if (this.rowClass) this._applyRowClass(r); // 行クラス(編集で行の色付けが即追従)
1196
1215
  }
1197
1216
 
1198
1217
  _colWidth(c) { return this.colW[c] != null ? this.colW[c] : this.defColW; }
@@ -1373,7 +1392,8 @@
1373
1392
  const inner = after ? (lbl + mark) : (mark + lbl); // ⠿ を番号の前/後に(rowReorderMark)。番号なしは前後どちらでも中央
1374
1393
  const rhCls = (canMove && this.rowReorderWhole) ? ' tg-rowmove-cell' : ''; // 'header'=行ヘッダーセル全体を掴み手に(grab カーソル)。番号/⠿の表示は独立
1375
1394
  const rcls = (r < this.frozenRows) ? (' tg-frozen-row' + (r === this.frozenRows - 1 ? ' tg-frozen-row-edge' : '')) : ''; // 行固定
1376
- let html = '<tr data-r="' + r + '" class="tg-row' + rcls + '" style="height:' + this._rowHeight(r) + 'px">' + (rh ? '<th class="rowhead' + rhCls + '" data-r="' + r + '">' + inner + rgrip + '</th>' : '');
1395
+ const rowc = this.rowClass ? this._rowClassStr(r) : ''; // 行クラス(rowClass)=<tr> に付与
1396
+ let html = '<tr data-r="' + r + '" class="tg-row' + rcls + (rowc ? ' ' + rowc : '') + '"' + (rowc ? ' data-rcls="' + TssGrid.esc(rowc) + '"' : '') + ' style="height:' + this._rowHeight(r) + 'px">' + (rh ? '<th class="rowhead' + rhCls + '" data-r="' + r + '">' + inner + rgrip + '</th>' : '');
1377
1397
  for (let c = 0; c < this.COLS; c++) {
1378
1398
  if (this._merges && this._isCovered(r, c)) continue; // 結合に覆われた従属セルは td を出さない(アンカーが colspan で覆う)
1379
1399
  const cs = this._merges ? this._colspanAt(r, c) : 1; // 結合アンカーは colspan で従属列ぶんを覆う
@@ -2545,7 +2565,10 @@
2545
2565
  const disabled = typeof entry.disabled === 'function' ? !!entry.disabled.call(this, s, ctx) : !!entry.disabled;
2546
2566
  const it = { label: String(name == null ? '' : name), disabled, danger: !!entry.danger };
2547
2567
  if (Array.isArray(entry.submenu)) it.submenu = entry.submenu;
2548
- else it.act = () => { if (typeof entry.callback === 'function') { try { entry.callback.call(this, { range: { ...s }, key: entry.key, ctx }); } catch (_) {} } };
2568
+ else if (typeof entry.callback === 'function') it.act = () => { try { entry.callback.call(this, { range: { ...s }, key: entry.key, ctx }); } catch (_) {} };
2569
+ // callback 無しで key が組込項目(row_above 等)を指すなら、その組込アクションを実行=
2570
+ // 「組込動作のまま name/disabled だけカスタムしたい」を可能に(key だけ書いて無反応だった不具合の解消)。
2571
+ else if (entry.key) { const bi = this._builtinItem(entry.key, s); if (bi && typeof bi.act === 'function') it.act = bi.act; }
2549
2572
  items.push(it);
2550
2573
  }
2551
2574
  return items;