@threestarsoftware/tssgrid 0.1.35 → 0.1.36

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,19 @@
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.36] - 2026-11-06
7
+
8
+ inline ピッカー3兄弟(**tss-time / tss-calendar / tss-datetime**)に共通して潜んでいた「ポップアップの `<select>` が使えない/確定がピッカー内部の値を書き戻す/編集中の表示が食い違う」を一掃。土台としてコアに2つの affordance を追加。
9
+
10
+ ### 修正(コア)
11
+ - **inline エディタの補助ポップアップ(`<select>` 等 focus を奪う要素を含む)へフォーカスが移っても編集を閉じない**ようにした。従来は共有 input が blur した瞬間に確定+ポップアップ破棄だったため、**時/分/年/月のプルダウンを開こうとクリックすると閉じてしまい選べなかった**。inline ctx に `setPopup(el)` を追加し、`blur` の `relatedTarget` が登録ポップアップ内なら編集継続。
12
+ - **カスタム `editor` を付けた date/time 列はネイティブ `<input type="date|time">` を重ねない**ようにした(`_nativeInputType` が `cfg.editor` を見る)。従来は `picker:false` を書き忘れるとネイティブ入力が裏に残り、**ブラウザのロケールで `09:00 AM` のように AM/PM 表示が出てセル表示(24h)と食い違っていた**。`picker:false` が無くてもカスタムエディタが入力/表示を一貫して握る。
13
+
14
+ ### 修正(プラグイン)
15
+ - **`tss-time`**: inline で**打鍵後にポップアップの[決定]/Enter を押すと、select が持つ元の値を書き戻して打鍵が消えていた**のを修正(inline は共有 input を確定・select を触ったら input へミラー+フォーカスを input へ戻す)。`hour12` 列で **AM/PM を切り替えると編集中だけ 24h 表記(`06:30`)がちらついた**のも、ミラーを 12h 表示形(`6:30 AM`・parser も受ける)にして解消。上記コア修正と合わせ、**時/分のプルダウンが選べる**ように。
16
+ - **`tss-calendar`**: **年/月のプルダウンを押すとポップアップが閉じて選べなかった**のを修正(`setPopup` 登録+選択後に共有 input へフォーカスを戻す)。値の確定(日クリック)・打鍵追従(`onInput`)は従来どおり。
17
+ - **`tss-datetime`**: 同じく**年/月/時/分のプルダウンが選べなかった**のに加え、**打鍵がピッカーに反映されない/ピッカー操作が共有 input に出ない/`<select>` 変更後にクリックアウトすると表示文字列がそのまま生保存されて保存値が壊れる**(`YYYY-MM-DDTHH:MM` にならない)という問題を一括修正。**`TssDatetime.parse`(表示形→保存形。`format` の逆)を新設**し、列に `parse` が無ければ自動付与=打鍵追従も確定(blur/Enter/決定)の round-trip も成立。ピッカー操作は表示形で共有 input にミラー+フォーカスを戻す。保存値は常に `YYYY-MM-DDTHH:MM`。
18
+
6
19
  ## [0.1.35] - 2026-11-06
7
20
 
8
21
  ### 追加(プラグイン)
package/README.md CHANGED
@@ -562,6 +562,8 @@ new TssGrid(el, { columns: [{ type: 'time', editor: t }] });
562
562
  ```
563
563
  - `opts`: `step`(分刻み, 既定1) / `hour12`(AM/PM 表示, 既定false) / `icon`(既定 `'🕐'`) / `openOnClick`(既定true) / `className`。
564
564
  - 🕐 を**シングルクリックで開閉**、**現在**/**決定**ボタン、キーボード(Enter 決定 / Esc 取消)、外側クリックで採用、フィルで下へコピー。見た目は `--time-*` 変数と `tss-time-*` クラスで自由に。
565
+ - カスタム `editor` を付けた列は**ネイティブ `<input type="time">` を重ねません**(`picker:false` を書かなくてもOK)。`hour12` 列は編集中も **12h 表示**で一貫(保存は 24h)。
566
+ - **日付+時刻**は姉妹プラグイン `tss-datetime`(`editor: TssDatetime({ step: 5 }), format: TssDatetime.format`)。保存値は `'YYYY-MM-DDTHH:MM'`、表示は曜日つき。表示形→保存形の逆変換 **`TssDatetime.parse`**(列に `parse` 未指定なら自動付与)で、打鍵追従も確定の round-trip も成立。年/月/時/分のプルダウンは inline でもそのまま選べます。
565
567
 
566
568
  ### 同梱プラグイン: セルのコメント(tss-comment・Excel のメモ風)
567
569
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threestarsoftware/tssgrid",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
5
5
  "license": "MIT",
6
6
  "author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
@@ -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,
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
  });