@threestarsoftware/tssgrid 0.1.35 → 0.1.37

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,28 @@
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.37] - 2026-11-06
7
+
8
+ ### 追加(公式フレームワークアダプタ)
9
+ - **React / Vue 3 の薄い公式アダプタを追加**(`@threestarsoftware/tssgrid/react`・`@threestarsoftware/tssgrid/vue`)。競合(`ag-grid-react` / `@handsontable/react`)と同じ「コアはフレームワーク非依存のまま・薄いコンポーネントがライフサイクルと props を橋渡し」の形。**コアは無依存を維持**=`react` / `vue` は **peerDependency(任意・`peerDependenciesMeta.optional`)**、アダプタは `React.createElement` / `Vue.h` で手書き=**ビルド不要(UMD)**。
10
+ - グリッドは**アンコントロールド**:マウントで生成・アンマウントで `destroy()`、**cell を React/Vue で描かない**(高速)。値は **ref で公開した grid 本体(命令API)** + `onAfterChange` 等のコールバック。`data` プロップは参照が変わった時だけ `setData`(毎レンダー新配列を渡すとリセット=頻繁な更新は命令APIで)。
11
+ - React: `<TssGridReact columns={..} data={..} onAfterChange={..} ref={ref} />`、`ref.current` が grid 本体。StrictMode 二重マウント安全(`destroy` 冪等)、SSR/Next 安全(構築は `useEffect`=クライアント専用)。
12
+ - Vue: `<TssGridVue :columns :data :options="{ onAfterChange }" ref="g" />`、`g.grid()` が grid 本体。
13
+ - `columns/data/plugins/options` と全 `on*` コールバックをそのまま透過。型(`.d.ts`)付き。例 `examples/react.html` / `examples/vue.html`(CDN UMD のノービルドデモ)。
14
+
15
+ ## [0.1.36] - 2026-11-06
16
+
17
+ inline ピッカー3兄弟(**tss-time / tss-calendar / tss-datetime**)に共通して潜んでいた「ポップアップの `<select>` が使えない/確定がピッカー内部の値を書き戻す/編集中の表示が食い違う」を一掃。土台としてコアに2つの affordance を追加。
18
+
19
+ ### 修正(コア)
20
+ - **inline エディタの補助ポップアップ(`<select>` 等 focus を奪う要素を含む)へフォーカスが移っても編集を閉じない**ようにした。従来は共有 input が blur した瞬間に確定+ポップアップ破棄だったため、**時/分/年/月のプルダウンを開こうとクリックすると閉じてしまい選べなかった**。inline ctx に `setPopup(el)` を追加し、`blur` の `relatedTarget` が登録ポップアップ内なら編集継続。
21
+ - **カスタム `editor` を付けた date/time 列はネイティブ `<input type="date|time">` を重ねない**ようにした(`_nativeInputType` が `cfg.editor` を見る)。従来は `picker:false` を書き忘れるとネイティブ入力が裏に残り、**ブラウザのロケールで `09:00 AM` のように AM/PM 表示が出てセル表示(24h)と食い違っていた**。`picker:false` が無くてもカスタムエディタが入力/表示を一貫して握る。
22
+
23
+ ### 修正(プラグイン)
24
+ - **`tss-time`**: inline で**打鍵後にポップアップの[決定]/Enter を押すと、select が持つ元の値を書き戻して打鍵が消えていた**のを修正(inline は共有 input を確定・select を触ったら input へミラー+フォーカスを input へ戻す)。`hour12` 列で **AM/PM を切り替えると編集中だけ 24h 表記(`06:30`)がちらついた**のも、ミラーを 12h 表示形(`6:30 AM`・parser も受ける)にして解消。上記コア修正と合わせ、**時/分のプルダウンが選べる**ように。
25
+ - **`tss-calendar`**: **年/月のプルダウンを押すとポップアップが閉じて選べなかった**のを修正(`setPopup` 登録+選択後に共有 input へフォーカスを戻す)。値の確定(日クリック)・打鍵追従(`onInput`)は従来どおり。
26
+ - **`tss-datetime`**: 同じく**年/月/時/分のプルダウンが選べなかった**のに加え、**打鍵がピッカーに反映されない/ピッカー操作が共有 input に出ない/`<select>` 変更後にクリックアウトすると表示文字列がそのまま生保存されて保存値が壊れる**(`YYYY-MM-DDTHH:MM` にならない)という問題を一括修正。**`TssDatetime.parse`(表示形→保存形。`format` の逆)を新設**し、列に `parse` が無ければ自動付与=打鍵追従も確定(blur/Enter/決定)の round-trip も成立。ピッカー操作は表示形で共有 input にミラー+フォーカスを戻す。保存値は常に `YYYY-MM-DDTHH:MM`。
27
+
6
28
  ## [0.1.35] - 2026-11-06
7
29
 
8
30
  ### 追加(プラグイン)
package/README.md CHANGED
@@ -78,9 +78,39 @@ const { TssGrid, HistoryManager } = require('@threestarsoftware/tssgrid');
78
78
 
79
79
  ## フレームワークで使う(React / Vue / Angular)
80
80
 
81
- 専用ラッパーは**作りません**。vanilla on DOM なので CRA/Vite/Next/Webpack/Angular/Nuxt でそのまま動きます。コツは **マウント時に生成し、アンマウント時に `grid.destroy()`** するだけ(リスナ・ResizeObserver・DOM を後始末。idempotent)。
81
+ **React / Vue には薄い公式アダプタ**(`@threestarsoftware/tssgrid/react`・`/vue`)があります。コアは無依存のまま=`react`・`vue` は **peerDependency(任意)**、アダプタは `React.createElement` / `Vue.h` で手書き=**ノービルド**。グリッドは「アンコントロールド」(マウントで生成・アンマウントで `destroy()`・**cell は React/Vue で描かない**=高速)、値は **ref で公開した grid 本体(命令API)** + `onAfterChange` 等のコールバック、`data` プロップは「差し替え」用(参照が変わった時だけ `setData`)。動く例: [`examples/react.html`](https://tssgrid.threestarsoftware.co.jp/examples/react.html) / [`examples/vue.html`](https://tssgrid.threestarsoftware.co.jp/examples/vue.html)。
82
82
 
83
- **React**:
83
+ **React(公式アダプタ)**:
84
+ ```jsx
85
+ import { useRef } from 'react';
86
+ import { TssGridReact } from '@threestarsoftware/tssgrid/react';
87
+ import '@threestarsoftware/tssgrid/tssgrid.css';
88
+
89
+ function Grid({ columns, data }) {
90
+ const ref = useRef(null); // ref.current = grid 本体(命令API)
91
+ return <TssGridReact columns={columns} data={data}
92
+ onAfterChange={(ch, src) => console.log(ch, src)} ref={ref} />;
93
+ // 例: ref.current.sortBy('age','desc') / ref.current.getRows()
94
+ }
95
+ ```
96
+
97
+ **Vue 3(公式アダプタ)**:
98
+ ```vue
99
+ <script setup>
100
+ import { ref } from 'vue';
101
+ import { TssGridVue } from '@threestarsoftware/tssgrid/vue';
102
+ import '@threestarsoftware/tssgrid/tssgrid.css';
103
+ const g = ref(null); // g.value.grid() = grid 本体
104
+ </script>
105
+ <template>
106
+ <TssGridVue :columns="columns" :data="data"
107
+ :options="{ onAfterChange: (ch, src) => console.log(ch, src) }" ref="g" />
108
+ </template>
109
+ ```
110
+
111
+ アダプタを使わず**依存ゼロで直接**使うこともできます(vanilla on DOM。CRA/Vite/Next/Webpack/Angular/Nuxt でそのまま。コツは **マウント時に生成し、アンマウント時に `grid.destroy()`**)。
112
+
113
+ **React(直接)**:
84
114
  ```jsx
85
115
  import { useEffect, useRef } from 'react';
86
116
  import { TssGrid } from '@threestarsoftware/tssgrid';
@@ -562,6 +592,8 @@ new TssGrid(el, { columns: [{ type: 'time', editor: t }] });
562
592
  ```
563
593
  - `opts`: `step`(分刻み, 既定1) / `hour12`(AM/PM 表示, 既定false) / `icon`(既定 `'🕐'`) / `openOnClick`(既定true) / `className`。
564
594
  - 🕐 を**シングルクリックで開閉**、**現在**/**決定**ボタン、キーボード(Enter 決定 / Esc 取消)、外側クリックで採用、フィルで下へコピー。見た目は `--time-*` 変数と `tss-time-*` クラスで自由に。
595
+ - カスタム `editor` を付けた列は**ネイティブ `<input type="time">` を重ねません**(`picker:false` を書かなくてもOK)。`hour12` 列は編集中も **12h 表示**で一貫(保存は 24h)。
596
+ - **日付+時刻**は姉妹プラグイン `tss-datetime`(`editor: TssDatetime({ step: 5 }), format: TssDatetime.format`)。保存値は `'YYYY-MM-DDTHH:MM'`、表示は曜日つき。表示形→保存形の逆変換 **`TssDatetime.parse`**(列に `parse` 未指定なら自動付与)で、打鍵追従も確定の round-trip も成立。年/月/時/分のプルダウンは inline でもそのまま選べます。
565
597
 
566
598
  ### 同梱プラグイン: セルのコメント(tss-comment・Excel のメモ風)
567
599
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threestarsoftware/tssgrid",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
5
5
  "license": "MIT",
6
6
  "author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
@@ -67,9 +67,29 @@
67
67
  "types": "./plugins/tss-comment.d.ts",
68
68
  "default": "./plugins/tss-comment.js"
69
69
  },
70
+ "./react": {
71
+ "types": "./react/index.d.ts",
72
+ "default": "./react/index.js"
73
+ },
74
+ "./vue": {
75
+ "types": "./vue/index.d.ts",
76
+ "default": "./vue/index.js"
77
+ },
70
78
  "./plugins/*": "./plugins/*",
71
79
  "./package.json": "./package.json"
72
80
  },
81
+ "peerDependencies": {
82
+ "react": ">=16.8.0",
83
+ "vue": ">=3.0.0"
84
+ },
85
+ "peerDependenciesMeta": {
86
+ "react": {
87
+ "optional": true
88
+ },
89
+ "vue": {
90
+ "optional": true
91
+ }
92
+ },
73
93
  "unpkg": "src/tssgrid.js",
74
94
  "jsdelivr": "src/tssgrid.js",
75
95
  "sideEffects": [
@@ -83,6 +103,8 @@
83
103
  "src/tssgrid.css",
84
104
  "src/tssgrid.d.ts",
85
105
  "plugins/",
106
+ "react/",
107
+ "vue/",
86
108
  "README.md",
87
109
  "CHANGELOG.md",
88
110
  "LICENSE"
@@ -34,7 +34,7 @@
34
34
  var ty = new Date().getFullYear();
35
35
  var yrLo = opts.yearMin || (minD ? minD.getFullYear() : ty - 12);
36
36
  var yrHi = opts.yearMax || (maxD ? maxD.getFullYear() : ty + 12);
37
- var pop = null, view = null, sel = null, focus = null, ref = null, done = false, outside = null;
37
+ var pop = null, view = null, sel = null, focus = null, ref = null, done = false, outside = null, ictx = null;
38
38
  var inline = opts.inline !== false; // true=共有 input に乗る(直打ち可・矢印は本体に返す)/false=従来のフォーカス奪取型
39
39
 
40
40
  // ── 範囲選択(range): 連続した日を1回の編集で選ぶ(休暇申請の開始〜終了 等)───────────
@@ -134,6 +134,8 @@
134
134
  var t = e.target;
135
135
  if (t.classList && t.classList.contains('tss-cal-sel-yr')) { view = new Date(+t.value, view.getMonth(), 1); render(); }
136
136
  else if (t.classList && t.classList.contains('tss-cal-sel-mon')) { view = new Date(view.getFullYear(), +t.value, 1); render(); }
137
+ // 年/月を選んだ後は共有 input にフォーカスを戻す(select は render で作り直され、残すと焦点が宙に浮く)。
138
+ if (ictx && ictx.inline && ictx.input) { try { ictx.input.focus(); } catch (err) {} }
137
139
  }
138
140
  function onKey(e) {
139
141
  if (e.target && e.target.tagName === 'SELECT') return; // 年/月プルダウン操作中は日移動しない
@@ -161,9 +163,9 @@
161
163
  try { rc.td.title = rangeTipOf(aIso, bIso); } catch (e) { /* noop */ }
162
164
  }
163
165
  function clearTip() { if (rc && rc.td) { try { rc.td.title = ''; } catch (e) { /* noop */ } } }
164
- function show(anchor, value, onPick, onCancel, rangeCfg) {
166
+ function show(anchor, value, onPick, onCancel, rangeCfg, ctx) {
165
167
  teardown(); // 既に開いていれば先に片付ける(前の pop 除去+outside リスナ解除・open-while-open の取り残し防止)
166
- done = false; rc = rangeCfg || null; rangeStart = null; hoverEnd = null;
168
+ done = false; rc = rangeCfg || null; rangeStart = null; hoverEnd = null; ictx = ctx || null;
167
169
  rangeActive = rc ? (isRangeValue(value) || !!rc.def) : false; // 既存値が範囲 or rangeDefault なら ON で開く
168
170
  sel = parseISO(value);
169
171
  // range の既存値('YYYY/MM/DD~…' 等)は parseISO では取れない=先頭日を緩く読んで月を合わせる。
@@ -181,13 +183,14 @@
181
183
  ref = { pick: onPick, cancel: onCancel };
182
184
  // inline は「外側クリック/フォーカス」を本体(_commitActive / blur)が持つ=二重に閉じない・共有 input から
183
185
  // フォーカスを奪わない(=打ち続けられる)。矢印/Enter/Esc も本体の既定に返す(onKey は pop 未フォーカスなので発火しない)。
184
- if (inline) return;
186
+ // ★ ポップアップを本体に登録=年/月の <select> へフォーカスが移っても編集を閉じない(②)。
187
+ if (inline) { if (ictx && ictx.inline && ictx.setPopup) ictx.setPopup(pop); return; }
185
188
  // 外側クリックで閉じる。ただしアンカー(開いた元セル)は除外=再クリックはグリッド側でトグル処理させる
186
189
  outside = function (e) { if (pop && !pop.contains(e.target) && !(anchor && anchor.contains && anchor.contains(e.target))) cancel(); };
187
190
  setTimeout(function () { document.addEventListener('mousedown', outside, true); }, 0);
188
191
  pop.focus();
189
192
  }
190
- function teardown() { clearTip(); if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } }
193
+ function teardown() { clearTip(); if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } ictx = null; }
191
194
  function pick(d) {
192
195
  if (done) return;
193
196
  if (rc && rangeActive) { // トグル ON=2クリック範囲
@@ -206,7 +209,7 @@
206
209
  var rangeMode = typeof rangeOpt === 'function' ? !!rangeOpt(ctx) : !!rangeOpt; // この行で範囲を使えるか(半休行は false=トグル無し)
207
210
  var def = typeof rangeDefault === 'function' ? !!rangeDefault(ctx) : !!rangeDefault; // トグル初期ON/OFF(行ごと可)
208
211
  var cfg = rangeMode ? { td: ctx.td, def: def } : null;
209
- show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }, cfg);
212
+ show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }, cfg, ctx);
210
213
  },
211
214
  close: function () { teardown(); },
212
215
  icon: opts.icon != null ? opts.icon : '📅', // セル右端に表示する合図(opts.icon:'' で消せる)
@@ -38,8 +38,16 @@
38
38
  var yrLo = opts.yearMin || (minD ? minD.getFullYear() : ty - 12);
39
39
  var yrHi = opts.yearMax || (maxD ? maxD.getFullYear() : ty + 12);
40
40
 
41
- var pop = null, view = null, selD = null, selH = null, selM = null, ref = null, done = false, outside = null;
41
+ var pop = null, view = null, selD = null, selH = null, selM = null, ref = null, done = false, outside = null, ictx = null;
42
42
  var inline = opts.inline !== false; // true=共有 input に乗る(直打ち可・矢印は本体に返す)/false=従来のフォーカス奪取型
43
+ // inline: ピッカー操作の結果を共有 input に「表示形」で反映+フォーカスを input に戻す。
44
+ // これで select へ移っても閉じず(②)、編集中の表示も一致(④)、クリックアウト確定も壊れない。
45
+ function syncInput() {
46
+ if (!(ictx && ictx.inline && ictx.input)) return;
47
+ var v = value();
48
+ ictx.input.value = v === '' ? '' : TssDatetime.format(v, { weekLabels: weekLabels });
49
+ try { ictx.input.focus(); } catch (e) {}
50
+ }
43
51
 
44
52
  function dDisabled(d) { if (minD && d < minD) return true; if (maxD && d > maxD) return true; return false; }
45
53
 
@@ -123,10 +131,10 @@
123
131
  function onClick(e) {
124
132
  var t = e.target;
125
133
  var nav = t.getAttribute && t.getAttribute('data-nav');
126
- if (nav) { view = new Date(view.getFullYear(), view.getMonth() + (+nav), 1); render(); return; }
134
+ if (nav) { view = new Date(view.getFullYear(), view.getMonth() + (+nav), 1); render(); syncInput(); return; }
127
135
  var dd = t.getAttribute && t.getAttribute('data-d');
128
- if (dd) { readTime(); var d = parseISOd(dd); if (!dDisabled(d)) { selD = d; render(); } return; } // 日付選択しても閉じない(時刻も決めるため)
129
- if (t.classList.contains('tss-dt-now')) { var now = new Date(); selD = now; selH = now.getHours(); selM = Math.round(now.getMinutes() / step) * step; if (selM >= 60) selM = 60 - step; view = new Date(now.getFullYear(), now.getMonth(), 1); render(); return; }
136
+ if (dd) { readTime(); var d = parseISOd(dd); if (!dDisabled(d)) { selD = d; render(); syncInput(); } return; } // 日付選択しても閉じない(時刻も決めるため)
137
+ if (t.classList.contains('tss-dt-now')) { var now = new Date(); selD = now; selH = now.getHours(); selM = Math.round(now.getMinutes() / step) * step; if (selM >= 60) selM = 60 - step; view = new Date(now.getFullYear(), now.getMonth(), 1); render(); syncInput(); return; }
130
138
  if (t.classList.contains('tss-dt-clear')) { commit(''); return; }
131
139
  if (t.classList.contains('tss-dt-ok')) { commit(value()); return; }
132
140
  }
@@ -135,14 +143,15 @@
135
143
  if (t.classList.contains('tss-cal-sel-yr')) { view = new Date(+t.value, view.getMonth(), 1); render(); }
136
144
  else if (t.classList.contains('tss-cal-sel-mon')) { view = new Date(view.getFullYear(), +t.value, 1); render(); }
137
145
  else if (t.classList.contains('tss-time-h') || t.classList.contains('tss-time-m') || t.classList.contains('tss-time-ap')) { readTime(); var ro = pop.querySelector('.tss-dt-readout'); if (ro) ro.innerHTML = readoutHTML(); }
146
+ syncInput(); // 年/月/時/分/AM-PM いずれの変更も共有 input(表示形)へ反映+input へフォーカスを戻す
138
147
  }
139
148
  function onKey(e) {
140
149
  if (e.key === 'Escape') { e.preventDefault(); cancel(); }
141
150
  else if (e.key === 'Enter') { e.preventDefault(); commit(value()); }
142
151
  }
143
- function show(anchor, val, onPick, onCancel) {
152
+ function show(anchor, val, onPick, onCancel, ctx) {
144
153
  teardown(); // 既に開いていれば先に片付ける(open-while-open の取り残し防止・tss-calendar と同方針)
145
- done = false;
154
+ done = false; ictx = ctx || null;
146
155
  var p = parseDT(val), tp = p ? null : parseTimeOnly(val); // 日付付き or 時刻のみ
147
156
  selD = p ? p.d : null;
148
157
  selH = p ? p.h : (tp ? tp.h : null);
@@ -156,21 +165,36 @@
156
165
  pop.addEventListener('keydown', onKey);
157
166
  render(); place(anchor);
158
167
  ref = { pick: onPick, cancel: onCancel };
159
- if (inline) return; // inline は外側クリック/フォーカスを本体が持つ=共有 input から奪わない(打ち続けられる)
168
+ // ★ ポップアップを本体に登録=年/月/時/分の <select> へフォーカスが移っても編集を閉じない(②)。
169
+ if (inline) { if (ictx && ictx.inline && ictx.setPopup) ictx.setPopup(pop); return; } // inline は外側クリック/フォーカスを本体が持つ(打ち続けられる)
160
170
  outside = function (e) { if (pop && !pop.contains(e.target) && !(anchor && anchor.contains && anchor.contains(e.target))) commit(value()); }; // 外側クリックは確定(time と同方針)
161
171
  setTimeout(function () { document.addEventListener('mousedown', outside, true); }, 0);
162
172
  pop.focus();
163
173
  }
164
- function teardown() { if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } }
174
+ function teardown() { if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } ictx = null; }
165
175
  function commit(v) { if (done) return; done = true; var f = ref.pick; teardown(); f(v); }
166
176
  function cancel() { if (done) return; done = true; var f = ref.cancel; teardown(); if (f) f(); }
167
177
 
168
178
  return {
169
- open: function (ctx) { show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }); },
179
+ open: function (ctx) {
180
+ // 列に parse が無ければ付与=表示形↔保存形の round-trip(打鍵追従・blur/Enter 確定)を成立させる。
181
+ if (ctx.grid && typeof ctx.c === 'number') { var cc = ctx.grid.colCfg(ctx.c); if (cc && typeof cc.parse !== 'function') cc.parse = TssDatetime.parse; }
182
+ show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }, ctx);
183
+ },
170
184
  close: function () { teardown(); },
171
185
  icon: opts.icon != null ? opts.icon : '📅',
172
186
  openOnClick: opts.openOnClick !== false,
173
187
  inline: inline, // true=共有 input に乗る=日時をそのまま直打ちできる(矢印/Enter/Esc は本体の既定)
188
+ // inline のみ: 打った内容にピッカーが追従(ctx.parseCell=セルの確定と同じ読み方。上で付けた parse を使う)。
189
+ onInput: inline ? function (v, ctx) {
190
+ if (!pop) return;
191
+ var iso = null; try { iso = ctx.parseCell ? ctx.parseCell(v) : v; } catch (e) { iso = null; }
192
+ var p = parseDT(iso), tp = p ? null : parseTimeOnly(iso);
193
+ if (!p && !tp) return; // 打ちかけ/不正はピッカーを動かさない
194
+ if (p) { selD = p.d; if (p.h != null) selH = p.h; if (p.mi != null) selM = p.mi; view = new Date(p.d.getFullYear(), p.d.getMonth(), 1); }
195
+ else { selH = tp.h; selM = tp.m; }
196
+ render();
197
+ } : undefined,
174
198
  openAt: function (anchorEl, val, onPick) { show(anchorEl, val, function (v) { onPick(v); }, null); },
175
199
  };
176
200
  }
@@ -185,6 +209,23 @@
185
209
  return d.getFullYear() + '/' + pad(d.getMonth() + 1) + '/' + pad(d.getDate()) + wd + t;
186
210
  };
187
211
 
212
+ // 表示形 → 保存形。format の逆。'YYYY/MM/DD(曜) HH:MM'・'YYYY-MM-DDTHH:MM'・'YYYY/MM/DD'・'HH:MM' を受け、
213
+ // 'YYYY-MM-DDTHH:MM'(または 'YYYY-MM-DD' / 'HH:MM')へ正規化。これを列 parse に付けると、打鍵追従も
214
+ // 確定(blur/Enter/決定)の round-trip も成立する(inline で editor が入力/表示を握るのに必須)。解釈不可は素通し。
215
+ TssDatetime.parse = function (v) {
216
+ var s = String(v == null ? '' : v).trim();
217
+ if (s === '') return '';
218
+ var m = /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})(?:\s*\([^)]*\))?(?:[ T]+(\d{1,2}):(\d{2}))?/.exec(s);
219
+ if (m) {
220
+ var iso = m[1] + '-' + pad(+m[2]) + '-' + pad(+m[3]);
221
+ if (m[4] != null) iso += 'T' + pad(+m[4]) + ':' + pad(+m[5]);
222
+ return iso;
223
+ }
224
+ var t = /^(\d{1,2}):(\d{2})$/.exec(s);
225
+ if (t) return pad(+t[1]) + ':' + pad(+t[2]);
226
+ return s;
227
+ };
228
+
188
229
  if (typeof module !== 'undefined' && module.exports) module.exports = TssDatetime;
189
230
  else root.TssDatetime = TssDatetime;
190
231
  })(typeof self !== 'undefined' ? self : this);
@@ -15,7 +15,7 @@
15
15
  opts = opts || {};
16
16
  var step = Math.max(1, opts.step || 1);
17
17
  var hour12 = !!opts.hour12;
18
- var pop = null, ref = null, done = false, outside = null;
18
+ var pop = null, ref = null, done = false, outside = null, ictx = null; // ictx: inline の ctx(共有 input が真実)
19
19
  var inline = opts.inline !== false; // true=共有 input に乗る(直打ち可・矢印は本体に返す)/false=従来のフォーカス奪取型
20
20
 
21
21
  function opt(val, label, sel) { return '<option value="' + val + '"' + (sel ? ' selected' : '') + '>' + label + '</option>'; }
@@ -45,10 +45,24 @@
45
45
  if (hour12 && aS) { var ap = aS.value; h = ap === 'PM' ? (h === 12 ? 12 : h + 12) : (h === 12 ? 0 : h); }
46
46
  return pad(h) + ':' + pad(m);
47
47
  }
48
+ // 共有 input に見せる文字列。hour12 は 12h 表示形("6:30 PM"=列のセル表示と一致・parser も受ける)、
49
+ // それ以外は 24h(= 保存値 = 表示)。これで編集中に 24h がちらっと出る食い違いを無くす。
50
+ function readDisplay() {
51
+ var hS = pop.querySelector('.tss-time-h'), mS = pop.querySelector('.tss-time-m'), aS = pop.querySelector('.tss-time-ap');
52
+ if (!hS || hS.value === '' || mS.value === '') return readValue();
53
+ if (hour12 && aS) return (+hS.value) + ':' + pad(+mS.value) + ' ' + aS.value;
54
+ return readValue();
55
+ }
48
56
  function setNow() {
49
57
  var d = new Date(), m = Math.round(d.getMinutes() / step) * step; if (m >= 60) m = 60 - step;
50
58
  render({ h: d.getHours(), m: m });
59
+ mirrorToInput(); // inline: 現在ボタンの結果も共有 input へ
51
60
  }
61
+ // inline のとき、select の内容を共有 input に流し込む(確定はしない)。これで「打つ」と「選ぶ」が input に集約。
62
+ // 選び終えたらフォーカスを共有 input に戻す=以降の「外クリックで確定」等が本体の既定どおり効く。
63
+ function mirrorToInput() { if (ictx && ictx.inline && ictx.input) { if (ictx.setValue) { var v = readDisplay(); if (v !== '') ictx.input.value = v; } try { ictx.input.focus(); } catch (e) {} } }
64
+ // 確定: inline は共有 input(打鍵 or ミラー済み)を真実として確定/非 inline は select を真実に。
65
+ function commitFinal() { if (ictx && ictx.inline && ictx.input) commit(ictx.input.value); else commit(readValue()); }
52
66
  function place(anchor) {
53
67
  var r = anchor.getBoundingClientRect();
54
68
  pop.style.left = Math.max(2, Math.min(r.left, window.innerWidth - pop.offsetWidth - 6)) + 'px';
@@ -58,34 +72,35 @@
58
72
  }
59
73
  function onClick(e) {
60
74
  if (e.target.classList.contains('tss-time-now')) { setNow(); return; }
61
- if (e.target.classList.contains('tss-time-ok')) { commit(readValue()); }
75
+ if (e.target.classList.contains('tss-time-ok')) { commitFinal(); }
62
76
  }
63
77
  function onKey(e) {
64
78
  if (e.key === 'Escape') { e.preventDefault(); cancel(); }
65
- else if (e.key === 'Enter') { e.preventDefault(); commit(readValue()); }
79
+ else if (e.key === 'Enter') { e.preventDefault(); commitFinal(); }
66
80
  }
67
- function show(anchor, value, onPick, onCancel) {
81
+ function show(anchor, value, onPick, onCancel, ctx) {
68
82
  teardown(); // 既に開いていれば先に片付ける(open-while-open の取り残し防止・tss-calendar と同方針)
69
- done = false;
83
+ done = false; ictx = ctx || null;
70
84
  pop = document.createElement('div'); pop.className = 'tss-time' + (opts.className ? ' ' + opts.className : ''); pop.tabIndex = -1;
71
85
  document.body.appendChild(pop);
72
86
  pop.addEventListener('mousedown', function (e) { if (e.target.tagName !== 'SELECT') e.preventDefault(); });
73
87
  pop.addEventListener('click', onClick);
74
88
  pop.addEventListener('keydown', onKey);
89
+ pop.addEventListener('change', function (e) { if (e.target.tagName === 'SELECT') mirrorToInput(); }); // inline: select を選んだら共有 input に反映(打鍵と集約)
75
90
  render(parseHM(value)); place(anchor);
76
91
  ref = { pick: onPick, cancel: onCancel };
77
- if (inline) return; // inline は外側クリック/フォーカスを本体が持つ=共有 input から奪わない(打ち続けられる)
92
+ if (inline) { if (ictx && ictx.inline && ictx.setPopup) ictx.setPopup(pop); return; } // inline: ポップアップを本体に登録=select へフォーカスが移っても編集を閉じない。外側クリック/フォーカスは本体が持つ(打ち続けられる)
78
93
  // 外側クリックは「確定」(時/分を選んで離す=採用)。未変更なら同値 commit=実質no-op。アンカーは除外(再クリックはグリッドがトグル)
79
94
  outside = function (e) { if (pop && !pop.contains(e.target) && !(anchor && anchor.contains && anchor.contains(e.target))) commit(readValue()); };
80
95
  setTimeout(function () { document.addEventListener('mousedown', outside, true); }, 0);
81
96
  pop.focus();
82
97
  }
83
- function teardown() { if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } }
98
+ function teardown() { if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } ictx = null; }
84
99
  function commit(v) { if (done) return; done = true; var f = ref.pick; teardown(); f(v); }
85
100
  function cancel() { if (done) return; done = true; var f = ref.cancel; teardown(); if (f) f(); }
86
101
 
87
102
  return {
88
- open: function (ctx) { show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }); },
103
+ open: function (ctx) { show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }, ctx); },
89
104
  close: function () { teardown(); },
90
105
  icon: opts.icon != null ? opts.icon : '🕐',
91
106
  openOnClick: opts.openOnClick !== false,
@@ -0,0 +1,19 @@
1
+ // TssGridReact — TssGrid の薄い React アダプタの型。
2
+ import * as React from 'react';
3
+ import { TssGrid, TssGridOptions } from '../src/tssgrid';
4
+
5
+ // トップレベルに書ける定番オプション+全 on* コールバックは Partial<TssGridOptions> として受ける。
6
+ // それ以外は options={{…}} でまとめて渡す。className / style は host の <div> に付く。
7
+ export interface TssGridReactProps extends Partial<TssGridOptions> {
8
+ className?: string;
9
+ style?: React.CSSProperties;
10
+ options?: TssGridOptions;
11
+ }
12
+
13
+ // ref には grid 本体(命令API)が入る。
14
+ declare const TssGridReact: React.ForwardRefExoticComponent<
15
+ TssGridReactProps & React.RefAttributes<TssGrid>
16
+ >;
17
+
18
+ export default TssGridReact;
19
+ export { TssGridReact };
package/react/index.js ADDED
@@ -0,0 +1,74 @@
1
+ /*! TssGridReact — TssGrid の薄い React アダプタ(UMD・ノービルド・react は peerDependency)
2
+ *
3
+ * 方針(競合 ag-grid-react / @handsontable/react と同じ):
4
+ * - グリッドは「アンコントロールド」= マウント時に1回生成し、アンマウントで destroy()。cell を React で描かない(高速)。
5
+ * - 値の読み書きは ref 経由の命令API(grid 本体)+ onAfterChange 等のコールバック。data プロップは「差し替え」用。
6
+ * - 全ての on* コールバック(onAfterChange / onEditEnd …)と columns/data/plugins/options をそのまま TssGrid へ渡す。
7
+ *
8
+ * 使い方:
9
+ * import { TssGridReact } from '@threestarsoftware/tssgrid/react';
10
+ * import '@threestarsoftware/tssgrid/tssgrid.css';
11
+ * const ref = useRef(null);
12
+ * <TssGridReact columns={cols} data={rows} plugins={[...]}
13
+ * onAfterChange={(ch, src) => ...} ref={ref} />
14
+ * // 命令API: ref.current.sortBy('col','desc') / ref.current.getRows() など(grid 本体)
15
+ *
16
+ * 注意:
17
+ * - data は「参照が変わった時だけ」setData されます(毎レンダー新配列を渡すと毎回リセット=カーソルも飛ぶ)。
18
+ * 頻繁な更新は ref の命令API(setValue/ appendRows 等)を使うのが定石。
19
+ * - 構築は useEffect(クライアント専用)なので SSR/Next でもそのまま(サーバでは空 div)。columns を変えたい時は key で remount。
20
+ */
21
+ (function (root, factory) {
22
+ if (typeof module !== 'undefined' && module.exports) {
23
+ module.exports = factory(require('react'), require('../src/tssgrid.js'));
24
+ } else {
25
+ root.TssGridReact = factory(root.React, { TssGrid: root.TssGrid });
26
+ }
27
+ })(typeof self !== 'undefined' ? self : this, function (React, core) {
28
+ 'use strict';
29
+ var TssGrid = core && (core.TssGrid || core);
30
+ var h = React.createElement;
31
+ // トップレベルに書ける定番オプション(それ以外は options={{…}} で)。
32
+ var PASS = { columns: 1, headers: 1, data: 1, plugins: 1, name: 1, width: 1, height: 1, rowHeight: 1, rowHeaders: 1, colHeaders: 1, minSpareRows: 1, maxRows: 1, history: 1, virtual: 1, hiddenColumns: 1 };
33
+
34
+ function buildOptions(props) {
35
+ var o = {};
36
+ if (props.options) for (var k in props.options) o[k] = props.options[k];
37
+ for (var key in props) {
38
+ if (PASS[key]) o[key] = props[key];
39
+ else if (/^on[A-Z]/.test(key) && typeof props[key] === 'function') o[key] = props[key]; // on* コールバック総通し
40
+ }
41
+ return o;
42
+ }
43
+
44
+ var TssGridReact = React.forwardRef(function (props, ref) {
45
+ var hostRef = React.useRef(null);
46
+ var gridRef = React.useRef(null);
47
+ var propsRef = React.useRef(props);
48
+ propsRef.current = props;
49
+ var firstData = React.useRef(true);
50
+ var readyState = React.useState(0); // grid 生成後に ref(命令API)を更新させるためのバンプ
51
+
52
+ React.useEffect(function () { // 生成は1回(アンコントロールド)。StrictMode 二重でも destroy が冪等。
53
+ var grid = new TssGrid(hostRef.current, buildOptions(propsRef.current));
54
+ gridRef.current = grid;
55
+ firstData.current = true;
56
+ readyState[1](function (n) { return n + 1; }); // ref.current = grid 本体 に更新(useImperativeHandle を再評価)
57
+ return function () { try { grid.destroy(); } catch (e) {} if (gridRef.current === grid) gridRef.current = null; };
58
+ }, []);
59
+
60
+ React.useEffect(function () { // data の参照が変わったら差し替え(初回=生成時の data を使うのでスキップ)
61
+ if (firstData.current) { firstData.current = false; return; }
62
+ var g = gridRef.current;
63
+ if (g && propsRef.current.data != null) g.setData(propsRef.current.data);
64
+ }, [props.data]);
65
+
66
+ React.useImperativeHandle(ref, function () { return gridRef.current; }, [readyState[0]]); // grid 本体を命令API として公開(生成後に更新)
67
+
68
+ return h('div', { ref: hostRef, className: props.className, style: props.style });
69
+ });
70
+ TssGridReact.displayName = 'TssGridReact';
71
+ TssGridReact.TssGridReact = TssGridReact; // import { TssGridReact } / import TssGridReact どちらも通す
72
+ TssGridReact.default = TssGridReact;
73
+ return TssGridReact;
74
+ });
package/src/tssgrid.js CHANGED
@@ -159,7 +159,7 @@
159
159
  this._headerSel = null; // 'col'/'row'/'all'=列/行/全選択モード(結合に拡張せず実列/行にクランプ=Excel流)。null=通常セル選択
160
160
  this._selCols = new Set(); // 飛び飛びの列選択(Ctrl+ヘッダクリック)。_headerSel==='col' の時だけ有効。連続選択は矩形と一致
161
161
  this._selRows = new Set(); // 飛び飛びの行選択(Ctrl+行ヘッダクリック)。_headerSel==='row' の時だけ有効
162
- this._inlineEd = null; this._inlineCtx = null; // inline エディタ(editor.inline=true): 共有 input に乗るエディタの現在値
162
+ this._inlineEd = null; this._inlineCtx = null; this._inlineEl = null; // inline エディタ(editor.inline=true): 共有 input に乗るエディタ。_inlineEl=補助ポップアップ参照
163
163
  this.disjointSelect = opts.disjointSelect !== false; // Ctrl+ヘッダで行/列を飛び飛び複数選択(false で無効=Ctrl+クリックは単一選択に戻る)
164
164
  // リサイズ(列幅・行高)
165
165
  // 列幅 colW[c]=px(未指定は既定)。指定方法は3通り、優先度=実行時リサイズ/明示colW > columns[c].width > 既定:
@@ -1158,7 +1158,9 @@
1158
1158
  // ネイティブ入力(date/time ピッカー)を使う列か。使わない=テキスト入力。
1159
1159
  _nativeInputType(c) {
1160
1160
  const cfg = this.colCfg(c), t = cfg.type;
1161
- if ((t === 'date' || t === 'time') && cfg.picker !== false && !cfg.format && typeof cfg.parse !== 'function' && !cfg.hour12) return t;
1161
+ // カスタム editor を付けた列は、そのエディタが入力/表示を握る=ネイティブ picker を重ねない
1162
+ // (重ねるとブラウザのロケールで AM/PM 等が出て、セル表示と食い違う)。picker:false を書き忘れても破綻しない。
1163
+ if ((t === 'date' || t === 'time') && cfg.picker !== false && !cfg.format && typeof cfg.parse !== 'function' && !cfg.hour12 && !cfg.editor) return t;
1162
1164
  return null;
1163
1165
  }
1164
1166
  _usesTextEditor(c) {
@@ -2003,12 +2005,15 @@
2003
2005
  setValue(v) { grid.editor.value = grid._cellStr(v); }, // 候補で置き換える用(確定はしない)
2004
2006
  commit(v) { if (v != null) grid.editor.value = grid._cellStr(v); grid.commit(); grid.toNav(); grid.updateSelectionUI(); },
2005
2007
  cancel() { grid.setActive(r, c); }, // 元値に戻して nav(Esc 相当)
2008
+ // inline 補助ポップアップ(<select> 等 focus を奪う要素を含む)を登録=その中へフォーカスが移っても編集を閉じない。
2009
+ setPopup(el) { grid._inlineEl = el || null; },
2006
2010
  };
2007
2011
  this._inlineEd = ed; this._inlineCtx = ctx;
2008
2012
  try { ed.open(ctx); } catch (_) { this._inlineEd = null; this._inlineCtx = null; }
2009
2013
  }
2010
2014
  _closeInlineEditor() {
2011
2015
  const ed = this._inlineEd;
2016
+ this._inlineEl = null; // 登録された補助ポップアップ参照を解除
2012
2017
  if (!ed) return;
2013
2018
  this._inlineEd = null; this._inlineCtx = null;
2014
2019
  if (ed.close) { try { ed.close(); } catch (_) {} }
@@ -3095,8 +3100,11 @@
3095
3100
  else if (t === 'date' || t === 'time') this.toEdit(this.data[this.active.r][c], true); // ネイティブピッカー
3096
3101
  // checkbox は文字入力を無視(Space / クリックでトグル)
3097
3102
  });
3098
- ed.addEventListener('blur', () => {
3103
+ ed.addEventListener('blur', (e) => {
3099
3104
  if (this._customEditor) return; // カスタムエディタが奪ったフォーカスでの blur は無視
3105
+ // inline エディタの補助ポップアップ(<select> 等)へフォーカスが移った blur は編集継続(閉じない)。
3106
+ const to = (e && e.relatedTarget) || document.activeElement;
3107
+ if (this._inlineEl && to && this._inlineEl.contains(to)) return;
3100
3108
  if (this.mode === 'edit' && (!this.select || this.select.style.display === 'none')) { this.commit(); this.toNav(); this.updateSelectionUI(); }
3101
3109
  });
3102
3110
  });
package/vue/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ // TssGridVue — TssGrid の薄い Vue 3 アダプタの型。
2
+ import { DefineComponent } from 'vue';
3
+ import { TssGrid, TssGridOptions } from '../src/tssgrid';
4
+
5
+ export type TssGridVueProps = Partial<TssGridOptions> & { options?: TssGridOptions };
6
+
7
+ // expose: grid() で grid 本体(命令API)を返す。
8
+ export interface TssGridVueExposed {
9
+ grid(): TssGrid | null;
10
+ }
11
+
12
+ declare const TssGridVue: DefineComponent<TssGridVueProps>;
13
+
14
+ export default TssGridVue;
15
+ export { TssGridVue };
package/vue/index.js ADDED
@@ -0,0 +1,56 @@
1
+ /*! TssGridVue — TssGrid の薄い Vue 3 アダプタ(UMD・ノービルド・vue は peerDependency)
2
+ *
3
+ * 方針(React アダプタと同じ): グリッドはアンコントロールド。マウントで生成・アンマウントで destroy()。
4
+ * 値の読み書きは expose した grid 本体(命令API)+ on* コールバック。data プロップは「差し替え」用。
5
+ *
6
+ * 使い方:
7
+ * import { TssGridVue } from '@threestarsoftware/tssgrid/vue';
8
+ * import '@threestarsoftware/tssgrid/tssgrid.css';
9
+ * <TssGridVue :columns="cols" :data="rows" :plugins="[...]" :options="{ onAfterChange }" ref="g" />
10
+ * // 命令API: this.$refs.g.grid().sortBy('col','desc') など(grid 本体を返す)
11
+ *
12
+ * 注意: on* コールバックは :options(例 :options="{ onAfterChange: fn }")か、@after-change などの属性で渡せます。
13
+ * data は参照が変わった時だけ setData(毎回新配列はリセット=カーソルが飛ぶ)。頻繁な更新は grid の命令APIで。
14
+ */
15
+ (function (root, factory) {
16
+ if (typeof module !== 'undefined' && module.exports) {
17
+ module.exports = factory(require('vue'), require('../src/tssgrid.js'));
18
+ } else {
19
+ root.TssGridVue = factory(root.Vue, { TssGrid: root.TssGrid });
20
+ }
21
+ })(typeof self !== 'undefined' ? self : this, function (Vue, core) {
22
+ 'use strict';
23
+ var TssGrid = core && (core.TssGrid || core);
24
+ var PASS = ['columns', 'data', 'plugins', 'headers', 'name', 'width', 'height', 'rowHeight', 'rowHeaders', 'colHeaders', 'minSpareRows', 'maxRows', 'history', 'virtual', 'hiddenColumns'];
25
+
26
+ var TssGridVue = Vue.defineComponent({
27
+ name: 'TssGridVue',
28
+ inheritAttrs: false, // on* 属性を div に二重付けしない
29
+ props: {
30
+ columns: Array, data: Array, plugins: Array, options: Object, headers: Array,
31
+ name: String, width: [Number, String], height: [Number, String],
32
+ rowHeight: Number, rowHeaders: { type: Boolean, default: undefined }, minSpareRows: Number,
33
+ },
34
+ setup: function (props, ctx) {
35
+ var el = Vue.ref(null);
36
+ var grid = null;
37
+ function buildOptions() {
38
+ var o = {};
39
+ if (props.options) for (var k in props.options) o[k] = props.options[k];
40
+ PASS.forEach(function (key) { if (props[key] !== undefined) o[key] = props[key]; });
41
+ var attrs = ctx.attrs || {};
42
+ for (var a in attrs) { if (/^on[A-Z]/.test(a) && typeof attrs[a] === 'function') o[a] = attrs[a]; } // @after-change 等=attrs の onAfterChange
43
+ return o;
44
+ }
45
+ Vue.onMounted(function () { grid = new TssGrid(el.value, buildOptions()); });
46
+ Vue.onBeforeUnmount(function () { if (grid) { try { grid.destroy(); } catch (e) {} grid = null; } });
47
+ Vue.watch(function () { return props.data; }, function (d) { if (grid && d != null) grid.setData(d); }); // 参照が変わったら差し替え
48
+ ctx.expose({ grid: function () { return grid; } }); // grid 本体を命令API として公開
49
+ return function () { return Vue.h('div', { ref: el }); };
50
+ },
51
+ });
52
+
53
+ TssGridVue.TssGridVue = TssGridVue;
54
+ TssGridVue.default = TssGridVue;
55
+ return TssGridVue;
56
+ });