@threestarsoftware/tssgrid 0.1.29-beta.0 → 0.1.29-beta.2

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
@@ -6,7 +6,8 @@ TssGrid の各リリースの変更点。日付は JST。形式は [Keep a Chang
6
6
  ## [Unreleased]
7
7
 
8
8
  ### 追加(プラグイン)
9
- - **`tss-calendar` に範囲選択 `range` を追加** — 連続した日(休暇申請の開始〜終了など)を**1回の編集で**選べる。`range:true` で有効、`range:(ctx)=>boolean` で**行ごとにON/OFF**(半休の行は範囲を選ばせない等)。未指定は単日=**従来動作**。1クリック目=開始・2クリック目=終了(**開始より前なら入替・同じ日を2回で単日確定**)、間の日に色が付く。1クリック目だけで閉じたら(Esc/外クリック)**値は変えない**。確定値は既定で `2026/10/13~2026/10/16`(単日は `2026/10/13`/区切りは `rangeSeparator`・既定 '~'/`rangeValue(startISO,endISO)` で完全差替可)。**入力中のセルに範囲ツールチップ**を出し、`rangeTooltip(startISO,endISO)` で文面を差し替え可能(休日を除いた日数などを外から足す口)。**休日スキップは利用側**=カレンダーは範囲を返すだけ。範囲列は `type:'date'` を付けない素の列にする(範囲文字列を日付パーサに通さないため)。例 `examples/calendar-range.html`。
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
+ - **`tss-calendar` 範囲に「範囲で選ぶ」トグルを追加** — `range` が有効な行のカレンダー上部にトグル(チェックボックス)を出し、**OFF=従来の単日**(1クリック確定・値 ISO・ツールチップ無し)/**ON=範囲**(2クリック・間の色・ツールチップ)を編集中に切替可能。休暇申請はほとんどが1日なので**既定 OFF**(1クリックで決まる)。初期状態は `rangeDefault`(`true`/`(ctx)=>boolean`・既定 `false`)で、**どちらでもセル値が既に範囲なら ON で開く**。`range:(ctx)=>false` の行はトグルを出さない。文言は `rangeToggleLabel`(既定 `'範囲で選ぶ'`)。
10
11
 
11
12
  ## [0.1.28] - 2026-10-20
12
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threestarsoftware/tssgrid",
3
- "version": "0.1.29-beta.0",
3
+ "version": "0.1.29-beta.2",
4
4
  "description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
5
5
  "license": "MIT",
6
6
  "author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
@@ -18,6 +18,10 @@
18
18
  border: 1px solid var(--cal-border); border-radius: var(--cal-radius);
19
19
  box-shadow: 0 6px 24px rgba(0,0,0,.18); padding: 8px; font: var(--cal-font); user-select: none; outline: none;
20
20
  }
21
+ /* 範囲選択トグル(range 有効行のみ上部に表示)。 */
22
+ .tss-cal-toggle { display: flex; align-items: center; gap: 6px; margin: 0 2px 6px; padding: 3px 4px; font-size: 12px; color: var(--cal-fg); cursor: pointer; border-bottom: 1px solid var(--cal-border); user-select: none; }
23
+ .tss-cal-toggle input { cursor: pointer; margin: 0; }
24
+ .tss-cal-toggle:hover { color: var(--cal-accent); }
21
25
  .tss-cal-head { display: flex; align-items: center; justify-content: space-between; margin: 2px 2px 6px; }
22
26
  .tss-cal-title { font-weight: 600; }
23
27
  .tss-cal-sels { display: flex; gap: 4px; }
@@ -28,14 +28,21 @@ 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回は単日確定)。確定値・ツールチップは下記で差し替え可。休日スキップは利用側。 */
31
+ /** 範囲選択を**使える行**にする。`true` で全行/`(ctx)=>boolean` で行ごと(例: 半休の行は `false`=単日のみ)。
32
+ * 有効な行では上部に「範囲で選ぶ」トグルが出る: **OFF=従来の単日**(1クリック確定・値 ISO・ツールチップ無し)/
33
+ * **ON=範囲**(1クリック目=開始・2クリック目=終了・開始より前なら入替・同日2回は単日・間の色・ツールチップ)。
34
+ * 未指定は常に単日=従来動作。初期ON/OFFは `rangeDefault`、確定値・ツールチップ・文言は下記で差替可。休日スキップは利用側。 */
34
35
  range?: boolean | ((ctx: any) => boolean);
36
+ /** 「範囲で選ぶ」トグルの初期状態(`true`=ON/`(ctx)=>boolean` で行ごと・既定 `false`=OFF)。
37
+ * いずれの場合も **セルの値が既に範囲なら ON で開く**。`range` が無効な行では無視(トグル自体が出ない)。 */
38
+ rangeDefault?: boolean | ((ctx: any) => boolean);
39
+ /** トグルの文言(既定 `'範囲で選ぶ'`)。 */
40
+ rangeToggleLabel?: string;
35
41
  /** 範囲値・ツールチップの日付区切り(既定 '~')。 */
36
42
  rangeSeparator?: string;
37
- /** 範囲の確定値を作る(既定 `'YYYY/MM/DD~YYYY/MM/DD'`/同日は `'YYYY/MM/DD'`)。
38
- * 引数は開始・終了の ISO('YYYY-MM-DD'・開始<=終了に整列済み)。別フィールド分割などはこの口で。 */
43
+ /** 範囲の確定値を作る。**同日(単日)のときも呼ばれる**。引数は開始・終了の ISO
44
+ * ('YYYY-MM-DD'・開始<=終了に整列済み)。未指定の既定は 範囲=`'YYYY/MM/DD~YYYY/MM/DD'`/
45
+ * **単日=ISO `'YYYY-MM-DD'`(範囲なしの単日と同じ形)**。別フィールド分割などはこの口で。 */
39
46
  rangeValue?: (startISO: string, endISO: string) => string;
40
47
  /** 編集中セルに出す範囲ツールチップの文面(既定は範囲文字列)。休日を除いた日数などを外から足す口。 */
41
48
  rangeTooltip?: (startISO: string, endISO: string) => string;
@@ -38,20 +38,29 @@
38
38
  var inline = opts.inline !== false; // true=共有 input に乗る(直打ち可・矢印は本体に返す)/false=従来のフォーカス奪取型
39
39
 
40
40
  // ── 範囲選択(range): 連続した日を1回の編集で選ぶ(休暇申請の開始〜終了 等)───────────
41
- // range:true で有効(行ごとに切替えたい時は range:(ctx)=>boolean)。未指定は単日=従来動作。
42
- // 1クリック目=開始、2クリック目=終了(開始より前なら入替・同日2回は単日確定)。間の日に色。
43
- // 確定値は rangeValue(開始ISO,終了ISO)(既定 'YYYY/MM/DD~YYYY/MM/DD'・単日は 'YYYY/MM/DD')。
44
- // 入力中はセルに range のツールチップ(rangeTooltip で文面差替=休日除く日数を外から足せる口)。
41
+ // range:true で「範囲も選べる」行になる(行ごとに切替えたい時は range:(ctx)=>boolean)。未指定は常に単日=従来動作。
42
+ // range が有効な行では、カレンダー上部に「範囲で選ぶ」トグルが出る(文言は rangeToggleLabel):
43
+ // OFF=従来の単日(1クリックで確定・値は ISO・ツールチップ無し)/ON=2クリック(開始→終了・間の色・ツールチップ)。
44
+ // トグルの初期状態は rangeDefault(true/関数(ctx)=>boolean・既定 OFF)。ただし **セル値が既に範囲なら ON で開く**。
45
+ // ON 時の確定値は rangeValue(開始ISO,終了ISO)(既定: 範囲 'YYYY/MM/DD~YYYY/MM/DD'/同日は ISO 'YYYY-MM-DD'。rangeValue は同日も呼ぶ)。
45
46
  // 休日スキップは利用側(返すのは範囲だけ)。
46
47
  var rangeOpt = opts.range;
48
+ var rangeDefault = opts.rangeDefault; // トグルの初期ON/OFF(bool or (ctx)=>bool・既定 false=OFF)
49
+ var rangeToggleLabel = opts.rangeToggleLabel != null ? opts.rangeToggleLabel : '範囲で選ぶ';
47
50
  var rangeSep = opts.rangeSeparator != null ? opts.rangeSeparator : '~';
48
51
  var rangeValueFn = typeof opts.rangeValue === 'function' ? opts.rangeValue : null;
49
52
  var rangeTooltipFn = typeof opts.rangeTooltip === 'function' ? opts.rangeTooltip : null;
50
- function defRangeStr(aIso, bIso) { return aIso === bIso ? fmtSlash(aIso) : fmtSlash(aIso) + rangeSep + fmtSlash(bIso); }
51
- function rangeValOf(aIso, bIso) { return (aIso !== bIso && rangeValueFn) ? rangeValueFn(aIso, bIso) : defRangeStr(aIso, bIso); }
52
- function rangeTipOf(aIso, bIso) { return rangeTooltipFn ? rangeTooltipFn(aIso, bIso) : defRangeStr(aIso, bIso); }
53
- var rc = null; // 現在の編集が range なら { td } を持つ(open(ctx) で ctx 判定して設定)
53
+ function defRangeStr(aIso, bIso) { return fmtSlash(aIso) + rangeSep + fmtSlash(bIso); } // 表示用(両端 slash・範囲)
54
+ function rangeValOf(aIso, bIso) {
55
+ if (rangeValueFn) return rangeValueFn(aIso, bIso); // rangeValue は同日も必ず呼ぶ(利用側で形を決められる)
56
+ return aIso === bIso ? aIso : defRangeStr(aIso, bIso); // 既定: 単日=ISO(範囲なしと同形)/範囲='YYYY/MM/DD~YYYY/MM/DD'
57
+ }
58
+ function rangeTipOf(aIso, bIso) { return rangeTooltipFn ? rangeTooltipFn(aIso, bIso) : (aIso === bIso ? fmtSlash(aIso) : defRangeStr(aIso, bIso)); }
59
+ function isRangeValue(v) { v = String(v == null ? '' : v); return !!rangeSep && v.indexOf(rangeSep) >= 0; } // 既定書式の範囲値か(区切りを含む)
60
+ var rc = null; // この行で範囲が使えるなら { td, def } を持つ=トグルを出す(open(ctx) で ctx 判定)
61
+ var rangeActive = false; // トグルの現在値(ON=2クリック範囲/OFF=単日)。rc 無しの行は常に false
54
62
  var rangeStart = null, hoverEnd = null; // 範囲選択の途中状態(1クリック目=rangeStart、プレビュー終端=hoverEnd)
63
+ function setRangeActive(on) { rangeActive = !!on; rangeStart = null; hoverEnd = null; clearTip(); render(); } // トグル切替=途中選択はリセット
55
64
 
56
65
  function disabled(d) {
57
66
  if (minD && d < minD) return true;
@@ -71,7 +80,9 @@
71
80
  var monSel = '<select class="tss-cal-sel-mon" aria-label="月">';
72
81
  for (var mm = 0; mm < 12; mm++) monSel += '<option value="' + mm + '"' + (mm === m ? ' selected' : '') + '>' + (mm + 1) + '月</option>';
73
82
  monSel += '</select>';
74
- var html = '<div class="tss-cal-head">'
83
+ // range が使える行はトグル(範囲で選ぶ)を上部に。OFF=単日1クリック/ON=2クリック範囲。
84
+ var toggle = rc ? '<label class="tss-cal-toggle"><input type="checkbox" class="tss-cal-range-cb"' + (rangeActive ? ' checked' : '') + '><span>' + esc(rangeToggleLabel) + '</span></label>' : '';
85
+ var html = toggle + '<div class="tss-cal-head">'
75
86
  + '<button type="button" class="tss-cal-nav" data-nav="-1" aria-label="前の月">‹</button>'
76
87
  + '<span class="tss-cal-sels">' + yrSel + monSel + '</span>'
77
88
  + '<button type="button" class="tss-cal-nav" data-nav="1" aria-label="次の月">›</button></div>'
@@ -89,7 +100,7 @@
89
100
  if (sameDay(d, today)) cls += ' tss-cal-today';
90
101
  if (sameDay(d, sel)) cls += ' tss-cal-sel';
91
102
  if (sameDay(d, focus)) cls += ' tss-cal-focus';
92
- if (rc && rangeStart) { // 範囲の途中=開始〜(プレビュー)終端を塗る
103
+ if (rc && rangeActive && rangeStart) { // 範囲の途中=開始〜(プレビュー)終端を塗る(ON のときだけ)
93
104
  var ra = rangeStart, rb = hoverEnd || rangeStart, lo = ra <= rb ? ra : rb, hi = ra <= rb ? rb : ra;
94
105
  if (d >= lo && d <= hi) cls += ' tss-cal-inrange';
95
106
  if (sameDay(d, ra)) cls += ' tss-cal-range-start';
@@ -116,7 +127,8 @@
116
127
  }
117
128
  function onChange(e) {
118
129
  var t = e.target;
119
- if (t.classList && t.classList.contains('tss-cal-sel-yr')) { view = new Date(+t.value, view.getMonth(), 1); render(); }
130
+ if (t.classList && t.classList.contains('tss-cal-range-cb')) { setRangeActive(t.checked); } // 「範囲で選ぶ」トグル
131
+ else if (t.classList && t.classList.contains('tss-cal-sel-yr')) { view = new Date(+t.value, view.getMonth(), 1); render(); }
120
132
  else if (t.classList && t.classList.contains('tss-cal-sel-mon')) { view = new Date(view.getFullYear(), +t.value, 1); render(); }
121
133
  }
122
134
  function onKey(e) {
@@ -126,12 +138,12 @@
126
138
  if (k === 'Escape') { e.preventDefault(); cancel(); return; }
127
139
  if (k === 'Enter') { e.preventDefault(); if (!disabled(focus)) pick(focus); return; }
128
140
  var dx = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[k];
129
- 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; }
141
+ if (dx != null) { e.preventDefault(); focus = new Date(focus); focus.setDate(focus.getDate() + dx); view = new Date(focus.getFullYear(), focus.getMonth(), 1); if (rc && rangeActive && rangeStart) { hoverEnd = new Date(focus); updateTip(); } render(); return; }
130
142
  if (k === 'PageUp' || k === 'PageDown') { e.preventDefault(); view = new Date(view.getFullYear(), view.getMonth() + (k === 'PageUp' ? -1 : 1), 1); render(); }
131
143
  }
132
144
  // range 途中: 日の上をなぞると終端プレビューを更新(間の色+ツールチップ)。
133
145
  function onOver(e) {
134
- if (!(rc && rangeStart)) return;
146
+ if (!(rc && rangeActive && rangeStart)) return;
135
147
  var dd = e.target.getAttribute && e.target.getAttribute('data-d');
136
148
  if (!dd) return;
137
149
  var d = parseISO(dd); if (!d || disabled(d)) return;
@@ -148,6 +160,7 @@
148
160
  function show(anchor, value, onPick, onCancel, rangeCfg) {
149
161
  teardown(); // 既に開いていれば先に片付ける(前の pop 除去+outside リスナ解除・open-while-open の取り残し防止)
150
162
  done = false; rc = rangeCfg || null; rangeStart = null; hoverEnd = null;
163
+ rangeActive = rc ? (isRangeValue(value) || !!rc.def) : false; // 既存値が範囲 or rangeDefault なら ON で開く
151
164
  sel = parseISO(value);
152
165
  // range の既存値('YYYY/MM/DD~…' 等)は parseISO では取れない=先頭日を緩く読んで月を合わせる。
153
166
  var initD = sel || (rc ? parseLoose(value) : null);
@@ -173,21 +186,22 @@
173
186
  function teardown() { clearTip(); if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } }
174
187
  function pick(d) {
175
188
  if (done) return;
176
- if (rc) {
189
+ if (rc && rangeActive) { // トグル ON=2クリック範囲
177
190
  if (!rangeStart) { rangeStart = d; hoverEnd = d; focus = new Date(d); render(); updateTip(); return; } // 1クリック目=開始(閉じない)
178
191
  var aIso = iso(rangeStart), bIso = iso(d); // 2クリック目=終了
179
192
  if (bIso < aIso) { var t = aIso; aIso = bIso; bIso = t; } // 逆順は入替/同日は単日
180
193
  done = true; var fr = ref.pick; teardown(); fr(rangeValOf(aIso, bIso)); return;
181
194
  }
182
- done = true; var f = ref.pick; teardown(); f(iso(d));
195
+ done = true; var f = ref.pick; teardown(); f(iso(d)); // トグル OFF/range 無しの行=単日1クリック(ISO)
183
196
  }
184
197
  function cancel() { if (done) return; done = true; var f = ref.cancel; teardown(); if (f) f(); } // range 途中の取消も値は変えない(ref.cancel)
185
198
 
186
199
  return {
187
200
  // TssGrid カスタムエディタ契約
188
201
  open: function (ctx) {
189
- var rangeMode = typeof rangeOpt === 'function' ? !!rangeOpt(ctx) : !!rangeOpt; // 行ごとに range を切替(半休行は単日)
190
- var cfg = rangeMode ? { td: ctx.td } : null;
202
+ var rangeMode = typeof rangeOpt === 'function' ? !!rangeOpt(ctx) : !!rangeOpt; // この行で範囲を使えるか(半休行は false=トグル無し)
203
+ var def = typeof rangeDefault === 'function' ? !!rangeDefault(ctx) : !!rangeDefault; // トグル初期ON/OFF(行ごと可)
204
+ var cfg = rangeMode ? { td: ctx.td, def: def } : null;
191
205
  show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }, cfg);
192
206
  },
193
207
  close: function () { teardown(); },