@threestarsoftware/tssgrid 0.0.6 → 0.0.8
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 +23 -0
- package/package.json +1 -1
- package/plugins/tss-autocomplete.js +1 -0
- package/plugins/tss-calendar.js +1 -0
- package/plugins/tss-datetime.js +1 -0
- package/plugins/tss-time.js +1 -0
- package/src/tssgrid.css +9 -0
- package/src/tssgrid.d.ts +8 -0
- package/src/tssgrid.js +137 -35
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
TssGrid の各リリースの変更点。日付は JST。形式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) 準拠(ゆるめ)。
|
|
4
4
|
数値は特記なき限り**自社調べ**(headless Chrome / Windows 11・環境差あり)。
|
|
5
5
|
|
|
6
|
+
## [0.0.8] - 2026-07-12
|
|
7
|
+
|
|
8
|
+
### 追加
|
|
9
|
+
- **セル内改行(複数行入力)`columns[c].multiline: true`**(コア)— 指定した列だけ**セル内で改行**できる一級の列タイプ。TssGrid の本命である**日本語 IME の直打ち(セルに来ていきなり変換入力・1文字目が落ちない)**をそのまま維持したまま複数行入力に対応(隠し `<textarea>` を編集時だけ使う二枚持ち設計)。
|
|
10
|
+
- **行の高さは固定**(内容で伸びない=行高一定なので**大量行・仮想スクロールでも崩れない**)。`defaultRowHeight`/`rowHeights` で見せる行数ぶんを確保し、はみ出しはクリップ。複数行セルの**右上に薄いヒント**が出て「ここは複数行」と分かる。
|
|
11
|
+
- **編集時は箱が内容ぶん下へ伸びて全行が見える**(浮いたオーバーレイなので行高・スクロールには不干渉・確定で戻る)。最大行数は `multilineEditMaxLines`(既定 10)で頭打ち+箱内スクロール、画面端では上へフリップしてビューポートからはみ出さない。
|
|
12
|
+
- オプション: `multilineEnter`(`'commit'` 既定=Excel 風 Enter=確定/Alt+Enter=改行 / `'newline'`=メモ帳風)・`multilineMark`(右上ヒントの ON/OFF・CSS 変数 `--tg-ml-mark` で色替え)・`multilineEditMaxLines`。デモ `examples/multiline.html`。
|
|
13
|
+
|
|
14
|
+
### 修正・強化
|
|
15
|
+
- **コピー/貼り付けの TSV を引用符対応(Excel/Sheets 互換)に**(コア)— タブ・改行・引用符を含むセルを `"…"` で囲んで往復するようにし、**セル内に改行を含む値が Excel コピペで崩れない**ように修正。多列・タブ混じりの値にも効く。
|
|
16
|
+
|
|
17
|
+
## [0.0.7] - 2026-07-10
|
|
18
|
+
|
|
19
|
+
### 修正
|
|
20
|
+
- カスタムエディタ(`tss-calendar` / `tss-datetime` 等、`body` 直下にポップアップを出す型)を**開いたままフィルタ/ソート/フリーズ/再描画**すると、ポップアップが画面に取り残されて浮く不具合を修正(`buildTable()` が開いているエディタを閉じていなかった)。1修正で全カスタムエディタに効く。
|
|
21
|
+
- ポップアップ型エディタ(`tss-calendar` / `tss-time` / `tss-datetime` / `tss-autocomplete`)で、**別のセルを続けて開く**と前のポップアップと `document` リスナが片付かず取り残し/リークする不具合を修正(各 `show()` 冒頭で `teardown()` するよう統一)。
|
|
22
|
+
|
|
23
|
+
### 補足(挙動変化・影響範囲)
|
|
24
|
+
- カスタムエディタを開いたまま再描画/フィルタ/ソートすると、そのエディタは**キャンセル確定**となり `onEditEnd({ canceled: true })` が発火するようになりました(従来は発火せずポップアップだけ取り残していた)。
|
|
25
|
+
- ネイティブのドロップダウン(`<select>`)は永続要素のため上記いずれの影響も受けません(確認済み)。
|
|
26
|
+
|
|
6
27
|
## [0.0.6] - 2026-06-30
|
|
7
28
|
|
|
8
29
|
### 追加
|
|
@@ -67,6 +88,8 @@ TssGrid の各リリースの変更点。日付は JST。形式は [Keep a Chang
|
|
|
67
88
|
- 初回公開。コア(編集グリッド/日本語 IME 堅牢・全角半角・セル結合・固定行列・非破壊フィルタ・条件付き書式・コピー/貼付・Undo/Redo ほか)+同梱プラグイン+サンプル一式。
|
|
68
89
|
- 目玉デモ(方眼ガント/在庫管理)をサイトでライブ化。
|
|
69
90
|
|
|
91
|
+
[0.0.8]: https://github.com/threestarsoftware/TssGrid/releases/tag/v0.0.8
|
|
92
|
+
[0.0.7]: https://github.com/threestarsoftware/TssGrid/releases/tag/v0.0.7
|
|
70
93
|
[0.0.6]: https://github.com/threestarsoftware/TssGrid/releases/tag/v0.0.6
|
|
71
94
|
[0.0.5]: https://github.com/threestarsoftware/TssGrid/releases/tag/v0.0.5
|
|
72
95
|
[0.0.4]: https://github.com/threestarsoftware/TssGrid/releases/tag/v0.0.4
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threestarsoftware/tssgrid",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
pop.style.minWidth = r.width + 'px';
|
|
89
89
|
}
|
|
90
90
|
function show(anchor, value, ctx) {
|
|
91
|
+
teardown(); // 既に開いていれば先に片付ける(open-while-open の取り残し防止・tss-calendar と同方針)
|
|
91
92
|
done = false; ref = { ctx: ctx };
|
|
92
93
|
pop = document.createElement('div'); pop.className = 'tss-ac' + (opts.className ? ' ' + opts.className : ''); pop.tabIndex = -1;
|
|
93
94
|
pop.innerHTML = '<input class="tss-ac-input" type="text"><div class="tss-ac-list"></div>';
|
package/plugins/tss-calendar.js
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
if (k === 'PageUp' || k === 'PageDown') { e.preventDefault(); view = new Date(view.getFullYear(), view.getMonth() + (k === 'PageUp' ? -1 : 1), 1); render(); }
|
|
99
99
|
}
|
|
100
100
|
function show(anchor, value, onPick, onCancel) {
|
|
101
|
+
teardown(); // 既に開いていれば先に片付ける(前の pop 除去+outside リスナ解除・open-while-open の取り残し防止)
|
|
101
102
|
done = false; sel = parseISO(value);
|
|
102
103
|
view = sel ? new Date(sel.getFullYear(), sel.getMonth(), 1) : new Date();
|
|
103
104
|
focus = sel ? new Date(sel) : new Date();
|
package/plugins/tss-datetime.js
CHANGED
|
@@ -137,6 +137,7 @@
|
|
|
137
137
|
else if (e.key === 'Enter') { e.preventDefault(); commit(value()); }
|
|
138
138
|
}
|
|
139
139
|
function show(anchor, val, onPick, onCancel) {
|
|
140
|
+
teardown(); // 既に開いていれば先に片付ける(open-while-open の取り残し防止・tss-calendar と同方針)
|
|
140
141
|
done = false;
|
|
141
142
|
var p = parseDT(val), tp = p ? null : parseTimeOnly(val); // 日付付き or 時刻のみ
|
|
142
143
|
selD = p ? p.d : null;
|
package/plugins/tss-time.js
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
else if (e.key === 'Enter') { e.preventDefault(); commit(readValue()); }
|
|
62
62
|
}
|
|
63
63
|
function show(anchor, value, onPick, onCancel) {
|
|
64
|
+
teardown(); // 既に開いていれば先に片付ける(open-while-open の取り残し防止・tss-calendar と同方針)
|
|
64
65
|
done = false;
|
|
65
66
|
pop = document.createElement('div'); pop.className = 'tss-time' + (opts.className ? ' ' + opts.className : ''); pop.tabIndex = -1;
|
|
66
67
|
document.body.appendChild(pop);
|
package/src/tssgrid.css
CHANGED
|
@@ -156,6 +156,15 @@
|
|
|
156
156
|
.tssgrid .tg-editor.nav { color: transparent; background: transparent; border: none; caret-color: transparent; pointer-events: none; }
|
|
157
157
|
.tssgrid .tg-editor.edit { color: #222; background: var(--tg-edit-bg); border: 2px solid var(--tg-edit-accent); caret-color: auto; pointer-events: auto; }
|
|
158
158
|
.tssgrid .tg-editor.invalid { border-color: #e23; background: #fff3f4; }
|
|
159
|
+
/* 複数行(改行)エディタ = 隠し textarea。nav=透明sink / edit=可視。列は columns[c].multiline:true */
|
|
160
|
+
.tssgrid .tg-editor-multi { padding: 3px 5px; line-height: 1.35; white-space: pre-wrap; overflow: hidden; resize: none; vertical-align: top; }
|
|
161
|
+
.tssgrid .tg-editor-multi.edit { overflow: auto; }
|
|
162
|
+
/* 複数行セル表示: 内容を絶対配置のクリップ箱に入れ、td を押し広げない=行高一定(仮想スクロール維持)。行高ぶんだけ見せて残りはクリップ。 */
|
|
163
|
+
.tssgrid .tg-table td.tg-multiline-cell { position: relative; padding: 0; vertical-align: top; }
|
|
164
|
+
.tssgrid .tg-multiline-cell .tg-ml-clip { position: absolute; inset: 0; overflow: hidden; box-sizing: border-box; padding: 3px 5px; white-space: pre-wrap; word-break: break-word; line-height: 1.35; }
|
|
165
|
+
/* 右上の「複数行」ヒント(非interactive・薄色)。掴める誤解を避け右下でなく右上。色は --tg-ml-mark、tg-no-ml-mark で無効。 */
|
|
166
|
+
.tssgrid .tg-multiline-cell::after { content: ""; position: absolute; top: 0; right: 0; border: 6px solid transparent; border-top-color: var(--tg-ml-mark, rgba(20,40,80,.18)); border-right-color: var(--tg-ml-mark, rgba(20,40,80,.18)); pointer-events: none; z-index: 1; }
|
|
167
|
+
.tssgrid.tg-no-ml-mark .tg-multiline-cell::after { display: none; }
|
|
159
168
|
|
|
160
169
|
/* チェックボックス列: セル中央に配置(クリックでトグル) */
|
|
161
170
|
.tssgrid .tg-table td .tg-cb { display: block; margin: 0 auto; cursor: pointer; }
|
package/src/tssgrid.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export interface ColumnDef {
|
|
|
77
77
|
html?: boolean;
|
|
78
78
|
/** 列単位の折り返し。 */
|
|
79
79
|
wordWrap?: boolean;
|
|
80
|
+
/** 複数行(改行)入力を有効化。編集は隠し textarea(IME 直打ち対応)、表示は改行保持(pre-wrap)。行高は自動で伸びないので rowHeights 等で確保する。 */
|
|
81
|
+
multiline?: boolean;
|
|
80
82
|
/** 空セルの薄いヒント文字。 */
|
|
81
83
|
placeholder?: string;
|
|
82
84
|
[k: string]: any;
|
|
@@ -140,6 +142,12 @@ export interface TssGridOptions {
|
|
|
140
142
|
colHeaders?: boolean;
|
|
141
143
|
hiddenColumns?: number[];
|
|
142
144
|
wordWrap?: boolean;
|
|
145
|
+
/** multiline 列の Enter 挙動。'commit'(既定・Excel風): Enter=確定 / Alt+Enter=改行。'newline'(メモ帳風): Enter=改行 / Ctrl+Enter=確定。 */
|
|
146
|
+
multilineEnter?: 'commit' | 'newline';
|
|
147
|
+
/** multiline セル右上の「複数行」ヒント表示(既定 true)。色は CSS 変数 --tg-ml-mark。false で非表示。 */
|
|
148
|
+
multilineMark?: boolean;
|
|
149
|
+
/** multiline 編集時に伸びる箱の最大行数(既定 10)。超えた分はボックス内スクロール。ビューポート/wrap を超える時はそちらが優先。 */
|
|
150
|
+
multilineEditMaxLines?: number;
|
|
143
151
|
placeholder?: string;
|
|
144
152
|
className?: string;
|
|
145
153
|
/** false でフィル完全OFF。詳細指定も可。 */
|
package/src/tssgrid.js
CHANGED
|
@@ -199,6 +199,12 @@
|
|
|
199
199
|
this.stretchH = opts.stretchH || 'none';
|
|
200
200
|
// セル内折り返し(既定 nowrap)。grid 全体 or columns[c].wordWrap。
|
|
201
201
|
this.wordWrap = !!opts.wordWrap;
|
|
202
|
+
// 複数行(改行)編集の Enter 挙動: 'commit'(既定・Excel風: Enter=確定 / Alt+Enter=改行) | 'newline'(メモ帳風: Enter=改行 / Ctrl+Enter=確定)。列は columns[c].multiline:true で有効化。
|
|
203
|
+
this.multilineEnter = opts.multilineEnter === 'newline' ? 'newline' : 'commit';
|
|
204
|
+
// multiline セル右上の「複数行」ヒント(既定 ON)。色は CSS 変数 --tg-ml-mark で。false で消す。
|
|
205
|
+
this.multilineMark = opts.multilineMark !== false;
|
|
206
|
+
// multiline 編集時に伸びる箱の最大行数(既定 10)。超えた分はボックス内スクロール。
|
|
207
|
+
this.multilineEditMaxLines = opts.multilineEditMaxLines > 0 ? opts.multilineEditMaxLines : 10;
|
|
202
208
|
// 空セルのヒント表示(grid 全体 or columns[c].placeholder)。getData には入らない。
|
|
203
209
|
this.placeholder = opts.placeholder != null ? String(opts.placeholder) : null;
|
|
204
210
|
// 機能トグル
|
|
@@ -596,6 +602,7 @@
|
|
|
596
602
|
'<div class="tg-copybox"></div><div class="tg-fillpreview"></div><div class="tg-fillhandle"></div>' +
|
|
597
603
|
'<div class="tg-resizeguide"></div>' +
|
|
598
604
|
'<input class="tg-editor nav" type="text" autocomplete="off" spellcheck="false">' +
|
|
605
|
+
'<textarea class="tg-editor tg-editor-multi nav" autocomplete="off" spellcheck="false" style="display:none"></textarea>' +
|
|
599
606
|
'<select class="tg-select" style="display:none"></select>' +
|
|
600
607
|
'</div>' + (this.showDump ? '<pre class="tg-dump"></pre>' : '') +
|
|
601
608
|
'<div class="tg-menu" style="display:none"></div></div>';
|
|
@@ -604,11 +611,13 @@
|
|
|
604
611
|
this.selrange = q('.tg-selrange'); this.selbox = q('.tg-selbox');
|
|
605
612
|
this.copybox = q('.tg-copybox'); this.fillhandle = q('.tg-fillhandle'); this.fillpreview = q('.tg-fillpreview');
|
|
606
613
|
this.resizeGuide = q('.tg-resizeguide');
|
|
607
|
-
this.
|
|
614
|
+
this._edInput = q('input.tg-editor'); this._edMulti = q('.tg-editor-multi'); this.editor = this._edInput;
|
|
615
|
+
this.select = q('.tg-select'); this.dumpEl = q('.tg-dump');
|
|
608
616
|
this.menuEl = q('.tg-menu');
|
|
609
617
|
this.root = q('.tssgrid');
|
|
610
618
|
if (this.className) this.root.className += ' ' + this.className; // 任意クラス(テーマ付け)
|
|
611
619
|
if (this.wordWrap) this.root.classList.add('tg-wordwrap'); // グリッド全体の折り返し
|
|
620
|
+
if (!this.multilineMark) this.root.classList.add('tg-no-ml-mark'); // multiline の右上ヒントを消す
|
|
612
621
|
if (!this.cursor) this.root.classList.add('tg-no-cursor'); // 表示専用(選択枠なし+セルカーソル既定)
|
|
613
622
|
if (this.width) {
|
|
614
623
|
this.wrap.style.width = this.width; // 幅制約(横スクロール/固定列用)
|
|
@@ -887,7 +896,7 @@
|
|
|
887
896
|
return null;
|
|
888
897
|
}
|
|
889
898
|
_usesTextEditor(c) {
|
|
890
|
-
if (this.colCfg(c).editor) return false; // カスタムエディタは共有 input
|
|
899
|
+
if (this.colCfg(c).editor && !this.colCfg(c).multiline) return false; // カスタムエディタは共有 input を使わない(multiline は textarea を優先)
|
|
891
900
|
const t = this.colType(c);
|
|
892
901
|
return t !== 'checkbox' && t !== 'dropdown' && !this._nativeInputType(c);
|
|
893
902
|
}
|
|
@@ -925,7 +934,10 @@
|
|
|
925
934
|
if (this.data[r][c] === '' && this._placeholder(c) != null) return TssGrid.esc(this._placeholder(c));
|
|
926
935
|
// html:true の列は表示だけ生HTML(値はテキスト保存のまま)。エスケープを外す=XSS責任は format/値の提供側。
|
|
927
936
|
if (this.colCfg(c).html) return this._displayValue(r, c);
|
|
928
|
-
|
|
937
|
+
const _h = TssGrid.esc(this._displayValue(r, c));
|
|
938
|
+
// 複数行セルは絶対配置のクリップ箱で包む=行を押し広げない(行高一定・仮想スクロール維持/行高ぶんだけ表示)
|
|
939
|
+
if (this.colCfg(c).multiline) return '<div class="tg-ml-clip">' + _h + '</div>';
|
|
940
|
+
return _h;
|
|
929
941
|
}
|
|
930
942
|
// 条件付き書式: 列版 columns[c].cellClass(value,{r,c,row,src}) + 全体版 cellClass(r,c,value,row,src)。
|
|
931
943
|
// src = 元レコード(オブジェクトデータ時。非表示フィールドも見える=休日区分など隠し列基準の色付けに)。
|
|
@@ -972,7 +984,7 @@
|
|
|
972
984
|
if (this._merges && this._isCovered(r, c)) return; // 従属セルは描画対象なし(cellEl はアンカーへ解決=誤って上書きしない)
|
|
973
985
|
const td = this.cellEl(r, c); if (!td) return;
|
|
974
986
|
const ph = (this.data[r][c] === '' && this.colType(c) !== 'checkbox') ? this._placeholder(c) : null;
|
|
975
|
-
if (this.colType(c) === 'checkbox' || (this.colCfg(c).html && ph == null)) td.innerHTML = this._cellHTML(r, c);
|
|
987
|
+
if (this.colType(c) === 'checkbox' || ((this.colCfg(c).html || this.colCfg(c).multiline) && ph == null)) td.innerHTML = this._cellHTML(r, c);
|
|
976
988
|
else td.textContent = ph != null ? ph : this._displayValue(r, c);
|
|
977
989
|
td.classList.toggle('tg-placeholder', ph != null);
|
|
978
990
|
if (this._marksInvalid) {
|
|
@@ -1145,6 +1157,7 @@
|
|
|
1145
1157
|
if (this.colType(c) === 'number') cls.push('tg-num'); // 数値は右寄せ
|
|
1146
1158
|
if (this._isReadOnly(r, c)) cls.push('tg-readonly'); // 読み取り専用(淡色+カーソル既定)
|
|
1147
1159
|
if (this.colCfg(c).wordWrap) cls.push('tg-wrap-cell'); // 列単位の折り返し
|
|
1160
|
+
if (this.colCfg(c).multiline) cls.push('tg-multiline-cell'); // 複数行(改行保持=pre-wrap)
|
|
1148
1161
|
if (this.data[r][c] === '' && this._placeholder(c) != null) cls.push('tg-placeholder'); // 空セルのヒント
|
|
1149
1162
|
const al = this._cellAlign(r, c); // 整列(列既定+セル上書き)
|
|
1150
1163
|
if (al.h) cls.push('tg-h-' + al.h); if (al.v) cls.push('tg-v-' + al.v);
|
|
@@ -1218,6 +1231,7 @@
|
|
|
1218
1231
|
this._renderWindow(this._vWindow(), false); // 窓を即同期(place 前に DOM を確定)
|
|
1219
1232
|
}
|
|
1220
1233
|
buildTable() {
|
|
1234
|
+
if (this._customEditor && this._customCancel) this._customCancel(); // 開いているカスタムエディタを閉じる(body直下ポップアップの取り残し防止・_commitActive と同じ扱い)
|
|
1221
1235
|
this._selhdrFg = null; // 選択ヘッダ文字色キャッシュ破棄(テーマ変更に追従)
|
|
1222
1236
|
const rh = this.rowHeaders;
|
|
1223
1237
|
let html = '<colgroup>' + (rh ? '<col style="width:' + this.rowHeaderW + 'px">' : '');
|
|
@@ -1359,6 +1373,8 @@
|
|
|
1359
1373
|
elem.style.left = R.left + 'px'; elem.style.top = R.top + 'px';
|
|
1360
1374
|
elem.style.width = R.width + 'px'; elem.style.height = R.height + 'px';
|
|
1361
1375
|
this._clipOverlay(elem, R.left, R.top, c, r);
|
|
1376
|
+
// multiline を編集中に置き直した時は grow をやり直す(_reflow/ResizeObserver が高さを戻すのを打ち消す)
|
|
1377
|
+
if (elem === this._edMulti && this.mode === 'edit') this._growMultiEditor();
|
|
1362
1378
|
}
|
|
1363
1379
|
placeRect(elem, r0, c0, r1, c1) {
|
|
1364
1380
|
const L = this._colLefts, T = this._rowTops;
|
|
@@ -1503,23 +1519,73 @@
|
|
|
1503
1519
|
this._clipOverlay(this.fillhandle, fhL, fhT, c1, r1); // 固定ペインに潜ったら隠す
|
|
1504
1520
|
} else this.fillhandle.style.display = 'none';
|
|
1505
1521
|
}
|
|
1522
|
+
_isMultiline(c) { return !!this.colCfg(c).multiline; }
|
|
1523
|
+
// アクティブ列に応じて this.editor を input↔textarea に張り替える(非アクティブ側は隠す)。
|
|
1524
|
+
_pickEditor(c) {
|
|
1525
|
+
const next = this._isMultiline(c) ? this._edMulti : this._edInput;
|
|
1526
|
+
if (next === this.editor) return;
|
|
1527
|
+
this.editor.style.display = 'none'; this.editor.value = '';
|
|
1528
|
+
this.editor = next; next.style.display = '';
|
|
1529
|
+
}
|
|
1530
|
+
// 「アクティブ editor を貼り直して nav へ」= 各選択メソッド共通の choke point(張り替えを place より前に)。
|
|
1531
|
+
_syncEditorNav() { this._pickEditor(this.active.c); this.place(this.editor, this.activeTd()); this.toNav(); }
|
|
1532
|
+
// カーソル位置に文字列を差し込む(multiline の改行挿入用)。
|
|
1533
|
+
_insertText(s) {
|
|
1534
|
+
const el = this.editor, st = el.selectionStart, en = el.selectionEnd, v = el.value;
|
|
1535
|
+
el.value = v.slice(0, st) + s + v.slice(en);
|
|
1536
|
+
const p = st + s.length; el.setSelectionRange(p, p);
|
|
1537
|
+
if (this.editor === this._edMulti) this._growMultiEditor();
|
|
1538
|
+
}
|
|
1539
|
+
// multiline 編集中: textarea を内容ぶん下へ伸ばす(浮いてる箱だけ=行高/スクロールには不干渉・確定で元に戻る)。
|
|
1540
|
+
_growMultiEditor() {
|
|
1541
|
+
const el = this._edMulti;
|
|
1542
|
+
if (this.editor !== el || this.mode !== 'edit' || !this.wrap) return;
|
|
1543
|
+
const td = this.activeTd(); if (!td) return;
|
|
1544
|
+
const tdRect = td.getBoundingClientRect(), cellH = Math.round(tdRect.height);
|
|
1545
|
+
el.style.height = 'auto'; // 一旦リセットして内容高さを測る
|
|
1546
|
+
const content = el.scrollHeight;
|
|
1547
|
+
// 行数上限(既定 multilineEditMaxLines)→ px。超えた分はボックス内スクロール。
|
|
1548
|
+
const cs = getComputedStyle(el);
|
|
1549
|
+
let lineH = parseFloat(cs.lineHeight); if (!lineH || isNaN(lineH)) lineH = (parseFloat(cs.fontSize) || 13) * 1.35;
|
|
1550
|
+
const vpad = (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0) + 4;
|
|
1551
|
+
const capByLines = Math.round(this.multilineEditMaxLines * lineH + vpad);
|
|
1552
|
+
// 収める枠 = wrap の可視域 ∩ ビューポート(下優先/足りなければ上へフリップ/それでも足りなければ広い方+内部スクロール)=ブラウザからはみ出さない
|
|
1553
|
+
const wr = this.wrap.getBoundingClientRect(), vh = (typeof window !== 'undefined' && window.innerHeight) || 800, m = 6;
|
|
1554
|
+
const boundTop = Math.max(wr.top, 0), boundBot = Math.min(wr.bottom, vh);
|
|
1555
|
+
const spaceDown = boundBot - tdRect.top - m, spaceUp = tdRect.bottom - boundTop - m;
|
|
1556
|
+
let want = Math.max(cellH, Math.min(content, capByLines)), h, up = false;
|
|
1557
|
+
if (want <= spaceDown) h = want; // 下にそのまま収まる
|
|
1558
|
+
else if (want <= spaceUp) { h = want; up = true; } // 上に開けば収まる
|
|
1559
|
+
else if (spaceDown >= spaceUp) h = Math.max(cellH, spaceDown); // 下が広い→下+内部スクロール
|
|
1560
|
+
else { h = Math.max(cellH, spaceUp); up = true; } // 上が広い→上+内部スクロール
|
|
1561
|
+
el.style.height = h + 'px';
|
|
1562
|
+
el.style.overflowY = content > h ? 'auto' : 'hidden';
|
|
1563
|
+
const baseTop = parseFloat(el.style.top) || 0; // place がセル上端(content座標)を入れている
|
|
1564
|
+
el.style.top = (up ? baseTop + cellH - h : baseTop) + 'px'; // up なら下端をセル下端に合わせて上へ伸ばす
|
|
1565
|
+
}
|
|
1506
1566
|
toNav() {
|
|
1507
|
-
this.mode = 'nav';
|
|
1508
|
-
this.editor.
|
|
1567
|
+
this.mode = 'nav';
|
|
1568
|
+
this.editor.className = (this.editor === this._edMulti) ? 'tg-editor tg-editor-multi nav' : 'tg-editor nav';
|
|
1569
|
+
if (this.editor === this._edInput) this.editor.type = 'text'; // type は input 専用(textarea は readonly で throw する)
|
|
1570
|
+
this.editor.value = '';
|
|
1509
1571
|
if (this.select) this.select.style.display = 'none';
|
|
1510
1572
|
}
|
|
1511
1573
|
toEdit(value, caretEnd) {
|
|
1512
1574
|
if (this._isReadOnly(this.active.r, this.active.c)) return; // 読み取り専用は編集モードに入らない
|
|
1513
1575
|
const starting = this.mode !== 'edit';
|
|
1514
1576
|
if (starting) this.clearCopyMarquee(); // 編集開始でコピー/カットのマーキー(点線)を消す(Excel 流)
|
|
1515
|
-
this.mode = 'edit';
|
|
1516
|
-
this.editor.
|
|
1577
|
+
this.mode = 'edit';
|
|
1578
|
+
this.editor.className = (this.editor === this._edMulti) ? 'tg-editor tg-editor-multi edit' : 'tg-editor edit';
|
|
1579
|
+
this.fillhandle.style.display = 'none';
|
|
1580
|
+
if (this.editor === this._edInput) this.editor.type = this._nativeInputType(this.active.c) || 'text'; // type は input 専用
|
|
1517
1581
|
const _ml = this.colCfg(this.active.c).maxLength; // 桁数(文字数)上限=打鍵をブロック(貼付/投入は _coerceCell で切り詰め)
|
|
1518
1582
|
if (typeof _ml === 'number' && _ml > 0) this.editor.maxLength = _ml; else this.editor.removeAttribute('maxlength');
|
|
1519
1583
|
if (value != null) this.editor.value = value;
|
|
1520
1584
|
this.editor.focus();
|
|
1521
|
-
// setSelectionRange は text
|
|
1522
|
-
|
|
1585
|
+
// setSelectionRange は text / textarea のみ(date/time/number input は例外を投げる)
|
|
1586
|
+
const _t = this.editor.type;
|
|
1587
|
+
if (caretEnd && _t !== 'date' && _t !== 'time' && _t !== 'number') { const l = this.editor.value.length; this.editor.setSelectionRange(l, l); }
|
|
1588
|
+
if (this.editor === this._edMulti) this._growMultiEditor(); // multiline は編集開始時に内容ぶん伸ばす
|
|
1523
1589
|
if (starting && this.onEditStart) { try { this.onEditStart({ r: this.active.r, c: this.active.c, value: this.editor.value }); } catch (_) {} }
|
|
1524
1590
|
}
|
|
1525
1591
|
_fireSelection() {
|
|
@@ -1540,7 +1606,7 @@
|
|
|
1540
1606
|
this._headerSel = null;
|
|
1541
1607
|
this.active = a; this.extent = { ...a };
|
|
1542
1608
|
if (this.virtual) this._vEnsureVisible(a.r); // 窓に入れてから placement(窓外なら activeTd=null になるため)
|
|
1543
|
-
this.
|
|
1609
|
+
this._syncEditorNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1544
1610
|
if (!this.virtual) this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' }); // 仮想は _vEnsureVisible が可視化済み
|
|
1545
1611
|
this._fireSelection();
|
|
1546
1612
|
}
|
|
@@ -1558,13 +1624,13 @@
|
|
|
1558
1624
|
if (!this._beforeSel(a, e)) return;
|
|
1559
1625
|
this._headerSel = null;
|
|
1560
1626
|
this.active = a; this.extent = e;
|
|
1561
|
-
this.
|
|
1627
|
+
this._syncEditorNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1562
1628
|
this._fireSelection();
|
|
1563
1629
|
}
|
|
1564
1630
|
focusAndSelect(sel) {
|
|
1565
1631
|
this.active = { ...sel.active }; this.extent = { ...sel.extent };
|
|
1566
1632
|
if (this.virtual) this._vEnsureVisible(this.active.r);
|
|
1567
|
-
this.
|
|
1633
|
+
this._syncEditorNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1568
1634
|
if (!this.virtual) this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1569
1635
|
this._fireSelection();
|
|
1570
1636
|
}
|
|
@@ -1574,9 +1640,9 @@
|
|
|
1574
1640
|
selectRow(r) { r = this.clampR(r); this._rowAnchor = r; this.selectRect(r, 0, r, this.COLS - 1); this._headerSel = 'row'; this.updateSelectionUI(); }
|
|
1575
1641
|
selectAll() { this._colAnchor = 0; this._rowAnchor = 0; this.selectRect(0, 0, this.ROWS - 1, this.COLS - 1); this._headerSel = 'all'; this.updateSelectionUI(); }
|
|
1576
1642
|
_extendCol(c) { const a = { r: 0, c: this._colAnchor }, e = { r: this.ROWS - 1, c: this.clampC(c) }; if (!this._beforeSel(a, e)) return; this._headerSel = 'col'; this.active = a; this.extent = e;
|
|
1577
|
-
this.
|
|
1643
|
+
this._syncEditorNav(); this.updateSelectionUI(); this._fireSelection(); }
|
|
1578
1644
|
_extendRow(r) { const a = { r: this._rowAnchor, c: 0 }, e = { r: this.clampR(r), c: this.COLS - 1 }; if (!this._beforeSel(a, e)) return; this._headerSel = 'row'; this.active = a; this.extent = e;
|
|
1579
|
-
this.
|
|
1645
|
+
this._syncEditorNav(); this.updateSelectionUI(); this._fireSelection(); }
|
|
1580
1646
|
// ---- 列幅 / 行高 のリサイズ ----
|
|
1581
1647
|
setColWidth(c, w) {
|
|
1582
1648
|
w = Math.max(this.minColW, Math.round(w)); this.colW[c] = w;
|
|
@@ -2149,7 +2215,7 @@
|
|
|
2149
2215
|
const before = cut ? this.onBeforeCut : this.onBeforeCopy;
|
|
2150
2216
|
if (before) { let r; try { r = before(buf, info); } catch (_) {} if (r === false) return; if (Array.isArray(r)) buf = r; }
|
|
2151
2217
|
sharedClip = buf;
|
|
2152
|
-
this._writeClip(
|
|
2218
|
+
this._writeClip(TssGrid.toTSV(buf));
|
|
2153
2219
|
this.pendingCut = cut ? { ...sel } : null;
|
|
2154
2220
|
this.placeRect(this.copybox, sel.r0, sel.c0, sel.r1, sel.c1); this.copybox.style.display = 'block';
|
|
2155
2221
|
if (this.onAfterCopy) { try { this.onAfterCopy(buf, info); } catch (_) {} }
|
|
@@ -2162,7 +2228,33 @@
|
|
|
2162
2228
|
const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px';
|
|
2163
2229
|
document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); } catch (_) {} document.body.removeChild(ta); this.editor.focus();
|
|
2164
2230
|
}
|
|
2165
|
-
|
|
2231
|
+
// 2次元配列 → クリップボード TSV。セル内に タブ/改行/引用符 を含む値は "…" で囲み、内部の " は "" に(Excel/Sheets 互換)。
|
|
2232
|
+
static toTSV(rows) {
|
|
2233
|
+
return rows.map(row => row.map(v => {
|
|
2234
|
+
const s = v == null ? '' : String(v);
|
|
2235
|
+
return /[\t\n\r"]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
|
|
2236
|
+
}).join('\t')).join('\n');
|
|
2237
|
+
}
|
|
2238
|
+
// クリップボード TSV → 2次元配列。引用符で囲まれたフィールド内の タブ/改行 はセルの中身として保持(=Excel の複数行セルが崩れない)。
|
|
2239
|
+
static parseTSV(t) {
|
|
2240
|
+
t = String(t).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
2241
|
+
const rows = []; let row = [], f = '', inQ = false, i = 0;
|
|
2242
|
+
const endField = () => { row.push(f); f = ''; };
|
|
2243
|
+
const endRow = () => { endField(); rows.push(row); row = []; };
|
|
2244
|
+
while (i < t.length) {
|
|
2245
|
+
const ch = t[i];
|
|
2246
|
+
if (inQ) {
|
|
2247
|
+
if (ch === '"') { if (t[i + 1] === '"') { f += '"'; i += 2; continue; } inQ = false; i++; continue; } // "" → "/単独 " で引用終了
|
|
2248
|
+
f += ch; i++; continue;
|
|
2249
|
+
}
|
|
2250
|
+
if (ch === '"' && f === '') { inQ = true; i++; continue; } // フィールド先頭の " のみ引用開始
|
|
2251
|
+
if (ch === '\t') { endField(); i++; continue; }
|
|
2252
|
+
if (ch === '\n') { endRow(); i++; continue; }
|
|
2253
|
+
f += ch; i++;
|
|
2254
|
+
}
|
|
2255
|
+
if (f !== '' || row.length > 0 || inQ) endRow(); // 末尾行を確定(\n 終端の空行は足さない)
|
|
2256
|
+
return rows.length ? rows : [['']];
|
|
2257
|
+
}
|
|
2166
2258
|
async paste() {
|
|
2167
2259
|
if (!this.copyPaste) return;
|
|
2168
2260
|
let text = null;
|
|
@@ -2292,25 +2384,29 @@
|
|
|
2292
2384
|
|
|
2293
2385
|
_bind() {
|
|
2294
2386
|
if (this.virtual) this.wrap.addEventListener('scroll', () => this._vSync(), { passive: true }); // 仮想スクロール: 窓再描画
|
|
2295
|
-
|
|
2296
|
-
this.
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2387
|
+
// input と textarea(multiline) の両方に同じ編集入口リスナを張る(this.editor はアクティブ列で張り替わる)。
|
|
2388
|
+
[this._edInput, this._edMulti].forEach((ed) => {
|
|
2389
|
+
ed.addEventListener('keydown', (e) => this._onKey(e));
|
|
2390
|
+
ed.addEventListener('compositionstart', () => {
|
|
2391
|
+
if (this.mode !== 'nav') return;
|
|
2392
|
+
if (this._usesTextEditor(this.active.c)) this.toEdit(null, false);
|
|
2393
|
+
});
|
|
2394
|
+
ed.addEventListener('beforeinput', (e) => {
|
|
2395
|
+
if (this.mode !== 'nav' || !(e.inputType || '').startsWith('insert')) return;
|
|
2396
|
+
const c = this.active.c, t = this.colType(c);
|
|
2397
|
+
if (this._usesTextEditor(c)) { this.toEdit(null, false); return; } // text / multiline / parse・format 付き date・time
|
|
2398
|
+
e.preventDefault();
|
|
2399
|
+
if (this.colCfg(c).editor) this._openCustomEditor(this.active.r, c); // カスタムエディタ
|
|
2400
|
+
else if (t === 'dropdown') this._openSelect();
|
|
2401
|
+
else if (t === 'date' || t === 'time') this.toEdit(this.data[this.active.r][c], true); // ネイティブピッカー
|
|
2402
|
+
// checkbox は文字入力を無視(Space / クリックでトグル)
|
|
2403
|
+
});
|
|
2404
|
+
ed.addEventListener('blur', () => {
|
|
2405
|
+
if (this._customEditor) return; // カスタムエディタが奪ったフォーカスでの blur は無視
|
|
2406
|
+
if (this.mode === 'edit' && (!this.select || this.select.style.display === 'none')) { this.commit(); this.toNav(); this.updateSelectionUI(); }
|
|
2407
|
+
});
|
|
2313
2408
|
});
|
|
2409
|
+
this._edMulti.addEventListener('input', () => this._growMultiEditor()); // 入力で内容ぶん伸ばす(multiline のみ)
|
|
2314
2410
|
this.select.addEventListener('keydown', (e) => {
|
|
2315
2411
|
if (e.key === 'Enter') { e.preventDefault(); this._commitSelect(); this._advance('enter', e.shiftKey); }
|
|
2316
2412
|
else if (e.key === 'Tab') { e.preventDefault(); this._commitSelect(); this._advance('tab', e.shiftKey); }
|
|
@@ -2597,6 +2693,12 @@
|
|
|
2597
2693
|
if (!composing && this._runShortcuts(e)) return;
|
|
2598
2694
|
if (this.mode === 'edit') {
|
|
2599
2695
|
if (composing) return;
|
|
2696
|
+
if (this.editor === this._edMulti && e.key === 'Enter') { // multiline: 案1 Enter=改行/Ctrl+Enter=確定, 案2 Enter=確定/Alt+Enter=改行
|
|
2697
|
+
const commit = this.multilineEnter === 'newline' ? (e.ctrlKey || e.metaKey) : !e.altKey;
|
|
2698
|
+
e.preventDefault();
|
|
2699
|
+
if (commit) { this.commit(); this._advance('enter', e.shiftKey); } else this._insertText('\n');
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2600
2702
|
if (e.key === 'Enter') { e.preventDefault(); this.commit(); this._advance('enter', e.shiftKey); return; }
|
|
2601
2703
|
if (e.key === 'Tab') { e.preventDefault(); this.commit(); this._advance('tab', e.shiftKey); return; }
|
|
2602
2704
|
// Esc/編集中の Ctrl+Z = この編集をキャンセルして元値に戻す(nav へ)。
|