@threestarsoftware/tssgrid 0.1.35 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +34 -2
- package/package.json +23 -1
- package/plugins/tss-calendar.js +9 -6
- package/plugins/tss-datetime.js +50 -9
- package/plugins/tss-time.js +23 -8
- package/react/index.d.ts +19 -0
- package/react/index.js +74 -0
- package/src/tssgrid.js +11 -3
- package/vue/index.d.ts +15 -0
- package/vue/index.js +56 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
TssGrid の各リリースの変更点。日付は JST。形式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) 準拠(ゆるめ)。
|
|
4
4
|
数値は特記なき限り**自社調べ**(headless Chrome / Windows 11・環境差あり)。
|
|
5
5
|
|
|
6
|
+
## [0.1.37] - 2026-11-06
|
|
7
|
+
|
|
8
|
+
### 追加(公式フレームワークアダプタ)
|
|
9
|
+
- **React / Vue 3 の薄い公式アダプタを追加**(`@threestarsoftware/tssgrid/react`・`@threestarsoftware/tssgrid/vue`)。競合(`ag-grid-react` / `@handsontable/react`)と同じ「コアはフレームワーク非依存のまま・薄いコンポーネントがライフサイクルと props を橋渡し」の形。**コアは無依存を維持**=`react` / `vue` は **peerDependency(任意・`peerDependenciesMeta.optional`)**、アダプタは `React.createElement` / `Vue.h` で手書き=**ビルド不要(UMD)**。
|
|
10
|
+
- グリッドは**アンコントロールド**:マウントで生成・アンマウントで `destroy()`、**cell を React/Vue で描かない**(高速)。値は **ref で公開した grid 本体(命令API)** + `onAfterChange` 等のコールバック。`data` プロップは参照が変わった時だけ `setData`(毎レンダー新配列を渡すとリセット=頻繁な更新は命令APIで)。
|
|
11
|
+
- React: `<TssGridReact columns={..} data={..} onAfterChange={..} ref={ref} />`、`ref.current` が grid 本体。StrictMode 二重マウント安全(`destroy` 冪等)、SSR/Next 安全(構築は `useEffect`=クライアント専用)。
|
|
12
|
+
- Vue: `<TssGridVue :columns :data :options="{ onAfterChange }" ref="g" />`、`g.grid()` が grid 本体。
|
|
13
|
+
- `columns/data/plugins/options` と全 `on*` コールバックをそのまま透過。型(`.d.ts`)付き。例 `examples/react.html` / `examples/vue.html`(CDN UMD のノービルドデモ)。
|
|
14
|
+
|
|
15
|
+
## [0.1.36] - 2026-11-06
|
|
16
|
+
|
|
17
|
+
inline ピッカー3兄弟(**tss-time / tss-calendar / tss-datetime**)に共通して潜んでいた「ポップアップの `<select>` が使えない/確定がピッカー内部の値を書き戻す/編集中の表示が食い違う」を一掃。土台としてコアに2つの affordance を追加。
|
|
18
|
+
|
|
19
|
+
### 修正(コア)
|
|
20
|
+
- **inline エディタの補助ポップアップ(`<select>` 等 focus を奪う要素を含む)へフォーカスが移っても編集を閉じない**ようにした。従来は共有 input が blur した瞬間に確定+ポップアップ破棄だったため、**時/分/年/月のプルダウンを開こうとクリックすると閉じてしまい選べなかった**。inline ctx に `setPopup(el)` を追加し、`blur` の `relatedTarget` が登録ポップアップ内なら編集継続。
|
|
21
|
+
- **カスタム `editor` を付けた date/time 列はネイティブ `<input type="date|time">` を重ねない**ようにした(`_nativeInputType` が `cfg.editor` を見る)。従来は `picker:false` を書き忘れるとネイティブ入力が裏に残り、**ブラウザのロケールで `09:00 AM` のように AM/PM 表示が出てセル表示(24h)と食い違っていた**。`picker:false` が無くてもカスタムエディタが入力/表示を一貫して握る。
|
|
22
|
+
|
|
23
|
+
### 修正(プラグイン)
|
|
24
|
+
- **`tss-time`**: inline で**打鍵後にポップアップの[決定]/Enter を押すと、select が持つ元の値を書き戻して打鍵が消えていた**のを修正(inline は共有 input を確定・select を触ったら input へミラー+フォーカスを input へ戻す)。`hour12` 列で **AM/PM を切り替えると編集中だけ 24h 表記(`06:30`)がちらついた**のも、ミラーを 12h 表示形(`6:30 AM`・parser も受ける)にして解消。上記コア修正と合わせ、**時/分のプルダウンが選べる**ように。
|
|
25
|
+
- **`tss-calendar`**: **年/月のプルダウンを押すとポップアップが閉じて選べなかった**のを修正(`setPopup` 登録+選択後に共有 input へフォーカスを戻す)。値の確定(日クリック)・打鍵追従(`onInput`)は従来どおり。
|
|
26
|
+
- **`tss-datetime`**: 同じく**年/月/時/分のプルダウンが選べなかった**のに加え、**打鍵がピッカーに反映されない/ピッカー操作が共有 input に出ない/`<select>` 変更後にクリックアウトすると表示文字列がそのまま生保存されて保存値が壊れる**(`YYYY-MM-DDTHH:MM` にならない)という問題を一括修正。**`TssDatetime.parse`(表示形→保存形。`format` の逆)を新設**し、列に `parse` が無ければ自動付与=打鍵追従も確定(blur/Enter/決定)の round-trip も成立。ピッカー操作は表示形で共有 input にミラー+フォーカスを戻す。保存値は常に `YYYY-MM-DDTHH:MM`。
|
|
27
|
+
|
|
6
28
|
## [0.1.35] - 2026-11-06
|
|
7
29
|
|
|
8
30
|
### 追加(プラグイン)
|
package/README.md
CHANGED
|
@@ -78,9 +78,39 @@ const { TssGrid, HistoryManager } = require('@threestarsoftware/tssgrid');
|
|
|
78
78
|
|
|
79
79
|
## フレームワークで使う(React / Vue / Angular)
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
**React / Vue には薄い公式アダプタ**(`@threestarsoftware/tssgrid/react`・`/vue`)があります。コアは無依存のまま=`react`・`vue` は **peerDependency(任意)**、アダプタは `React.createElement` / `Vue.h` で手書き=**ノービルド**。グリッドは「アンコントロールド」(マウントで生成・アンマウントで `destroy()`・**cell は React/Vue で描かない**=高速)、値は **ref で公開した grid 本体(命令API)** + `onAfterChange` 等のコールバック、`data` プロップは「差し替え」用(参照が変わった時だけ `setData`)。動く例: [`examples/react.html`](https://tssgrid.threestarsoftware.co.jp/examples/react.html) / [`examples/vue.html`](https://tssgrid.threestarsoftware.co.jp/examples/vue.html)。
|
|
82
82
|
|
|
83
|
-
**React
|
|
83
|
+
**React(公式アダプタ)**:
|
|
84
|
+
```jsx
|
|
85
|
+
import { useRef } from 'react';
|
|
86
|
+
import { TssGridReact } from '@threestarsoftware/tssgrid/react';
|
|
87
|
+
import '@threestarsoftware/tssgrid/tssgrid.css';
|
|
88
|
+
|
|
89
|
+
function Grid({ columns, data }) {
|
|
90
|
+
const ref = useRef(null); // ref.current = grid 本体(命令API)
|
|
91
|
+
return <TssGridReact columns={columns} data={data}
|
|
92
|
+
onAfterChange={(ch, src) => console.log(ch, src)} ref={ref} />;
|
|
93
|
+
// 例: ref.current.sortBy('age','desc') / ref.current.getRows()
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Vue 3(公式アダプタ)**:
|
|
98
|
+
```vue
|
|
99
|
+
<script setup>
|
|
100
|
+
import { ref } from 'vue';
|
|
101
|
+
import { TssGridVue } from '@threestarsoftware/tssgrid/vue';
|
|
102
|
+
import '@threestarsoftware/tssgrid/tssgrid.css';
|
|
103
|
+
const g = ref(null); // g.value.grid() = grid 本体
|
|
104
|
+
</script>
|
|
105
|
+
<template>
|
|
106
|
+
<TssGridVue :columns="columns" :data="data"
|
|
107
|
+
:options="{ onAfterChange: (ch, src) => console.log(ch, src) }" ref="g" />
|
|
108
|
+
</template>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
アダプタを使わず**依存ゼロで直接**使うこともできます(vanilla on DOM。CRA/Vite/Next/Webpack/Angular/Nuxt でそのまま。コツは **マウント時に生成し、アンマウント時に `grid.destroy()`**)。
|
|
112
|
+
|
|
113
|
+
**React(直接)**:
|
|
84
114
|
```jsx
|
|
85
115
|
import { useEffect, useRef } from 'react';
|
|
86
116
|
import { TssGrid } from '@threestarsoftware/tssgrid';
|
|
@@ -562,6 +592,8 @@ new TssGrid(el, { columns: [{ type: 'time', editor: t }] });
|
|
|
562
592
|
```
|
|
563
593
|
- `opts`: `step`(分刻み, 既定1) / `hour12`(AM/PM 表示, 既定false) / `icon`(既定 `'🕐'`) / `openOnClick`(既定true) / `className`。
|
|
564
594
|
- 🕐 を**シングルクリックで開閉**、**現在**/**決定**ボタン、キーボード(Enter 決定 / Esc 取消)、外側クリックで採用、フィルで下へコピー。見た目は `--time-*` 変数と `tss-time-*` クラスで自由に。
|
|
595
|
+
- カスタム `editor` を付けた列は**ネイティブ `<input type="time">` を重ねません**(`picker:false` を書かなくてもOK)。`hour12` 列は編集中も **12h 表示**で一貫(保存は 24h)。
|
|
596
|
+
- **日付+時刻**は姉妹プラグイン `tss-datetime`(`editor: TssDatetime({ step: 5 }), format: TssDatetime.format`)。保存値は `'YYYY-MM-DDTHH:MM'`、表示は曜日つき。表示形→保存形の逆変換 **`TssDatetime.parse`**(列に `parse` 未指定なら自動付与)で、打鍵追従も確定の round-trip も成立。年/月/時/分のプルダウンは inline でもそのまま選べます。
|
|
565
597
|
|
|
566
598
|
### 同梱プラグイン: セルのコメント(tss-comment・Excel のメモ風)
|
|
567
599
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threestarsoftware/tssgrid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
|
|
@@ -67,9 +67,29 @@
|
|
|
67
67
|
"types": "./plugins/tss-comment.d.ts",
|
|
68
68
|
"default": "./plugins/tss-comment.js"
|
|
69
69
|
},
|
|
70
|
+
"./react": {
|
|
71
|
+
"types": "./react/index.d.ts",
|
|
72
|
+
"default": "./react/index.js"
|
|
73
|
+
},
|
|
74
|
+
"./vue": {
|
|
75
|
+
"types": "./vue/index.d.ts",
|
|
76
|
+
"default": "./vue/index.js"
|
|
77
|
+
},
|
|
70
78
|
"./plugins/*": "./plugins/*",
|
|
71
79
|
"./package.json": "./package.json"
|
|
72
80
|
},
|
|
81
|
+
"peerDependencies": {
|
|
82
|
+
"react": ">=16.8.0",
|
|
83
|
+
"vue": ">=3.0.0"
|
|
84
|
+
},
|
|
85
|
+
"peerDependenciesMeta": {
|
|
86
|
+
"react": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"vue": {
|
|
90
|
+
"optional": true
|
|
91
|
+
}
|
|
92
|
+
},
|
|
73
93
|
"unpkg": "src/tssgrid.js",
|
|
74
94
|
"jsdelivr": "src/tssgrid.js",
|
|
75
95
|
"sideEffects": [
|
|
@@ -83,6 +103,8 @@
|
|
|
83
103
|
"src/tssgrid.css",
|
|
84
104
|
"src/tssgrid.d.ts",
|
|
85
105
|
"plugins/",
|
|
106
|
+
"react/",
|
|
107
|
+
"vue/",
|
|
86
108
|
"README.md",
|
|
87
109
|
"CHANGELOG.md",
|
|
88
110
|
"LICENSE"
|
package/plugins/tss-calendar.js
CHANGED
|
@@ -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
|
-
|
|
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:'' で消せる)
|
package/plugins/tss-datetime.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
|
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);
|
package/plugins/tss-time.js
CHANGED
|
@@ -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')) {
|
|
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();
|
|
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
|
|
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/react/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// TssGridReact — TssGrid の薄い React アダプタの型。
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { TssGrid, TssGridOptions } from '../src/tssgrid';
|
|
4
|
+
|
|
5
|
+
// トップレベルに書ける定番オプション+全 on* コールバックは Partial<TssGridOptions> として受ける。
|
|
6
|
+
// それ以外は options={{…}} でまとめて渡す。className / style は host の <div> に付く。
|
|
7
|
+
export interface TssGridReactProps extends Partial<TssGridOptions> {
|
|
8
|
+
className?: string;
|
|
9
|
+
style?: React.CSSProperties;
|
|
10
|
+
options?: TssGridOptions;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ref には grid 本体(命令API)が入る。
|
|
14
|
+
declare const TssGridReact: React.ForwardRefExoticComponent<
|
|
15
|
+
TssGridReactProps & React.RefAttributes<TssGrid>
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
export default TssGridReact;
|
|
19
|
+
export { TssGridReact };
|
package/react/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*! TssGridReact — TssGrid の薄い React アダプタ(UMD・ノービルド・react は peerDependency)
|
|
2
|
+
*
|
|
3
|
+
* 方針(競合 ag-grid-react / @handsontable/react と同じ):
|
|
4
|
+
* - グリッドは「アンコントロールド」= マウント時に1回生成し、アンマウントで destroy()。cell を React で描かない(高速)。
|
|
5
|
+
* - 値の読み書きは ref 経由の命令API(grid 本体)+ onAfterChange 等のコールバック。data プロップは「差し替え」用。
|
|
6
|
+
* - 全ての on* コールバック(onAfterChange / onEditEnd …)と columns/data/plugins/options をそのまま TssGrid へ渡す。
|
|
7
|
+
*
|
|
8
|
+
* 使い方:
|
|
9
|
+
* import { TssGridReact } from '@threestarsoftware/tssgrid/react';
|
|
10
|
+
* import '@threestarsoftware/tssgrid/tssgrid.css';
|
|
11
|
+
* const ref = useRef(null);
|
|
12
|
+
* <TssGridReact columns={cols} data={rows} plugins={[...]}
|
|
13
|
+
* onAfterChange={(ch, src) => ...} ref={ref} />
|
|
14
|
+
* // 命令API: ref.current.sortBy('col','desc') / ref.current.getRows() など(grid 本体)
|
|
15
|
+
*
|
|
16
|
+
* 注意:
|
|
17
|
+
* - data は「参照が変わった時だけ」setData されます(毎レンダー新配列を渡すと毎回リセット=カーソルも飛ぶ)。
|
|
18
|
+
* 頻繁な更新は ref の命令API(setValue/ appendRows 等)を使うのが定石。
|
|
19
|
+
* - 構築は useEffect(クライアント専用)なので SSR/Next でもそのまま(サーバでは空 div)。columns を変えたい時は key で remount。
|
|
20
|
+
*/
|
|
21
|
+
(function (root, factory) {
|
|
22
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
23
|
+
module.exports = factory(require('react'), require('../src/tssgrid.js'));
|
|
24
|
+
} else {
|
|
25
|
+
root.TssGridReact = factory(root.React, { TssGrid: root.TssGrid });
|
|
26
|
+
}
|
|
27
|
+
})(typeof self !== 'undefined' ? self : this, function (React, core) {
|
|
28
|
+
'use strict';
|
|
29
|
+
var TssGrid = core && (core.TssGrid || core);
|
|
30
|
+
var h = React.createElement;
|
|
31
|
+
// トップレベルに書ける定番オプション(それ以外は options={{…}} で)。
|
|
32
|
+
var PASS = { columns: 1, headers: 1, data: 1, plugins: 1, name: 1, width: 1, height: 1, rowHeight: 1, rowHeaders: 1, colHeaders: 1, minSpareRows: 1, maxRows: 1, history: 1, virtual: 1, hiddenColumns: 1 };
|
|
33
|
+
|
|
34
|
+
function buildOptions(props) {
|
|
35
|
+
var o = {};
|
|
36
|
+
if (props.options) for (var k in props.options) o[k] = props.options[k];
|
|
37
|
+
for (var key in props) {
|
|
38
|
+
if (PASS[key]) o[key] = props[key];
|
|
39
|
+
else if (/^on[A-Z]/.test(key) && typeof props[key] === 'function') o[key] = props[key]; // on* コールバック総通し
|
|
40
|
+
}
|
|
41
|
+
return o;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var TssGridReact = React.forwardRef(function (props, ref) {
|
|
45
|
+
var hostRef = React.useRef(null);
|
|
46
|
+
var gridRef = React.useRef(null);
|
|
47
|
+
var propsRef = React.useRef(props);
|
|
48
|
+
propsRef.current = props;
|
|
49
|
+
var firstData = React.useRef(true);
|
|
50
|
+
var readyState = React.useState(0); // grid 生成後に ref(命令API)を更新させるためのバンプ
|
|
51
|
+
|
|
52
|
+
React.useEffect(function () { // 生成は1回(アンコントロールド)。StrictMode 二重でも destroy が冪等。
|
|
53
|
+
var grid = new TssGrid(hostRef.current, buildOptions(propsRef.current));
|
|
54
|
+
gridRef.current = grid;
|
|
55
|
+
firstData.current = true;
|
|
56
|
+
readyState[1](function (n) { return n + 1; }); // ref.current = grid 本体 に更新(useImperativeHandle を再評価)
|
|
57
|
+
return function () { try { grid.destroy(); } catch (e) {} if (gridRef.current === grid) gridRef.current = null; };
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
React.useEffect(function () { // data の参照が変わったら差し替え(初回=生成時の data を使うのでスキップ)
|
|
61
|
+
if (firstData.current) { firstData.current = false; return; }
|
|
62
|
+
var g = gridRef.current;
|
|
63
|
+
if (g && propsRef.current.data != null) g.setData(propsRef.current.data);
|
|
64
|
+
}, [props.data]);
|
|
65
|
+
|
|
66
|
+
React.useImperativeHandle(ref, function () { return gridRef.current; }, [readyState[0]]); // grid 本体を命令API として公開(生成後に更新)
|
|
67
|
+
|
|
68
|
+
return h('div', { ref: hostRef, className: props.className, style: props.style });
|
|
69
|
+
});
|
|
70
|
+
TssGridReact.displayName = 'TssGridReact';
|
|
71
|
+
TssGridReact.TssGridReact = TssGridReact; // import { TssGridReact } / import TssGridReact どちらも通す
|
|
72
|
+
TssGridReact.default = TssGridReact;
|
|
73
|
+
return TssGridReact;
|
|
74
|
+
});
|
package/src/tssgrid.js
CHANGED
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
this._headerSel = null; // 'col'/'row'/'all'=列/行/全選択モード(結合に拡張せず実列/行にクランプ=Excel流)。null=通常セル選択
|
|
160
160
|
this._selCols = new Set(); // 飛び飛びの列選択(Ctrl+ヘッダクリック)。_headerSel==='col' の時だけ有効。連続選択は矩形と一致
|
|
161
161
|
this._selRows = new Set(); // 飛び飛びの行選択(Ctrl+行ヘッダクリック)。_headerSel==='row' の時だけ有効
|
|
162
|
-
this._inlineEd = null; this._inlineCtx = null; // inline エディタ(editor.inline=true): 共有 input
|
|
162
|
+
this._inlineEd = null; this._inlineCtx = null; this._inlineEl = null; // inline エディタ(editor.inline=true): 共有 input に乗るエディタ。_inlineEl=補助ポップアップ参照
|
|
163
163
|
this.disjointSelect = opts.disjointSelect !== false; // Ctrl+ヘッダで行/列を飛び飛び複数選択(false で無効=Ctrl+クリックは単一選択に戻る)
|
|
164
164
|
// リサイズ(列幅・行高)
|
|
165
165
|
// 列幅 colW[c]=px(未指定は既定)。指定方法は3通り、優先度=実行時リサイズ/明示colW > columns[c].width > 既定:
|
|
@@ -1158,7 +1158,9 @@
|
|
|
1158
1158
|
// ネイティブ入力(date/time ピッカー)を使う列か。使わない=テキスト入力。
|
|
1159
1159
|
_nativeInputType(c) {
|
|
1160
1160
|
const cfg = this.colCfg(c), t = cfg.type;
|
|
1161
|
-
|
|
1161
|
+
// カスタム editor を付けた列は、そのエディタが入力/表示を握る=ネイティブ picker を重ねない
|
|
1162
|
+
// (重ねるとブラウザのロケールで AM/PM 等が出て、セル表示と食い違う)。picker:false を書き忘れても破綻しない。
|
|
1163
|
+
if ((t === 'date' || t === 'time') && cfg.picker !== false && !cfg.format && typeof cfg.parse !== 'function' && !cfg.hour12 && !cfg.editor) return t;
|
|
1162
1164
|
return null;
|
|
1163
1165
|
}
|
|
1164
1166
|
_usesTextEditor(c) {
|
|
@@ -2003,12 +2005,15 @@
|
|
|
2003
2005
|
setValue(v) { grid.editor.value = grid._cellStr(v); }, // 候補で置き換える用(確定はしない)
|
|
2004
2006
|
commit(v) { if (v != null) grid.editor.value = grid._cellStr(v); grid.commit(); grid.toNav(); grid.updateSelectionUI(); },
|
|
2005
2007
|
cancel() { grid.setActive(r, c); }, // 元値に戻して nav(Esc 相当)
|
|
2008
|
+
// inline 補助ポップアップ(<select> 等 focus を奪う要素を含む)を登録=その中へフォーカスが移っても編集を閉じない。
|
|
2009
|
+
setPopup(el) { grid._inlineEl = el || null; },
|
|
2006
2010
|
};
|
|
2007
2011
|
this._inlineEd = ed; this._inlineCtx = ctx;
|
|
2008
2012
|
try { ed.open(ctx); } catch (_) { this._inlineEd = null; this._inlineCtx = null; }
|
|
2009
2013
|
}
|
|
2010
2014
|
_closeInlineEditor() {
|
|
2011
2015
|
const ed = this._inlineEd;
|
|
2016
|
+
this._inlineEl = null; // 登録された補助ポップアップ参照を解除
|
|
2012
2017
|
if (!ed) return;
|
|
2013
2018
|
this._inlineEd = null; this._inlineCtx = null;
|
|
2014
2019
|
if (ed.close) { try { ed.close(); } catch (_) {} }
|
|
@@ -3095,8 +3100,11 @@
|
|
|
3095
3100
|
else if (t === 'date' || t === 'time') this.toEdit(this.data[this.active.r][c], true); // ネイティブピッカー
|
|
3096
3101
|
// checkbox は文字入力を無視(Space / クリックでトグル)
|
|
3097
3102
|
});
|
|
3098
|
-
ed.addEventListener('blur', () => {
|
|
3103
|
+
ed.addEventListener('blur', (e) => {
|
|
3099
3104
|
if (this._customEditor) return; // カスタムエディタが奪ったフォーカスでの blur は無視
|
|
3105
|
+
// inline エディタの補助ポップアップ(<select> 等)へフォーカスが移った blur は編集継続(閉じない)。
|
|
3106
|
+
const to = (e && e.relatedTarget) || document.activeElement;
|
|
3107
|
+
if (this._inlineEl && to && this._inlineEl.contains(to)) return;
|
|
3100
3108
|
if (this.mode === 'edit' && (!this.select || this.select.style.display === 'none')) { this.commit(); this.toNav(); this.updateSelectionUI(); }
|
|
3101
3109
|
});
|
|
3102
3110
|
});
|
package/vue/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// TssGridVue — TssGrid の薄い Vue 3 アダプタの型。
|
|
2
|
+
import { DefineComponent } from 'vue';
|
|
3
|
+
import { TssGrid, TssGridOptions } from '../src/tssgrid';
|
|
4
|
+
|
|
5
|
+
export type TssGridVueProps = Partial<TssGridOptions> & { options?: TssGridOptions };
|
|
6
|
+
|
|
7
|
+
// expose: grid() で grid 本体(命令API)を返す。
|
|
8
|
+
export interface TssGridVueExposed {
|
|
9
|
+
grid(): TssGrid | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare const TssGridVue: DefineComponent<TssGridVueProps>;
|
|
13
|
+
|
|
14
|
+
export default TssGridVue;
|
|
15
|
+
export { TssGridVue };
|
package/vue/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*! TssGridVue — TssGrid の薄い Vue 3 アダプタ(UMD・ノービルド・vue は peerDependency)
|
|
2
|
+
*
|
|
3
|
+
* 方針(React アダプタと同じ): グリッドはアンコントロールド。マウントで生成・アンマウントで destroy()。
|
|
4
|
+
* 値の読み書きは expose した grid 本体(命令API)+ on* コールバック。data プロップは「差し替え」用。
|
|
5
|
+
*
|
|
6
|
+
* 使い方:
|
|
7
|
+
* import { TssGridVue } from '@threestarsoftware/tssgrid/vue';
|
|
8
|
+
* import '@threestarsoftware/tssgrid/tssgrid.css';
|
|
9
|
+
* <TssGridVue :columns="cols" :data="rows" :plugins="[...]" :options="{ onAfterChange }" ref="g" />
|
|
10
|
+
* // 命令API: this.$refs.g.grid().sortBy('col','desc') など(grid 本体を返す)
|
|
11
|
+
*
|
|
12
|
+
* 注意: on* コールバックは :options(例 :options="{ onAfterChange: fn }")か、@after-change などの属性で渡せます。
|
|
13
|
+
* data は参照が変わった時だけ setData(毎回新配列はリセット=カーソルが飛ぶ)。頻繁な更新は grid の命令APIで。
|
|
14
|
+
*/
|
|
15
|
+
(function (root, factory) {
|
|
16
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
17
|
+
module.exports = factory(require('vue'), require('../src/tssgrid.js'));
|
|
18
|
+
} else {
|
|
19
|
+
root.TssGridVue = factory(root.Vue, { TssGrid: root.TssGrid });
|
|
20
|
+
}
|
|
21
|
+
})(typeof self !== 'undefined' ? self : this, function (Vue, core) {
|
|
22
|
+
'use strict';
|
|
23
|
+
var TssGrid = core && (core.TssGrid || core);
|
|
24
|
+
var PASS = ['columns', 'data', 'plugins', 'headers', 'name', 'width', 'height', 'rowHeight', 'rowHeaders', 'colHeaders', 'minSpareRows', 'maxRows', 'history', 'virtual', 'hiddenColumns'];
|
|
25
|
+
|
|
26
|
+
var TssGridVue = Vue.defineComponent({
|
|
27
|
+
name: 'TssGridVue',
|
|
28
|
+
inheritAttrs: false, // on* 属性を div に二重付けしない
|
|
29
|
+
props: {
|
|
30
|
+
columns: Array, data: Array, plugins: Array, options: Object, headers: Array,
|
|
31
|
+
name: String, width: [Number, String], height: [Number, String],
|
|
32
|
+
rowHeight: Number, rowHeaders: { type: Boolean, default: undefined }, minSpareRows: Number,
|
|
33
|
+
},
|
|
34
|
+
setup: function (props, ctx) {
|
|
35
|
+
var el = Vue.ref(null);
|
|
36
|
+
var grid = null;
|
|
37
|
+
function buildOptions() {
|
|
38
|
+
var o = {};
|
|
39
|
+
if (props.options) for (var k in props.options) o[k] = props.options[k];
|
|
40
|
+
PASS.forEach(function (key) { if (props[key] !== undefined) o[key] = props[key]; });
|
|
41
|
+
var attrs = ctx.attrs || {};
|
|
42
|
+
for (var a in attrs) { if (/^on[A-Z]/.test(a) && typeof attrs[a] === 'function') o[a] = attrs[a]; } // @after-change 等=attrs の onAfterChange
|
|
43
|
+
return o;
|
|
44
|
+
}
|
|
45
|
+
Vue.onMounted(function () { grid = new TssGrid(el.value, buildOptions()); });
|
|
46
|
+
Vue.onBeforeUnmount(function () { if (grid) { try { grid.destroy(); } catch (e) {} grid = null; } });
|
|
47
|
+
Vue.watch(function () { return props.data; }, function (d) { if (grid && d != null) grid.setData(d); }); // 参照が変わったら差し替え
|
|
48
|
+
ctx.expose({ grid: function () { return grid; } }); // grid 本体を命令API として公開
|
|
49
|
+
return function () { return Vue.h('div', { ref: el }); };
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
TssGridVue.TssGridVue = TssGridVue;
|
|
54
|
+
TssGridVue.default = TssGridVue;
|
|
55
|
+
return TssGridVue;
|
|
56
|
+
});
|