@threestarsoftware/tssgrid 0.1.5 → 0.1.7

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,18 @@
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.7] - 2026-08-01
7
+
8
+ ### 追加
9
+ - **`format` に第3引数 `row`(その行の値配列=`grid.data[r]`)**(コア)— `format(value, { r, c }, row)` の第3引数 `row` を追加(`options` の `(r, row)` と同形)。**行の状態でセル内容(ボタン/アイコン等)を出し分ける**時、`format` の中で `grid` を参照せず `row[列index]` から状態を読める=**初回描画(`const grid = new TssGrid(...)` の構築中に `format` が走る時点で `grid` 未代入=TDZ で例外)を回避**できる。既存の2引数 `format` はそのまま(後方互換)。
10
+ - **`examples/cell-button.html` を「行の状態で切替」に刷新**+**`examples/file-upload.html` を追加**(コア無改修のサンプル)— 前者は 未申請/申請中/編集中 で出すボタンを切替(申請/コピー・編集/削除・保存/取消)、click 委譲で状態遷移→`redraw`、複製・削除も。後者は**行ごとのファイル添付**(セル内ボタン+ドラッグ&ドロップ/落とした行を `closest('tr[data-r]')` で特定して `setValue`)。
11
+
12
+ ## [0.1.6] - 2026-07-30
13
+
14
+ ### 追加
15
+ - **依存ドロップダウン=`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`。
16
+ - **`html:true` 列は `format` を空セルでも実行**(コア)— 従来は空セルだと `_displayValue` が値を素通しして `format` が走らず、**ボタン/アイコン等の"飾り"を空セルに描けなかった**。`html:true` かつ `format` が関数のときは空でも走らせ、**全行にボタン列などを描ける**ように(素テキスト列・非空 html セルは不変)。セル内ボタンは専用 API を持たず `html:true`+`format`+`grid.table` への click 委譲で作る(動く例 `examples/cell-button.html`)。
17
+
6
18
  ## [0.1.5] - 2026-07-25
7
19
 
8
20
  ### 追加
package/README.md CHANGED
@@ -269,6 +269,45 @@ new TssGrid(el, { suppressAutofill: true }); // 既定 false
269
269
  - 副作用として環境により**鍵アイコン**や**生成パスワードの提案**が出ることがあるため **opt-in**(既定は付けません)。値はマスクされません(`type` は `text` のまま)。
270
270
  - **あわせて、あなたのページ側の素の `<input>`(テーブル名欄など)にも同じ `autocomplete="new-password"` を付けてください**。誘発の主因はグリッドではなく周囲のフォーム塊のことが多いためです。
271
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-*` で拾うのが定石です。**行の状態でボタンを出し分け**たいときは、状態を**列に持たせて** `format` の**第3引数 `row`(その行の値配列)**から読みます(→ 動く例: [`examples/cell-button.html`](https://tssgrid.threestarsoftware.co.jp/examples/cell-button.html)=未申請/申請中/編集中でボタン切替)。
290
+
291
+ ```js
292
+ // format の引数は (value, { r, c }, row)。row = その行の値配列(grid.data[r])
293
+ columns: [ /* …, 状態列(index 2), … */,
294
+ { html:true, readOnly:true, format: (_v, _cell, row) => {
295
+ const st = row[2]; // 状態を「行の値配列」から読む(grid は参照しない)
296
+ if (st === '編集中') return '<button class="mybtn" data-act="save">保存</button>';
297
+ if (st === '申請中') return '<button class="mybtn" data-act="edit">編集</button>';
298
+ return '<button class="mybtn" data-act="submit">申請</button>';
299
+ } } ]
300
+ grid.table.addEventListener('click', e => {
301
+ const b = e.target.closest('.mybtn'); if (!b) return;
302
+ const r = +b.closest('tr[data-r]').dataset.r;
303
+ grid.setValue(r, '状態', '申請中', true); // 状態遷移(readOnly列は force=true)
304
+ grid.redraw(); // ← format 再実行でボタンが変わる
305
+ });
306
+ ```
307
+ - ボタン列は `readOnly:true` にして編集モードに入らないように。行番号は `closest('tr[data-r]')` から。空セルでも `format` は走ります(飾り列を全行に描ける)。
308
+ - **⚠️ 初回描画は `new TssGrid(...)` の構築中に走る**ので、`format` の中で **`grid` 変数を参照しない**(まだ未代入=TDZ で例外)。状態は**第3引数 `row`** から読むのが安全(`grid.getValue` を使いたくなるが初回で落ちる)。状態を変えたら **`grid.redraw()`** で `format` を再実行してボタンを更新します。
309
+ - **`html:true` はエスケープを外す**ので、ラベルにユーザー入力を入れる時は自分でエスケープを(XSS 責任は HTML 提供側)。
310
+
272
311
  ### 非破壊フィルタ(`filter` / `clearFilter`)
273
312
 
274
313
  **コア内蔵**の非破壊フィルタ(行インデックスマッパ)。全行は内部の `_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.5",
3
+ "version": "0.1.7",
4
4
  "description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
5
5
  "license": "MIT",
6
6
  "author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
package/src/tssgrid.d.ts CHANGED
@@ -46,8 +46,10 @@ export interface ColumnDef {
46
46
  title?: string;
47
47
  /** セルタイプ。既定 'text'。 */
48
48
  type?: CellType;
49
- /** dropdown の選択肢。 */
50
- options?: Array<string | { value: string; label?: string }>;
49
+ /** dropdown の選択肢。関数を渡すと**行ごとに異なる依存ドロップダウン**(例: 都道府県→市区町村)。
50
+ * `(r, row)=>[…]` の row はその行の値配列(`grid.data[r]`)。静的配列は全行共通(後方互換)。 */
51
+ options?: Array<string | { value: string; label?: string }>
52
+ | ((r: number, row: any[]) => Array<string | { value: string; label?: string }>);
51
53
  /** 列ごとの初期幅(px)。列定義に同居=並べ替え/コピペで幅が追従。 */
52
54
  width?: number;
53
55
  /** false でこの列だけリサイズ禁止。 */
@@ -63,8 +65,10 @@ export interface ColumnDef {
63
65
  editor?: EditorDef | (() => EditorDef);
64
66
  /** 入力の正規化(保存値へ変換)。 */
65
67
  parse?: (input: string, cell: CellCoord) => any;
66
- /** 表示の整形(保存値→表示)。文字列 or 宣言的 CellFormat。 */
67
- format?: string | object | ((value: any, cell: CellCoord) => string);
68
+ /** 表示の整形(保存値→表示)。文字列 or 宣言的 CellFormat or 関数。
69
+ * 関数は `(value, { r, c }, row)`=第3引数 `row` はその行の値配列(`grid.data[r]`・options と同形)。
70
+ * html:true 併用でセル内にボタン/アイコン等の HTML を描ける(空セルでも実行される)。 */
71
+ format?: string | object | ((value: any, cell: CellCoord, row: any[]) => string);
68
72
  /** 数値型: 千区切り表示。 */
69
73
  thousands?: boolean;
70
74
  /** 数値型: 接頭辞(例 '¥')。 */
package/src/tssgrid.js CHANGED
@@ -1031,7 +1031,7 @@
1031
1031
  }
1032
1032
  if (t === 'dropdown') {
1033
1033
  if (v === '') { if (cfg.allowEmpty === false) return { ok: false }; } // allowEmpty:false=必須選択(空は不可)
1034
- 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 で解決)
1035
1035
  } else if (t === 'checkbox') {
1036
1036
  v = this._isCheckedVal(c, v) ? this._cbChecked(c) : this._cbUnchecked(c);
1037
1037
  }
@@ -1093,21 +1093,25 @@
1093
1093
  const cfg = this.colCfg(c), v = this.data[r][c];
1094
1094
  if (v === '') return v;
1095
1095
  if (typeof cfg.format === 'function') {
1096
- try { const out = cfg.format(v, { r, c }); return out == null ? '' : String(out); } catch (_) { return v; }
1096
+ try { const out = cfg.format(v, { r, c }, this.data[r]); return out == null ? '' : String(out); } catch (_) { return v; } // 第3引数=その行の値配列(options と同形)
1097
1097
  }
1098
1098
  if (typeof cfg.format === 'string') return TssGrid._applyDatePattern(v, cfg.format); // 日付パターン表示
1099
1099
  if (cfg.type === 'number') return TssGrid._formatNumber(v, cfg); // 数値書式(カンマ/小数/前後綴り)
1100
1100
  if (cfg.type === 'time' && cfg.hour12) return TssGrid._fmtTime12(v); // 組込 12時間表示
1101
- 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 で解決)
1102
1102
  return v;
1103
1103
  }
1104
1104
  // dropdown の options を [{value,label}] に正規化(文字列なら value=label)。値↔表示を分けられる。
1105
- _optList(c) {
1106
- return (this.colCfg(c).options || []).map(o =>
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 =>
1107
1111
  (o && typeof o === 'object') ? { value: String(o.value), label: String(o.label != null ? o.label : o.value) }
1108
1112
  : { value: String(o), label: String(o) });
1109
1113
  }
1110
- _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; }
1111
1115
  // 編集開始時にエディタへ入れる文字列。number は Excel風に「素の数値」を出す(書式は表示専用)。
1112
1116
  // ネイティブ(date/time picker)は保存値、それ以外のテキスト編集は表示形。
1113
1117
  _editText(r, c) {
@@ -1121,7 +1125,12 @@
1121
1125
  return '<input type="checkbox" class="tg-cb" tabindex="-1"' + (this._isReadOnly(r, c) ? ' disabled' : '') + (this._isCheckedVal(c, this.data[r][c]) ? ' checked' : '') + '>';
1122
1126
  if (this.data[r][c] === '' && this._placeholder(c) != null) return TssGrid.esc(this._placeholder(c));
1123
1127
  // html:true の列は表示だけ生HTML(値はテキスト保存のまま)。エスケープを外す=XSS責任は format/値の提供側。
1124
- if (this.colCfg(c).html) return this._displayValue(r, c);
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 }, this.data[r]); return o == null ? '' : String(o); } catch (_) { return ''; } }
1132
+ return this._displayValue(r, c);
1133
+ }
1125
1134
  const _h = TssGrid.esc(this._displayValue(r, c));
1126
1135
  // 複数行セルは絶対配置のクリップ箱で包む=行を押し広げない(行高一定・仮想スクロール維持/行高ぶんだけ表示)
1127
1136
  if (this.colCfg(c).multiline) return '<div class="tg-ml-clip">' + _h + '</div>';
@@ -3129,7 +3138,7 @@
3129
3138
  _openSelect() {
3130
3139
  const r = this.active.r, c = this.active.c, cfg = this.colCfg(c);
3131
3140
  if (this._isReadOnly(r, c)) return; // 読み取り専用は <select> を開かない
3132
- 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];
3133
3142
  const allowEmpty = cfg.allowEmpty !== false; // 既定: クリア用の空オプションを先頭に付ける
3134
3143
  let html = allowEmpty ? '<option value=""></option>' : '';
3135
3144
  for (const o of opts) html += '<option value="' + TssGrid.esc(o.value) + '">' + TssGrid.esc(o.label) + '</option>'; // value↔label 分離