@threestarsoftware/tssgrid 0.1.20 → 0.1.22

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.22] - 2026-10-07
7
+
8
+ ### 追加
9
+ - **`renderCell(r, cOrKey)` を公開**(コア)— セル1つだけ表示を描き直す(テキスト/HTML・条件付き書式〔cellClass/cellStyle/rowClass〕・無効表示を最新データで)。全体 `redraw()` より軽い。`format` が外部状態に依存する/セル内ボタンの状態を更新した等で「このセルだけ」描き直したい時に。`c` は列 index か data キー、範囲外 `r`・未解決 key・窓外/非表示は安全に no-op。**TypeScript の型定義(`.d.ts`)にも追加**。
10
+ - **`date` / `time` 列が IME ON の全角入力を受理**(コア)— 従来は半角しか受け付けず弾いていた全角の日付/時刻(`2026/01/05`・`9:30` 等)を、**半角化して受理**するように(`number` 列と同じく `_zen2han` を通す。全角スラッシュ `/`・全角コロン `:` も対応)。全角の日付/時刻は元々不正なので常に正規化=受理が広がるだけ(半角は従来どおり・実在しない日付/時刻は引き続き拒否)。
11
+ - **ページネーション(サーバ側ページング)のサンプルを追加** — `examples/pagination.html`。ページネーションはサーバ API 形に依存するため**本体は「渡された1ページ分を描く」だけ**に徹する方針を、モックサーバ `fetchPage(offset,limit)→{rows,total}` + `grid.setData(rows)` + グリッド外の前/次/ページサイズ/「N件中 M〜」で提示。README に「ページネーション(サーバ側ページング)」節も追加。
12
+
13
+ ## [0.1.21] - 2026-10-06
14
+
15
+ ### 修正
16
+ - **チェックボックス列のヘッダ全選択が、read-only 行のせいで「選べる行は全部 ON なのに − になる」のを修正**(コア)— v0.1.20 で全選択の無効判定(`_columnAllReadOnly`)は read-only 行を除外したが、**状態判定(`_columnCheckState`=✓/−/☐)は read-only 行を total に数えたまま**だった。全選択(`setColumnChecked`)は read-only をスキップするので、状態判定も**「操作できる行(非空かつ非 read-only)」だけで数える**よう統一。選べる行が全部 ON なら **✓**(押せば全部外れる)。あわせて**全 read-only 列では右クリックメニュー「すべてチェック/外す」を出さない**(no-op なので・ヘッダ無効と整合)。これでチェックボックス×read-only の全経路(状態判定/無効判定/全選択書込/body トグル/コンテキストメニュー)が「操作できる集合」で揃った。
17
+
6
18
  ## [0.1.20] - 2026-10-06
7
19
 
8
20
  ### 修正
package/README.md CHANGED
@@ -351,6 +351,22 @@ new TssGrid(el, { virtual: { buffer: 10 }, data }); // 窓の上下バッフ
351
351
  - 制約(v1): **行高一定**/**固定行列・折り返し・セル結合は非対応**(指定時は自動で無効化+警告)。行番号の桁数が増える場合は `rowHeaderWidth` で行ヘッダ幅を広げられます。
352
352
  - フィルタとの関係: `filter()` は「絞り込んだ部分集合だけ描く」軽さ、`virtual` は「絞らず全件をスクロールする」軽さ。用途で使い分け(v1 では併用は未対応)。
353
353
 
354
+ ### ページネーション(サーバ側ページング)
355
+
356
+ ページネーションは**サーバの API 形(offset/limit・`total` 件数の有無)に依存**するので、**TssGrid 本体は「渡された1ページ分を描く」だけ**に徹します(コアに作り込みません)。ロジックはアプリ側で、**ページ変更 → fetch → `grid.setData(pageRows)`**、ページ番号ボタンや「N 件中 M〜」は**グリッドの外**に置きます。動く例: [`examples/pagination.html`](https://tssgrid.threestarsoftware.co.jp/examples/pagination.html)。
357
+
358
+ ```js
359
+ async function load() {
360
+ const { rows, total } = await fetchPage((page - 1) * size, size); // サーバの offset/limit
361
+ grid.setData(rows); // ← TssGrid は1ページ分を描くだけ
362
+ renderCount(total); // 「N 件中 M〜」はグリッドの外で
363
+ }
364
+ // 前/次/ページ番号は <button> で外に置き、go(p) → load() を呼ぶ
365
+ ```
366
+ - **`total` が返るサーバ**: 「全何件・何ページ」まで出せる(上の例)。**`total` が無い(cursor のみ)**: ページ番号は出せないので **`appendRows` で「もっと読む」=無限スクロール型**に。
367
+ - **1本の長いスクロールで必要分だけ取得**したいなら、ページ送りでなく `setRowCount(total)` + `onViewportChange` + `fillRows(start, rows)`(AG Grid の Infinite Row Model 相当・上の「メソッド」参照)。
368
+ - sort/filter をサーバ側でやる場合も同じ=**操作 → 再 fetch → `setData`**(遅延スクロール型なら `onSortFilterChange` で委譲)。
369
+
354
370
  ### 同梱プラグイン: ヘッダ オートフィルタ(filter UI)
355
371
 
356
372
  `plugins/tss-filter.js`+`.css`= 各見出しの**ロート(漏斗)アイコン**をクリックで**値チェックリスト+検索**のポップアップ(Excel風。ソートの▲▼と区別できるアイコン)。複数列の条件は **AND**。中身は上の**コア `filter()` を呼ぶだけ**。動く例: [`examples/filter-ui.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-ui.html)。
@@ -972,7 +988,7 @@ new TssGrid(el, {
972
988
  });
973
989
  ```
974
990
 
975
- メソッド: `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)` / `scrollToRow(r, align?)`(**指定行までスクロール=HTML の `#` アンカー相当**。`align` は `'start'`〔既定=先頭・sticky ヘッダ直下〕/`'center'`/`'end'`/`'nearest'`〔可視ならそのまま〕。仮想・非仮想どちらでも動き、範囲外 `r` は自動クランプ。**選択は変えない**ので目印は `rowClass` 等を併用。例 `examples/scroll-to-row.html`) / `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()`(列定義変更後の再描画, データ・選択は保持)ほか。
991
+ メソッド: `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)` / `scrollToRow(r, align?)`(**指定行までスクロール=HTML の `#` アンカー相当**。`align` は `'start'`〔既定=先頭・sticky ヘッダ直下〕/`'center'`/`'end'`/`'nearest'`〔可視ならそのまま〕。仮想・非仮想どちらでも動き、範囲外 `r` は自動クランプ。**選択は変えない**ので目印は `rowClass` 等を併用。例 `examples/scroll-to-row.html`) / `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()`(列定義変更後の再描画, データ・選択は保持) / `renderCell(r, cOrKey)`(**セル1つだけ表示を描き直す**=テキスト/HTML・条件付き書式・無効表示を最新データで。全体 `redraw()` より軽い。`format` が外部状態に依存する/セル内ボタンの状態を更新した等に。窓外/非表示は no-op)ほか。
976
992
 
977
993
  ## イベント
978
994
 
@@ -1046,8 +1062,8 @@ new TssGrid(el, {
1046
1062
  | `'text'`(既定) | テキスト入力 | そのまま |
1047
1063
  | `'dropdown'` | `<select>`(`options` 必須)。セルに ▾ を表示、**1クリックで一覧が開き**、選ぶと即確定 | 選択肢 or 空のみ許可。範囲外は拒否。**クリア用の空オプションは自動で先頭に付く**ので `options` に `''` を入れない(入れると空が二重)。`allowEmpty:false` で**空オプションを出さず空値も弾く**=ラジオ的な必須選択。**`options` は `string[]` または `{value,label}[]`=保存値(value=内部コード)と表示(label)を分離**(`getData`/`getRows` は value で返る・検証も value で照合) |
1048
1064
  | `'checkbox'` | Space / クリックでトグル | `checked`(既定 `'1'`) / `unchecked`(既定 `''`)。貼付値も正規化 |
1049
- | `'date'` | `<input type="date">` | `YYYY-MM-DD` のみ。実在しない日付(例 `2026-02-30`)は拒否 |
1050
- | `'time'` | `<input type="time">` / テキスト | `HH:MM[:SS]`(24時間制)で保存。`hour12:true` で AM/PM 表示・入力 |
1065
+ | `'date'` | `<input type="date">` | `YYYY-MM-DD` のみ。実在しない日付(例 `2026-02-30`)は拒否。**全角→半角を正規化**(IME ON のまま `2026/01/05` と打っても受理。区切りは `-` `/` `.` + 8桁 `20260105` も可) |
1066
+ | `'time'` | `<input type="time">` / テキスト | `HH:MM[:SS]`(24時間制)で保存。`hour12:true` で AM/PM 表示・入力。**全角→半角を正規化**(IME ON のまま `9:30` と打っても受理) |
1051
1067
  | `'number'` | テキスト(編集時は素の数値) | 数値のみ。`decimals`/`thousands`/`prefix`/`suffix` で表示書式(→ [CellFormat](#cellformat宣言的な表示書式数値--日付))。**全角→半角を既定で正規化**(`123`→`123`、`zenkaku:false`で無効化)。右寄せ |
1052
1068
 
1053
1069
  - **`validator(value, { r, c })`**: `true`/`undefined` で許可、`false` で拒否、**文字列を返すとエラーメッセージ付きで拒否**。型と併用可(型 → validator の順で評価)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threestarsoftware/tssgrid",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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
@@ -462,6 +462,9 @@ export declare class TssGrid {
462
462
  downloadCSV(filename: string, opts?: CsvOptions): void;
463
463
  /** 列定義変更後などの再描画(データ・選択は保持)。 */
464
464
  redraw(): void;
465
+ /** セル1つだけ表示を描き直す(テキスト/HTML・条件付き書式・無効表示を最新データで)。全体 redraw より軽い。
466
+ * format が外部状態に依存する/セル内ボタンの状態を更新した等に。`c` は列 index か data キー。窓外/非表示は no-op。 */
467
+ renderCell(r: number, cOrKey: number | string): void;
465
468
  /** 画像/フォント等の遅延レイアウト後に幾何キャッシュを再構築する保険。 */
466
469
  refreshGeometry(): void;
467
470
  /** グリッドを破棄(リスナ除去・DOM 撤去・idempotent)。フレームワークの teardown で呼ぶ。 */
package/src/tssgrid.js CHANGED
@@ -526,6 +526,13 @@
526
526
  }
527
527
  // 列定義(format/hour12 等)を変えた後の再描画。データと選択は保持。
528
528
  redraw() { this.buildTable(); this.setActive(this.active.r, this.active.c); }
529
+ // セル1つだけ表示を描き直す(テキスト/HTML・条件付き書式〔cellClass/cellStyle/rowClass〕・無効表示を最新データで)。
530
+ // 全体 redraw() より軽い。format が外部状態に依存する/セル内ボタンの状態を更新した等で「このセルだけ」を描き直す用。
531
+ // c は列 index か data キー。非表示・仮想で窓外のセルは安全に no-op(描画対象の DOM が無いだけ)。
532
+ renderCell(r, c) {
533
+ const ci = this._resolveCol(c); if (ci < 0 || r < 0 || r >= this.ROWS) return;
534
+ this._renderCell(r, ci);
535
+ }
529
536
 
530
537
  // ---- 並べ替え(A方式: データ実体を並べ替え。Undo には積まない/状態は行に追従) ----
531
538
  // 行を order(=新しい並びの「元index配列」)で再配置。data/rowH/_src と セル状態(整列/cellRO)も行ごと追従。
@@ -954,7 +961,8 @@
954
961
  static _colLabel(n) { let s = ''; n = n | 0; do { s = String.fromCharCode(65 + (n % 26)) + s; n = Math.floor(n / 26) - 1; } while (n >= 0); return s; }
955
962
  // 入力文字列 → 'yyyy-mm-dd'(不正は null)。yyyymmdd / yyyy-mm-dd / yyyy/mm/dd / yyyy.mm.dd を許容。
956
963
  static _parseDate(s) {
957
- s = String(s).trim(); if (s === '') return null;
964
+ s = TssGrid._zen2han(String(s).trim()); // IME ON の全角(2026/01/01 等)も半角化して受理
965
+ if (s === '') return null;
958
966
  let y, m, d;
959
967
  const sep = /^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})$/.exec(s);
960
968
  if (sep) { y = +sep[1]; m = +sep[2]; d = +sep[3]; }
@@ -965,7 +973,8 @@
965
973
  }
966
974
  // 入力文字列 → 24時間制 'HH:MM' / 'HH:MM:SS'(不正は null)。24h と 12h(AM/PM) の両方を受理。
967
975
  static _parseTime(s) {
968
- const m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?\s*([AaPp][.]?[Mm][.]?)?$/.exec(String(s).trim());
976
+ // IME ON の全角(9:30 等)も半角化して受理
977
+ const m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?\s*([AaPp][.]?[Mm][.]?)?$/.exec(TssGrid._zen2han(String(s).trim()));
969
978
  if (!m) return null;
970
979
  let h = +m[1]; const mi = +m[2], se = m[3] == null ? null : +m[3];
971
980
  const ap = m[4] ? m[4][0].toLowerCase() : null; // 'a' | 'p' | null
@@ -982,12 +991,14 @@
982
991
  return h + ':' + m[2] + (m[3] != null ? ':' + m[3] : '') + ' ' + ap;
983
992
  }
984
993
  // CellFormat: 入力(グルーピング/前後綴り混じり) → 正規化した数値文字列(保存値)。不正は null。
985
- // 全角→半角(数値文脈)。数字・符号・小数点・カンマ・%・¥・空白を半角化。業務フォームの IME 誤入力対策。
994
+ // 全角→半角(数値・日付・時刻文脈)。数字・符号・小数点・カンマ・%・¥・空白+日付/時刻の区切り(/:)を半角化。
995
+ // 業務フォームの IME 誤入力対策(IME ON のまま「2026/01/01」「9:30」等を打っても弾かず半角で受理)。
986
996
  static _zen2han(s) {
987
997
  return String(s)
988
998
  .replace(/[0-9]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0))
989
999
  .replace(/+/g, '+').replace(/[-−ー―]/g, '-')
990
1000
  .replace(/./g, '.').replace(/,/g, ',')
1001
+ .replace(///g, '/').replace(/:/g, ':')
991
1002
  .replace(/%/g, '%').replace(/¥/g, '¥').replace(/ /g, ' ');
992
1003
  }
993
1004
  static _parseNumber(v, cfg) {
@@ -1611,7 +1622,9 @@
1611
1622
  _columnCheckState(c) {
1612
1623
  let checked = 0, total = 0;
1613
1624
  for (let r = 0; r < this.ROWS; r++) {
1614
- if (this._rowBlankExcept(r, c)) continue;
1625
+ // 全選択は readOnly をスキップする(setColumnChecked と同じ)ので、状態判定も「操作できる行」だけで数える。
1626
+ // これを入れないと「選べる行は全部 ON なのに、押せない行のせいで − 表示」になる。
1627
+ if (this._rowBlankExcept(r, c) || this._isReadOnly(r, c)) continue;
1615
1628
  total++; if (this._isCheckedVal(c, this.data[r][c])) checked++;
1616
1629
  }
1617
1630
  return total === 0 ? 'none' : (checked === total ? 'all' : (checked === 0 ? 'none' : 'some'));
@@ -2642,8 +2655,8 @@
2642
2655
  case 'cut': return { label: '切り取り', disabled: !this.copyPaste, act: () => this.copy(true) };
2643
2656
  case 'paste': return { label: '貼り付け', disabled: !this.copyPaste, act: () => this.paste() };
2644
2657
  case 'clear': return { label: '内容をクリア', act: () => this.clearRange() };
2645
- case 'check_column': return this.colType(s.c0) === 'checkbox' ? { label: 'すべてチェック', act: () => this.setColumnChecked(s.c0, true) } : null;
2646
- case 'uncheck_column': return this.colType(s.c0) === 'checkbox' ? { label: 'すべて外す', act: () => this.setColumnChecked(s.c0, false) } : null;
2658
+ case 'check_column': return (this.colType(s.c0) === 'checkbox' && !this._columnAllReadOnly(s.c0)) ? { label: 'すべてチェック', act: () => this.setColumnChecked(s.c0, true) } : null; // 全 read-only 列は no-op なので出さない(ヘッダ無効と整合)
2659
+ case 'uncheck_column': return (this.colType(s.c0) === 'checkbox' && !this._columnAllReadOnly(s.c0)) ? { label: 'すべて外す', act: () => this.setColumnChecked(s.c0, false) } : null;
2647
2660
  case 'undo': return { label: '元に戻す', disabled: !this.history.canUndo(), act: () => this.history.undo() };
2648
2661
  case 'redo': return { label: 'やり直し', disabled: !this.history.canRedo(), act: () => this.history.redo() };
2649
2662
  default: return /^-+$/.test(key) || key === 'separator' ? { sep: true } : null;