@threestarsoftware/tssgrid 0.0.1 → 0.0.4
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 +24 -0
- package/package.json +1 -1
- package/plugins/tss-datetime.css +36 -0
- package/plugins/tss-datetime.js +183 -0
- package/plugins/tss-sum.js +111 -0
- package/src/tssgrid.css +6 -0
- package/src/tssgrid.js +57 -23
package/README.md
CHANGED
|
@@ -427,6 +427,30 @@ new TssGrid(el, {
|
|
|
427
427
|
- **コアに合計行を埋めない**理由=置き場が `minSpareRows`/ソート/空行処理と衝突して肥大化するため。集計を外出しして「どこに出すかは自由」にした。
|
|
428
428
|
- formula と併用時は **`plugins: [TssFormula, TssTotals(...)]` の順**(合計を派生列の結果の後に計算するため)。`sum`/`avg`/`min`/`max` は数値セルのみ・`count` は非空セル数。
|
|
429
429
|
|
|
430
|
+
### 同梱プラグイン: 条件付き合計(tss-sum / ラベル付き合計行・整列表示)
|
|
431
|
+
|
|
432
|
+
`plugins/tss-sum.js`= **ラベル付きの"合計行"**(小計・消費税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
|
+
|
|
434
|
+
```html
|
|
435
|
+
<script src="plugins/tss-merge.js"></script> <!-- ラベルの横結合に利用 -->
|
|
436
|
+
<script src="plugins/tss-sum.js"></script>
|
|
437
|
+
```
|
|
438
|
+
```js
|
|
439
|
+
const sum = TssSum.attach(grid, document.getElementById('sumgrid'), {
|
|
440
|
+
format: v => '¥' + Number(v).toLocaleString('ja-JP'),
|
|
441
|
+
rows: [
|
|
442
|
+
{ label: '小計(税抜)', of: r => (+r[2]||0)*(+r[3]||0) }, // 全データ行の合計
|
|
443
|
+
{ label: '消費税(10%)', when: r => r[4]==='10%', of: r => Math.round((+r[2]||0)*(+r[3]||0)*0.10) }, // 条件付き
|
|
444
|
+
{ label: '消費税(8%)', when: r => r[4]==='8%', of: r => Math.round((+r[2]||0)*(+r[3]||0)*0.08) },
|
|
445
|
+
{ label: '合計(税込)', total: v => v[0]+v[1]+v[2] }, // 既出行から派生
|
|
446
|
+
],
|
|
447
|
+
onCompute: vals => { /* 上部の総額表示など */ },
|
|
448
|
+
});
|
|
449
|
+
// sum.refresh() 手動再計算 / sum.detach() フック解除+集計グリッド破棄
|
|
450
|
+
```
|
|
451
|
+
- 行は **`{label, when?, of}`**(`when` 一致行で `of(row)` を合計=SUMIF)または **`{label, total}`**(既出行の値から派生)。`row` は内部配列(列index)で受ける。
|
|
452
|
+
- ソースの **`onAfterChange`/`onStructureChange` を自動で包んで再計算**=編集・行追加削除・Undo すべてに追従。集計グリッドは**表示専用**(同一列幅で `金額` 列が揃う)。
|
|
453
|
+
|
|
430
454
|
### 同梱プラグイン: 残高/累計(running total・縦の連鎖)
|
|
431
455
|
|
|
432
456
|
`plugins/tss-running.js`=「**前の行の結果 + この行の増減**」で縦に積む列。`formula`(行内)でも `totals`(全体合計)でも出せない**縦方向の連鎖**を担う。出納帳/通帳の**残高**、在庫受払の**在庫数**、経費・ポイントの**累計**など。動く例: [`examples/running.html`](https://tssgrid.threestarsoftware.co.jp/examples/running.html)(現金出納帳=入金・出金→残高)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threestarsoftware/tssgrid",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
|
|
@@ -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,183 @@
|
|
|
1
|
+
/*! TssDatetime — 日付+時刻を同時に設定するピッカー(TssGrid カスタムエディタ / 単体ウィジェット両用)
|
|
2
|
+
* 保存値は常に 'YYYY-MM-DDTHH:MM'(kintone DATETIME と相互変換しやすい正規形。表示は列 format で整形)。
|
|
3
|
+
* 使い方(グリッド): columns: [{ type:'text', editor: TssDatetime({ step:5 }), format: TssDatetime.format }]
|
|
4
|
+
* 使い方(単体) : TssDatetime({step:15}).openAt(anchorEl, 'YYYY-MM-DDTHH:MM', onPick)
|
|
5
|
+
* opts: holidays / weekend / weekLabels / min / max(日付) / step(分刻み,既定1) / hour12 / icon(既定 '📅') / openOnClick(既定true) / className
|
|
6
|
+
*/
|
|
7
|
+
(function (root) {
|
|
8
|
+
'use strict';
|
|
9
|
+
function pad(n) { return String(n).padStart(2, '0'); }
|
|
10
|
+
function isoD(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
|
|
11
|
+
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; }
|
|
12
|
+
// 'YYYY-MM-DD[ T]HH:MM'(時刻省略可)→ { d:Date, h:int|null, mi:int|null }
|
|
13
|
+
function parseDT(s) {
|
|
14
|
+
var m = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{1,2}):(\d{2}))?/.exec(String(s || '').trim());
|
|
15
|
+
if (!m) return null;
|
|
16
|
+
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 };
|
|
17
|
+
}
|
|
18
|
+
// 'HH:MM'(日付無し=時刻のみ。kintone TIME フィールド互換)→ { h, m }
|
|
19
|
+
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; }
|
|
20
|
+
function sameDay(a, b) { return !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); }
|
|
21
|
+
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|
22
|
+
var WD = ['日', '月', '火', '水', '木', '金', '土']; // 既定の曜日ラベル(標準の日時に無い「曜日」を補う=差別化点)
|
|
23
|
+
|
|
24
|
+
function TssDatetime(opts) {
|
|
25
|
+
opts = opts || {};
|
|
26
|
+
var holidays = opts.holidays || {};
|
|
27
|
+
var weekend = opts.weekend || [0, 6];
|
|
28
|
+
var weekLabels = opts.weekLabels || ['日', '月', '火', '水', '木', '金', '土'];
|
|
29
|
+
var step = Math.max(1, opts.step || 1);
|
|
30
|
+
var hour12 = !!opts.hour12;
|
|
31
|
+
var minD = parseISOd(opts.min), maxD = parseISOd(opts.max);
|
|
32
|
+
var holName = function (h) { return h == null ? null : (typeof h === 'object' ? h.name : h); };
|
|
33
|
+
var holType = function (h) { return (h && typeof h === 'object' && h.type) ? h.type : null; };
|
|
34
|
+
var ty = new Date().getFullYear();
|
|
35
|
+
var yrLo = opts.yearMin || (minD ? minD.getFullYear() : ty - 12);
|
|
36
|
+
var yrHi = opts.yearMax || (maxD ? maxD.getFullYear() : ty + 12);
|
|
37
|
+
|
|
38
|
+
var pop = null, view = null, selD = null, selH = null, selM = null, ref = null, done = false, outside = null;
|
|
39
|
+
|
|
40
|
+
function dDisabled(d) { if (minD && d < minD) return true; if (maxD && d > maxD) return true; return false; }
|
|
41
|
+
|
|
42
|
+
function calHTML() {
|
|
43
|
+
var y = view.getFullYear(), m = view.getMonth(), start = new Date(y, m, 1).getDay(), today = new Date();
|
|
44
|
+
var lo = Math.min(yrLo, y), hi = Math.max(yrHi, y), yrSel = '<select class="tss-cal-sel-yr" aria-label="年">';
|
|
45
|
+
for (var yy = lo; yy <= hi; yy++) yrSel += '<option value="' + yy + '"' + (yy === y ? ' selected' : '') + '>' + yy + '年</option>';
|
|
46
|
+
yrSel += '</select>';
|
|
47
|
+
var monSel = '<select class="tss-cal-sel-mon" aria-label="月">';
|
|
48
|
+
for (var mm = 0; mm < 12; mm++) monSel += '<option value="' + mm + '"' + (mm === m ? ' selected' : '') + '>' + (mm + 1) + '月</option>';
|
|
49
|
+
monSel += '</select>';
|
|
50
|
+
var html = '<div class="tss-cal-head">'
|
|
51
|
+
+ '<button type="button" class="tss-cal-nav" data-nav="-1" aria-label="前の月">‹</button>'
|
|
52
|
+
+ '<span class="tss-cal-sels">' + yrSel + monSel + '</span>'
|
|
53
|
+
+ '<button type="button" class="tss-cal-nav" data-nav="1" aria-label="次の月">›</button></div>'
|
|
54
|
+
+ '<div class="tss-cal-grid">';
|
|
55
|
+
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>'; }
|
|
56
|
+
var d = new Date(y, m, 1 - start);
|
|
57
|
+
for (var i = 0; i < 42; i++) {
|
|
58
|
+
var cls = 'tss-cal-day', dow = d.getDay(), k = isoD(d), h = holidays[k], nm = holName(h), tp = holType(h);
|
|
59
|
+
if (d.getMonth() !== m) cls += ' tss-cal-other';
|
|
60
|
+
if (dow === 0) cls += ' tss-cal-sun'; else if (dow === 6) cls += ' tss-cal-sat';
|
|
61
|
+
if (h) { cls += ' tss-cal-holiday'; if (tp) cls += ' tss-cal-hol-' + tp; }
|
|
62
|
+
if (sameDay(d, today)) cls += ' tss-cal-today';
|
|
63
|
+
if (sameDay(d, selD)) cls += ' tss-cal-sel';
|
|
64
|
+
if (dDisabled(d)) cls += ' tss-cal-disabled';
|
|
65
|
+
html += '<div class="' + cls + '" data-d="' + k + '"' + (nm ? ' title="' + esc(nm) + '"' : '') + '>' + d.getDate() + '</div>';
|
|
66
|
+
d.setDate(d.getDate() + 1);
|
|
67
|
+
}
|
|
68
|
+
return html + '</div>';
|
|
69
|
+
}
|
|
70
|
+
function timeHTML() {
|
|
71
|
+
var hSel = '<select class="tss-time-h" aria-label="時"><option value=""></option>';
|
|
72
|
+
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>'; } }
|
|
73
|
+
else { for (var j = 0; j <= 23; j++) hSel += '<option value="' + j + '"' + (selH === j ? ' selected' : '') + '>' + pad(j) + '</option>'; }
|
|
74
|
+
hSel += '</select>';
|
|
75
|
+
var mins = []; for (var k = 0; k < 60; k += step) mins.push(k);
|
|
76
|
+
if (selM != null && mins.indexOf(selM) < 0) { mins.push(selM); mins.sort(function (a, b) { return a - b; }); }
|
|
77
|
+
var mSel = '<select class="tss-time-m" aria-label="分"><option value=""></option>';
|
|
78
|
+
for (var n = 0; n < mins.length; n++) mSel += '<option value="' + mins[n] + '"' + (selM === mins[n] ? ' selected' : '') + '>' + pad(mins[n]) + '</option>';
|
|
79
|
+
mSel += '</select>';
|
|
80
|
+
var aSel = '';
|
|
81
|
+
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>'; }
|
|
82
|
+
return '<div class="tss-dt-time-lbl">時刻</div><div class="tss-time-row">' + hSel + '<span class="tss-time-sep">:</span>' + mSel + aSel + '</div>';
|
|
83
|
+
}
|
|
84
|
+
function readoutHTML() {
|
|
85
|
+
var hasT = selH != null || selM != null;
|
|
86
|
+
var t = hasT ? pad(selH == null ? 0 : selH) + ':' + pad(selM == null ? 0 : selM) : '';
|
|
87
|
+
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>';
|
|
88
|
+
var wd = (opts.weekLabels || WD)[selD.getDay()];
|
|
89
|
+
return isoD(selD).replace(/-/g, '/') + '<b class="tss-dt-ro-wd">(' + esc(wd) + ')</b>' + (hasT ? ' ' + t : '');
|
|
90
|
+
}
|
|
91
|
+
function render() {
|
|
92
|
+
pop.innerHTML = '<div class="tss-dt-readout">' + readoutHTML() + '</div>'
|
|
93
|
+
+ '<div class="tss-dt-body"><div class="tss-dt-cal">' + calHTML() + '</div><div class="tss-dt-time">' + timeHTML() + '</div></div>'
|
|
94
|
+
+ '<div class="tss-dt-btns"><button type="button" class="tss-dt-clear">クリア</button><span class="tss-dt-spacer"></span>'
|
|
95
|
+
+ '<button type="button" class="tss-dt-now">現在</button><button type="button" class="tss-dt-ok">決定</button></div>';
|
|
96
|
+
}
|
|
97
|
+
function readTime() {
|
|
98
|
+
var hS = pop.querySelector('.tss-time-h'), mS = pop.querySelector('.tss-time-m'), aS = pop.querySelector('.tss-time-ap');
|
|
99
|
+
selH = hS && hS.value !== '' ? +hS.value : null;
|
|
100
|
+
selM = mS && mS.value !== '' ? +mS.value : null;
|
|
101
|
+
if (hour12 && aS && selH != null) { selH = aS.value === 'PM' ? (selH === 12 ? 12 : selH + 12) : (selH === 12 ? 0 : selH); }
|
|
102
|
+
}
|
|
103
|
+
function value() {
|
|
104
|
+
readTime();
|
|
105
|
+
var hasT = selH != null || selM != null;
|
|
106
|
+
var t = hasT ? pad(selH == null ? 0 : selH) + ':' + pad(selM == null ? 0 : selM) : '';
|
|
107
|
+
if (selD && hasT) return isoD(selD) + 'T' + t; // 日付+時刻
|
|
108
|
+
if (selD) return isoD(selD); // 日付のみ
|
|
109
|
+
if (hasT) return t; // 時刻のみ(日付無し='HH:MM')
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
function place(anchor) {
|
|
113
|
+
var r = anchor.getBoundingClientRect();
|
|
114
|
+
pop.style.left = Math.max(2, Math.min(r.left, window.innerWidth - pop.offsetWidth - 6)) + 'px';
|
|
115
|
+
var top = r.bottom + 2;
|
|
116
|
+
if (top + pop.offsetHeight > window.innerHeight) top = Math.max(2, r.top - pop.offsetHeight - 2);
|
|
117
|
+
pop.style.top = top + 'px';
|
|
118
|
+
}
|
|
119
|
+
function onClick(e) {
|
|
120
|
+
var t = e.target;
|
|
121
|
+
var nav = t.getAttribute && t.getAttribute('data-nav');
|
|
122
|
+
if (nav) { view = new Date(view.getFullYear(), view.getMonth() + (+nav), 1); render(); return; }
|
|
123
|
+
var dd = t.getAttribute && t.getAttribute('data-d');
|
|
124
|
+
if (dd) { readTime(); var d = parseISOd(dd); if (!dDisabled(d)) { selD = d; render(); } return; } // 日付選択しても閉じない(時刻も決めるため)
|
|
125
|
+
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; }
|
|
126
|
+
if (t.classList.contains('tss-dt-clear')) { commit(''); return; }
|
|
127
|
+
if (t.classList.contains('tss-dt-ok')) { commit(value()); return; }
|
|
128
|
+
}
|
|
129
|
+
function onChange(e) {
|
|
130
|
+
var t = e.target;
|
|
131
|
+
if (t.classList.contains('tss-cal-sel-yr')) { view = new Date(+t.value, view.getMonth(), 1); render(); }
|
|
132
|
+
else if (t.classList.contains('tss-cal-sel-mon')) { view = new Date(view.getFullYear(), +t.value, 1); render(); }
|
|
133
|
+
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(); }
|
|
134
|
+
}
|
|
135
|
+
function onKey(e) {
|
|
136
|
+
if (e.key === 'Escape') { e.preventDefault(); cancel(); }
|
|
137
|
+
else if (e.key === 'Enter') { e.preventDefault(); commit(value()); }
|
|
138
|
+
}
|
|
139
|
+
function show(anchor, val, onPick, onCancel) {
|
|
140
|
+
done = false;
|
|
141
|
+
var p = parseDT(val), tp = p ? null : parseTimeOnly(val); // 日付付き or 時刻のみ
|
|
142
|
+
selD = p ? p.d : null;
|
|
143
|
+
selH = p ? p.h : (tp ? tp.h : null);
|
|
144
|
+
selM = p ? p.mi : (tp ? tp.m : null);
|
|
145
|
+
view = selD ? new Date(selD.getFullYear(), selD.getMonth(), 1) : new Date();
|
|
146
|
+
pop = document.createElement('div'); pop.className = 'tss-dt' + (opts.className ? ' ' + opts.className : ''); pop.tabIndex = -1;
|
|
147
|
+
document.body.appendChild(pop);
|
|
148
|
+
pop.addEventListener('mousedown', function (e) { if (e.target.tagName !== 'SELECT') e.preventDefault(); }); // フォーカスを奪わない(select 除く)
|
|
149
|
+
pop.addEventListener('click', onClick);
|
|
150
|
+
pop.addEventListener('change', onChange);
|
|
151
|
+
pop.addEventListener('keydown', onKey);
|
|
152
|
+
render(); place(anchor);
|
|
153
|
+
ref = { pick: onPick, cancel: onCancel };
|
|
154
|
+
outside = function (e) { if (pop && !pop.contains(e.target) && !(anchor && anchor.contains && anchor.contains(e.target))) commit(value()); }; // 外側クリックは確定(time と同方針)
|
|
155
|
+
setTimeout(function () { document.addEventListener('mousedown', outside, true); }, 0);
|
|
156
|
+
pop.focus();
|
|
157
|
+
}
|
|
158
|
+
function teardown() { if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } }
|
|
159
|
+
function commit(v) { if (done) return; done = true; var f = ref.pick; teardown(); f(v); }
|
|
160
|
+
function cancel() { if (done) return; done = true; var f = ref.cancel; teardown(); if (f) f(); }
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
open: function (ctx) { show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }); },
|
|
164
|
+
close: function () { teardown(); },
|
|
165
|
+
icon: opts.icon != null ? opts.icon : '📅',
|
|
166
|
+
openOnClick: opts.openOnClick !== false,
|
|
167
|
+
openAt: function (anchorEl, val, onPick) { show(anchorEl, val, function (v) { onPick(v); }, null); },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 表示整形ヘルパ: 'YYYY-MM-DDTHH:MM' → 'YYYY/MM/DD(曜) HH:MM'(列 format に渡せる)。
|
|
172
|
+
// 標準の日時表示で空欄になる「曜日」を必ず埋める=本プラグインの目玉。weekday:false で曜日を消せる。
|
|
173
|
+
TssDatetime.format = function (v, o) {
|
|
174
|
+
var p = parseDT(v);
|
|
175
|
+
if (!p) return parseTimeOnly(v) ? v : (v || ''); // 時刻のみ('HH:MM')はそのまま表示/不正値は素通し
|
|
176
|
+
var d = p.d, wd = (o && o.weekday === false) ? '' : '(' + (o && o.weekLabels || WD)[d.getDay()] + ')';
|
|
177
|
+
var t = (p.h != null) ? ' ' + pad(p.h) + ':' + pad(p.mi == null ? 0 : p.mi) : '';
|
|
178
|
+
return d.getFullYear() + '/' + pad(d.getMonth() + 1) + '/' + pad(d.getDate()) + wd + t;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = TssDatetime;
|
|
182
|
+
else root.TssDatetime = TssDatetime;
|
|
183
|
+
})(typeof self !== 'undefined' ? self : this);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*! TssSum — 条件付き合計グリッドの軽量プラグイン。
|
|
2
|
+
*
|
|
3
|
+
* tss-totals との住み分け: tss-totals は「列ごとの集計(sum/avg/min/max/count)」を onTotals で返す。
|
|
4
|
+
* tss-sum は「同じ列を条件別に複数集計=ラベル付きの"合計行"(小計/消費税10%/消費税8%/合計 など)」+
|
|
5
|
+
* ソースと整列した表示グリッドまで担う(per-column キーでは消費税10%と8%が衝突するため別抽象が要る)。
|
|
6
|
+
*
|
|
7
|
+
* ソースグリッドと「同一の列構成」の readonly 集計グリッドを作るので、金額列の幾何を共有し
|
|
8
|
+
* 画面でも印刷でも値が縦にピッタリ揃う。各合計行は宣言で(条件+対象)または派生で定義する。
|
|
9
|
+
* ソースの onAfterChange / onStructureChange を包んで自動で再計算する。
|
|
10
|
+
*
|
|
11
|
+
* 使い方:
|
|
12
|
+
* const sum = TssSum.attach(grid, document.getElementById('sumgrid'), {
|
|
13
|
+
* rows: [
|
|
14
|
+
* { label: '小計(税抜)', of: r => (+r[2]||0)*(+r[3]||0) }, // 全データ行の合計
|
|
15
|
+
* { label: '消費税(10%)', when: r => r[4]==='10%', of: r => Math.round((+r[2]||0)*(+r[3]||0)*0.10) }, // 条件付き
|
|
16
|
+
* { label: '消費税(8%)', when: r => r[4]==='8%', of: r => Math.round((+r[2]||0)*(+r[3]||0)*0.08) },
|
|
17
|
+
* { label: '合計(税込)', total: v => v[0]+v[1]+v[2] }, // 既出行から派生
|
|
18
|
+
* ],
|
|
19
|
+
* format: v => '¥' + Number(v).toLocaleString('ja-JP'),
|
|
20
|
+
* onCompute: vals => { ... }, // 計算後フック(上部の総額表示など)
|
|
21
|
+
* });
|
|
22
|
+
* sum.refresh(); // 手動再計算 / sum.detach(); // フック解除+集計グリッド破棄
|
|
23
|
+
*
|
|
24
|
+
* 行の定義:
|
|
25
|
+
* { label, when?(row)->bool, of(row)->number } … when に一致するデータ行で of(row) を合計(when 省略=全行)
|
|
26
|
+
* { label, total(priorValues[])->number } … それまでの行の値配列から派生(小計+税 など)
|
|
27
|
+
* opts: rows(必須) / valueCol(既定=最終列) / sumCol(of 省略時に合計する列) / format / rowHeight(既定28)
|
|
28
|
+
* / onCompute(vals) / name / TssGrid(モジュール利用時にコンストラクタを注入)
|
|
29
|
+
* 返り値: { grid, refresh, detach }
|
|
30
|
+
* 注意: ラベルの横結合に window.TssMerge を使う(無ければラベルは先頭列のみ)。集計グリッドは表示専用。
|
|
31
|
+
*/
|
|
32
|
+
(function (root) {
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
function TssSum() { /* 名前空間。attach を使う */ }
|
|
36
|
+
|
|
37
|
+
TssSum.attach = function (source, mountEl, opts) {
|
|
38
|
+
opts = opts || {};
|
|
39
|
+
var Grid = opts.TssGrid || root.TssGrid;
|
|
40
|
+
if (!Grid) throw new Error('TssSum: TssGrid が見つかりません(opts.TssGrid で渡してください)');
|
|
41
|
+
var rows = opts.rows || [];
|
|
42
|
+
var COLS = source.COLS;
|
|
43
|
+
var valueCol = (opts.valueCol != null) ? opts.valueCol : COLS - 1;
|
|
44
|
+
var fmt = opts.format || function (v) { return v; };
|
|
45
|
+
|
|
46
|
+
// ソースと同じ列幅・行ヘッダ・全体幅でミラー(=金額列が揃う)。全セル readonly。
|
|
47
|
+
var columns = [];
|
|
48
|
+
for (var c = 0; c < COLS; c++) {
|
|
49
|
+
var col = { width: source._colWidth(c), readOnly: true };
|
|
50
|
+
if (c === valueCol) { col.align = 'right'; col.format = fmt; }
|
|
51
|
+
columns.push(col);
|
|
52
|
+
}
|
|
53
|
+
var data = rows.map(function (spec) {
|
|
54
|
+
var r = new Array(COLS).fill('');
|
|
55
|
+
r[0] = (spec.label != null) ? spec.label : ''; // ラベルは先頭列(アンカー)
|
|
56
|
+
return r;
|
|
57
|
+
});
|
|
58
|
+
var plugins = [];
|
|
59
|
+
if (root.TssMerge && valueCol > 1) { // ラベルを valueCol の手前まで横結合
|
|
60
|
+
plugins.push(root.TssMerge.plugin({
|
|
61
|
+
merges: rows.map(function (_, r) { return { r: r, c: 0, colspan: valueCol }; }),
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
var tg = new Grid(mountEl, {
|
|
65
|
+
name: opts.name || (source.name ? source.name + '-sum' : 'sum'),
|
|
66
|
+
width: source.width, rowHeight: opts.rowHeight || 28,
|
|
67
|
+
rowHeaders: source.rowHeaders, colHeaders: false,
|
|
68
|
+
columns: columns, data: data, plugins: plugins,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function num(v) { return Number(v) || 0; }
|
|
72
|
+
function refresh() {
|
|
73
|
+
var src = source.data, vals = [];
|
|
74
|
+
for (var i = 0; i < rows.length; i++) {
|
|
75
|
+
var spec = rows[i], v;
|
|
76
|
+
if (typeof spec.total === 'function') {
|
|
77
|
+
v = spec.total(vals.slice());
|
|
78
|
+
} else {
|
|
79
|
+
v = 0;
|
|
80
|
+
for (var k = 0; k < src.length; k++) {
|
|
81
|
+
var row = src[k];
|
|
82
|
+
if (spec.when && !spec.when(row)) continue;
|
|
83
|
+
v += spec.of ? num(spec.of(row)) : num(row[(opts.sumCol != null) ? opts.sumCol : valueCol]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
vals[i] = v;
|
|
87
|
+
tg.data[i][valueCol] = String(v);
|
|
88
|
+
}
|
|
89
|
+
tg.buildTable();
|
|
90
|
+
if (opts.onCompute) { try { opts.onCompute(vals.slice()); } catch (e) {} }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ソースの変更フックを包んで自動再計算(既存ハンドラは温存)
|
|
94
|
+
var origAC = source.onAfterChange, origSC = source.onStructureChange;
|
|
95
|
+
source.onAfterChange = function () { if (origAC) origAC.apply(this, arguments); refresh(); };
|
|
96
|
+
source.onStructureChange = function () { if (origSC) origSC.apply(this, arguments); refresh(); };
|
|
97
|
+
refresh();
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
grid: tg,
|
|
101
|
+
refresh: refresh,
|
|
102
|
+
detach: function () {
|
|
103
|
+
source.onAfterChange = origAC; source.onStructureChange = origSC;
|
|
104
|
+
if (tg.destroy) tg.destroy();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (typeof module === 'object' && module.exports) module.exports = TssSum;
|
|
110
|
+
root.TssSum = TssSum;
|
|
111
|
+
})(typeof window !== 'undefined' ? window : this);
|
package/src/tssgrid.css
CHANGED
|
@@ -60,6 +60,12 @@
|
|
|
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; }
|
|
63
69
|
.tssgrid .tg-table th { background: var(--tg-colheader-bg); color: var(--tg-header-fg); font-weight: 600; text-align: center; }
|
|
64
70
|
/* z-index 階層(.tg-wrap 内): 通常セル 0 < 本体sticky(rowhead/frozen) 2 < 選択オーバーレイ 3
|
|
65
71
|
< editor/select 4 < fillhandle 5 < ヘッダ thead 6 < 角/固定列ヘッダ 7 < リサイズ線 8。
|
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, …] … 位置指定(従来)
|
|
@@ -210,6 +211,7 @@
|
|
|
210
211
|
this.minSpareRows = opts.minSpareRows | 0; // 末尾に常に確保する空行数
|
|
211
212
|
this.autoWrapRow = !!opts.autoWrapRow; // Tab で行末→次行頭に折り返し
|
|
212
213
|
this.autoWrapCol = !!opts.autoWrapCol; // Enter で列末→次列頭に折り返し
|
|
214
|
+
this.navSkipReadOnly = !!opts.navSkipReadOnly; // Tab/Enter 送りで readonly セルを飛ばす(伝票入力向け=編集する所だけ移動)
|
|
213
215
|
// 行/列数の下限・上限(挿入/削除の境界。min は初期パディングにも使う)
|
|
214
216
|
this.minRows = opts.minRows | 0; this.maxRows = opts.maxRows | 0;
|
|
215
217
|
this.minCols = opts.minCols | 0; this.maxCols = opts.maxCols | 0;
|
|
@@ -1266,10 +1268,20 @@
|
|
|
1266
1268
|
this._clipOverlay(elem, R.left, R.top, c, r);
|
|
1267
1269
|
}
|
|
1268
1270
|
placeRect(elem, r0, c0, r1, c1) {
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1271
|
+
const L = this._colLefts, T = this._rowTops;
|
|
1272
|
+
let left, top, w, h;
|
|
1273
|
+
// 列/行/全選択(_headerSel)時は、角セルの結合アンカー解決で枠が範囲外へ膨らむのを避け、
|
|
1274
|
+
// 列左/行上の累積で「実列/行ピッタリ」にクランプ(非固定領域のみ。固定行列が絡む場合は従来へフォールバック)。
|
|
1275
|
+
if (this._headerSel && L && T && r0 >= this.frozenRows && c0 >= this.frozenCols) {
|
|
1276
|
+
left = L[c0]; top = T[r0];
|
|
1277
|
+
w = L[Math.min(c1 + 1, this.COLS)] - left; h = T[Math.min(r1 + 1, this.ROWS)] - top;
|
|
1278
|
+
} else {
|
|
1279
|
+
const A = this._cellRectAuto(r0, c0), B = this._cellRectAuto(r1, c1);
|
|
1280
|
+
left = A.left; top = A.top; w = B.left + B.width - A.left; h = B.top + B.height - A.top;
|
|
1281
|
+
}
|
|
1282
|
+
elem.style.left = left + 'px'; elem.style.top = top + 'px';
|
|
1283
|
+
elem.style.width = w + 'px'; elem.style.height = h + 'px';
|
|
1284
|
+
this._clipOverlay(elem, left, top, c0, r0);
|
|
1273
1285
|
}
|
|
1274
1286
|
// ---- 幾何キャッシュ(step2: DOM非依存幾何の土台)----
|
|
1275
1287
|
// buildTable / _reflow で1回だけ実測して列左・行上の累積を作る。
|
|
@@ -1340,7 +1352,14 @@
|
|
|
1340
1352
|
for (let r = r0; r <= r1; r++) { const th = this.table.querySelector('th.rowhead[data-r="' + r + '"]'); if (th) th.classList.add('selhdr'); }
|
|
1341
1353
|
this.wrap.style.setProperty('--tg-selhdr-fg', this._selhdrTextColor()); // 選択ヘッダ文字色を背景輝度から自動(暗背景→白)
|
|
1342
1354
|
if (r0 === r1 && c0 === c1) return;
|
|
1343
|
-
|
|
1355
|
+
const clampMerge = this._headerSel && this._merges; // 列/行選択では「範囲に収まらない結合」は塗らない(帯を塗り広げない)
|
|
1356
|
+
for (let r = r0; r <= r1; r++) for (let c = c0; c <= c1; c++) {
|
|
1357
|
+
if (clampMerge) {
|
|
1358
|
+
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;
|
|
1359
|
+
if (a.r < r0 || mr1 > r1 || a.c < c0 || mc1 > c1) continue; // 結合が選択範囲に収まらない=塗らない
|
|
1360
|
+
}
|
|
1361
|
+
this.cellEl(r, c).classList.add('sel');
|
|
1362
|
+
}
|
|
1344
1363
|
}
|
|
1345
1364
|
// 選択ヘッダの文字色を --tg-selhdr-bg の輝度から自動決定(暗背景→白/明背景→濃紺)。
|
|
1346
1365
|
// 既定の明るい選択色は従来どおり濃紺、暗い選択ヘッダ(slate等)でも文字が読める。buildTable でキャッシュ破棄。
|
|
@@ -1416,6 +1435,7 @@
|
|
|
1416
1435
|
const a = { r: this.clampR(r), c: this._snapVisCol(this.clampC(c)) }; // 隠し列にはカーソルを置かない
|
|
1417
1436
|
if (this._merges) { const an = this._anchorOf(a.r, a.c); a.r = an.r; a.c = an.c; } // 結合内はアンカーへ
|
|
1418
1437
|
if (!this._beforeSel(a, a)) return;
|
|
1438
|
+
this._headerSel = null;
|
|
1419
1439
|
this.active = a; this.extent = { ...a };
|
|
1420
1440
|
this.place(this.editor, this.activeTd()); this.toNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1421
1441
|
this.activeTd().scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
@@ -1431,6 +1451,7 @@
|
|
|
1431
1451
|
selectRect(r0, c0, r1, c1) {
|
|
1432
1452
|
const a = { r: r0, c: c0 }, e = { r: r1, c: c1 };
|
|
1433
1453
|
if (!this._beforeSel(a, e)) return;
|
|
1454
|
+
this._headerSel = null;
|
|
1434
1455
|
this.active = a; this.extent = e;
|
|
1435
1456
|
this.place(this.editor, this.activeTd()); this.toNav(); this.editor.focus(); this.updateSelectionUI();
|
|
1436
1457
|
this._fireSelection();
|
|
@@ -1443,12 +1464,12 @@
|
|
|
1443
1464
|
}
|
|
1444
1465
|
|
|
1445
1466
|
// ---- 行 / 列 / 全体 の選択(ヘッダクリック) ----
|
|
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;
|
|
1467
|
+
selectCol(c) { c = this.clampC(c); this._colAnchor = c; this.selectRect(0, c, this.ROWS - 1, c); this._headerSel = 'col'; this.updateSelectionUI(); }
|
|
1468
|
+
selectRow(r) { r = this.clampR(r); this._rowAnchor = r; this.selectRect(r, 0, r, this.COLS - 1); this._headerSel = 'row'; this.updateSelectionUI(); }
|
|
1469
|
+
selectAll() { this._colAnchor = 0; this._rowAnchor = 0; this.selectRect(0, 0, this.ROWS - 1, this.COLS - 1); this._headerSel = 'all'; this.updateSelectionUI(); }
|
|
1470
|
+
_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
1471
|
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;
|
|
1472
|
+
_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
1473
|
this.place(this.editor, this.activeTd()); this.toNav(); this.updateSelectionUI(); this._fireSelection(); }
|
|
1453
1474
|
// ---- 列幅 / 行高 のリサイズ ----
|
|
1454
1475
|
setColWidth(c, w) {
|
|
@@ -1844,8 +1865,8 @@
|
|
|
1844
1865
|
if (td) {
|
|
1845
1866
|
const r = +td.dataset.r, c = +td.dataset.c, s = this.rectRange();
|
|
1846
1867
|
if (!(r >= s.r0 && r <= s.r1 && c >= s.c0 && c <= s.c1)) this.setActive(r, c);
|
|
1847
|
-
} else if (th.dataset.c != null)
|
|
1848
|
-
else if (th.dataset.r != null)
|
|
1868
|
+
} else if (th.dataset.c != null) { const c = +th.dataset.c, s = this.rectRange(); if (!(c >= s.c0 && c <= s.c1)) this.selectCol(c); } // 既存選択の中なら維持(複数列右クリックで解除しない)
|
|
1869
|
+
else if (th.dataset.r != null) { const r = +th.dataset.r, s = this.rectRange(); if (!(r >= s.r0 && r <= s.r1)) this.selectRow(r); } // 同上(複数行)
|
|
1849
1870
|
this._showMenu(e.clientX, e.clientY);
|
|
1850
1871
|
}
|
|
1851
1872
|
// 既定のメニュー項目キー(allow フラグ・copyPaste で出し分け)。
|
|
@@ -2366,17 +2387,26 @@
|
|
|
2366
2387
|
let [dr, dc] = D[dir] || D.down;
|
|
2367
2388
|
if (reverse) { dr = -dr; dc = -dc; }
|
|
2368
2389
|
let r = this.active.r, c = this.active.c;
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2390
|
+
// navSkipReadOnly 時は readonly セルを飛ばして次の編集可能セルまで送る(伝票入力向け)。
|
|
2391
|
+
// OFF 時はループは1回で抜け=従来挙動と完全同一(回帰ゼロ)。
|
|
2392
|
+
for (let g = 0, lim = this.ROWS * this.COLS + 2; g < lim; g++) {
|
|
2393
|
+
let nr = r, nc = c;
|
|
2394
|
+
if (dc !== 0) { // 横移動は隠し列をスキップ+結合幅を跨ぐ
|
|
2395
|
+
const stepc = this._stepColM(r, c, dc > 0 ? 1 : -1);
|
|
2396
|
+
if (stepc !== c) nc = stepc;
|
|
2397
|
+
else if (this.autoWrapRow) { nc = dc > 0 ? this._firstVisCol() : this._lastVisCol(); nr = r + (dc > 0 ? 1 : -1); }
|
|
2398
|
+
}
|
|
2399
|
+
if (dr !== 0) {
|
|
2400
|
+
nr = this._stepRowM(r, c, dr > 0 ? 1 : -1); // 縦移動は結合高を跨ぐ
|
|
2401
|
+
if (this.autoWrapCol) { // 縦の端で列送り(送り先も可視列)
|
|
2402
|
+
if (nr >= this.ROWS) { nr = 0; nc = this._stepVisCol(c, 1); }
|
|
2403
|
+
else if (nr < 0) { nr = this.ROWS - 1; nc = this._stepVisCol(c, -1); }
|
|
2404
|
+
}
|
|
2379
2405
|
}
|
|
2406
|
+
if (nr === r && nc === c) break; // 端で動かない=終了
|
|
2407
|
+
r = nr; c = nc;
|
|
2408
|
+
if (r < 0 || r >= this.ROWS || c < 0 || c >= this.COLS) break; // 範囲外=setActive のクランプに委ねる
|
|
2409
|
+
if (!this.navSkipReadOnly || !this._isReadOnly(r, c)) break; // skip無効 or readonly でない=確定
|
|
2380
2410
|
}
|
|
2381
2411
|
this.setActive(r, c);
|
|
2382
2412
|
}
|
|
@@ -2391,7 +2421,11 @@
|
|
|
2391
2421
|
if (composing) return;
|
|
2392
2422
|
if (e.key === 'Enter') { e.preventDefault(); this.commit(); this._advance('enter', e.shiftKey); return; }
|
|
2393
2423
|
if (e.key === 'Tab') { e.preventDefault(); this.commit(); this._advance('tab', e.shiftKey); return; }
|
|
2394
|
-
|
|
2424
|
+
// Esc/編集中の Ctrl+Z = この編集をキャンセルして元値に戻す(nav へ)。
|
|
2425
|
+
// input ベース編集では「打ち始め」時に元値が input に入らないためネイティブ undo では元値に戻せない。
|
|
2426
|
+
// ここで元値(this.data[r][c]=未確定)に戻す=Excel流。続けて Ctrl+Z すると nav の history.undo で前の確定操作へ連鎖。
|
|
2427
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
2428
|
+
if (e.key === 'Escape' || (ctrl && !e.shiftKey && e.key.toLowerCase() === 'z')) {
|
|
2395
2429
|
e.preventDefault();
|
|
2396
2430
|
const r = this.active.r, c = this.active.c, v = this.data[r][c];
|
|
2397
2431
|
if (this.onEditEnd) { try { this.onEditEnd({ r, c, oldValue: v, newValue: v, canceled: true }); } catch (_) {} }
|