@threestarsoftware/tssgrid 0.0.3 → 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/package.json +1 -1
- package/plugins/tss-datetime.css +36 -0
- package/plugins/tss-datetime.js +183 -0
- package/src/tssgrid.css +6 -0
- package/src/tssgrid.js +55 -21
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);
|
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) {
|
|
@@ -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 (_) {} }
|