@threestarsoftware/tssgrid 0.1.28 → 0.1.29-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,11 @@
3
3
  TssGrid の各リリースの変更点。日付は JST。形式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) 準拠(ゆるめ)。
4
4
  数値は特記なき限り**自社調べ**(headless Chrome / Windows 11・環境差あり)。
5
5
 
6
+ ## [Unreleased]
7
+
8
+ ### 追加(プラグイン)
9
+ - **`tss-calendar` に範囲選択 `range` を追加** — 連続した日(休暇申請の開始〜終了など)を**1回の編集で**選べる。`range:true` で有効、`range:(ctx)=>boolean` で**行ごとにON/OFF**(半休の行は範囲を選ばせない等)。未指定は単日=**従来動作**。1クリック目=開始・2クリック目=終了(**開始より前なら入替・同じ日を2回で単日確定**)、間の日に色が付く。1クリック目だけで閉じたら(Esc/外クリック)**値は変えない**。確定値は既定で `2026/10/13~2026/10/16`(**単日は ISO `2026-10-13`=範囲なしの単日と同じ形**で保存値が混ざらない/区切りは `rangeSeparator`・既定 '~'/`rangeValue(startISO,endISO)` で完全差替可・**同日も呼ばれる**)。**入力中のセルに範囲ツールチップ**を出し、`rangeTooltip(startISO,endISO)` で文面を差し替え可能(休日を除いた日数などを外から足す口)。**休日スキップは利用側**=カレンダーは範囲を返すだけ。範囲列は `type:'date'` を付けない素の列にする(範囲文字列を日付パーサに通さないため)。例 `examples/calendar-range.html`。
10
+
6
11
  ## [0.1.28] - 2026-10-20
7
12
 
8
13
  ### 追加(プラグイン)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threestarsoftware/tssgrid",
3
- "version": "0.1.28",
3
+ "version": "0.1.29-beta.1",
4
4
  "description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
5
5
  "license": "MIT",
6
6
  "author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
@@ -37,4 +37,11 @@
37
37
  .tss-cal-day.tss-cal-today { box-shadow: inset 0 0 0 1px var(--cal-accent); }
38
38
  .tss-cal-day.tss-cal-focus { box-shadow: inset 0 0 0 2px var(--cal-accent); }
39
39
  .tss-cal-day.tss-cal-sel { background: var(--cal-accent); color: #fff; }
40
+ /* 範囲選択(range:true)。間の日は淡色、両端は濃色。 */
41
+ .tss-cal-day.tss-cal-inrange { background: var(--cal-hover); border-radius: 0; }
42
+ .tss-cal-day.tss-cal-range-start,
43
+ .tss-cal-day.tss-cal-range-end { background: var(--cal-accent); color: #fff; }
44
+ .tss-cal-day.tss-cal-range-start { border-radius: 6px 0 0 6px; }
45
+ .tss-cal-day.tss-cal-range-end { border-radius: 0 6px 6px 0; }
46
+ .tss-cal-day.tss-cal-range-start.tss-cal-range-end { border-radius: 6px; } /* 開始=終端(1日だけ選択中) */
40
47
  .tss-cal-day.tss-cal-disabled { color: var(--cal-muted); background: repeating-linear-gradient(45deg, transparent, transparent 4px, rgba(0,0,0,.04) 4px, rgba(0,0,0,.04) 8px); cursor: not-allowed; }
@@ -28,6 +28,18 @@ declare namespace TssCalendar {
28
28
  disable?: Array<'holiday' | 'weekend' | string>;
29
29
  /** 共有 input に乗る型(既定 true)=直打ち可・矢印は本体へ。false でフォーカス奪取型。 */
30
30
  inline?: boolean;
31
+ /** 範囲選択(開始〜終了を1回の編集で)。`true` で全行有効/`(ctx)=>boolean` で行ごとに切替
32
+ * (例: 半休の行は範囲を選ばせない)。未指定は単日=従来動作。1クリック目=開始・2クリック目=終了
33
+ * (開始より前なら入替・同日2回は単日確定)。確定値・ツールチップは下記で差し替え可。休日スキップは利用側。 */
34
+ range?: boolean | ((ctx: any) => boolean);
35
+ /** 範囲値・ツールチップの日付区切り(既定 '~')。 */
36
+ rangeSeparator?: string;
37
+ /** 範囲の確定値を作る。**同日(単日)のときも呼ばれる**。引数は開始・終了の ISO
38
+ * ('YYYY-MM-DD'・開始<=終了に整列済み)。未指定の既定は 範囲=`'YYYY/MM/DD~YYYY/MM/DD'`/
39
+ * **単日=ISO `'YYYY-MM-DD'`(範囲なしの単日と同じ形)**。別フィールド分割などはこの口で。 */
40
+ rangeValue?: (startISO: string, endISO: string) => string;
41
+ /** 編集中セルに出す範囲ツールチップの文面(既定は範囲文字列)。休日を除いた日数などを外から足す口。 */
42
+ rangeTooltip?: (startISO: string, endISO: string) => string;
31
43
  /** シングルクリックで開く(既定 true)。 */
32
44
  openOnClick?: boolean;
33
45
  /** セル右端の目印(既定 '📅'。'' で消す)。 */
@@ -17,6 +17,8 @@
17
17
  function parseISO(s) { var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(s || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; }
18
18
  function sameDay(a, b) { return !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); }
19
19
  function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
20
+ function fmtSlash(isoStr) { return String(isoStr).replace(/-/g, '/'); } // 'YYYY-MM-DD' → 'YYYY/MM/DD'(range の既定表示)
21
+ function parseLoose(s) { var m = /^(\d{4})[/-](\d{1,2})[/-](\d{1,2})/.exec(String(s || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; } // slash/dash どちらも
20
22
 
21
23
  function TssCalendar(opts) {
22
24
  opts = opts || {};
@@ -35,6 +37,25 @@
35
37
  var pop = null, view = null, sel = null, focus = null, ref = null, done = false, outside = null;
36
38
  var inline = opts.inline !== false; // true=共有 input に乗る(直打ち可・矢印は本体に返す)/false=従来のフォーカス奪取型
37
39
 
40
+ // ── 範囲選択(range): 連続した日を1回の編集で選ぶ(休暇申請の開始〜終了 等)───────────
41
+ // range:true で有効(行ごとに切替えたい時は range:(ctx)=>boolean)。未指定は単日=従来動作。
42
+ // 1クリック目=開始、2クリック目=終了(開始より前なら入替・同日2回は単日確定)。間の日に色。
43
+ // 確定値は rangeValue(開始ISO,終了ISO)(既定: 範囲 'YYYY/MM/DD~YYYY/MM/DD'/単日は ISO 'YYYY-MM-DD'=範囲なしと同形。rangeValue は同日も呼ぶ)。
44
+ // 入力中はセルに range のツールチップ(rangeTooltip で文面差替=休日除く日数を外から足せる口)。
45
+ // 休日スキップは利用側(返すのは範囲だけ)。
46
+ var rangeOpt = opts.range;
47
+ var rangeSep = opts.rangeSeparator != null ? opts.rangeSeparator : '~';
48
+ var rangeValueFn = typeof opts.rangeValue === 'function' ? opts.rangeValue : null;
49
+ var rangeTooltipFn = typeof opts.rangeTooltip === 'function' ? opts.rangeTooltip : null;
50
+ function defRangeStr(aIso, bIso) { return fmtSlash(aIso) + rangeSep + fmtSlash(bIso); } // 表示用(両端 slash・範囲)
51
+ function rangeValOf(aIso, bIso) {
52
+ if (rangeValueFn) return rangeValueFn(aIso, bIso); // rangeValue は同日も必ず呼ぶ(利用側で形を決められる)
53
+ return aIso === bIso ? aIso : defRangeStr(aIso, bIso); // 既定: 単日=ISO(範囲なしと同形)/範囲='YYYY/MM/DD~YYYY/MM/DD'
54
+ }
55
+ function rangeTipOf(aIso, bIso) { return rangeTooltipFn ? rangeTooltipFn(aIso, bIso) : (aIso === bIso ? fmtSlash(aIso) : defRangeStr(aIso, bIso)); }
56
+ var rc = null; // 現在の編集が range なら { td } を持つ(open(ctx) で ctx 判定して設定)
57
+ var rangeStart = null, hoverEnd = null; // 範囲選択の途中状態(1クリック目=rangeStart、プレビュー終端=hoverEnd)
58
+
38
59
  function disabled(d) {
39
60
  if (minD && d < minD) return true;
40
61
  if (maxD && d > maxD) return true;
@@ -71,6 +92,12 @@
71
92
  if (sameDay(d, today)) cls += ' tss-cal-today';
72
93
  if (sameDay(d, sel)) cls += ' tss-cal-sel';
73
94
  if (sameDay(d, focus)) cls += ' tss-cal-focus';
95
+ if (rc && rangeStart) { // 範囲の途中=開始〜(プレビュー)終端を塗る
96
+ var ra = rangeStart, rb = hoverEnd || rangeStart, lo = ra <= rb ? ra : rb, hi = ra <= rb ? rb : ra;
97
+ if (d >= lo && d <= hi) cls += ' tss-cal-inrange';
98
+ if (sameDay(d, ra)) cls += ' tss-cal-range-start';
99
+ if (sameDay(d, rb)) cls += ' tss-cal-range-end';
100
+ }
74
101
  if (disabled(d)) cls += ' tss-cal-disabled';
75
102
  html += '<div class="' + cls + '" data-d="' + k + '"' + (nm ? ' title="' + esc(nm) + '"' : '') + '>' + d.getDate() + '</div>';
76
103
  d.setDate(d.getDate() + 1);
@@ -102,20 +129,40 @@
102
129
  if (k === 'Escape') { e.preventDefault(); cancel(); return; }
103
130
  if (k === 'Enter') { e.preventDefault(); if (!disabled(focus)) pick(focus); return; }
104
131
  var dx = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[k];
105
- if (dx != null) { e.preventDefault(); focus = new Date(focus); focus.setDate(focus.getDate() + dx); view = new Date(focus.getFullYear(), focus.getMonth(), 1); render(); return; }
132
+ if (dx != null) { e.preventDefault(); focus = new Date(focus); focus.setDate(focus.getDate() + dx); view = new Date(focus.getFullYear(), focus.getMonth(), 1); if (rc && rangeStart) { hoverEnd = new Date(focus); updateTip(); } render(); return; }
106
133
  if (k === 'PageUp' || k === 'PageDown') { e.preventDefault(); view = new Date(view.getFullYear(), view.getMonth() + (k === 'PageUp' ? -1 : 1), 1); render(); }
107
134
  }
108
- function show(anchor, value, onPick, onCancel) {
135
+ // range 途中: 日の上をなぞると終端プレビューを更新(間の色+ツールチップ)。
136
+ function onOver(e) {
137
+ if (!(rc && rangeStart)) return;
138
+ var dd = e.target.getAttribute && e.target.getAttribute('data-d');
139
+ if (!dd) return;
140
+ var d = parseISO(dd); if (!d || disabled(d)) return;
141
+ if (!hoverEnd || !sameDay(hoverEnd, d)) { hoverEnd = d; render(); updateTip(); }
142
+ }
143
+ // 編集中セルに範囲ツールチップを出す(rangeTooltip で文面差替可)。
144
+ function updateTip() {
145
+ if (!(rc && rc.td)) return;
146
+ var aIso = iso(rangeStart), bIso = iso(hoverEnd || rangeStart);
147
+ if (bIso < aIso) { var t = aIso; aIso = bIso; bIso = t; }
148
+ try { rc.td.title = rangeTipOf(aIso, bIso); } catch (e) { /* noop */ }
149
+ }
150
+ function clearTip() { if (rc && rc.td) { try { rc.td.title = ''; } catch (e) { /* noop */ } } }
151
+ function show(anchor, value, onPick, onCancel, rangeCfg) {
109
152
  teardown(); // 既に開いていれば先に片付ける(前の pop 除去+outside リスナ解除・open-while-open の取り残し防止)
110
- done = false; sel = parseISO(value);
111
- view = sel ? new Date(sel.getFullYear(), sel.getMonth(), 1) : new Date();
112
- focus = sel ? new Date(sel) : new Date();
113
- pop = document.createElement('div'); pop.className = 'tss-cal' + (opts.className ? ' ' + opts.className : '');
153
+ done = false; rc = rangeCfg || null; rangeStart = null; hoverEnd = null;
154
+ sel = parseISO(value);
155
+ // range の既存値('YYYY/MM/DD~…' 等)は parseISO では取れない=先頭日を緩く読んで月を合わせる。
156
+ var initD = sel || (rc ? parseLoose(value) : null);
157
+ view = initD ? new Date(initD.getFullYear(), initD.getMonth(), 1) : new Date();
158
+ focus = initD ? new Date(initD) : new Date();
159
+ pop = document.createElement('div'); pop.className = 'tss-cal' + (opts.className ? ' ' + opts.className : '') + (rc ? ' tss-cal-range' : '');
114
160
  pop.tabIndex = -1; document.body.appendChild(pop);
115
161
  pop.addEventListener('mousedown', function (e) { if (e.target.tagName !== 'SELECT') e.preventDefault(); }); // フォーカスを奪わない(select は除く)
116
162
  pop.addEventListener('click', onClick);
117
163
  pop.addEventListener('change', onChange);
118
164
  pop.addEventListener('keydown', onKey);
165
+ if (rc) pop.addEventListener('mouseover', onOver);
119
166
  render(); place(anchor);
120
167
  ref = { pick: onPick, cancel: onCancel };
121
168
  // inline は「外側クリック/フォーカス」を本体(_commitActive / blur)が持つ=二重に閉じない・共有 input から
@@ -126,13 +173,26 @@
126
173
  setTimeout(function () { document.addEventListener('mousedown', outside, true); }, 0);
127
174
  pop.focus();
128
175
  }
129
- function teardown() { if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } }
130
- function pick(d) { if (done) return; done = true; var f = ref.pick; teardown(); f(iso(d)); }
131
- function cancel() { if (done) return; done = true; var f = ref.cancel; teardown(); if (f) f(); }
176
+ function teardown() { clearTip(); if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } }
177
+ function pick(d) {
178
+ if (done) return;
179
+ if (rc) {
180
+ if (!rangeStart) { rangeStart = d; hoverEnd = d; focus = new Date(d); render(); updateTip(); return; } // 1クリック目=開始(閉じない)
181
+ var aIso = iso(rangeStart), bIso = iso(d); // 2クリック目=終了
182
+ if (bIso < aIso) { var t = aIso; aIso = bIso; bIso = t; } // 逆順は入替/同日は単日
183
+ done = true; var fr = ref.pick; teardown(); fr(rangeValOf(aIso, bIso)); return;
184
+ }
185
+ done = true; var f = ref.pick; teardown(); f(iso(d));
186
+ }
187
+ function cancel() { if (done) return; done = true; var f = ref.cancel; teardown(); if (f) f(); } // range 途中の取消も値は変えない(ref.cancel)
132
188
 
133
189
  return {
134
190
  // TssGrid カスタムエディタ契約
135
- open: function (ctx) { show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }); },
191
+ open: function (ctx) {
192
+ var rangeMode = typeof rangeOpt === 'function' ? !!rangeOpt(ctx) : !!rangeOpt; // 行ごとに range を切替(半休行は単日)
193
+ var cfg = rangeMode ? { td: ctx.td } : null;
194
+ show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }, cfg);
195
+ },
136
196
  close: function () { teardown(); },
137
197
  icon: opts.icon != null ? opts.icon : '📅', // セル右端に表示する合図(opts.icon:'' で消せる)
138
198
  openOnClick: opts.openOnClick !== false, // シングルクリックで開く(既定 ON)