@threestarsoftware/tssgrid 0.0.3 → 0.0.5
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/README.md +37 -24
- package/package.json +1 -1
- package/plugins/tss-autocomplete.js +1 -0
- package/plugins/tss-bar.js +1 -0
- package/plugins/tss-calc.js +1 -0
- package/plugins/tss-calendar.js +1 -0
- package/plugins/tss-datetime.css +36 -0
- package/plugins/tss-datetime.js +184 -0
- package/plugins/tss-filter.js +1 -0
- package/plugins/tss-formula.js +1 -0
- package/plugins/tss-furigana.js +1 -0
- package/plugins/tss-heatmap.js +1 -0
- package/plugins/tss-merge.js +1 -0
- package/plugins/tss-messages.js +1 -0
- package/plugins/tss-running.js +1 -0
- package/plugins/tss-shared.js +1 -0
- package/plugins/tss-sparkline.js +1 -0
- package/plugins/tss-sum.js +1 -0
- package/plugins/tss-syncscroll.js +1 -0
- package/plugins/tss-time.js +1 -0
- package/plugins/tss-toggle.js +1 -0
- package/plugins/tss-totals.js +1 -0
- package/src/tssgrid.css +8 -0
- package/src/tssgrid.d.ts +5 -0
- package/src/tssgrid.js +195 -71
package/README.md
CHANGED
|
@@ -238,7 +238,8 @@ grid.moveRows(r0, r1, to); // 連続する複数行をま
|
|
|
238
238
|
|
|
239
239
|
### 非破壊フィルタ(`filter` / `clearFilter`)
|
|
240
240
|
|
|
241
|
-
**コア内蔵**の非破壊フィルタ(行インデックスマッパ)。全行は内部の `_allRows
|
|
241
|
+
**コア内蔵**の非破壊フィルタ(行インデックスマッパ)。全行は内部の `_allRows`(マスタ)に保持したまま、**絞り込んだ可視部分集合だけを描画**します。要点は**絞り込み後の描画・ライブ検索が軽い**こと(自社調べ:10万行を保持して約200行に絞り込み → 初回描画 ≈ 0.5 秒、絞り込み済みのライブ検索 ≈ 数 ms)→ 動く例: [`examples/filter-live.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-live.html)。
|
|
242
|
+
なお**全件をそのままスクロール表示**したい大量行は、仮想スクロール `virtual: true` を使います(下記)。
|
|
242
243
|
|
|
243
244
|
```js
|
|
244
245
|
grid.filter(row => row[2] >= 100); // 述語: (row, masterIndex, src) => bool
|
|
@@ -254,9 +255,21 @@ grid.getAllRows(); // マスタ全行(フィルタ中で
|
|
|
254
255
|
- 制限(v1): フィルタ中は **列の挿入削除・行移動・セル結合**はロック(解除してから)。`filter`/`clearFilter` は**Undo履歴をクリア**(ビューindex基準の履歴が境界を跨ぐと不整合になるため)。
|
|
255
256
|
- **フィルタUI**(ヘッダの絞り込み等)はコアに入れず**プラグイン** `tss-filter`(下記)で。素朴な `setData` 差し替え方式の旧サンプルは [`examples/filter.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter.html)。
|
|
256
257
|
|
|
258
|
+
### 仮想スクロール(`virtual: true`)
|
|
259
|
+
|
|
260
|
+
大量行を**全件そのままスクロール表示**したい時に。全行は `data` に保持したまま、**画面に見える窓ぶん+バッファだけを描画**します(固定行高ウィンドウイング)。全件を一度に DOM 化しないので**初期描画が一瞬・スクロールも軽い**。有効化は**オプション一行**だけ。
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
new TssGrid(el, { virtual: true, data, /* … */ });
|
|
264
|
+
new TssGrid(el, { virtual: { buffer: 10 }, data }); // 窓の上下バッファ行数(既定6)
|
|
265
|
+
```
|
|
266
|
+
- **自社調べ**(headless Chrome / Win11): 10 万行で**初回描画 ≈ 46 ms**(全件描画 ≈ 10.5 秒に対して)。スクロール 1 コマの窓差し替え ≈ 3 ms。動く例: [`examples/virtual.html`](https://tssgrid.threestarsoftware.co.jp/examples/virtual.html)。
|
|
267
|
+
- 制約(v1): **行高一定**/**固定行列・折り返し・セル結合は非対応**(指定時は自動で無効化+警告)。行番号の桁数が増える場合は `rowHeaderWidth` で行ヘッダ幅を広げられます。
|
|
268
|
+
- フィルタとの関係: `filter()` は「絞り込んだ部分集合だけ描く」軽さ、`virtual` は「絞らず全件をスクロールする」軽さ。用途で使い分け(v1 では併用は未対応)。
|
|
269
|
+
|
|
257
270
|
### 同梱プラグイン: ヘッダ オートフィルタ(filter UI)
|
|
258
271
|
|
|
259
|
-
`plugins/tss-filter.js`+`.css
|
|
272
|
+
`plugins/tss-filter.js`+`.css`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= 各見出しの**ロート(漏斗)アイコン**をクリックで**値チェックリスト+検索**のポップアップ(Excel風。ソートの▲▼と区別できるアイコン)。複数列の条件は **AND**。中身は上の**コア `filter()` を呼ぶだけ**。動く例: [`examples/filter-ui.html`](https://tssgrid.threestarsoftware.co.jp/examples/filter-ui.html)。
|
|
260
273
|
|
|
261
274
|
```html
|
|
262
275
|
<link rel="stylesheet" href="plugins/tss-filter.css">
|
|
@@ -271,7 +284,7 @@ grid.getPlugin('filter').clearAll(); // 全条件クリア
|
|
|
271
284
|
|
|
272
285
|
### 同梱プラグイン: 共有マスタ(複数グリッドで1データ)
|
|
273
286
|
|
|
274
|
-
`plugins/tss-shared.js
|
|
287
|
+
`plugins/tss-shared.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= **1つのマスタ(同じ行データ)を複数グリッドで共有**。左右を**別フィルタ/別ソート**で表示でき、**片方の編集がもう片方の同じ行へ即反映**(行参照を共有)。A系/B系を並べて突き合わせ編集する用途。動く例: [`examples/shared-master.html`](https://tssgrid.threestarsoftware.co.jp/examples/shared-master.html)。
|
|
275
288
|
|
|
276
289
|
```html
|
|
277
290
|
<script src="plugins/tss-shared.js"></script>
|
|
@@ -322,7 +335,7 @@ TssGrid.registerPlugin('autosave', autoSavePlugin); // 名前登録 → plugin
|
|
|
322
335
|
|
|
323
336
|
## カスタムエディタ
|
|
324
337
|
|
|
325
|
-
組込エディタ(text/dropdown/checkbox/date/time/number)以外に、**独自の編集UI**を列に差せます。`columns[c].editor`
|
|
338
|
+
組込エディタ(text/dropdown/checkbox/date/time/number)以外に、**独自の編集UI**を列に差せます。`columns[c].editor` に**薄いオブジェクト契約**を渡すだけ<!--DEV-->(無依存)<!--/DEV-->。
|
|
326
339
|
|
|
327
340
|
```js
|
|
328
341
|
const myEditor = {
|
|
@@ -349,7 +362,7 @@ new TssGrid(el, { columns: [{ editor: myEditor }] });
|
|
|
349
362
|
|
|
350
363
|
### 同梱プラグイン: 休日カレンダー対応の日付ピッカー
|
|
351
364
|
|
|
352
|
-
`plugins/tss-calendar.js
|
|
365
|
+
`plugins/tss-calendar.js`<!--DEV-->(**無依存**)<!--/DEV-->= ネイティブ `<input type=date>` では出来ない**会社の休日カレンダー(JSON)対応**の日付ピッカー。勤怠・シフト・予約などで効きます(動く例: [`examples/calendar.html`](https://tssgrid.threestarsoftware.co.jp/examples/calendar.html))。
|
|
353
366
|
|
|
354
367
|
```html
|
|
355
368
|
<link rel="stylesheet" href="plugins/tss-calendar.css">
|
|
@@ -370,7 +383,7 @@ new TssGrid(el, { columns: [{ type: 'date', editor: cal, format: 'yyyy/mm/dd' }]
|
|
|
370
383
|
|
|
371
384
|
### 同梱プラグイン: 時刻ピッカー(時/分プルダウン)
|
|
372
385
|
|
|
373
|
-
`plugins/tss-time.js
|
|
386
|
+
`plugins/tss-time.js`<!--DEV-->(**無依存**)<!--/DEV-->= 業務の時刻入力向けの**時/分プルダウン**ピッカー(クロックフェイスのようなリッチさは省き、任意時刻を素早く)。保存値は常に **24h `'HH:MM'`**(組込 `type:'time'` と互換)。動く例: [`examples/time.html`](https://tssgrid.threestarsoftware.co.jp/examples/time.html)。
|
|
374
387
|
|
|
375
388
|
```html
|
|
376
389
|
<link rel="stylesheet" href="plugins/tss-time.css">
|
|
@@ -385,7 +398,7 @@ new TssGrid(el, { columns: [{ type: 'time', editor: t }] });
|
|
|
385
398
|
|
|
386
399
|
### 同梱プラグイン: 派生列(formula / computed column)
|
|
387
400
|
|
|
388
|
-
`plugins/tss-formula.js
|
|
401
|
+
`plugins/tss-formula.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->=「その行の値だけから計算する列」を `columns[c].formula` 関数で書けるようにする。金額=数量×単価、税込=金額×1.1 のような**派生列**を宣言的に。動く例: [`examples/formula.html`](https://tssgrid.threestarsoftware.co.jp/examples/formula.html)。
|
|
389
402
|
|
|
390
403
|
```html
|
|
391
404
|
<script src="plugins/tss-formula.js"></script>
|
|
@@ -408,7 +421,7 @@ new TssGrid(el, {
|
|
|
408
421
|
|
|
409
422
|
### 同梱プラグイン: 列の集計(totals / 縦の SUM・平均…)
|
|
410
423
|
|
|
411
|
-
`plugins/tss-totals.js
|
|
424
|
+
`plugins/tss-totals.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= 列方向の集計(`sum`/`avg`/`count`/`min`/`max`/独自関数)を担う。**計算だけで描画はしない**=結果を `onTotals` で受けて好きな場所へ(フッタ・ラベル・**別の TssGrid の合計表**でも)。`formula` と併用すると派生列の後に集計が連動。動く例: [`examples/formula.html`](https://tssgrid.threestarsoftware.co.jp/examples/formula.html)(明細=formula/合計=totals)。
|
|
412
425
|
|
|
413
426
|
```html
|
|
414
427
|
<script src="plugins/tss-totals.js"></script>
|
|
@@ -429,7 +442,7 @@ new TssGrid(el, {
|
|
|
429
442
|
|
|
430
443
|
### 同梱プラグイン: 条件付き合計(tss-sum / ラベル付き合計行・整列表示)
|
|
431
444
|
|
|
432
|
-
`plugins/tss-sum.js
|
|
445
|
+
`plugins/tss-sum.js`<!--DEV-->(**無依存・コア無改変**/ラベル横結合に TssMerge を任意利用)<!--/DEV-->= **ラベル付きの"合計行"**(小計・消費税10%・消費税8%・合計 など)を、**条件(`when`)+対象(`of`)を宣言**で定義して作る。`tss-totals` が「列ごと1値」なのに対し、こちらは**同じ列を条件別に何本でも集計**でき(消費税10%と8%は両方 amount 由来=per-column では衝突する)、**ソースと同一列構成の readonly 集計グリッドを生成**するので**金額が縦にピッタリ揃う**(画面でも印刷でも)。請求書・見積などの合計欄に。動く例: [`examples/invoice.html`](https://tssgrid.threestarsoftware.co.jp/examples/invoice.html)(インボイス請求書の合計欄)。
|
|
433
446
|
|
|
434
447
|
```html
|
|
435
448
|
<script src="plugins/tss-merge.js"></script> <!-- ラベルの横結合に利用 -->
|
|
@@ -453,7 +466,7 @@ const sum = TssSum.attach(grid, document.getElementById('sumgrid'), {
|
|
|
453
466
|
|
|
454
467
|
### 同梱プラグイン: 残高/累計(running total・縦の連鎖)
|
|
455
468
|
|
|
456
|
-
`plugins/tss-running.js
|
|
469
|
+
`plugins/tss-running.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->=「**前の行の結果 + この行の増減**」で縦に積む列。`formula`(行内)でも `totals`(全体合計)でも出せない**縦方向の連鎖**を担う。出納帳/通帳の**残高**、在庫受払の**在庫数**、経費・ポイントの**累計**など。動く例: [`examples/running.html`](https://tssgrid.threestarsoftware.co.jp/examples/running.html)(現金出納帳=入金・出金→残高)。
|
|
457
470
|
|
|
458
471
|
```html
|
|
459
472
|
<script src="plugins/tss-running.js"></script>
|
|
@@ -472,11 +485,11 @@ new TssGrid(el, {
|
|
|
472
485
|
- `column`(残高列) / `initial`(前残) / `delta` か `step` / `skipEmpty`(全列空の行は連鎖を進めず空表示、既定true)。
|
|
473
486
|
- **行ドラッグ移動(rowReorder)併用時**は順序が変わるので `onAfterRowMove` で `handle.recompute()` を呼ぶ。
|
|
474
487
|
|
|
475
|
-
> 「**行=formula/列=totals/縦=running**」の3点が揃う。いずれも `setValueRaw`+`onAfterChange`
|
|
488
|
+
> 「**行=formula/列=totals/縦=running**」の3点が揃う。いずれも `setValueRaw`+`onAfterChange` ラップで動く**<!--DEV-->コア無改変<!--/DEV-->プラグイン**。
|
|
476
489
|
|
|
477
490
|
### 同梱プラグイン: 氏名→フリガナ 自動入力(furigana・辞書不要)
|
|
478
491
|
|
|
479
|
-
`plugins/tss-furigana.js
|
|
492
|
+
`plugins/tss-furigana.js`(**<!--DEV-->無依存・<!--/DEV-->辞書不要<!--DEV-->・コア無改変<!--/DEV-->**)= 漢字を入力(IME変換)すると**読みが別列に自動で入る**(スマホ連絡先の よみがな 自動入力と同じ)。辞書引きではなく、**IME変換の composition イベントから「変換前のかな」を拾う**方式。TssGrid は**アクティブセルに本物の `<input>` を重ねる**設計なので composition が確実に取れる=「**IMEに強い**」を機能で体現。動く例: [`examples/furigana.html`](https://tssgrid.threestarsoftware.co.jp/examples/furigana.html)。
|
|
480
493
|
|
|
481
494
|
```html
|
|
482
495
|
<script src="plugins/tss-furigana.js"></script>
|
|
@@ -493,7 +506,7 @@ new TssGrid(el, {
|
|
|
493
506
|
|
|
494
507
|
### 同梱プラグイン: セル内ミニ計算式(calc・=12*3)
|
|
495
508
|
|
|
496
|
-
`plugins/tss-calc.js
|
|
509
|
+
`plugins/tss-calc.js`(**<!--DEV-->無依存・<!--/DEV-->eval/Function 不使用<!--DEV-->・コア無改変<!--/DEV-->**)= セルに `=12*3+5` と打つと確定時に **41** に。四則 `+ - * /`・括弧・小数対応の**自前再帰下降パーサ**(CSP安全)。`columns[c].parse`(入力変換)seam に乗るだけ。動く例: [`examples/calc.html`](https://tssgrid.threestarsoftware.co.jp/examples/calc.html)。
|
|
497
510
|
|
|
498
511
|
```html
|
|
499
512
|
<script src="plugins/tss-calc.js"></script>
|
|
@@ -509,7 +522,7 @@ plugins: [ TssCalc.plugin({ columns: ['quantity', 'price'] }) ] // columns 省
|
|
|
509
522
|
|
|
510
523
|
### 同梱プラグイン: オートコンプリート(autocomplete エディタ)
|
|
511
524
|
|
|
512
|
-
`plugins/tss-autocomplete.js
|
|
525
|
+
`plugins/tss-autocomplete.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= 自由入力+**候補の絞り込み**。固定 dropdown の上位版で「だいたい決まってるが新規もある」項目(取引先・商品名)向け。**IME変換中の Enter は奪わない**(変換確定が最優先)。動く例: [`examples/autocomplete.html`](https://tssgrid.threestarsoftware.co.jp/examples/autocomplete.html)。
|
|
513
526
|
|
|
514
527
|
```html
|
|
515
528
|
<link rel="stylesheet" href="plugins/tss-autocomplete.css">
|
|
@@ -527,7 +540,7 @@ columns: [
|
|
|
527
540
|
|
|
528
541
|
### 同梱プラグイン: セル内スパークライン(sparkline・ミニグラフ)
|
|
529
542
|
|
|
530
|
-
`plugins/tss-sparkline.js
|
|
543
|
+
`plugins/tss-sparkline.js`(**<!--DEV-->無依存・<!--/DEV-->SVG自前描画<!--DEV-->・コア無改変<!--/DEV-->**)= セルの数列を**ミニ折れ線/棒**で描く。`columns[c].html:true` + `format`(format が返す SVG がセルに入る)seam に乗るだけ。Chart.js 等は積まない。動く例: [`examples/sparkline.html`](https://tssgrid.threestarsoftware.co.jp/examples/sparkline.html)(月次売上→動向グラフ・編集でライブ更新)。
|
|
531
544
|
|
|
532
545
|
```html
|
|
533
546
|
<script src="plugins/tss-sparkline.js"></script>
|
|
@@ -541,11 +554,11 @@ columns: [
|
|
|
541
554
|
formula: (row) => ['m4','m5','m6','m7','m8','m9'].map(k=>row[k]).filter(v=>v!=='').join(',') }
|
|
542
555
|
```
|
|
543
556
|
- `type`('line'/'bar') / `width` / `height` / `color` / `strokeWidth` / `fill`(折れ線下の塗り) / `min`・`max`(既定は自動)。
|
|
544
|
-
- 数値しか SVG に入れない=安全。`formula`/`html-cells` と同じ仕組みで、**有料グリッドの "セル内グラフ"
|
|
557
|
+
- 数値しか SVG に入れない=安全。`formula`/`html-cells` と同じ仕組みで、**有料グリッドの "セル内グラフ" を無料<!--DEV-->・無依存<!--/DEV-->で**。
|
|
545
558
|
|
|
546
559
|
### 同梱プラグイン: セル内 横棒グラフ(bar・データバー)
|
|
547
560
|
|
|
548
|
-
`plugins/tss-bar.js
|
|
561
|
+
`plugins/tss-bar.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= セルの数値を**横棒**で描く(認知率・進捗%・スコア)。`html:true`+`format` seam。`type:'number'` 併用で**バーのまま編集**(値を打つと伸びる)。動く例: [`examples/bar.html`](https://tssgrid.threestarsoftware.co.jp/examples/bar.html)。
|
|
549
562
|
|
|
550
563
|
```html
|
|
551
564
|
<script src="plugins/tss-bar.js"></script>
|
|
@@ -560,7 +573,7 @@ columns: [
|
|
|
560
573
|
|
|
561
574
|
### 同梱プラグイン: ヒートマップ / 背景データバー(cellStyle)
|
|
562
575
|
|
|
563
|
-
`plugins/tss-heatmap.js
|
|
576
|
+
`plugins/tss-heatmap.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= コアの `cellStyle` フックに乗せる**連続色(ヒートマップ)**と**背景データバー**。`tss-bar.js`(前景の棒+ラベル)と違い、**セル背景**を塗る=**数値はそのまま見せて編集できる**。動く例: [`examples/heatmap.html`](https://tssgrid.threestarsoftware.co.jp/examples/heatmap.html)。
|
|
564
577
|
|
|
565
578
|
```html
|
|
566
579
|
<script src="plugins/tss-heatmap.js"></script>
|
|
@@ -574,7 +587,7 @@ columns: [
|
|
|
574
587
|
- `TssHeatmap`: `min`/`max`(明示。セル単位呼び出しで列範囲を知らないため)/ `colors`(2色以上のストップ) or `low`/`high` / `text`(自動文字色, 既定on)。
|
|
575
588
|
- `TssDataBar`: `min`/`max` / `color` / `align`('left'/'right')。編集で即更新。
|
|
576
589
|
|
|
577
|
-
> 「セル内グラフ」3点: **前景の折れ線/棒=`html:true`+`format`(tss-sparkline / tss-bar)**、**背景の連続色/バー=`cellStyle`(tss-heatmap
|
|
590
|
+
> 「セル内グラフ」3点: **前景の折れ線/棒=`html:true`+`format`(tss-sparkline / tss-bar)**、**背景の連続色/バー=`cellStyle`(tss-heatmap)**。どれも<!--DEV-->無依存・<!--/DEV-->ノービルド。
|
|
578
591
|
|
|
579
592
|
### グラフ連携(Chart.js などと)— 編集でライブ更新
|
|
580
593
|
|
|
@@ -588,7 +601,7 @@ new TssGrid(el, { columns, data, onAfterChange: () => { chart.data = toChartData
|
|
|
588
601
|
|
|
589
602
|
### 同梱プラグイン: 2択トグル(toggle・有効|無効)
|
|
590
603
|
|
|
591
|
-
`plugins/tss-toggle.js`+`.css
|
|
604
|
+
`plugins/tss-toggle.js`+`.css`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= ラジオの2択を**並んだセグメント(有効|無効)**で表示、クリックで切替。チェックボックスより**2ラベルが見える**。値は dropdown 同様 **value を保存**。動く例: [`examples/toggle.html`](https://tssgrid.threestarsoftware.co.jp/examples/toggle.html)。
|
|
592
605
|
|
|
593
606
|
```html
|
|
594
607
|
<link rel="stylesheet" href="plugins/tss-toggle.css">
|
|
@@ -605,7 +618,7 @@ plugins: [ TssToggle.plugin({ columns: {
|
|
|
605
618
|
|
|
606
619
|
### 同梱プラグイン: セル結合(merge・横/縦/矩形)
|
|
607
620
|
|
|
608
|
-
`plugins/tss-merge.js
|
|
621
|
+
`plugins/tss-merge.js`<!--DEV-->(**無依存**)<!--/DEV-->= データセルの**矩形結合**(`colspan`×`rowspan`)。左上の**アンカーが値・編集・選択の対象**で、覆われた従属セルは**描画スキップ+読み取り専用**(貼り付け・フィルは弾く)。動く例: [`examples/merge.html`](https://tssgrid.threestarsoftware.co.jp/examples/merge.html)。(見出しの結合は `nestedHeaders` を参照)
|
|
609
622
|
|
|
610
623
|
```html
|
|
611
624
|
<script src="plugins/tss-merge.js"></script>
|
|
@@ -631,7 +644,7 @@ plugins: [ TssMerge.plugin({ merges: [
|
|
|
631
644
|
|
|
632
645
|
### 同梱プラグイン: 同期スクロール(syncscroll・左右2表を並べて比較)
|
|
633
646
|
|
|
634
|
-
`plugins/tss-syncscroll.js
|
|
647
|
+
`plugins/tss-syncscroll.js`<!--DEV-->(**無依存・コア無改変**)<!--/DEV-->= 並べた複数グリッドのスクロールを連動(Excel「並べて比較+同時にスクロール」相当)。A系/B系で別々に絞り込んだ表を**突き合わせながら編集**する用途に。動く例: [`examples/sync-scroll.html`](https://tssgrid.threestarsoftware.co.jp/examples/sync-scroll.html)。
|
|
635
648
|
|
|
636
649
|
```html
|
|
637
650
|
<script src="plugins/tss-syncscroll.js"></script>
|
|
@@ -898,7 +911,7 @@ new TssGrid(el, {
|
|
|
898
911
|
|
|
899
912
|
### 入力の正規化 / 表示の整形(`parse` / `format`)
|
|
900
913
|
|
|
901
|
-
**保存値**(`getData()`
|
|
914
|
+
**保存値**(`getData()` が返す内部値)と、**入力**・**表示**を分離できます。外部ライブラリは同梱せず、必要なら列のフックに差し込む方針です<!--DEV-->(コアは無依存のまま)<!--/DEV-->。
|
|
902
915
|
|
|
903
916
|
| フック | 向き | 役割 |
|
|
904
917
|
|---|---|---|
|
|
@@ -1108,7 +1121,7 @@ command は `{ apply(), revert(), label }`。
|
|
|
1108
1121
|
## 制限
|
|
1109
1122
|
|
|
1110
1123
|
- 外部(Excel等)→グリッドの貼り付けはブラウザのクリップボード読取許可が必要。`file://` で読めない場合は http(ローカルサーバ)で開く。グリッド間・グリッド内のコピペは内部バッファで常に動く。
|
|
1111
|
-
-
|
|
1124
|
+
- 仮想スクロールは `virtual: true` で対応(固定行高・固定行列/折り返し/セル結合は非対応)。日付の連番フィルはコピー扱い(数値のみ連番)。
|
|
1112
1125
|
|
|
1113
1126
|
## License
|
|
1114
1127
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threestarsoftware/tssgrid",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
|
package/plugins/tss-bar.js
CHANGED
package/plugins/tss-calc.js
CHANGED
package/plugins/tss-calendar.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/*! TssCalendar — 日付ピッカー(TssGrid のカスタムエディタ / 単体ウィジェット両用)
|
|
2
|
+
* DEV: 無依存
|
|
2
3
|
* 休日カレンダーを JSON で渡せる(勤怠・シフト等向け)。CSS は tss-calendar.css をクラス/変数で自由に。
|
|
3
4
|
* 使い方(グリッド): columns: [{ type:'date', editor: TssCalendar({ holidays }) }]
|
|
4
5
|
* 使い方(単体) : TssCalendar({holidays}).openAt(anchorEl, isoValue, onPick)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* TssDatetime — 日付+時刻 同時ピッカーのレイアウト。
|
|
2
|
+
カレンダー部は tss-calendar.css(.tss-cal-*)、時刻部は tss-time.css(.tss-time-*)のスタイルを再利用するので、
|
|
3
|
+
この3枚を読み込む(または .tss-dt 配下に必要分を内包)。ここは「合体ポップアップ」の枠だけ定義。 */
|
|
4
|
+
.tss-dt {
|
|
5
|
+
--dt-bg: #fff;
|
|
6
|
+
--dt-fg: #222;
|
|
7
|
+
--dt-border: #c3ccd6;
|
|
8
|
+
--dt-accent: #2aa0d8;
|
|
9
|
+
--dt-radius: 8px;
|
|
10
|
+
--dt-font: 13px "Segoe UI", "Meiryo", sans-serif;
|
|
11
|
+
|
|
12
|
+
position: fixed; z-index: 10000; background: var(--dt-bg); color: var(--dt-fg);
|
|
13
|
+
border: 1px solid var(--dt-border); border-radius: var(--dt-radius);
|
|
14
|
+
box-shadow: 0 6px 24px rgba(0,0,0,.18); padding: 8px; font: var(--dt-font); user-select: none; outline: none;
|
|
15
|
+
}
|
|
16
|
+
/* 選択中の値リードアウト=標準の日時に無い「曜日」をここで必ず見せる */
|
|
17
|
+
.tss-dt-readout { font-size: 13px; padding: 3px 6px 7px; margin-bottom: 4px; border-bottom: 1px dashed var(--dt-border); font-variant-numeric: tabular-nums; }
|
|
18
|
+
.tss-dt-readout .tss-dt-ro-wd { color: var(--dt-accent); margin: 0 2px; }
|
|
19
|
+
.tss-dt-readout .tss-dt-ro-empty { color: #99a; }
|
|
20
|
+
|
|
21
|
+
.tss-dt-body { display: flex; gap: 12px; align-items: flex-start; }
|
|
22
|
+
.tss-dt-time { padding-top: 2px; }
|
|
23
|
+
.tss-dt-time-lbl { font-size: 11px; color: #889; margin: 0 0 4px 2px; }
|
|
24
|
+
|
|
25
|
+
/* 合体ポップアップ内ではカレンダー/時刻の各ポップアップ枠(影・border・fixed)を無効化=中身だけ使う */
|
|
26
|
+
.tss-dt .tss-cal, .tss-dt .tss-time { position: static; box-shadow: none; border: 0; padding: 0; z-index: auto; }
|
|
27
|
+
.tss-dt .tss-time-row { display: flex; align-items: center; gap: 4px; }
|
|
28
|
+
.tss-dt .tss-time-row select { font: inherit; padding: 2px 4px; border: 1px solid var(--dt-border); border-radius: 5px; background: var(--dt-bg); color: var(--dt-fg); cursor: pointer; }
|
|
29
|
+
.tss-dt .tss-time-sep { font-weight: 600; }
|
|
30
|
+
|
|
31
|
+
.tss-dt-btns { display: flex; align-items: center; gap: 6px; margin-top: 8px; }
|
|
32
|
+
.tss-dt-btns .tss-dt-spacer { flex: 1 1 auto; }
|
|
33
|
+
.tss-dt-btns button { font: inherit; padding: 4px 12px; border: 1px solid var(--dt-border); background: var(--dt-bg); color: var(--dt-fg); border-radius: 6px; cursor: pointer; }
|
|
34
|
+
.tss-dt-btns button:hover { background: #eef4ff; }
|
|
35
|
+
.tss-dt-btns .tss-dt-ok { border-color: var(--dt-accent); background: var(--dt-accent); color: #fff; }
|
|
36
|
+
.tss-dt-btns .tss-dt-clear { color: #b6473e; }
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/*! TssDatetime — 日付+時刻を同時に設定するピッカー(TssGrid カスタムエディタ / 単体ウィジェット両用)
|
|
2
|
+
* DEV: 無依存。カレンダー部は tss-calendar.css、時刻部は tss-time.css のクラスを再利用+薄い tss-datetime.css。
|
|
3
|
+
* 保存値は常に 'YYYY-MM-DDTHH:MM'(kintone DATETIME と相互変換しやすい正規形。表示は列 format で整形)。
|
|
4
|
+
* 使い方(グリッド): columns: [{ type:'text', editor: TssDatetime({ step:5 }), format: TssDatetime.format }]
|
|
5
|
+
* 使い方(単体) : TssDatetime({step:15}).openAt(anchorEl, 'YYYY-MM-DDTHH:MM', onPick)
|
|
6
|
+
* opts: holidays / weekend / weekLabels / min / max(日付) / step(分刻み,既定1) / hour12 / icon(既定 '📅') / openOnClick(既定true) / className
|
|
7
|
+
*/
|
|
8
|
+
(function (root) {
|
|
9
|
+
'use strict';
|
|
10
|
+
function pad(n) { return String(n).padStart(2, '0'); }
|
|
11
|
+
function isoD(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
|
|
12
|
+
function parseISOd(s) { var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(s || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; }
|
|
13
|
+
// 'YYYY-MM-DD[ T]HH:MM'(時刻省略可)→ { d:Date, h:int|null, mi:int|null }
|
|
14
|
+
function parseDT(s) {
|
|
15
|
+
var m = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{1,2}):(\d{2}))?/.exec(String(s || '').trim());
|
|
16
|
+
if (!m) return null;
|
|
17
|
+
return { d: new Date(+m[1], +m[2] - 1, +m[3]), h: m[4] != null ? +m[4] : null, mi: m[5] != null ? +m[5] : null };
|
|
18
|
+
}
|
|
19
|
+
// 'HH:MM'(日付無し=時刻のみ。kintone TIME フィールド互換)→ { h, m }
|
|
20
|
+
function parseTimeOnly(s) { var m = /^(\d{1,2}):(\d{2})$/.exec(String(s || '').trim()); if (!m) return null; var h = +m[1], mi = +m[2]; return (h <= 23 && mi <= 59) ? { h: h, m: mi } : null; }
|
|
21
|
+
function sameDay(a, b) { return !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); }
|
|
22
|
+
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|
23
|
+
var WD = ['日', '月', '火', '水', '木', '金', '土']; // 既定の曜日ラベル(標準の日時に無い「曜日」を補う=差別化点)
|
|
24
|
+
|
|
25
|
+
function TssDatetime(opts) {
|
|
26
|
+
opts = opts || {};
|
|
27
|
+
var holidays = opts.holidays || {};
|
|
28
|
+
var weekend = opts.weekend || [0, 6];
|
|
29
|
+
var weekLabels = opts.weekLabels || ['日', '月', '火', '水', '木', '金', '土'];
|
|
30
|
+
var step = Math.max(1, opts.step || 1);
|
|
31
|
+
var hour12 = !!opts.hour12;
|
|
32
|
+
var minD = parseISOd(opts.min), maxD = parseISOd(opts.max);
|
|
33
|
+
var holName = function (h) { return h == null ? null : (typeof h === 'object' ? h.name : h); };
|
|
34
|
+
var holType = function (h) { return (h && typeof h === 'object' && h.type) ? h.type : null; };
|
|
35
|
+
var ty = new Date().getFullYear();
|
|
36
|
+
var yrLo = opts.yearMin || (minD ? minD.getFullYear() : ty - 12);
|
|
37
|
+
var yrHi = opts.yearMax || (maxD ? maxD.getFullYear() : ty + 12);
|
|
38
|
+
|
|
39
|
+
var pop = null, view = null, selD = null, selH = null, selM = null, ref = null, done = false, outside = null;
|
|
40
|
+
|
|
41
|
+
function dDisabled(d) { if (minD && d < minD) return true; if (maxD && d > maxD) return true; return false; }
|
|
42
|
+
|
|
43
|
+
function calHTML() {
|
|
44
|
+
var y = view.getFullYear(), m = view.getMonth(), start = new Date(y, m, 1).getDay(), today = new Date();
|
|
45
|
+
var lo = Math.min(yrLo, y), hi = Math.max(yrHi, y), yrSel = '<select class="tss-cal-sel-yr" aria-label="年">';
|
|
46
|
+
for (var yy = lo; yy <= hi; yy++) yrSel += '<option value="' + yy + '"' + (yy === y ? ' selected' : '') + '>' + yy + '年</option>';
|
|
47
|
+
yrSel += '</select>';
|
|
48
|
+
var monSel = '<select class="tss-cal-sel-mon" aria-label="月">';
|
|
49
|
+
for (var mm = 0; mm < 12; mm++) monSel += '<option value="' + mm + '"' + (mm === m ? ' selected' : '') + '>' + (mm + 1) + '月</option>';
|
|
50
|
+
monSel += '</select>';
|
|
51
|
+
var html = '<div class="tss-cal-head">'
|
|
52
|
+
+ '<button type="button" class="tss-cal-nav" data-nav="-1" aria-label="前の月">‹</button>'
|
|
53
|
+
+ '<span class="tss-cal-sels">' + yrSel + monSel + '</span>'
|
|
54
|
+
+ '<button type="button" class="tss-cal-nav" data-nav="1" aria-label="次の月">›</button></div>'
|
|
55
|
+
+ '<div class="tss-cal-grid">';
|
|
56
|
+
for (var w = 0; w < 7; w++) { var wc = w === 0 ? ' tss-cal-sun' : (w === 6 ? ' tss-cal-sat' : ''); html += '<div class="tss-cal-wl' + wc + '">' + esc(weekLabels[w]) + '</div>'; }
|
|
57
|
+
var d = new Date(y, m, 1 - start);
|
|
58
|
+
for (var i = 0; i < 42; i++) {
|
|
59
|
+
var cls = 'tss-cal-day', dow = d.getDay(), k = isoD(d), h = holidays[k], nm = holName(h), tp = holType(h);
|
|
60
|
+
if (d.getMonth() !== m) cls += ' tss-cal-other';
|
|
61
|
+
if (dow === 0) cls += ' tss-cal-sun'; else if (dow === 6) cls += ' tss-cal-sat';
|
|
62
|
+
if (h) { cls += ' tss-cal-holiday'; if (tp) cls += ' tss-cal-hol-' + tp; }
|
|
63
|
+
if (sameDay(d, today)) cls += ' tss-cal-today';
|
|
64
|
+
if (sameDay(d, selD)) cls += ' tss-cal-sel';
|
|
65
|
+
if (dDisabled(d)) cls += ' tss-cal-disabled';
|
|
66
|
+
html += '<div class="' + cls + '" data-d="' + k + '"' + (nm ? ' title="' + esc(nm) + '"' : '') + '>' + d.getDate() + '</div>';
|
|
67
|
+
d.setDate(d.getDate() + 1);
|
|
68
|
+
}
|
|
69
|
+
return html + '</div>';
|
|
70
|
+
}
|
|
71
|
+
function timeHTML() {
|
|
72
|
+
var hSel = '<select class="tss-time-h" aria-label="時"><option value=""></option>';
|
|
73
|
+
if (hour12) { for (var i = 1; i <= 12; i++) { var d24 = (selH == null) ? null : ((selH % 12) || 12); hSel += '<option value="' + i + '"' + (d24 === i ? ' selected' : '') + '>' + i + '</option>'; } }
|
|
74
|
+
else { for (var j = 0; j <= 23; j++) hSel += '<option value="' + j + '"' + (selH === j ? ' selected' : '') + '>' + pad(j) + '</option>'; }
|
|
75
|
+
hSel += '</select>';
|
|
76
|
+
var mins = []; for (var k = 0; k < 60; k += step) mins.push(k);
|
|
77
|
+
if (selM != null && mins.indexOf(selM) < 0) { mins.push(selM); mins.sort(function (a, b) { return a - b; }); }
|
|
78
|
+
var mSel = '<select class="tss-time-m" aria-label="分"><option value=""></option>';
|
|
79
|
+
for (var n = 0; n < mins.length; n++) mSel += '<option value="' + mins[n] + '"' + (selM === mins[n] ? ' selected' : '') + '>' + pad(mins[n]) + '</option>';
|
|
80
|
+
mSel += '</select>';
|
|
81
|
+
var aSel = '';
|
|
82
|
+
if (hour12) { var pm = selH != null && selH >= 12; aSel = '<select class="tss-time-ap" aria-label="午前/午後"><option value="AM"' + (!pm ? ' selected' : '') + '>AM</option><option value="PM"' + (pm ? ' selected' : '') + '>PM</option></select>'; }
|
|
83
|
+
return '<div class="tss-dt-time-lbl">時刻</div><div class="tss-time-row">' + hSel + '<span class="tss-time-sep">:</span>' + mSel + aSel + '</div>';
|
|
84
|
+
}
|
|
85
|
+
function readoutHTML() {
|
|
86
|
+
var hasT = selH != null || selM != null;
|
|
87
|
+
var t = hasT ? pad(selH == null ? 0 : selH) + ':' + pad(selM == null ? 0 : selM) : '';
|
|
88
|
+
if (!selD) return hasT ? '<b class="tss-dt-ro-wd">' + t + '</b><span class="tss-dt-ro-empty">(日付なし)</span>' : '<span class="tss-dt-ro-empty">日付/時刻を選択</span>';
|
|
89
|
+
var wd = (opts.weekLabels || WD)[selD.getDay()];
|
|
90
|
+
return isoD(selD).replace(/-/g, '/') + '<b class="tss-dt-ro-wd">(' + esc(wd) + ')</b>' + (hasT ? ' ' + t : '');
|
|
91
|
+
}
|
|
92
|
+
function render() {
|
|
93
|
+
pop.innerHTML = '<div class="tss-dt-readout">' + readoutHTML() + '</div>'
|
|
94
|
+
+ '<div class="tss-dt-body"><div class="tss-dt-cal">' + calHTML() + '</div><div class="tss-dt-time">' + timeHTML() + '</div></div>'
|
|
95
|
+
+ '<div class="tss-dt-btns"><button type="button" class="tss-dt-clear">クリア</button><span class="tss-dt-spacer"></span>'
|
|
96
|
+
+ '<button type="button" class="tss-dt-now">現在</button><button type="button" class="tss-dt-ok">決定</button></div>';
|
|
97
|
+
}
|
|
98
|
+
function readTime() {
|
|
99
|
+
var hS = pop.querySelector('.tss-time-h'), mS = pop.querySelector('.tss-time-m'), aS = pop.querySelector('.tss-time-ap');
|
|
100
|
+
selH = hS && hS.value !== '' ? +hS.value : null;
|
|
101
|
+
selM = mS && mS.value !== '' ? +mS.value : null;
|
|
102
|
+
if (hour12 && aS && selH != null) { selH = aS.value === 'PM' ? (selH === 12 ? 12 : selH + 12) : (selH === 12 ? 0 : selH); }
|
|
103
|
+
}
|
|
104
|
+
function value() {
|
|
105
|
+
readTime();
|
|
106
|
+
var hasT = selH != null || selM != null;
|
|
107
|
+
var t = hasT ? pad(selH == null ? 0 : selH) + ':' + pad(selM == null ? 0 : selM) : '';
|
|
108
|
+
if (selD && hasT) return isoD(selD) + 'T' + t; // 日付+時刻
|
|
109
|
+
if (selD) return isoD(selD); // 日付のみ
|
|
110
|
+
if (hasT) return t; // 時刻のみ(日付無し='HH:MM')
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
function place(anchor) {
|
|
114
|
+
var r = anchor.getBoundingClientRect();
|
|
115
|
+
pop.style.left = Math.max(2, Math.min(r.left, window.innerWidth - pop.offsetWidth - 6)) + 'px';
|
|
116
|
+
var top = r.bottom + 2;
|
|
117
|
+
if (top + pop.offsetHeight > window.innerHeight) top = Math.max(2, r.top - pop.offsetHeight - 2);
|
|
118
|
+
pop.style.top = top + 'px';
|
|
119
|
+
}
|
|
120
|
+
function onClick(e) {
|
|
121
|
+
var t = e.target;
|
|
122
|
+
var nav = t.getAttribute && t.getAttribute('data-nav');
|
|
123
|
+
if (nav) { view = new Date(view.getFullYear(), view.getMonth() + (+nav), 1); render(); return; }
|
|
124
|
+
var dd = t.getAttribute && t.getAttribute('data-d');
|
|
125
|
+
if (dd) { readTime(); var d = parseISOd(dd); if (!dDisabled(d)) { selD = d; render(); } return; } // 日付選択しても閉じない(時刻も決めるため)
|
|
126
|
+
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; }
|
|
127
|
+
if (t.classList.contains('tss-dt-clear')) { commit(''); return; }
|
|
128
|
+
if (t.classList.contains('tss-dt-ok')) { commit(value()); return; }
|
|
129
|
+
}
|
|
130
|
+
function onChange(e) {
|
|
131
|
+
var t = e.target;
|
|
132
|
+
if (t.classList.contains('tss-cal-sel-yr')) { view = new Date(+t.value, view.getMonth(), 1); render(); }
|
|
133
|
+
else if (t.classList.contains('tss-cal-sel-mon')) { view = new Date(view.getFullYear(), +t.value, 1); render(); }
|
|
134
|
+
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(); }
|
|
135
|
+
}
|
|
136
|
+
function onKey(e) {
|
|
137
|
+
if (e.key === 'Escape') { e.preventDefault(); cancel(); }
|
|
138
|
+
else if (e.key === 'Enter') { e.preventDefault(); commit(value()); }
|
|
139
|
+
}
|
|
140
|
+
function show(anchor, val, onPick, onCancel) {
|
|
141
|
+
done = false;
|
|
142
|
+
var p = parseDT(val), tp = p ? null : parseTimeOnly(val); // 日付付き or 時刻のみ
|
|
143
|
+
selD = p ? p.d : null;
|
|
144
|
+
selH = p ? p.h : (tp ? tp.h : null);
|
|
145
|
+
selM = p ? p.mi : (tp ? tp.m : null);
|
|
146
|
+
view = selD ? new Date(selD.getFullYear(), selD.getMonth(), 1) : new Date();
|
|
147
|
+
pop = document.createElement('div'); pop.className = 'tss-dt' + (opts.className ? ' ' + opts.className : ''); pop.tabIndex = -1;
|
|
148
|
+
document.body.appendChild(pop);
|
|
149
|
+
pop.addEventListener('mousedown', function (e) { if (e.target.tagName !== 'SELECT') e.preventDefault(); }); // フォーカスを奪わない(select 除く)
|
|
150
|
+
pop.addEventListener('click', onClick);
|
|
151
|
+
pop.addEventListener('change', onChange);
|
|
152
|
+
pop.addEventListener('keydown', onKey);
|
|
153
|
+
render(); place(anchor);
|
|
154
|
+
ref = { pick: onPick, cancel: onCancel };
|
|
155
|
+
outside = function (e) { if (pop && !pop.contains(e.target) && !(anchor && anchor.contains && anchor.contains(e.target))) commit(value()); }; // 外側クリックは確定(time と同方針)
|
|
156
|
+
setTimeout(function () { document.addEventListener('mousedown', outside, true); }, 0);
|
|
157
|
+
pop.focus();
|
|
158
|
+
}
|
|
159
|
+
function teardown() { if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } }
|
|
160
|
+
function commit(v) { if (done) return; done = true; var f = ref.pick; teardown(); f(v); }
|
|
161
|
+
function cancel() { if (done) return; done = true; var f = ref.cancel; teardown(); if (f) f(); }
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
open: function (ctx) { show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }); },
|
|
165
|
+
close: function () { teardown(); },
|
|
166
|
+
icon: opts.icon != null ? opts.icon : '📅',
|
|
167
|
+
openOnClick: opts.openOnClick !== false,
|
|
168
|
+
openAt: function (anchorEl, val, onPick) { show(anchorEl, val, function (v) { onPick(v); }, null); },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 表示整形ヘルパ: 'YYYY-MM-DDTHH:MM' → 'YYYY/MM/DD(曜) HH:MM'(列 format に渡せる)。
|
|
173
|
+
// 標準の日時表示で空欄になる「曜日」を必ず埋める=本プラグインの目玉。weekday:false で曜日を消せる。
|
|
174
|
+
TssDatetime.format = function (v, o) {
|
|
175
|
+
var p = parseDT(v);
|
|
176
|
+
if (!p) return parseTimeOnly(v) ? v : (v || ''); // 時刻のみ('HH:MM')はそのまま表示/不正値は素通し
|
|
177
|
+
var d = p.d, wd = (o && o.weekday === false) ? '' : '(' + (o && o.weekLabels || WD)[d.getDay()] + ')';
|
|
178
|
+
var t = (p.h != null) ? ' ' + pad(p.h) + ':' + pad(p.mi == null ? 0 : p.mi) : '';
|
|
179
|
+
return d.getFullYear() + '/' + pad(d.getMonth() + 1) + '/' + pad(d.getDate()) + wd + t;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = TssDatetime;
|
|
183
|
+
else root.TssDatetime = TssDatetime;
|
|
184
|
+
})(typeof self !== 'undefined' ? self : this);
|
package/plugins/tss-filter.js
CHANGED
package/plugins/tss-formula.js
CHANGED
package/plugins/tss-furigana.js
CHANGED
package/plugins/tss-heatmap.js
CHANGED
package/plugins/tss-merge.js
CHANGED
package/plugins/tss-messages.js
CHANGED
package/plugins/tss-running.js
CHANGED
package/plugins/tss-shared.js
CHANGED
package/plugins/tss-sparkline.js
CHANGED
package/plugins/tss-sum.js
CHANGED
package/plugins/tss-time.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/*! TssTime — 時刻ピッカー(TssGrid のカスタムエディタ / 単体ウィジェット両用)
|
|
2
|
+
* DEV: 無依存
|
|
2
3
|
* 時/分プルダウン方式(業務の任意時刻入力向き)。保存値は常に 24h 'HH:MM'(type:'time' と互換)。
|
|
3
4
|
* 使い方(グリッド): columns: [{ type:'time', editor: TssTime({ step:15 }) }]
|
|
4
5
|
* opts: step(分刻み,既定1) / hour12(AM/PM表示,既定false) / icon(既定 '🕐') / openOnClick(既定true) / className
|
package/plugins/tss-toggle.js
CHANGED
package/plugins/tss-totals.js
CHANGED
package/src/tssgrid.css
CHANGED
|
@@ -60,6 +60,14 @@
|
|
|
60
60
|
border-right: 1px solid var(--tg-border); border-bottom: 1px solid var(--tg-border); padding: 0 6px;
|
|
61
61
|
font-size: var(--tg-font-size); box-sizing: border-box; white-space: nowrap; overflow: hidden;
|
|
62
62
|
}
|
|
63
|
+
/* 外周の二重線を解消=最終行の下罫線・最終列の右罫線は描かない(外枠は .tg-wrap の単線が担当)。
|
|
64
|
+
右下だけ「セル罫線+枠線」が重なって昔のHTMLTable風に見えるのを防ぐ=上下左右が均一な単線になる。 */
|
|
65
|
+
.tssgrid .tg-table tr:last-child > td,
|
|
66
|
+
.tssgrid .tg-table tr:last-child > th { border-bottom: none; }
|
|
67
|
+
.tssgrid .tg-table tr > td:last-child,
|
|
68
|
+
.tssgrid .tg-table tr > th:last-child { border-right: none; }
|
|
69
|
+
/* 仮想スクロールのスペーサ行(窓の上下を埋めて総高を保つだけの不可視行)。罫線/背景/余白を持たない。 */
|
|
70
|
+
.tssgrid .tg-table tr.tg-vspace > td { border: none; padding: 0; background: transparent; }
|
|
63
71
|
.tssgrid .tg-table th { background: var(--tg-colheader-bg); color: var(--tg-header-fg); font-weight: 600; text-align: center; }
|
|
64
72
|
/* z-index 階層(.tg-wrap 内): 通常セル 0 < 本体sticky(rowhead/frozen) 2 < 選択オーバーレイ 3
|
|
65
73
|
< editor/select 4 < fillhandle 5 < ヘッダ thead 6 < 角/固定列ヘッダ 7 < リサイズ線 8。
|
package/src/tssgrid.d.ts
CHANGED
|
@@ -126,6 +126,11 @@ export interface TssGridOptions {
|
|
|
126
126
|
cursor?: boolean;
|
|
127
127
|
stretchH?: 'none' | 'last' | 'all';
|
|
128
128
|
rowHeaders?: boolean;
|
|
129
|
+
/** 行番号列の幅(px)。既定 40。大量行で桁数が増える時に広げる。 */
|
|
130
|
+
rowHeaderWidth?: number;
|
|
131
|
+
/** 仮想スクロール(固定行高ウィンドウイング)。大量行を可視窓だけ描画。true か { buffer:窓上下バッファ行数(既定6) }。
|
|
132
|
+
* 制約(v1): 行高一定/固定行列・折り返し・セル結合は非対応(指定時は自動無効化)。 */
|
|
133
|
+
virtual?: boolean | { buffer?: number };
|
|
129
134
|
colHeaders?: boolean;
|
|
130
135
|
hiddenColumns?: number[];
|
|
131
136
|
wordWrap?: boolean;
|
package/src/tssgrid.js
CHANGED
|
@@ -148,6 +148,7 @@
|
|
|
148
148
|
this.mode = 'nav'; this.dragging = false; this.filling = null;
|
|
149
149
|
this.pendingCut = null;
|
|
150
150
|
this.headerDrag = null; this._colAnchor = 0; this._rowAnchor = 0; // 行/列ヘッダ選択
|
|
151
|
+
this._headerSel = null; // 'col'/'row'/'all'=列/行/全選択モード(結合に拡張せず実列/行にクランプ=Excel流)。null=通常セル選択
|
|
151
152
|
// リサイズ(列幅・行高)
|
|
152
153
|
// 列幅 colW[c]=px(未指定は既定)。指定方法は3通り、優先度=実行時リサイズ/明示colW > columns[c].width > 既定:
|
|
153
154
|
// 1) colWidths: [px, …] … 位置指定(従来)
|
|
@@ -182,6 +183,7 @@
|
|
|
182
183
|
this.resizeRows = opts.resizeRows !== false;
|
|
183
184
|
// 行/列ヘッダの表示(false で行番号列・ヘッダ行を隠す)
|
|
184
185
|
this.rowHeaders = opts.rowHeaders !== false;
|
|
186
|
+
this.rowHeaderW = (opts.rowHeaderWidth | 0) || 40; // 行番号列の幅(px)。大量行で桁数が増える時に広げる
|
|
185
187
|
this.colHeaders = opts.colHeaders !== false;
|
|
186
188
|
// 行ヘッダの「つまみ」をドラッグして行を並べ替える(行番号列が必要)
|
|
187
189
|
this.rowReorder = !!opts.rowReorder;
|
|
@@ -210,6 +212,17 @@
|
|
|
210
212
|
this.minSpareRows = opts.minSpareRows | 0; // 末尾に常に確保する空行数
|
|
211
213
|
this.autoWrapRow = !!opts.autoWrapRow; // Tab で行末→次行頭に折り返し
|
|
212
214
|
this.autoWrapCol = !!opts.autoWrapCol; // Enter で列末→次列頭に折り返し
|
|
215
|
+
this.navSkipReadOnly = !!opts.navSkipReadOnly; // Tab/Enter 送りで readonly セルを飛ばす(伝票入力向け=編集する所だけ移動)
|
|
216
|
+
// 仮想スクロール(簡易版・固定行高ウィンドウイング): 大量行で「可視窓+バッファ」だけ描画。
|
|
217
|
+
// 全行は data に保持・行座標は絶対のまま、tbody は窓ぶん+上下スペーサ行のみ。幾何はモデル(一定行高)。
|
|
218
|
+
// 制約(v1): 行高一定/セル結合・固定行列・折り返し非対応。指定時は無効化して警告。OFF時は一切作動せず従来通り。
|
|
219
|
+
this.virtual = !!opts.virtual;
|
|
220
|
+
this._vBuf = (opts.virtual && typeof opts.virtual === 'object' && opts.virtual.buffer) || 6; // 窓の上下バッファ行数
|
|
221
|
+
this._vStart = 0; this._vEnd = 0; this._vRAF = 0; this._vRendered = false;
|
|
222
|
+
if (this.virtual && (this.frozenRows || this.frozenCols || this.wordWrap)) {
|
|
223
|
+
try { console.warn('[TssGrid] virtual:true は固定行列/折り返し非対応のため、それらを無効化しました'); } catch (_) {}
|
|
224
|
+
this.frozenRows = 0; this.frozenCols = 0; this.wordWrap = false;
|
|
225
|
+
}
|
|
213
226
|
// 行/列数の下限・上限(挿入/削除の境界。min は初期パディングにも使う)
|
|
214
227
|
this.minRows = opts.minRows | 0; this.maxRows = opts.maxRows | 0;
|
|
215
228
|
this.minCols = opts.minCols | 0; this.maxCols = opts.maxCols | 0;
|
|
@@ -508,6 +521,7 @@
|
|
|
508
521
|
destroy() {
|
|
509
522
|
if (this._destroyed) return;
|
|
510
523
|
this._destroyed = true;
|
|
524
|
+
if (this._vRAF) { try { cancelAnimationFrame(this._vRAF); } catch (_) {} this._vRAF = 0; } // 仮想スクロールの保留フレームを破棄
|
|
511
525
|
for (const p of this._plugins) { if (p.handle && typeof p.handle.destroy === 'function') { try { p.handle.destroy(); } catch (_) {} } }
|
|
512
526
|
this._plugins = [];
|
|
513
527
|
if (this._ro) { try { this._ro.disconnect(); } catch (_) {} this._ro = null; }
|
|
@@ -968,7 +982,7 @@
|
|
|
968
982
|
|
|
969
983
|
_colWidth(c) { return this.colW[c] != null ? this.colW[c] : this.defColW; }
|
|
970
984
|
_rowHeight(r) { return this.rowH[r] != null ? this.rowH[r] : this.defRowH; }
|
|
971
|
-
_rhW() { return this.rowHeaders ?
|
|
985
|
+
_rhW() { return this.rowHeaders ? this.rowHeaderW : 0; } // 行ヘッダ列の幅(非表示なら 0)
|
|
972
986
|
// ---- 列の非表示 ----
|
|
973
987
|
_isHidden(c) { return this.hiddenCols.has(c); }
|
|
974
988
|
_visCols() { const a = []; for (let c = 0; c < this.COLS; c++) if (!this.hiddenCols.has(c)) a.push(c); return a; }
|
|
@@ -1094,10 +1108,101 @@
|
|
|
1094
1108
|
top += tr.offsetHeight;
|
|
1095
1109
|
});
|
|
1096
1110
|
}
|
|
1111
|
+
// 1行ぶんの <tr>…</tr> を生成(buildTable の本体ループと、仮想スクロールの窓再描画 _renderWindow で共用)。
|
|
1112
|
+
_rowHTML(r) {
|
|
1113
|
+
const rh = this.rowHeaders;
|
|
1114
|
+
const rgrip = (rh && this.resizeRows) ? '<div class="tg-rowgrip"></div>' : '';
|
|
1115
|
+
const dgrip = (rh && this.rowReorder && !this._hasVMerge()) ? '<span class="tg-rowmove" title="ドラッグで行を移動">⠿</span>' : ''; // 横結合は行と一緒に移動可/縦結合中は不可
|
|
1116
|
+
const rcls = (r < this.frozenRows) ? (' tg-frozen-row' + (r === this.frozenRows - 1 ? ' tg-frozen-row-edge' : '')) : ''; // 行固定
|
|
1117
|
+
let html = '<tr data-r="' + r + '" class="tg-row' + rcls + '" style="height:' + this._rowHeight(r) + 'px">' + (rh ? '<th class="rowhead" data-r="' + r + '">' + dgrip + (r + 1) + rgrip + '</th>' : '');
|
|
1118
|
+
for (let c = 0; c < this.COLS; c++) {
|
|
1119
|
+
if (this._merges && this._isCovered(r, c)) continue; // 結合に覆われた従属セルは td を出さない(アンカーが colspan で覆う)
|
|
1120
|
+
const cs = this._merges ? this._colspanAt(r, c) : 1; // 結合アンカーは colspan で従属列ぶんを覆う
|
|
1121
|
+
const rs = this._merges ? this._rowspanAt(r, c) : 1; // 縦結合は rowspan で従属行ぶんを覆う
|
|
1122
|
+
const cls = [];
|
|
1123
|
+
if (cs > 1 || rs > 1) cls.push('tg-merged'); // 結合アンカー(CSS で中央寄せ等のフック)
|
|
1124
|
+
if (c < this.frozenCols) { cls.push('tg-frozen'); if (c === this.frozenCols - 1) cls.push('tg-frozen-edge'); }
|
|
1125
|
+
if (this._isHidden(c)) cls.push('tg-hidden'); // 非表示列
|
|
1126
|
+
if (this.colType(c) === 'dropdown') cls.push('tg-dropdown'); // ▾ キャレット表示用
|
|
1127
|
+
if (this.colType(c) === 'number') cls.push('tg-num'); // 数値は右寄せ
|
|
1128
|
+
if (this._isReadOnly(r, c)) cls.push('tg-readonly'); // 読み取り専用(淡色+カーソル既定)
|
|
1129
|
+
if (this.colCfg(c).wordWrap) cls.push('tg-wrap-cell'); // 列単位の折り返し
|
|
1130
|
+
if (this.data[r][c] === '' && this._placeholder(c) != null) cls.push('tg-placeholder'); // 空セルのヒント
|
|
1131
|
+
const al = this._cellAlign(r, c); // 整列(列既定+セル上書き)
|
|
1132
|
+
if (al.h) cls.push('tg-h-' + al.h); if (al.v) cls.push('tg-v-' + al.v);
|
|
1133
|
+
let titleAttr = '';
|
|
1134
|
+
if (this._marksInvalid) { const _iv = this._coerceCell(r, c, this.data[r][c]); if (!_iv.ok) { cls.push('tg-invalid'); titleAttr = ' data-err-level="' + TssGrid.esc(_iv.level || 'error') + '"'; if (this.invalidTitle) titleAttr += ' title="' + TssGrid.esc(_iv.message || '入力値が不正です') + '"'; } }
|
|
1135
|
+
const ed = this.colCfg(c).editor; // カスタムエディタが icon を申告していれば右端に表示
|
|
1136
|
+
const ico = (ed && ed.icon) ? ' data-ico="' + TssGrid.esc(ed.icon) + '"' : '';
|
|
1137
|
+
if (ico) cls.push('tg-haspicker');
|
|
1138
|
+
const ccls = (this.cellClass || this.colCfg(c).cellClass) ? this._cellClasses(r, c) : []; // 条件付き書式
|
|
1139
|
+
for (const k of ccls) cls.push(k);
|
|
1140
|
+
const dccls = ccls.length ? ' data-ccls="' + TssGrid.esc(ccls.join(' ')) + '"' : '';
|
|
1141
|
+
let sty = ''; // セル単位インラインスタイル(連続色・データバー幅などCSS変数も)
|
|
1142
|
+
if (this.cellStyle || this.colCfg(c).cellStyle) {
|
|
1143
|
+
const so = this._cellStyleObj(r, c), sk = Object.keys(so);
|
|
1144
|
+
if (sk.length) { let css = ''; for (const k of sk) css += k + ':' + so[k] + ';'; sty = ' style="' + TssGrid.esc(css) + '" data-cstyle="' + TssGrid.esc(sk.join(' ')) + '"'; }
|
|
1145
|
+
}
|
|
1146
|
+
const a = cls.length ? ' class="' + cls.join(' ') + '"' : '';
|
|
1147
|
+
const csAttr = (cs > 1 ? ' colspan="' + cs + '"' : '') + (rs > 1 ? ' rowspan="' + rs + '"' : '');
|
|
1148
|
+
html += '<td' + a + ico + dccls + sty + titleAttr + csAttr + ' data-r="' + r + '" data-c="' + c + '">' + this._cellHTML(r, c) + '</td>';
|
|
1149
|
+
}
|
|
1150
|
+
return html + '</tr>';
|
|
1151
|
+
}
|
|
1152
|
+
// 仮想スクロール: 現在の scrollTop から描画すべき窓 [start,end)(絶対行)を算出。バッファ込み。
|
|
1153
|
+
_vWindow() {
|
|
1154
|
+
const rowH = this.defRowH;
|
|
1155
|
+
const viewH = (this.wrap && this.wrap.clientHeight) || parseInt(this.height, 10) || 400;
|
|
1156
|
+
const visible = Math.ceil(viewH / rowH) + 1;
|
|
1157
|
+
const scrollTop = this.wrap ? this.wrap.scrollTop : 0;
|
|
1158
|
+
let start = Math.floor(scrollTop / rowH) - this._vBuf;
|
|
1159
|
+
if (start < 0) start = 0;
|
|
1160
|
+
let end = start + visible + this._vBuf * 2;
|
|
1161
|
+
if (end > this.ROWS) end = this.ROWS;
|
|
1162
|
+
return { start, end };
|
|
1163
|
+
}
|
|
1164
|
+
// 仮想スクロール: tbody を窓ぶん+上下スペーサ行に差し替え(buildTable せず軽量再描画)。
|
|
1165
|
+
_renderWindow(win, force) {
|
|
1166
|
+
if (!force && this._vRendered && win.start === this._vStart && win.end === this._vEnd) return false;
|
|
1167
|
+
const tbody = this.table && this.table.querySelector('tbody');
|
|
1168
|
+
if (!tbody) return false;
|
|
1169
|
+
const rh = this.rowHeaders, colsSpan = this.COLS + (rh ? 1 : 0);
|
|
1170
|
+
const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
|
|
1171
|
+
let html = '';
|
|
1172
|
+
if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
1173
|
+
for (let r = win.start; r < win.end; r++) html += this._rowHTML(r);
|
|
1174
|
+
if (botH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + botH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
1175
|
+
tbody.innerHTML = html;
|
|
1176
|
+
this._vStart = win.start; this._vEnd = win.end; this._vRendered = true;
|
|
1177
|
+
this._syncHeaderCheckboxes();
|
|
1178
|
+
this.updateSelectionUI(); // セルが作り直されたので窓内の選択シェード/オーバーレイを再適用
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
// 仮想スクロール: スクロールで窓がずれたら再描画(rAF スロットル)。
|
|
1182
|
+
_vSync() {
|
|
1183
|
+
if (!this.virtual || this._vRAF) return;
|
|
1184
|
+
this._vRAF = requestAnimationFrame(() => {
|
|
1185
|
+
this._vRAF = 0;
|
|
1186
|
+
if (this._destroyed) return;
|
|
1187
|
+
this._renderWindow(this._vWindow(), false);
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
// 仮想スクロール: 行 r を可視域に入れる(scrollTop 調整→窓を同期描画)。setActive/setExtent から。
|
|
1191
|
+
_vEnsureVisible(r) {
|
|
1192
|
+
if (!this.virtual || !this.wrap) return;
|
|
1193
|
+
const rowH = this.defRowH, head = this._theadH || 0;
|
|
1194
|
+
const rowTop = head + r * rowH;
|
|
1195
|
+
const cur = this.wrap.scrollTop, viewH = this.wrap.clientHeight || 400;
|
|
1196
|
+
let st = cur;
|
|
1197
|
+
if (rowTop < cur + head) st = rowTop - head; // 上のヘッダ下に隠れてる→上端へ
|
|
1198
|
+
else if (rowTop + rowH > cur + viewH) st = rowTop + rowH - viewH; // 下に隠れてる→下端へ
|
|
1199
|
+
if (st !== cur) this.wrap.scrollTop = st;
|
|
1200
|
+
this._renderWindow(this._vWindow(), false); // 窓を即同期(place 前に DOM を確定)
|
|
1201
|
+
}
|
|
1097
1202
|
buildTable() {
|
|
1098
1203
|
this._selhdrFg = null; // 選択ヘッダ文字色キャッシュ破棄(テーマ変更に追従)
|
|
1099
1204
|
const rh = this.rowHeaders;
|
|
1100
|
-
let html = '<colgroup>' + (rh ? '<col style="width:
|
|
1205
|
+
let html = '<colgroup>' + (rh ? '<col style="width:' + this.rowHeaderW + 'px">' : '');
|
|
1101
1206
|
for (let c = 0; c < this.COLS; c++) html += '<col data-c="' + c + '"' + (this._isHidden(c) ? ' class="tg-hidden"' : '') + ' style="width:' + (this._isHidden(c) ? 0 : this._colWidth(c)) + 'px">';
|
|
1102
1207
|
html += '</colgroup>';
|
|
1103
1208
|
if (this.colHeaders) {
|
|
@@ -1126,44 +1231,15 @@
|
|
|
1126
1231
|
html += '</thead>';
|
|
1127
1232
|
}
|
|
1128
1233
|
html += '<tbody>';
|
|
1129
|
-
|
|
1130
|
-
const
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
html += '<tr
|
|
1134
|
-
for (let
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
const cls = [];
|
|
1139
|
-
if (cs > 1 || rs > 1) cls.push('tg-merged'); // 結合アンカー(CSS で中央寄せ等のフック)
|
|
1140
|
-
if (c < this.frozenCols) { cls.push('tg-frozen'); if (c === this.frozenCols - 1) cls.push('tg-frozen-edge'); }
|
|
1141
|
-
if (this._isHidden(c)) cls.push('tg-hidden'); // 非表示列
|
|
1142
|
-
if (this.colType(c) === 'dropdown') cls.push('tg-dropdown'); // ▾ キャレット表示用
|
|
1143
|
-
if (this.colType(c) === 'number') cls.push('tg-num'); // 数値は右寄せ
|
|
1144
|
-
if (this._isReadOnly(r, c)) cls.push('tg-readonly'); // 読み取り専用(淡色+カーソル既定)
|
|
1145
|
-
if (this.colCfg(c).wordWrap) cls.push('tg-wrap-cell'); // 列単位の折り返し
|
|
1146
|
-
if (this.data[r][c] === '' && this._placeholder(c) != null) cls.push('tg-placeholder'); // 空セルのヒント
|
|
1147
|
-
const al = this._cellAlign(r, c); // 整列(列既定+セル上書き)
|
|
1148
|
-
if (al.h) cls.push('tg-h-' + al.h); if (al.v) cls.push('tg-v-' + al.v);
|
|
1149
|
-
let titleAttr = '';
|
|
1150
|
-
if (this._marksInvalid) { const _iv = this._coerceCell(r, c, this.data[r][c]); if (!_iv.ok) { cls.push('tg-invalid'); titleAttr = ' data-err-level="' + TssGrid.esc(_iv.level || 'error') + '"'; if (this.invalidTitle) titleAttr += ' title="' + TssGrid.esc(_iv.message || '入力値が不正です') + '"'; } }
|
|
1151
|
-
const ed = this.colCfg(c).editor; // カスタムエディタが icon を申告していれば右端に表示
|
|
1152
|
-
const ico = (ed && ed.icon) ? ' data-ico="' + TssGrid.esc(ed.icon) + '"' : '';
|
|
1153
|
-
if (ico) cls.push('tg-haspicker');
|
|
1154
|
-
const ccls = (this.cellClass || this.colCfg(c).cellClass) ? this._cellClasses(r, c) : []; // 条件付き書式
|
|
1155
|
-
for (const k of ccls) cls.push(k);
|
|
1156
|
-
const dccls = ccls.length ? ' data-ccls="' + TssGrid.esc(ccls.join(' ')) + '"' : '';
|
|
1157
|
-
let sty = ''; // セル単位インラインスタイル(連続色・データバー幅などCSS変数も)
|
|
1158
|
-
if (this.cellStyle || this.colCfg(c).cellStyle) {
|
|
1159
|
-
const so = this._cellStyleObj(r, c), sk = Object.keys(so);
|
|
1160
|
-
if (sk.length) { let css = ''; for (const k of sk) css += k + ':' + so[k] + ';'; sty = ' style="' + TssGrid.esc(css) + '" data-cstyle="' + TssGrid.esc(sk.join(' ')) + '"'; }
|
|
1161
|
-
}
|
|
1162
|
-
const a = cls.length ? ' class="' + cls.join(' ') + '"' : '';
|
|
1163
|
-
const csAttr = (cs > 1 ? ' colspan="' + cs + '"' : '') + (rs > 1 ? ' rowspan="' + rs + '"' : '');
|
|
1164
|
-
html += '<td' + a + ico + dccls + sty + titleAttr + csAttr + ' data-r="' + r + '" data-c="' + c + '">' + this._cellHTML(r, c) + '</td>';
|
|
1165
|
-
}
|
|
1166
|
-
html += '</tr>';
|
|
1234
|
+
if (this.virtual) {
|
|
1235
|
+
const win = this._vWindow(); this._vStart = win.start; this._vEnd = win.end; this._vRendered = true;
|
|
1236
|
+
const colsSpan = this.COLS + (rh ? 1 : 0);
|
|
1237
|
+
const topH = win.start * this.defRowH, botH = (this.ROWS - win.end) * this.defRowH;
|
|
1238
|
+
if (topH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + topH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
1239
|
+
for (let r = win.start; r < win.end; r++) html += this._rowHTML(r);
|
|
1240
|
+
if (botH > 0) html += '<tr class="tg-vspace" aria-hidden="true" style="height:' + botH + 'px"><td colspan="' + colsSpan + '"></td></tr>';
|
|
1241
|
+
} else {
|
|
1242
|
+
for (let r = 0; r < this.ROWS; r++) html += this._rowHTML(r);
|
|
1167
1243
|
}
|
|
1168
1244
|
html += '</tbody>';
|
|
1169
1245
|
this.table.innerHTML = html;
|
|
@@ -1260,16 +1336,27 @@
|
|
|
1260
1336
|
// 通常は buildTable/_reflow(ResizeObserver が table/wrap を監視)で自動更新される)。
|
|
1261
1337
|
refreshGeometry() { this._buildGeomCache(); this.updateSelectionUI(); }
|
|
1262
1338
|
place(elem, td) {
|
|
1263
|
-
|
|
1339
|
+
// 仮想スクロールで active が窓外だと td=null。その場合は active 座標+モデル幾何で配置(窓外なら overflow で隠れる)。
|
|
1340
|
+
const r = td ? +td.dataset.r : this.active.r, c = td ? +td.dataset.c : this.active.c, R = this._cellRectAuto(r, c);
|
|
1264
1341
|
elem.style.left = R.left + 'px'; elem.style.top = R.top + 'px';
|
|
1265
1342
|
elem.style.width = R.width + 'px'; elem.style.height = R.height + 'px';
|
|
1266
1343
|
this._clipOverlay(elem, R.left, R.top, c, r);
|
|
1267
1344
|
}
|
|
1268
1345
|
placeRect(elem, r0, c0, r1, c1) {
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1346
|
+
const L = this._colLefts, T = this._rowTops;
|
|
1347
|
+
let left, top, w, h;
|
|
1348
|
+
// 列/行/全選択(_headerSel)時は、角セルの結合アンカー解決で枠が範囲外へ膨らむのを避け、
|
|
1349
|
+
// 列左/行上の累積で「実列/行ピッタリ」にクランプ(非固定領域のみ。固定行列が絡む場合は従来へフォールバック)。
|
|
1350
|
+
if (this._headerSel && L && T && r0 >= this.frozenRows && c0 >= this.frozenCols) {
|
|
1351
|
+
left = L[c0]; top = T[r0];
|
|
1352
|
+
w = L[Math.min(c1 + 1, this.COLS)] - left; h = T[Math.min(r1 + 1, this.ROWS)] - top;
|
|
1353
|
+
} else {
|
|
1354
|
+
const A = this._cellRectAuto(r0, c0), B = this._cellRectAuto(r1, c1);
|
|
1355
|
+
left = A.left; top = A.top; w = B.left + B.width - A.left; h = B.top + B.height - A.top;
|
|
1356
|
+
}
|
|
1357
|
+
elem.style.left = left + 'px'; elem.style.top = top + 'px';
|
|
1358
|
+
elem.style.width = w + 'px'; elem.style.height = h + 'px';
|
|
1359
|
+
this._clipOverlay(elem, left, top, c0, r0);
|
|
1273
1360
|
}
|
|
1274
1361
|
// ---- 幾何キャッシュ(step2: DOM非依存幾何の土台)----
|
|
1275
1362
|
// buildTable / _reflow で1回だけ実測して列左・行上の累積を作る。
|
|
@@ -1278,9 +1365,15 @@
|
|
|
1278
1365
|
// 行高は wordWrap で、列幅は stretch の DOM 改変で可変なので、モデル計算でなく実測が要る。
|
|
1279
1366
|
_buildGeomCache() {
|
|
1280
1367
|
if (!this.wrap || !this.table) { this._rowTops = this._colLefts = null; return; }
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1368
|
+
const RT = new Array(this.ROWS + 1);
|
|
1369
|
+
if (this.virtual) {
|
|
1370
|
+
const base = this._theadH || 0, rowH = this.defRowH; // 仮想モードは一定行高=モデルで全行(窓外も算出可)
|
|
1371
|
+
for (let r = 0; r <= this.ROWS; r++) RT[r] = base + r * rowH;
|
|
1372
|
+
} else {
|
|
1373
|
+
const trs = this.table.querySelectorAll('tbody tr');
|
|
1374
|
+
RT[0] = this._theadH || 0;
|
|
1375
|
+
for (let r = 0; r < this.ROWS; r++) { const tr = trs[r]; RT[r + 1] = RT[r] + (tr ? tr.getBoundingClientRect().height : this._rowHeight(r)); }
|
|
1376
|
+
}
|
|
1284
1377
|
this._rowTops = RT;
|
|
1285
1378
|
// 列幅: リーフヘッダ th(マージ無しで確実)→ 無ければ colgroup col。stretch 後の実幅を拾う。
|
|
1286
1379
|
const colEl = c => (this.colHeaders ? this.table.querySelector('thead th[data-c="' + c + '"]') : null) || this.table.querySelector('colgroup col[data-c="' + c + '"]');
|
|
@@ -1335,12 +1428,22 @@
|
|
|
1335
1428
|
highlightRange() {
|
|
1336
1429
|
this.table.querySelectorAll('td.sel, th.selhdr').forEach(el => el.classList.remove('sel', 'selhdr'));
|
|
1337
1430
|
const { r0, r1, c0, c1 } = this.rectRange();
|
|
1338
|
-
// 選択範囲が及ぶ行/列ヘッダをハイライト(Excel
|
|
1431
|
+
// 選択範囲が及ぶ行/列ヘッダをハイライト(Excel 風のアフォーダンス)。仮想時は行ヘッダを窓内にクランプ(大量行で querySelector を無駄打ちしない)。
|
|
1432
|
+
const hlo = this.virtual ? Math.max(r0, this._vStart) : r0;
|
|
1433
|
+
const hhi = this.virtual ? Math.min(r1, this._vEnd - 1) : r1;
|
|
1339
1434
|
for (let c = c0; c <= c1; c++) { const th = this.table.querySelector('thead th[data-c="' + c + '"]'); if (th) th.classList.add('selhdr'); }
|
|
1340
|
-
for (let r =
|
|
1435
|
+
for (let r = hlo; r <= hhi; r++) { const th = this.table.querySelector('th.rowhead[data-r="' + r + '"]'); if (th) th.classList.add('selhdr'); }
|
|
1341
1436
|
this.wrap.style.setProperty('--tg-selhdr-fg', this._selhdrTextColor()); // 選択ヘッダ文字色を背景輝度から自動(暗背景→白)
|
|
1342
1437
|
if (r0 === r1 && c0 === c1) return;
|
|
1343
|
-
|
|
1438
|
+
const clampMerge = this._headerSel && this._merges; // 列/行選択では「範囲に収まらない結合」は塗らない(帯を塗り広げない)
|
|
1439
|
+
// 仮想スクロール: 窓内の行だけシェード(範囲枠 selrange は placeRect がモデル幾何で全域を描く)。
|
|
1440
|
+
for (let r = hlo; r <= hhi; r++) for (let c = c0; c <= c1; c++) {
|
|
1441
|
+
if (clampMerge) {
|
|
1442
|
+
const a = this._anchorOf(r, c), mc1 = a.c + this._colspanAt(a.r, a.c) - 1, mr1 = a.r + this._rowspanAt(a.r, a.c) - 1;
|
|
1443
|
+
if (a.r < r0 || mr1 > r1 || a.c < c0 || mc1 > c1) continue; // 結合が選択範囲に収まらない=塗らない
|
|
1444
|
+
}
|
|
1445
|
+
const el = this.cellEl(r, c); if (el) el.classList.add('sel');
|
|
1446
|
+
}
|
|
1344
1447
|
}
|
|
1345
1448
|
// 選択ヘッダの文字色を --tg-selhdr-bg の輝度から自動決定(暗背景→白/明背景→濃紺)。
|
|
1346
1449
|
// 既定の明るい選択色は従来どおり濃紺、暗い選択ヘッダ(slate等)でも文字が読める。buildTable でキャッシュ破棄。
|
|
@@ -1416,39 +1519,45 @@
|
|
|
1416
1519
|
const a = { r: this.clampR(r), c: this._snapVisCol(this.clampC(c)) }; // 隠し列にはカーソルを置かない
|
|
1417
1520
|
if (this._merges) { const an = this._anchorOf(a.r, a.c); a.r = an.r; a.c = an.c; } // 結合内はアンカーへ
|
|
1418
1521
|
if (!this._beforeSel(a, a)) return;
|
|
1522
|
+
this._headerSel = null;
|
|
1419
1523
|
this.active = a; this.extent = { ...a };
|
|
1524
|
+
if (this.virtual) this._vEnsureVisible(a.r); // 窓に入れてから placement(窓外なら activeTd=null になるため)
|
|
1420
1525
|
this.place(this.editor, this.activeTd()); this.toNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1421
|
-
this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1526
|
+
if (!this.virtual) this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' }); // 仮想は _vEnsureVisible が可視化済み
|
|
1422
1527
|
this._fireSelection();
|
|
1423
1528
|
}
|
|
1424
1529
|
setExtent(r, c) {
|
|
1425
1530
|
const e = { r: this.clampR(r), c: this.clampC(c) };
|
|
1426
1531
|
if (!this._beforeSel(this.active, e)) return;
|
|
1427
|
-
this.extent = e;
|
|
1428
|
-
|
|
1532
|
+
this.extent = e;
|
|
1533
|
+
if (this.virtual) this._vEnsureVisible(e.r);
|
|
1534
|
+
this.updateSelectionUI();
|
|
1535
|
+
if (!this.virtual) this.cellEl(this.extent.r, this.extent.c).scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1429
1536
|
this._fireSelection();
|
|
1430
1537
|
}
|
|
1431
1538
|
selectRect(r0, c0, r1, c1) {
|
|
1432
1539
|
const a = { r: r0, c: c0 }, e = { r: r1, c: c1 };
|
|
1433
1540
|
if (!this._beforeSel(a, e)) return;
|
|
1541
|
+
this._headerSel = null;
|
|
1434
1542
|
this.active = a; this.extent = e;
|
|
1435
1543
|
this.place(this.editor, this.activeTd()); this.toNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1436
1544
|
this._fireSelection();
|
|
1437
1545
|
}
|
|
1438
1546
|
focusAndSelect(sel) {
|
|
1439
1547
|
this.active = { ...sel.active }; this.extent = { ...sel.extent };
|
|
1548
|
+
if (this.virtual) this._vEnsureVisible(this.active.r);
|
|
1440
1549
|
this.place(this.editor, this.activeTd()); this.toNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1441
|
-
this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1550
|
+
if (!this.virtual) this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1442
1551
|
this._fireSelection();
|
|
1443
1552
|
}
|
|
1444
1553
|
|
|
1445
1554
|
// ---- 行 / 列 / 全体 の選択(ヘッダクリック) ----
|
|
1446
|
-
selectCol(c) { c = this.clampC(c); this._colAnchor = c; this.selectRect(0, c, this.ROWS - 1, c); }
|
|
1447
|
-
selectRow(r) { r = this.clampR(r); this._rowAnchor = r; this.selectRect(r, 0, r, this.COLS - 1); }
|
|
1448
|
-
selectAll() { this._colAnchor = 0; this._rowAnchor = 0; this.selectRect(0, 0, this.ROWS - 1, this.COLS - 1); }
|
|
1449
|
-
_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.active = a; this.extent = e;
|
|
1555
|
+
selectCol(c) { c = this.clampC(c); this._colAnchor = c; this.selectRect(0, c, this.ROWS - 1, c); this._headerSel = 'col'; this.updateSelectionUI(); }
|
|
1556
|
+
selectRow(r) { r = this.clampR(r); this._rowAnchor = r; this.selectRect(r, 0, r, this.COLS - 1); this._headerSel = 'row'; this.updateSelectionUI(); }
|
|
1557
|
+
selectAll() { this._colAnchor = 0; this._rowAnchor = 0; this.selectRect(0, 0, this.ROWS - 1, this.COLS - 1); this._headerSel = 'all'; this.updateSelectionUI(); }
|
|
1558
|
+
_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;
|
|
1450
1559
|
this.place(this.editor, this.activeTd()); this.toNav(); this.updateSelectionUI(); this._fireSelection(); }
|
|
1451
|
-
_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.active = a; this.extent = e;
|
|
1560
|
+
_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;
|
|
1452
1561
|
this.place(this.editor, this.activeTd()); this.toNav(); this.updateSelectionUI(); this._fireSelection(); }
|
|
1453
1562
|
// ---- 列幅 / 行高 のリサイズ ----
|
|
1454
1563
|
setColWidth(c, w) {
|
|
@@ -2093,6 +2202,7 @@
|
|
|
2093
2202
|
_reflow() {
|
|
2094
2203
|
if (this.stretchH !== 'none') this._applyStretch(); // 枠幅が変わったら余白を再配分
|
|
2095
2204
|
this._buildGeomCache(); // 幅/枠が変わったので実測幾何を取り直す(リサイズ/ResizeObserver/window resize)
|
|
2205
|
+
if (this.virtual) this._renderWindow(this._vWindow(), false); // 枠高が変われば窓行数も変わる=窓を同期
|
|
2096
2206
|
const td = this.activeTd(); if (!td) return;
|
|
2097
2207
|
this.place(this.editor, td);
|
|
2098
2208
|
if (this.select && this.select.style.display !== 'none') this.place(this.select, td);
|
|
@@ -2100,6 +2210,7 @@
|
|
|
2100
2210
|
}
|
|
2101
2211
|
|
|
2102
2212
|
_bind() {
|
|
2213
|
+
if (this.virtual) this.wrap.addEventListener('scroll', () => this._vSync(), { passive: true }); // 仮想スクロール: 窓再描画
|
|
2103
2214
|
this.editor.addEventListener('keydown', (e) => this._onKey(e));
|
|
2104
2215
|
this.editor.addEventListener('compositionstart', () => {
|
|
2105
2216
|
if (this.mode !== 'nav') return;
|
|
@@ -2366,17 +2477,26 @@
|
|
|
2366
2477
|
let [dr, dc] = D[dir] || D.down;
|
|
2367
2478
|
if (reverse) { dr = -dr; dc = -dc; }
|
|
2368
2479
|
let r = this.active.r, c = this.active.c;
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2480
|
+
// navSkipReadOnly 時は readonly セルを飛ばして次の編集可能セルまで送る(伝票入力向け)。
|
|
2481
|
+
// OFF 時はループは1回で抜け=従来挙動と完全同一(回帰ゼロ)。
|
|
2482
|
+
for (let g = 0, lim = this.ROWS * this.COLS + 2; g < lim; g++) {
|
|
2483
|
+
let nr = r, nc = c;
|
|
2484
|
+
if (dc !== 0) { // 横移動は隠し列をスキップ+結合幅を跨ぐ
|
|
2485
|
+
const stepc = this._stepColM(r, c, dc > 0 ? 1 : -1);
|
|
2486
|
+
if (stepc !== c) nc = stepc;
|
|
2487
|
+
else if (this.autoWrapRow) { nc = dc > 0 ? this._firstVisCol() : this._lastVisCol(); nr = r + (dc > 0 ? 1 : -1); }
|
|
2488
|
+
}
|
|
2489
|
+
if (dr !== 0) {
|
|
2490
|
+
nr = this._stepRowM(r, c, dr > 0 ? 1 : -1); // 縦移動は結合高を跨ぐ
|
|
2491
|
+
if (this.autoWrapCol) { // 縦の端で列送り(送り先も可視列)
|
|
2492
|
+
if (nr >= this.ROWS) { nr = 0; nc = this._stepVisCol(c, 1); }
|
|
2493
|
+
else if (nr < 0) { nr = this.ROWS - 1; nc = this._stepVisCol(c, -1); }
|
|
2494
|
+
}
|
|
2379
2495
|
}
|
|
2496
|
+
if (nr === r && nc === c) break; // 端で動かない=終了
|
|
2497
|
+
r = nr; c = nc;
|
|
2498
|
+
if (r < 0 || r >= this.ROWS || c < 0 || c >= this.COLS) break; // 範囲外=setActive のクランプに委ねる
|
|
2499
|
+
if (!this.navSkipReadOnly || !this._isReadOnly(r, c)) break; // skip無効 or readonly でない=確定
|
|
2380
2500
|
}
|
|
2381
2501
|
this.setActive(r, c);
|
|
2382
2502
|
}
|
|
@@ -2391,7 +2511,11 @@
|
|
|
2391
2511
|
if (composing) return;
|
|
2392
2512
|
if (e.key === 'Enter') { e.preventDefault(); this.commit(); this._advance('enter', e.shiftKey); return; }
|
|
2393
2513
|
if (e.key === 'Tab') { e.preventDefault(); this.commit(); this._advance('tab', e.shiftKey); return; }
|
|
2394
|
-
|
|
2514
|
+
// Esc/編集中の Ctrl+Z = この編集をキャンセルして元値に戻す(nav へ)。
|
|
2515
|
+
// input ベース編集では「打ち始め」時に元値が input に入らないためネイティブ undo では元値に戻せない。
|
|
2516
|
+
// ここで元値(this.data[r][c]=未確定)に戻す=Excel流。続けて Ctrl+Z すると nav の history.undo で前の確定操作へ連鎖。
|
|
2517
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
2518
|
+
if (e.key === 'Escape' || (ctrl && !e.shiftKey && e.key.toLowerCase() === 'z')) {
|
|
2395
2519
|
e.preventDefault();
|
|
2396
2520
|
const r = this.active.r, c = this.active.c, v = this.data[r][c];
|
|
2397
2521
|
if (this.onEditEnd) { try { this.onEditEnd({ r, c, oldValue: v, newValue: v, canceled: true }); } catch (_) {} }
|