@threestarsoftware/tssgrid 0.0.10 → 0.1.0

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,16 @@
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.0] - 2026-07-17
7
+
8
+ ### 追加
9
+ - **`TssAutocomplete({ inline: true })`=「セルに打ちながら候補を絞る」サジェスト**(プラグイン)— 上記 `editor.inline` に載せた版。従来の同プラグインは**自前 input を持つポップアップ**(クリック/F2 で開き、その中に打つ)だったため、**セルに直接打ち始める/IME 直打ちで始める**経路が使えなかった(サンプルは「開いてから打つ」ので症状が見えていなかった)。`inline: true` を付けると自前 input を作らずフォーカスも奪わず、**打鍵の1文字目も IME 直打ちも素のテキスト列と同じ**に効き、候補リストだけを出す。**IME 変換中の ↑↓/Enter は奪わない**(変換確定が最優先=従来からの流儀を維持)。確定は本体 `commit()`=関所を通る。`examples/autocomplete.html` を inline 版に更新。**`inline` 無しは従来どおり(挙動不変・opt-in)**。
10
+ - **`editor.inline`=打鍵の1文字目・IME 直打ちを殺さないカスタムエディタ**(コア)— 従来 `columns[c].editor` を付けた列は**クリック/F2 でしか編集を開始できず**、打鍵は `preventDefault` されて1文字目が捨てられ、IME 直打ち(`compositionstart`)は素通りで編集にすら入れなかった(=TssGrid の看板である IME 堅牢な直打ちがカスタムエディタ列だけ効かない)。`editor.inline = true` を宣言すると **入力は本体の共有 input のまま**=**打鍵も IME も素のテキスト列と完全に同じ**になり、エディタは候補表示に徹する型になる(「セルに打ちながら候補を絞るサジェスト/ルックアップ」がこれで作れる)。エディタ側の口: `open(ctx)`(ctx に **`input`**=共有input / **`setValue(v)`** / `inline:true` を追加)・**`onKeyDown(e, ctx)`**(本体より先に受ける。**IME 変換中も呼ばれる**=変換中の↑↓を奪わない実装が書ける。`false` で本体既定を止める)・**`onInput(value, ctx)`**(候補の絞り込み用)・`close()`。`icon`/`openOnClick` は inline でもそのまま使える。確定は本体 `commit()`=関所を通るので validator/parse/format/readOnly/Undo はそのまま。対象は text/number 系(dropdown/checkbox/ネイティブ日付は従来どおり対象外)。**完全 opt-in=`inline` を書かない既存エディタ(TssCalendar 等)の挙動は一切変わらない**。tssgrid-kintone のルックアップ実装で必要になり起票。
11
+
12
+ ### 修正
13
+ - **非仮想グリッドで↑キー等で先頭行へ移動すると、その行が sticky な列ヘッダの下に潜って読めなくなる不具合を修正**(コアCSS+JS)— キーボード移動の可視化に使う `scrollIntoView({block:'nearest'})` が、枠内に浮く sticky ヘッダ(≈29px)を考慮せず「一部でも見えていれば再スクロールしない」ため、先頭行がヘッダ下に潜ったまま止まっていた。`buildTable` でヘッダ高を CSS 変数 `--tg-thead-h` にセットし、`td { scroll-margin-top: var(--tg-thead-h) }` でヘッダ高ぶんの余白を確保(`scrollIntoView` がヘッダ下に潜らなくなる。ヘッダ高の変化にも追従・ヘッダ無しは0px)。tssgrid-kintone 実機(Chrome)で発覚・実測。
14
+ - **`filter({ 0: '営業' })` のように「列 index を数値キー」にした絞り込みが効かなかったのを修正**(コア)— `Object.keys` が数値キーを文字列化する(`{0:…}`→`'0'`)ため `_resolveCol('0')` が列 index に解決できず、条件が空=全行 true(絞り込まれない)になっていた。数値文字列キーは列 index として解決するフォールバックを追加(data キー/見出し名が優先=未解決の時だけ index 扱い)。関数述語・data キー・見出し名・関数値・AND は従来どおり。tss-filter の漏斗UI経由は元々無影響。
15
+
6
16
  ## [0.0.10] - 2026-07-15
7
17
 
8
18
  ### 修正
package/README.md CHANGED
@@ -364,6 +364,30 @@ new TssGrid(el, { columns: [{ editor: myEditor }] });
364
364
  - `open` が受け取る `{ grid, r, c, value, td, commit, cancel }` だけ知っていれば作れます。F2 / ダブルクリック / 文字入力で開きます。
365
365
  - **`commit(value)` は通常の確定経路(pushCmd)を通る**ので、`validator`/`parse`/`format`/読み取り専用がそのまま効き、**Undo にも積まれます**。
366
366
  - 編集中に別セルをクリックすると `cancel()` 相当で閉じます。描画(セルの見た目)は従来どおり `format` 等で制御。
367
+ - ⚠️ 上記の(フォーカスを奪う)エディタは **クリック / F2 で開く**型です。**打鍵の1文字目や IME 直打ちから始めたい**なら次の `inline` を使ってください。
368
+
369
+ ### `editor.inline`: 打鍵・IME 直打ちを殺さないエディタ(サジェスト向け)
370
+
371
+ `editor.inline = true` を宣言すると、**入力は本体の IME 堅牢な共有 input のまま**で、エディタは「候補を描く」役に徹します。=**打った1文字目が残り、IME 直打ち(変換)も素のテキスト列と完全に同じ**に効きます。「セルに打ちながら候補を絞るサジェスト/ルックアップ」はこの型で作ります。
372
+
373
+ ```js
374
+ const suggest = {
375
+ inline: true, icon: '🔍', // icon / openOnClick は inline でも使えます
376
+ open(ctx) { /* ctx.input = 打鍵/IMEが入る共有input。ここに候補ポップアップを添える(フォーカスは奪わない) */ },
377
+ onInput(value, ctx) { /* 打つたびに呼ばれる=候補を絞る */ },
378
+ onKeyDown(e, ctx) { // 本体より先に受ける。IME 変換中も呼ばれる
379
+ if (e.isComposing || e.keyCode === 229) return; // ★変換中は手を引く(↑↓を奪わない)
380
+ if (e.key === 'ArrowDown') { /* 候補を選ぶ */ return false; } // false で本体既定を止める
381
+ if (e.key === 'Enter') { ctx.commit(選んだ値); return false; } // commit=関所を通る
382
+ },
383
+ close() { /* 候補を片付ける */ },
384
+ };
385
+ new TssGrid(el, { columns: [{ editor: suggest }] });
386
+ ```
387
+ - `ctx` は通常の `{grid,r,c,value,td,commit,cancel}` に加えて **`input`(共有input)/ `setValue(v)`(候補で置換・確定はしない)/ `inline:true`** が入ります。
388
+ - **確定は本体の `commit()`=関所を通る**ので `validator`/`parse`/`format`/読み取り専用/Undo はそのまま効きます。
389
+ - ポップアップ側は **`mousedown` を `preventDefault`** してフォーカスを奪わないこと(共有 input に打ち続けられる)。
390
+ - 対象は **text/number 系の列**(`dropdown`/`checkbox`/ネイティブ日付は従来どおり対象外)。**`inline` を書かない既存エディタの挙動は一切変わりません**(完全 opt-in)。
367
391
 
368
392
  ### 同梱プラグイン: 休日カレンダー対応の日付ピッカー
369
393
 
@@ -769,7 +793,7 @@ new TssGrid(el, { hiddenColumns: [1] }); // 初期で隠す
769
793
  | `onBeforeAutofill` / `onAfterAutofill` | `(src,target)=>…` | — | フィル前後。before は `false` で取消。`src`/`target` = `{r0,c0,r1,c1}` |
770
794
  | `autoColumnSize` | `boolean \| number` | `false` | 初期化時に列幅を内容に合わせて自動調整(数値=上限px)。`autoSizeColumn`/`autoSizeAllColumns` API・列境界ダブルクリックでも |
771
795
  | `columns[c].align` / `valign` | `string` | — | 列既定の整列(→ [整列](#整列--セル単位の読み取り専用実行時)) |
772
- | `columns[c].editor` | `object \| ()=>object` | — | カスタム編集UI(→ [カスタムエディタ](#カスタムエディタ)) |
796
+ | `columns[c].editor` | `object \| ()=>object` | — | カスタム編集UI(→ [カスタムエディタ](#カスタムエディタ))。`editor.inline:true` で**打鍵の1文字目・IME 直打ちを殺さない**サジェスト型に(→ [`editor.inline`](#editorinline-打鍵ime-直打ちを殺さないエディタサジェスト向け)) |
773
797
  | `copyPaste` | `boolean` | `true` | `false` でコピー/カット/ペーストを無効化 |
774
798
  | `minSpareRows` | `number` | `0` | 末尾に常に確保する空行数(Excel風に下へ打ち足せる) |
775
799
  | `autoWrapRow` / `autoWrapCol` | `boolean` | `false` | Tab で行末→次行頭 / Enter で列末→次列頭に折り返し |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threestarsoftware/tssgrid",
3
- "version": "0.0.10",
3
+ "version": "0.1.0",
4
4
  "description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
5
5
  "license": "MIT",
6
6
  "author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
@@ -9,6 +9,9 @@
9
9
  *
10
10
  * opts:
11
11
  * source : string[] か (query, ctx)=>string[]。必須。
12
+ * inline : true=「セルに打ちながら絞る」型(既定 false)。本体の共有 input に乗る=**打鍵の1文字目も IME 直打ちも
13
+ * 素のテキスト列と同じに効く**(自前 input を作らず focus も奪わない)。false は従来どおり
14
+ * クリック/F2 でポップアップの自前 input を開く型(挙動不変)。
12
15
  * strict : true=候補から選んだ値のみ許可(既定 false=自由入力可)。
13
16
  * minChars : この文字数以上で候補を出す(既定 0=開いた時点で全件)。
14
17
  * max : 候補表示の最大件数(既定 20)。
@@ -22,6 +25,7 @@
22
25
 
23
26
  function TssAutocomplete(opts) {
24
27
  opts = opts || {};
28
+ var inline = !!opts.inline; // true=共有 input に乗る(打鍵/IME直打ちが効く)/false=従来の自前 input ポップアップ
25
29
  var getSource = typeof opts.source === 'function' ? opts.source : function () { return opts.source || []; };
26
30
  var strict = !!opts.strict;
27
31
  var minChars = opts.minChars || 0;
@@ -30,7 +34,7 @@
30
34
  : (opts.match === 'startsWith' ? function (it, q) { return norm(it).indexOf(q) === 0; }
31
35
  : function (it, q) { return norm(it).indexOf(q) >= 0; });
32
36
 
33
- var pop, input, list, items = [], hi = -1, composing = false, ref = null, done = false, outside = null;
37
+ var pop, input, list, items = [], hi = -1, composing = false, ref = null, done = false, outside = null, unbindComp = null;
34
38
 
35
39
  function filter(q) {
36
40
  var ctx = ref ? ref.ctx : {};
@@ -67,11 +71,13 @@
67
71
  if (strict) return null; // strict は候補必須
68
72
  return input.value; // 自由入力を採用
69
73
  }
74
+ // IME 変換中か(inline では本体の共有 input 上のキーを受けるので keyCode 229 も見る)
75
+ function imeOn(e) { return composing || e.isComposing || e.keyCode === 229; }
70
76
  function onKey(e) {
71
- if (e.key === 'ArrowDown') { if (composing || e.isComposing) return; e.preventDefault(); moveHi(1); }
72
- else if (e.key === 'ArrowUp') { if (composing || e.isComposing) return; e.preventDefault(); moveHi(-1); }
77
+ if (e.key === 'ArrowDown') { if (imeOn(e)) return; e.preventDefault(); moveHi(1); }
78
+ else if (e.key === 'ArrowUp') { if (imeOn(e)) return; e.preventDefault(); moveHi(-1); }
73
79
  else if (e.key === 'Enter') {
74
- if (composing || e.isComposing) return; // ★ IME変換確定の Enter は奪わない
80
+ if (imeOn(e)) return; // ★ IME変換確定の Enter は奪わない
75
81
  e.preventDefault();
76
82
  var v = chosenValue(); if (v == null) return; // strict で候補なし=確定しない
77
83
  commit(v);
@@ -91,30 +97,47 @@
91
97
  teardown(); // 既に開いていれば先に片付ける(open-while-open の取り残し防止・tss-calendar と同方針)
92
98
  done = false; ref = { ctx: ctx };
93
99
  pop = document.createElement('div'); pop.className = 'tss-ac' + (opts.className ? ' ' + opts.className : ''); pop.tabIndex = -1;
94
- pop.innerHTML = '<input class="tss-ac-input" type="text"><div class="tss-ac-list"></div>';
100
+ // inline は候補リストだけ(自前 input を作らない=打鍵/IME は本体の共有 input のまま)
101
+ pop.innerHTML = (inline ? '' : '<input class="tss-ac-input" type="text">') + '<div class="tss-ac-list"></div>';
95
102
  document.body.appendChild(pop);
96
- input = pop.querySelector('.tss-ac-input'); list = pop.querySelector('.tss-ac-list');
97
- input.value = value == null ? '' : value;
98
- input.addEventListener('compositionstart', function () { composing = true; });
99
- input.addEventListener('compositionend', function () { composing = false; render(); }); // 変換確定後に候補更新
100
- input.addEventListener('input', function () { if (!composing) render(); });
101
- input.addEventListener('keydown', onKey);
102
- list.addEventListener('mousedown', function (e) { var it = e.target.closest('.tss-ac-item'); if (it) { e.preventDefault(); commit(items[+it.dataset.i]); } });
103
+ list = pop.querySelector('.tss-ac-list');
104
+ if (inline) {
105
+ input = ctx.input; // ★ 打鍵/IME が入る本体の共有 input をそのまま参照(値は本体が持つ)
106
+ // 変換中は候補を更新しない(classic と同じ挙動)。確定後に更新。
107
+ var cs = function () { composing = true; }, ce = function () { composing = false; render(); };
108
+ input.addEventListener('compositionstart', cs);
109
+ input.addEventListener('compositionend', ce);
110
+ unbindComp = function () { input.removeEventListener('compositionstart', cs); input.removeEventListener('compositionend', ce); };
111
+ } else {
112
+ input = pop.querySelector('.tss-ac-input');
113
+ input.value = value == null ? '' : value;
114
+ input.addEventListener('compositionstart', function () { composing = true; });
115
+ input.addEventListener('compositionend', function () { composing = false; render(); }); // 変換確定後に候補更新
116
+ input.addEventListener('input', function () { if (!composing) render(); });
117
+ input.addEventListener('keydown', onKey);
118
+ }
119
+ list.addEventListener('mousedown', function (e) { var it = e.target.closest('.tss-ac-item'); if (it) { e.preventDefault(); commit(items[+it.dataset.i]); } }); // preventDefault=フォーカスを奪わない
103
120
  render(); place(anchor);
104
- outside = function (e) { if (pop && !pop.contains(e.target) && !(anchor && anchor.contains && anchor.contains(e.target))) { var v = chosenOnOutside(); if (v == null) cancel(); else commit(v); } };
105
- setTimeout(function () { document.addEventListener('mousedown', outside, true); }, 0);
106
- input.focus(); input.setSelectionRange(input.value.length, input.value.length);
121
+ if (!inline) { // inline は外側クリック/blur を本体(_commitActive / blur)が処理する=二重に閉じない
122
+ outside = function (e) { if (pop && !pop.contains(e.target) && !(anchor && anchor.contains && anchor.contains(e.target))) { var v = chosenOnOutside(); if (v == null) cancel(); else commit(v); } };
123
+ setTimeout(function () { document.addEventListener('mousedown', outside, true); }, 0);
124
+ input.focus(); input.setSelectionRange(input.value.length, input.value.length);
125
+ }
107
126
  }
108
127
  function chosenOnOutside() { if (strict) { var v = input.value; var src = getSource(v, {}) || []; return src.indexOf(v) >= 0 ? v : null; } return input.value; }
109
- function teardown() { if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } items = []; hi = -1; composing = false; }
128
+ function teardown() { if (unbindComp) { unbindComp(); unbindComp = null; } if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } items = []; hi = -1; composing = false; }
110
129
  function commit(v) { if (done) return; done = true; var f = ref.ctx.commit; teardown(); f(v); }
111
130
  function cancel() { if (done) return; done = true; var f = ref.ctx.cancel; teardown(); if (f) f(); }
112
131
 
113
132
  return {
133
+ inline: inline, // true=本体の共有 input に乗る(_usesTextEditor が見る)
114
134
  openOnClick: opts.openOnClick !== false,
115
135
  icon: opts.icon != null ? opts.icon : '▾',
116
136
  open: function (ctx) { show(ctx.td, ctx.value, ctx); },
117
137
  close: function () { teardown(); },
138
+ // ---- inline のみ: 入力/キーは本体の共有 input 経由で届く(自前 listener は張らない)----
139
+ onInput: inline ? function () { if (!composing) render(); } : undefined, // 打つたびに候補を絞る(変換中は据え置き)
140
+ onKeyDown: inline ? function (e) { onKey(e); } : undefined, // ↑↓/Enter/Tab/Esc。preventDefault で本体既定を止める
118
141
  };
119
142
  }
120
143
  if (typeof module !== 'undefined' && module.exports) module.exports = TssAutocomplete;
package/src/tssgrid.css CHANGED
@@ -77,7 +77,7 @@
77
77
  .tssgrid .tg-table thead th { height: 28px; position: sticky; top: 0; z-index: 6; }
78
78
  /* ヘッダはクリックで行/列選択(角は全選択) */
79
79
  .tssgrid .tg-table thead th[data-c], .tssgrid .tg-table th.rowhead[data-r], .tssgrid .tg-table th.corner { cursor: pointer; }
80
- .tssgrid .tg-table td { background: var(--tg-cell-bg); cursor: cell; }
80
+ .tssgrid .tg-table td { background: var(--tg-cell-bg); cursor: cell; scroll-margin-top: var(--tg-thead-h, 0px); } /* キーボード移動の scrollIntoView が sticky ヘッダ下に潜らないよう、ヘッダ高ぶん余白を確保(--tg-thead-h は buildTable が設定) */
81
81
  /* 表示専用(cursor:false): セルのカーソルを既定に(選択枠・ハイライトは JS 側で非表示) */
82
82
  .tssgrid.tg-no-cursor .tg-table td { cursor: default; }
83
83
  /* 非表示列: col を幅0にし、セルは padding/罫線を消して内容を overflow:hidden で畳む。
package/src/tssgrid.d.ts CHANGED
@@ -60,7 +60,7 @@ export interface ColumnDef {
60
60
  /** 入力検証。編集/貼付/フィルの全経路に効く。 */
61
61
  validator?: (value: any, cell: CellCoord) => ValidatorResult;
62
62
  /** カスタム編集UI(薄いオブジェクト契約 or それを返す関数)。 */
63
- editor?: object | (() => object);
63
+ editor?: EditorDef | (() => EditorDef);
64
64
  /** 入力の正規化(保存値へ変換)。 */
65
65
  parse?: (input: string, cell: CellCoord) => any;
66
66
  /** 表示の整形(保存値→表示)。文字列 or 宣言的 CellFormat。 */
@@ -212,6 +212,39 @@ export interface CsvOptions {
212
212
  delimiter?: string; [k: string]: any;
213
213
  }
214
214
 
215
+ /** カスタム編集UIが `open` で受け取る ctx。確定は commit=本体の関所(検証/整形/readOnly/履歴)を通る。 */
216
+ export interface EditorContext {
217
+ grid: TssGrid; r: number; c: number; value: any; td: HTMLTableCellElement | null;
218
+ commit(value?: any): void; cancel(): void;
219
+ /** inline エディタのみ: true。 */
220
+ inline?: boolean;
221
+ /** inline エディタのみ: 打鍵/IME が入る本体の共有 input(読み取り・listen 用)。 */
222
+ input?: HTMLInputElement | HTMLTextAreaElement;
223
+ /** inline エディタのみ: 共有 input の値を差し替える(候補で置換する用・確定はしない)。 */
224
+ setValue?(value: any): void;
225
+ [k: string]: any;
226
+ }
227
+ /** カスタム編集UIの契約(`columns[c].editor`)。 */
228
+ export interface EditorDef {
229
+ open(ctx: EditorContext): void;
230
+ close?(): void;
231
+ /** セル右端に出すアイコン。 */
232
+ icon?: string;
233
+ /** シングルクリックで開く(ドロップダウン同様)。 */
234
+ openOnClick?: boolean;
235
+ /**
236
+ * `true` で「本体の共有 input に乗るエディタ」=**打鍵の1文字目・IME 直打ちが素のテキスト列と同じに効く**。
237
+ * 入力は本体(IME 堅牢な共有 input)に任せ、エディタは候補表示等だけを担当する型。text/number 系の列で有効
238
+ * (dropdown/checkbox/ネイティブ日付は従来どおり対象外)。未指定なら従来のフォーカスを奪うエディタ(挙動不変)。
239
+ */
240
+ inline?: boolean;
241
+ /** inline のみ: 本体より先にキーを受ける(**IME 変換中も呼ばれる**=変換中の↑↓を奪わない実装が書ける)。`false` で本体既定を止める。 */
242
+ onKeyDown?(e: KeyboardEvent, ctx: EditorContext): boolean | void;
243
+ /** inline のみ: 共有 input の入力通知(候補の絞り込み用)。 */
244
+ onInput?(value: string, ctx: EditorContext): void;
245
+ [k: string]: any;
246
+ }
247
+
215
248
  /** 右クリックの対象種別。カスタムメニュー項目の name/disabled/hidden の第2引数、callback の {ctx} で受け取る。 */
216
249
  export interface MenuContext { on: 'col' | 'row' | 'cell' | 'corner'; r: number; c: number; }
217
250
 
package/src/tssgrid.js CHANGED
@@ -154,6 +154,7 @@
154
154
  this._headerSel = null; // 'col'/'row'/'all'=列/行/全選択モード(結合に拡張せず実列/行にクランプ=Excel流)。null=通常セル選択
155
155
  this._selCols = new Set(); // 飛び飛びの列選択(Ctrl+ヘッダクリック)。_headerSel==='col' の時だけ有効。連続選択は矩形と一致
156
156
  this._selRows = new Set(); // 飛び飛びの行選択(Ctrl+行ヘッダクリック)。_headerSel==='row' の時だけ有効
157
+ this._inlineEd = null; this._inlineCtx = null; // inline エディタ(editor.inline=true): 共有 input に乗るエディタの現在値
157
158
  this.disjointSelect = opts.disjointSelect !== false; // Ctrl+ヘッダで行/列を飛び飛び複数選択(false で無効=Ctrl+クリックは単一選択に戻る)
158
159
  // リサイズ(列幅・行高)
159
160
  // 列幅 colW[c]=px(未指定は既定)。指定方法は3通り、優先度=実行時リサイズ/明示colW > columns[c].width > 既定:
@@ -495,6 +496,7 @@
495
496
  if (typeof pred === 'object') {
496
497
  const conds = Object.keys(pred).map(k => {
497
498
  let ci = this._resolveCol(k); if (ci < 0) ci = this.headers.indexOf(k); // data キー → 見出し名でも解決
499
+ if (ci < 0 && /^\d+$/.test(k) && +k < this.COLS) ci = +k; // 数値文字列キー {0:…} は列 index として解決(Object.keys が数値キーを文字列化するため。data キー優先=上で未解決の時だけ)
498
500
  const want = pred[k]; return { ci, test: typeof want === 'function' ? want : (v => String(v) === String(want)) };
499
501
  }).filter(x => x.ci >= 0);
500
502
  return row => conds.every(cd => cd.test(row[cd.ci]));
@@ -928,9 +930,12 @@
928
930
  return null;
929
931
  }
930
932
  _usesTextEditor(c) {
931
- if (this.colCfg(c).editor && !this.colCfg(c).multiline) return false; // カスタムエディタは共有 input を使わない(multiline は textarea を優先)
933
+ const ed = this.colCfg(c).editor;
934
+ // カスタムエディタは共有 input を使わない(multiline は textarea を優先)。
935
+ // ただし editor.inline=true は「入力は本体の IME 堅牢な共有 input に任せ、エディタは候補を描くだけ」=共有 input に乗る。
936
+ if (ed && !ed.inline && !this.colCfg(c).multiline) return false;
932
937
  const t = this.colType(c);
933
- return t !== 'checkbox' && t !== 'dropdown' && !this._nativeInputType(c);
938
+ return t !== 'checkbox' && t !== 'dropdown' && !this._nativeInputType(c); // inline でも dropdown/checkbox/ネイティブ日付は対象外
934
939
  }
935
940
  // 保存値 → セル表示文字列(format 適用)。getData は保存値のまま。
936
941
  _displayValue(r, c) {
@@ -1310,6 +1315,7 @@
1310
1315
  if (this.onHeaderRender && this.colHeaders) this._applyHeaderRender(); // 外部がヘッダthを装飾(ソートアイコン等)。高さ確定前に走らせて幾何へ反映
1311
1316
  const thd = this.table.querySelector('thead');
1312
1317
  this._theadH = (this.colHeaders && thd) ? thd.offsetHeight : 0; // ヘッダ高をキャッシュ(クリップ計算用)
1318
+ this.wrap.style.setProperty('--tg-thead-h', this._theadH + 'px'); // scrollIntoView が sticky ヘッダに潜らないよう td の scroll-margin-top に使う(動的ヘッダ高に追従)
1313
1319
  this._applyTableWidth();
1314
1320
  this._applyStretch();
1315
1321
  this._applyHeaderSticky();
@@ -1619,7 +1625,35 @@
1619
1625
  const baseTop = parseFloat(el.style.top) || 0; // place がセル上端(content座標)を入れている
1620
1626
  el.style.top = (up ? baseTop + cellH - h : baseTop) + 'px'; // up なら下端をセル下端に合わせて上へ伸ばす
1621
1627
  }
1628
+ // inline エディタ(editor.inline=true)を開く。入力は共有 input のまま=打鍵の1文字目も IME 直打ちも殺さない。
1629
+ // エディタはフォーカスを持たない前提(ポップアップ側は mousedown を preventDefault すること)。
1630
+ // 確定は本体 commit=関所を通るので検証/整形/readOnly/履歴はそのまま効く。
1631
+ _openInlineEditor() {
1632
+ const r = this.active.r, c = this.active.c, def = this.colCfg(c).editor;
1633
+ if (!def || !def.inline || this._inlineEd) return;
1634
+ const ed = typeof def === 'function' ? def() : def;
1635
+ if (!ed || typeof ed.open !== 'function') return;
1636
+ const grid = this;
1637
+ const ctx = {
1638
+ grid, r, c, inline: true,
1639
+ value: this.data[r][c],
1640
+ td: this.cellEl(r, c),
1641
+ input: this.editor, // 打鍵/IME が入る共有 input(読み取り・listen 用)
1642
+ setValue(v) { grid.editor.value = grid._cellStr(v); }, // 候補で置き換える用(確定はしない)
1643
+ commit(v) { if (v != null) grid.editor.value = grid._cellStr(v); grid.commit(); grid.toNav(); grid.updateSelectionUI(); },
1644
+ cancel() { grid.setActive(r, c); }, // 元値に戻して nav(Esc 相当)
1645
+ };
1646
+ this._inlineEd = ed; this._inlineCtx = ctx;
1647
+ try { ed.open(ctx); } catch (_) { this._inlineEd = null; this._inlineCtx = null; }
1648
+ }
1649
+ _closeInlineEditor() {
1650
+ const ed = this._inlineEd;
1651
+ if (!ed) return;
1652
+ this._inlineEd = null; this._inlineCtx = null;
1653
+ if (ed.close) { try { ed.close(); } catch (_) {} }
1654
+ }
1622
1655
  toNav() {
1656
+ this._closeInlineEditor(); // 編集終了の締めどころ(commit/Esc/blur/選択移動=全経路がここを通る)
1623
1657
  this.mode = 'nav';
1624
1658
  this.editor.className = (this.editor === this._edMulti) ? 'tg-editor tg-editor-multi nav' : 'tg-editor nav';
1625
1659
  if (this.editor === this._edInput) this.editor.type = 'text'; // type は input 専用(textarea は readonly で throw する)
@@ -1642,6 +1676,7 @@
1642
1676
  const _t = this.editor.type;
1643
1677
  if (caretEnd && _t !== 'date' && _t !== 'time' && _t !== 'number') { const l = this.editor.value.length; this.editor.setSelectionRange(l, l); }
1644
1678
  if (this.editor === this._edMulti) this._growMultiEditor(); // multiline は編集開始時に内容ぶん伸ばす
1679
+ if (starting) this._openInlineEditor(); // inline エディタ列なら ed.open(ctx)(共有 input はそのまま=打鍵/IME を維持)
1645
1680
  if (starting && this.onEditStart) { try { this.onEditStart({ r: this.active.r, c: this.active.c, value: this.editor.value }); } catch (_) {} }
1646
1681
  }
1647
1682
  _fireSelection() {
@@ -2542,6 +2577,10 @@
2542
2577
  // input と textarea(multiline) の両方に同じ編集入口リスナを張る(this.editor はアクティブ列で張り替わる)。
2543
2578
  [this._edInput, this._edMulti].forEach((ed) => {
2544
2579
  ed.addEventListener('keydown', (e) => this._onKey(e));
2580
+ // inline エディタへ入力を通知(打った/変換確定した内容で候補を絞る用)
2581
+ ed.addEventListener('input', () => {
2582
+ if (this._inlineEd && this._inlineEd.onInput) { try { this._inlineEd.onInput(this.editor.value, this._inlineCtx); } catch (_) {} }
2583
+ });
2545
2584
  ed.addEventListener('compositionstart', () => {
2546
2585
  if (this.mode !== 'nav') return;
2547
2586
  if (this._usesTextEditor(this.active.c)) this.toEdit(null, false);
@@ -2596,7 +2635,7 @@
2596
2635
  // ドロップダウンはシングルクリックで即開く
2597
2636
  if (this.colType(c) === 'dropdown') { this.dragging = false; this._openSelect(); }
2598
2637
  // カスタムエディタが openOnClick を申告していれば、ドロップダウン同様シングルクリックで開く(再クリックは開かない=トグル)
2599
- else { const ed = this.colCfg(c).editor; if (ed && ed.openOnClick && !wasOpenHere) { this.dragging = false; this._openCustomEditor(r, c); } }
2638
+ else { const ed = this.colCfg(c).editor; if (ed && ed.openOnClick && !wasOpenHere) { this.dragging = false; if (ed.inline) this._beginEdit(r, c); else this._openCustomEditor(r, c); } } // inline は共有 input を開く(_beginEdit→toEdit→ed.open)
2600
2639
  });
2601
2640
  // ヘッダ全選択チェックは mousedown で処理済み。click のネイティブ・トグルを止めて二重反転を防ぐ
2602
2641
  this.table.addEventListener('click', (e) => {
@@ -2733,7 +2772,8 @@
2733
2772
  // ---- 型に応じた編集の入口 ----
2734
2773
  _beginEdit(r, c) {
2735
2774
  this.clearCopyMarquee(); // 編集開始でコピー/カットのマーキー(点線)を消す(dropdown/checkbox/カスタム含む)
2736
- if (this.colCfg(c).editor) { this._openCustomEditor(r, c); return; } // カスタムエディタ
2775
+ const _ed = this.colCfg(c).editor;
2776
+ if (_ed && !_ed.inline) { this._openCustomEditor(r, c); return; } // カスタムエディタ(inline は共有 input の経路へ落として toEdit が open する)
2737
2777
  const t = this.colType(c);
2738
2778
  if (t === 'checkbox') { this._toggleCheckbox(r, c); return; }
2739
2779
  if (t === 'dropdown') { this._openSelect(); return; }
@@ -2845,6 +2885,12 @@
2845
2885
  const composing = e.isComposing || e.keyCode === 229;
2846
2886
  // onBeforeKeyDown: グリッドより先にユーザーへ。false / preventDefault で既定を止める。
2847
2887
  if (this.onBeforeKeyDown) { let r; try { r = this.onBeforeKeyDown(e); } catch (_) {} if (r === false || e.defaultPrevented) return; }
2888
+ // inline エディタにもキーを先に渡す(IME 変換中も渡す=「変換中の↑↓は奪わない」実装を書けるように。
2889
+ // 本体の `if (composing) return` より前=エディタ側で isComposing/keyCode 229 を見て手を引ける)。false で既定を止める。
2890
+ if (this._inlineEd && this._inlineEd.onKeyDown) {
2891
+ let r; try { r = this._inlineEd.onKeyDown(e, this._inlineCtx); } catch (_) {}
2892
+ if (r === false || e.defaultPrevented) return;
2893
+ }
2848
2894
  // カスタムショートカット(IME 変換中は除く)
2849
2895
  if (!composing && this._runShortcuts(e)) return;
2850
2896
  if (this.mode === 'edit') {