@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 +10 -0
- package/README.md +25 -1
- package/package.json +1 -1
- package/plugins/tss-autocomplete.js +39 -16
- package/src/tssgrid.css +1 -1
- package/src/tssgrid.d.ts +34 -1
- package/src/tssgrid.js +50 -4
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
|
|
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 (
|
|
72
|
-
else if (e.key === 'ArrowUp') { if (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
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') {
|