@threestarsoftware/tssgrid 0.0.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/LICENSE +21 -0
- package/README.md +1091 -0
- package/package.json +57 -0
- package/plugins/tss-autocomplete.css +16 -0
- package/plugins/tss-autocomplete.js +121 -0
- package/plugins/tss-bar.js +51 -0
- package/plugins/tss-calc.js +103 -0
- package/plugins/tss-calendar.css +40 -0
- package/plugins/tss-calendar.js +133 -0
- package/plugins/tss-filter.css +32 -0
- package/plugins/tss-filter.js +146 -0
- package/plugins/tss-formula.js +87 -0
- package/plugins/tss-furigana.js +84 -0
- package/plugins/tss-heatmap.js +66 -0
- package/plugins/tss-merge.js +144 -0
- package/plugins/tss-messages.js +68 -0
- package/plugins/tss-running.js +82 -0
- package/plugins/tss-shared.js +114 -0
- package/plugins/tss-sparkline.js +57 -0
- package/plugins/tss-syncscroll.js +95 -0
- package/plugins/tss-time.css +23 -0
- package/plugins/tss-time.js +91 -0
- package/plugins/tss-toggle.css +7 -0
- package/plugins/tss-toggle.js +86 -0
- package/plugins/tss-totals.js +92 -0
- package/src/tssgrid.css +201 -0
- package/src/tssgrid.d.ts +329 -0
- package/src/tssgrid.js +2440 -0
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@threestarsoftware/tssgrid",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
|
|
7
|
+
"homepage": "https://tssgrid.threestarsoftware.co.jp",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/threestarsoftware/TssGrid.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/threestarsoftware/TssGrid/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"grid",
|
|
17
|
+
"data-grid",
|
|
18
|
+
"datagrid",
|
|
19
|
+
"spreadsheet",
|
|
20
|
+
"table",
|
|
21
|
+
"editable",
|
|
22
|
+
"vanilla-js",
|
|
23
|
+
"no-build",
|
|
24
|
+
"dependency-free",
|
|
25
|
+
"ime",
|
|
26
|
+
"japanese",
|
|
27
|
+
"excel",
|
|
28
|
+
"mit"
|
|
29
|
+
],
|
|
30
|
+
"main": "src/tssgrid.js",
|
|
31
|
+
"types": "src/tssgrid.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./src/tssgrid.d.ts",
|
|
35
|
+
"default": "./src/tssgrid.js"
|
|
36
|
+
},
|
|
37
|
+
"./tssgrid.css": "./src/tssgrid.css",
|
|
38
|
+
"./plugins/*": "./plugins/*",
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"unpkg": "src/tssgrid.js",
|
|
42
|
+
"jsdelivr": "src/tssgrid.js",
|
|
43
|
+
"sideEffects": [
|
|
44
|
+
"**/*.css"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"src/tssgrid.js",
|
|
51
|
+
"src/tssgrid.css",
|
|
52
|
+
"src/tssgrid.d.ts",
|
|
53
|
+
"plugins/",
|
|
54
|
+
"README.md",
|
|
55
|
+
"LICENSE"
|
|
56
|
+
]
|
|
57
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* TssAutocomplete — 候補サジェスト ポップアップ。見た目は変数とクラスで自由に。 */
|
|
2
|
+
.tss-ac {
|
|
3
|
+
position: absolute; z-index: 9999; background: #fff;
|
|
4
|
+
border: 1px solid var(--ac-border, #c3ccd6); border-radius: 6px;
|
|
5
|
+
box-shadow: 0 6px 24px rgba(20, 40, 80, .18); padding: 4px;
|
|
6
|
+
font: inherit; box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
.tss-ac-input {
|
|
9
|
+
width: 100%; box-sizing: border-box; font: inherit; padding: 4px 6px;
|
|
10
|
+
border: 1px solid var(--ac-input-border, #cfd7e0); border-radius: 4px; outline: none;
|
|
11
|
+
}
|
|
12
|
+
.tss-ac-input:focus { border-color: var(--ac-accent, #0d9488); }
|
|
13
|
+
.tss-ac-list { max-height: 220px; overflow-y: auto; margin-top: 4px; }
|
|
14
|
+
.tss-ac-item { padding: 5px 8px; border-radius: 4px; cursor: pointer; white-space: nowrap; }
|
|
15
|
+
.tss-ac-item:hover { background: var(--ac-hover, #eef2f6); }
|
|
16
|
+
.tss-ac-item.hi { background: var(--ac-accent, #0d9488); color: #fff; }
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/*! TssAutocomplete — 候補サジェスト付きテキスト入力(TssGrid カスタムエディタ)。
|
|
2
|
+
*
|
|
3
|
+
* 自由入力+候補の絞り込み。固定 dropdown の上位版(取引先名・商品名など「だいたい決まってるが新規もある」項目向け)。
|
|
4
|
+
* **IME変換中の Enter は奪わない**(変換確定が最優先=TssGrid の流儀)。
|
|
5
|
+
*
|
|
6
|
+
* 使い方:
|
|
7
|
+
* columns: [ { data:'client', editor: TssAutocomplete({ source: ['三星商事','三星製作所', …] }) } ]
|
|
8
|
+
* // 動的候補: source: (query, {row,r,c,grid}) => string[]
|
|
9
|
+
*
|
|
10
|
+
* opts:
|
|
11
|
+
* source : string[] か (query, ctx)=>string[]。必須。
|
|
12
|
+
* strict : true=候補から選んだ値のみ許可(既定 false=自由入力可)。
|
|
13
|
+
* minChars : この文字数以上で候補を出す(既定 0=開いた時点で全件)。
|
|
14
|
+
* max : 候補表示の最大件数(既定 20)。
|
|
15
|
+
* match : 'includes'(既定) | 'startsWith' | (item, query)=>bool。大文字小文字は無視。
|
|
16
|
+
* openOnClick: 既定 true(プルダウン同様シングルクリックで開く)。
|
|
17
|
+
* icon : セル右の目印(既定 '▾')。className: ポップアップに付ける任意クラス。
|
|
18
|
+
*/
|
|
19
|
+
(function (root) {
|
|
20
|
+
'use strict';
|
|
21
|
+
function norm(s) { return String(s == null ? '' : s).toLowerCase(); }
|
|
22
|
+
|
|
23
|
+
function TssAutocomplete(opts) {
|
|
24
|
+
opts = opts || {};
|
|
25
|
+
var getSource = typeof opts.source === 'function' ? opts.source : function () { return opts.source || []; };
|
|
26
|
+
var strict = !!opts.strict;
|
|
27
|
+
var minChars = opts.minChars || 0;
|
|
28
|
+
var max = opts.max || 20;
|
|
29
|
+
var matcher = (typeof opts.match === 'function') ? opts.match
|
|
30
|
+
: (opts.match === 'startsWith' ? function (it, q) { return norm(it).indexOf(q) === 0; }
|
|
31
|
+
: function (it, q) { return norm(it).indexOf(q) >= 0; });
|
|
32
|
+
|
|
33
|
+
var pop, input, list, items = [], hi = -1, composing = false, ref = null, done = false, outside = null;
|
|
34
|
+
|
|
35
|
+
function filter(q) {
|
|
36
|
+
var ctx = ref ? ref.ctx : {};
|
|
37
|
+
var src = getSource(q, { row: grid_row(ctx), r: ctx.r, c: ctx.c, grid: ctx.grid }) || [];
|
|
38
|
+
var nq = norm(q);
|
|
39
|
+
var out = [];
|
|
40
|
+
for (var i = 0; i < src.length && out.length < max; i++) {
|
|
41
|
+
if (nq.length < minChars) { out.push(src[i]); continue; }
|
|
42
|
+
if (nq === '' || matcher(src[i], nq)) out.push(src[i]);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function grid_row(ctx) { try { return ctx.grid.getRow(ctx.r); } catch (e) { return null; } }
|
|
47
|
+
|
|
48
|
+
function render() {
|
|
49
|
+
var q = input.value;
|
|
50
|
+
items = filter(q);
|
|
51
|
+
hi = items.length ? 0 : -1;
|
|
52
|
+
list.innerHTML = items.map(function (it, i) {
|
|
53
|
+
return '<div class="tss-ac-item' + (i === hi ? ' hi' : '') + '" data-i="' + i + '">' + esc(it) + '</div>';
|
|
54
|
+
}).join('');
|
|
55
|
+
list.style.display = items.length ? 'block' : 'none';
|
|
56
|
+
}
|
|
57
|
+
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|
58
|
+
function moveHi(d) {
|
|
59
|
+
if (!items.length) return;
|
|
60
|
+
hi = (hi + d + items.length) % items.length;
|
|
61
|
+
var els = list.querySelectorAll('.tss-ac-item');
|
|
62
|
+
for (var i = 0; i < els.length; i++) els[i].className = 'tss-ac-item' + (i === hi ? ' hi' : '');
|
|
63
|
+
if (els[hi]) els[hi].scrollIntoView({ block: 'nearest' });
|
|
64
|
+
}
|
|
65
|
+
function chosenValue() {
|
|
66
|
+
if (hi >= 0 && items[hi] != null) return items[hi]; // 候補ハイライトを採用
|
|
67
|
+
if (strict) return null; // strict は候補必須
|
|
68
|
+
return input.value; // 自由入力を採用
|
|
69
|
+
}
|
|
70
|
+
function onKey(e) {
|
|
71
|
+
if (e.key === 'ArrowDown') { if (composing || e.isComposing) return; e.preventDefault(); moveHi(1); }
|
|
72
|
+
else if (e.key === 'ArrowUp') { if (composing || e.isComposing) return; e.preventDefault(); moveHi(-1); }
|
|
73
|
+
else if (e.key === 'Enter') {
|
|
74
|
+
if (composing || e.isComposing) return; // ★ IME変換確定の Enter は奪わない
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
var v = chosenValue(); if (v == null) return; // strict で候補なし=確定しない
|
|
77
|
+
commit(v);
|
|
78
|
+
} else if (e.key === 'Tab') {
|
|
79
|
+
var t = chosenValue(); if (t != null) { e.preventDefault(); commit(t); }
|
|
80
|
+
} else if (e.key === 'Escape') { e.preventDefault(); cancel(); }
|
|
81
|
+
}
|
|
82
|
+
function place(anchor) {
|
|
83
|
+
var r = anchor.getBoundingClientRect();
|
|
84
|
+
pop.style.left = Math.max(2, Math.min(r.left, window.innerWidth - pop.offsetWidth - 6)) + 'px';
|
|
85
|
+
var top = r.bottom + 1;
|
|
86
|
+
if (top + 220 > window.innerHeight) top = Math.max(2, r.top - 4 - Math.min(220, list.offsetHeight + input.offsetHeight));
|
|
87
|
+
pop.style.top = top + 'px';
|
|
88
|
+
pop.style.minWidth = r.width + 'px';
|
|
89
|
+
}
|
|
90
|
+
function show(anchor, value, ctx) {
|
|
91
|
+
done = false; ref = { ctx: ctx };
|
|
92
|
+
pop = document.createElement('div'); pop.className = 'tss-ac' + (opts.className ? ' ' + opts.className : ''); pop.tabIndex = -1;
|
|
93
|
+
pop.innerHTML = '<input class="tss-ac-input" type="text"><div class="tss-ac-list"></div>';
|
|
94
|
+
document.body.appendChild(pop);
|
|
95
|
+
input = pop.querySelector('.tss-ac-input'); list = pop.querySelector('.tss-ac-list');
|
|
96
|
+
input.value = value == null ? '' : value;
|
|
97
|
+
input.addEventListener('compositionstart', function () { composing = true; });
|
|
98
|
+
input.addEventListener('compositionend', function () { composing = false; render(); }); // 変換確定後に候補更新
|
|
99
|
+
input.addEventListener('input', function () { if (!composing) render(); });
|
|
100
|
+
input.addEventListener('keydown', onKey);
|
|
101
|
+
list.addEventListener('mousedown', function (e) { var it = e.target.closest('.tss-ac-item'); if (it) { e.preventDefault(); commit(items[+it.dataset.i]); } });
|
|
102
|
+
render(); place(anchor);
|
|
103
|
+
outside = function (e) { if (pop && !pop.contains(e.target) && !(anchor && anchor.contains && anchor.contains(e.target))) { var v = chosenOnOutside(); if (v == null) cancel(); else commit(v); } };
|
|
104
|
+
setTimeout(function () { document.addEventListener('mousedown', outside, true); }, 0);
|
|
105
|
+
input.focus(); input.setSelectionRange(input.value.length, input.value.length);
|
|
106
|
+
}
|
|
107
|
+
function chosenOnOutside() { if (strict) { var v = input.value; var src = getSource(v, {}) || []; return src.indexOf(v) >= 0 ? v : null; } return input.value; }
|
|
108
|
+
function teardown() { if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } items = []; hi = -1; composing = false; }
|
|
109
|
+
function commit(v) { if (done) return; done = true; var f = ref.ctx.commit; teardown(); f(v); }
|
|
110
|
+
function cancel() { if (done) return; done = true; var f = ref.ctx.cancel; teardown(); if (f) f(); }
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
openOnClick: opts.openOnClick !== false,
|
|
114
|
+
icon: opts.icon != null ? opts.icon : '▾',
|
|
115
|
+
open: function (ctx) { show(ctx.td, ctx.value, ctx); },
|
|
116
|
+
close: function () { teardown(); },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = TssAutocomplete;
|
|
120
|
+
else root.TssAutocomplete = TssAutocomplete;
|
|
121
|
+
})(typeof self !== 'undefined' ? self : this);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*! TssBar — セル内 横棒グラフ(データバー)。
|
|
2
|
+
*
|
|
3
|
+
* セルの数値を**横棒**で描く(アンケートの認知率バー、進捗%、スコアなど)。
|
|
4
|
+
* `columns[c].html:true` + `format` 関数 seam に乗るだけ(format が返す <div> がセルに入る)。
|
|
5
|
+
* 数値・色(dev指定)しか入れない=安全。type:'number' と併用すれば**バーのまま編集**(打つと伸びる)。
|
|
6
|
+
*
|
|
7
|
+
* 使い方:
|
|
8
|
+
* // 表示専用バー
|
|
9
|
+
* { data:'rate', title:'認知率', html:true, readOnly:true, format: TssBar({ max:100, suffix:'%', decimals:1 }) }
|
|
10
|
+
* // バーのまま編集(値を打つと伸びる)
|
|
11
|
+
* { data:'rate', type:'number', html:true, format: TssBar({ max:100, suffix:'%' }) }
|
|
12
|
+
*
|
|
13
|
+
* opts:
|
|
14
|
+
* min/max : スケール(既定 0 / 100)。値は min..max を 0..100% に。範囲外はクランプ。
|
|
15
|
+
* color : バー色(文字列 or (value)=>色=閾値で色分け)。track: 下地色。
|
|
16
|
+
* height : バーの高さ(px・既定 12) radius: 角丸(px・既定 3)
|
|
17
|
+
* showValue: 値ラベルを右に出す(既定 true)。decimals: ラベルの小数桁。suffix: 単位('%' 等)。
|
|
18
|
+
* labelWidth: ラベル幅(px・既定 44)。
|
|
19
|
+
*/
|
|
20
|
+
(function (root) {
|
|
21
|
+
'use strict';
|
|
22
|
+
function r2(n) { return Math.round(n * 100) / 100; }
|
|
23
|
+
|
|
24
|
+
function TssBar(opts) {
|
|
25
|
+
opts = opts || {};
|
|
26
|
+
var min = (opts.min != null) ? opts.min : 0, max = (opts.max != null) ? opts.max : 100;
|
|
27
|
+
var color = opts.color || '#0d9488', track = opts.track || '#eef2f6';
|
|
28
|
+
var height = opts.height || 12, radius = (opts.radius != null) ? opts.radius : 3;
|
|
29
|
+
var showValue = opts.showValue !== false, decimals = opts.decimals, suffix = opts.suffix || '';
|
|
30
|
+
var labelW = (opts.labelWidth != null) ? opts.labelWidth : 44;
|
|
31
|
+
|
|
32
|
+
return function (value) {
|
|
33
|
+
var s = String(value == null ? '' : value).trim();
|
|
34
|
+
if (s === '' || !isFinite(Number(s))) return '';
|
|
35
|
+
var v = Number(s);
|
|
36
|
+
var pct = Math.max(0, Math.min(100, ((v - min) / ((max - min) || 1)) * 100));
|
|
37
|
+
var col = (typeof color === 'function') ? color(v) : color;
|
|
38
|
+
var label = '';
|
|
39
|
+
if (showValue) {
|
|
40
|
+
var t = (decimals != null) ? v.toFixed(decimals) : String(v);
|
|
41
|
+
label = '<span style="min-width:' + labelW + 'px;text-align:right;font-variant-numeric:tabular-nums">' + t + suffix + '</span>';
|
|
42
|
+
}
|
|
43
|
+
return '<div style="display:flex;align-items:center;gap:6px;height:100%">'
|
|
44
|
+
+ '<div style="flex:1;background:' + track + ';border-radius:' + radius + 'px;height:' + height + 'px;overflow:hidden">'
|
|
45
|
+
+ '<div style="width:' + r2(pct) + '%;background:' + col + ';height:100%;border-radius:' + radius + 'px"></div>'
|
|
46
|
+
+ '</div>' + label + '</div>';
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = TssBar;
|
|
50
|
+
else root.TssBar = TssBar;
|
|
51
|
+
})(typeof self !== 'undefined' ? self : this);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*! TssCalc — セル内ミニ計算式(=12*3+5 → 41)。**eval/Function 不使用**。
|
|
2
|
+
*
|
|
3
|
+
* セルに数式を打つと確定時に計算結果へ。四則 + - * / と括弧 () と小数に対応。
|
|
4
|
+
* パーサは再帰下降の自前実装=**eval を使わない**(CSP安全)。`columns[c].parse` seam に乗るだけ。
|
|
5
|
+
*
|
|
6
|
+
* 使い方(2通り):
|
|
7
|
+
* // (a) 列の parse に直接(一番素直=入力変換そのもの)
|
|
8
|
+
* columns: [ { data:'amount', type:'number', parse: TssCalc() } ]
|
|
9
|
+
*
|
|
10
|
+
* // (b) プラグインで対象列にまとめて注入(既存 parse があれば合成)
|
|
11
|
+
* plugins: [ TssCalc.plugin({ columns: ['quantity','price'] }) ] // columns 省略で number 列すべて
|
|
12
|
+
*
|
|
13
|
+
* opts:
|
|
14
|
+
* prefix : これで始まると強制的に式評価(既定 '=')。'=12*3' → 36。
|
|
15
|
+
* bare : 接頭辞なしの素の式も評価(既定 true)。'12*3' → 36 / '100' は数値なので素通り。
|
|
16
|
+
* decimals: 結果の小数桁(既定 null=丸めない。浮動小数の誤差だけは常に10桁で整える)。
|
|
17
|
+
*
|
|
18
|
+
* 返り: parse 関数 `(value) => string`。式でなければ入力をそのまま返す(=通常入力は素通り)。
|
|
19
|
+
*/
|
|
20
|
+
(function (root) {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
// 安全な四則評価(再帰下降)。式文字列 → 数値 or null(構文/不正文字/0除算は null)。
|
|
24
|
+
function evaluate(s) {
|
|
25
|
+
var toks = [], i = 0;
|
|
26
|
+
while (i < s.length) {
|
|
27
|
+
var c = s.charAt(i);
|
|
28
|
+
if (c === ' ' || c === '\t') { i++; continue; }
|
|
29
|
+
if ((c >= '0' && c <= '9') || c === '.') {
|
|
30
|
+
var j = i + 1; while (j < s.length && ((s.charAt(j) >= '0' && s.charAt(j) <= '9') || s.charAt(j) === '.')) j++;
|
|
31
|
+
var num = parseFloat(s.slice(i, j)); if (!isFinite(num)) return null; toks.push({ t: 'n', v: num }); i = j; continue;
|
|
32
|
+
}
|
|
33
|
+
if ('+-*/()'.indexOf(c) >= 0) { toks.push({ t: c }); i++; continue; }
|
|
34
|
+
return null; // 不正文字
|
|
35
|
+
}
|
|
36
|
+
var p = 0;
|
|
37
|
+
function peek() { return toks[p]; }
|
|
38
|
+
function parseExpr() {
|
|
39
|
+
var v = parseTerm(); if (v === null) return null;
|
|
40
|
+
while (peek() && (peek().t === '+' || peek().t === '-')) { var op = toks[p++].t; var r = parseTerm(); if (r === null) return null; v = (op === '+') ? v + r : v - r; }
|
|
41
|
+
return v;
|
|
42
|
+
}
|
|
43
|
+
function parseTerm() {
|
|
44
|
+
var v = parseFactor(); if (v === null) return null;
|
|
45
|
+
while (peek() && (peek().t === '*' || peek().t === '/')) { var op = toks[p++].t; var r = parseFactor(); if (r === null) return null; if (op === '/') { if (r === 0) return null; v = v / r; } else v = v * r; }
|
|
46
|
+
return v;
|
|
47
|
+
}
|
|
48
|
+
function parseFactor() {
|
|
49
|
+
var tk = peek(); if (!tk) return null;
|
|
50
|
+
if (tk.t === '+') { p++; return parseFactor(); }
|
|
51
|
+
if (tk.t === '-') { p++; var f = parseFactor(); return f === null ? null : -f; }
|
|
52
|
+
if (tk.t === 'n') { p++; return tk.v; }
|
|
53
|
+
if (tk.t === '(') { p++; var e = parseExpr(); if (e === null) return null; if (!peek() || peek().t !== ')') return null; p++; return e; }
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
var result = parseExpr();
|
|
57
|
+
if (result === null || p !== toks.length || !isFinite(result)) return null; // 余りトークン=構文エラー
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 素の式(数字+演算子だけ・演算子を最低1つ含む)か。'100' は数値なので式扱いしない=素通り。
|
|
62
|
+
function looksArithmetic(s) { return /^[\d\s.+\-*/()]+$/.test(s) && /[+\-*/]/.test(s) && /\d/.test(s); }
|
|
63
|
+
|
|
64
|
+
function TssCalc(opts) {
|
|
65
|
+
opts = opts || {};
|
|
66
|
+
var prefix = (opts.prefix == null) ? '=' : opts.prefix;
|
|
67
|
+
var bare = opts.bare !== false;
|
|
68
|
+
var decimals = (opts.decimals == null) ? null : opts.decimals;
|
|
69
|
+
return function (value) {
|
|
70
|
+
var s = String(value == null ? '' : value).trim();
|
|
71
|
+
if (s === '') return value;
|
|
72
|
+
var expr = null;
|
|
73
|
+
if (prefix && s.charAt(0) === prefix) expr = s.slice(1);
|
|
74
|
+
else if (bare && looksArithmetic(s)) expr = s;
|
|
75
|
+
if (expr === null) return value; // 式でない=素通り(通常入力)
|
|
76
|
+
var r = evaluate(expr);
|
|
77
|
+
if (r === null) return value; // 構文エラー等=打った値を残す(validator が弾けば赤)
|
|
78
|
+
r = Math.round(r * 1e10) / 1e10; // 浮動小数の誤差を均す
|
|
79
|
+
if (decimals != null) { var f = Math.pow(10, decimals); r = Math.round(r * f) / f; }
|
|
80
|
+
return String(r);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// プラグイン形: 対象列の parse に TssCalc を注入(既存 parse があれば「calc→既存」で合成)。
|
|
85
|
+
TssCalc.plugin = function (opts) {
|
|
86
|
+
opts = opts || {};
|
|
87
|
+
var calc = TssCalc(opts);
|
|
88
|
+
return function (grid) {
|
|
89
|
+
var keys = opts.columns || null;
|
|
90
|
+
for (var c = 0; c < grid.COLS; c++) {
|
|
91
|
+
var cfg = grid.columns[c]; if (!cfg) continue;
|
|
92
|
+
var hit = keys ? (keys.indexOf(cfg.data) >= 0) : (cfg.type === 'number');
|
|
93
|
+
if (!hit) continue;
|
|
94
|
+
var prev = (typeof cfg.parse === 'function') ? cfg.parse : null;
|
|
95
|
+
cfg.parse = prev ? (function (pv) { return function (v) { return pv(calc(v)); }; })(prev) : calc;
|
|
96
|
+
}
|
|
97
|
+
return { name: 'calc' };
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = TssCalc;
|
|
102
|
+
else root.TssCalc = TssCalc;
|
|
103
|
+
})(typeof self !== 'undefined' ? self : this);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* TssCalendar — スタイル。業務側は下の CSS 変数を上書きするだけでテーマを変えられる。
|
|
2
|
+
例: .tss-cal { --cal-accent:#c0392b; --cal-radius:10px; } / 特定業務だけ .myapp .tss-cal { ... } */
|
|
3
|
+
.tss-cal {
|
|
4
|
+
/* ▼ テーマ変数(上書きポイント) */
|
|
5
|
+
--cal-bg: #fff;
|
|
6
|
+
--cal-fg: #222;
|
|
7
|
+
--cal-border: #c3ccd6;
|
|
8
|
+
--cal-accent: #2aa0d8; /* 選択日 */
|
|
9
|
+
--cal-hover: #eef4ff;
|
|
10
|
+
--cal-sun: #d61f2b; /* 日曜・祝日の文字 */
|
|
11
|
+
--cal-sat: #2456b0; /* 土曜の文字 */
|
|
12
|
+
--cal-holiday-bg: #fff0f1; /* 祝日セルの背景 */
|
|
13
|
+
--cal-muted: #b6bdc6; /* 前後月・無効日 */
|
|
14
|
+
--cal-radius: 8px;
|
|
15
|
+
--cal-font: 13px "Segoe UI", "Meiryo", sans-serif;
|
|
16
|
+
|
|
17
|
+
position: fixed; z-index: 10000; background: var(--cal-bg); color: var(--cal-fg);
|
|
18
|
+
border: 1px solid var(--cal-border); border-radius: var(--cal-radius);
|
|
19
|
+
box-shadow: 0 6px 24px rgba(0,0,0,.18); padding: 8px; font: var(--cal-font); user-select: none; outline: none;
|
|
20
|
+
}
|
|
21
|
+
.tss-cal-head { display: flex; align-items: center; justify-content: space-between; margin: 2px 2px 6px; }
|
|
22
|
+
.tss-cal-title { font-weight: 600; }
|
|
23
|
+
.tss-cal-sels { display: flex; gap: 4px; }
|
|
24
|
+
.tss-cal-sels select { font: inherit; padding: 2px 4px; border: 1px solid var(--cal-border); border-radius: 5px; background: var(--cal-bg); color: var(--cal-fg); cursor: pointer; }
|
|
25
|
+
.tss-cal-sels select:focus { outline: 2px solid var(--cal-accent); outline-offset: -1px; }
|
|
26
|
+
.tss-cal-nav { border: 1px solid var(--cal-border); background: var(--cal-bg); color: var(--cal-fg); border-radius: 6px; width: 26px; height: 26px; cursor: pointer; font-size: 15px; line-height: 1; }
|
|
27
|
+
.tss-cal-nav:hover { background: var(--cal-hover); }
|
|
28
|
+
.tss-cal-grid { display: grid; grid-template-columns: repeat(7, 32px); gap: 2px; }
|
|
29
|
+
.tss-cal-wl { text-align: center; font-size: 11px; color: #889; padding: 2px 0; }
|
|
30
|
+
.tss-cal-wl.tss-cal-sun { color: var(--cal-sun); } .tss-cal-wl.tss-cal-sat { color: var(--cal-sat); }
|
|
31
|
+
.tss-cal-day { text-align: center; padding: 6px 0; border-radius: 6px; cursor: pointer; }
|
|
32
|
+
.tss-cal-day:hover { background: var(--cal-hover); }
|
|
33
|
+
.tss-cal-day.tss-cal-sun { color: var(--cal-sun); }
|
|
34
|
+
.tss-cal-day.tss-cal-sat { color: var(--cal-sat); }
|
|
35
|
+
.tss-cal-day.tss-cal-holiday { color: var(--cal-sun); background: var(--cal-holiday-bg); }
|
|
36
|
+
.tss-cal-day.tss-cal-other { color: var(--cal-muted); }
|
|
37
|
+
.tss-cal-day.tss-cal-today { box-shadow: inset 0 0 0 1px var(--cal-accent); }
|
|
38
|
+
.tss-cal-day.tss-cal-focus { box-shadow: inset 0 0 0 2px var(--cal-accent); }
|
|
39
|
+
.tss-cal-day.tss-cal-sel { background: var(--cal-accent); color: #fff; }
|
|
40
|
+
.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; }
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*! TssCalendar — 日付ピッカー(TssGrid のカスタムエディタ / 単体ウィジェット両用)
|
|
2
|
+
* 休日カレンダーを JSON で渡せる(勤怠・シフト等向け)。CSS は tss-calendar.css をクラス/変数で自由に。
|
|
3
|
+
* 使い方(グリッド): columns: [{ type:'date', editor: TssCalendar({ holidays }) }]
|
|
4
|
+
* 使い方(単体) : TssCalendar({holidays}).openAt(anchorEl, isoValue, onPick)
|
|
5
|
+
*/
|
|
6
|
+
(function (root) {
|
|
7
|
+
'use strict';
|
|
8
|
+
function pad(n) { return String(n).padStart(2, '0'); }
|
|
9
|
+
function iso(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
|
|
10
|
+
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; }
|
|
11
|
+
function sameDay(a, b) { return !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); }
|
|
12
|
+
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|
13
|
+
|
|
14
|
+
function TssCalendar(opts) {
|
|
15
|
+
opts = opts || {};
|
|
16
|
+
var holidays = opts.holidays || {}; // { 'YYYY-MM-DD': '名称'(string) | {name,type} }
|
|
17
|
+
var weekend = opts.weekend || [0, 6]; // 0=日, 6=土
|
|
18
|
+
var weekLabels = opts.weekLabels || ['日', '月', '火', '水', '木', '金', '土'];
|
|
19
|
+
var monthLabel = opts.monthLabel || function (y, m) { return y + '年' + (m + 1) + '月'; };
|
|
20
|
+
var minD = parseISO(opts.min), maxD = parseISO(opts.max);
|
|
21
|
+
var disable = opts.disable || []; // ['holiday','weekend'] など、選択不可にする種別
|
|
22
|
+
var holName = function (h) { return h == null ? null : (typeof h === 'object' ? h.name : h); };
|
|
23
|
+
var holType = function (h) { return (h && typeof h === 'object' && h.type) ? h.type : null; };
|
|
24
|
+
|
|
25
|
+
var ty = new Date().getFullYear();
|
|
26
|
+
var yrLo = opts.yearMin || (minD ? minD.getFullYear() : ty - 12);
|
|
27
|
+
var yrHi = opts.yearMax || (maxD ? maxD.getFullYear() : ty + 12);
|
|
28
|
+
var pop = null, view = null, sel = null, focus = null, ref = null, done = false, outside = null;
|
|
29
|
+
|
|
30
|
+
function disabled(d) {
|
|
31
|
+
if (minD && d < minD) return true;
|
|
32
|
+
if (maxD && d > maxD) return true;
|
|
33
|
+
var k = iso(d);
|
|
34
|
+
if (disable.indexOf('holiday') >= 0 && holidays[k]) return true;
|
|
35
|
+
if (disable.indexOf('weekend') >= 0 && weekend.indexOf(d.getDay()) >= 0) return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
function render() {
|
|
39
|
+
var y = view.getFullYear(), m = view.getMonth();
|
|
40
|
+
var start = new Date(y, m, 1).getDay(), today = new Date();
|
|
41
|
+
// 年・月はプルダウンで直接選べる(年送りが遅い問題の解消)。年範囲は min/max or 今日±12(view も内包)
|
|
42
|
+
var lo = Math.min(yrLo, y), hi = Math.max(yrHi, y), yrSel = '<select class="tss-cal-sel-yr" aria-label="年">';
|
|
43
|
+
for (var yy = lo; yy <= hi; yy++) yrSel += '<option value="' + yy + '"' + (yy === y ? ' selected' : '') + '>' + yy + '年</option>';
|
|
44
|
+
yrSel += '</select>';
|
|
45
|
+
var monSel = '<select class="tss-cal-sel-mon" aria-label="月">';
|
|
46
|
+
for (var mm = 0; mm < 12; mm++) monSel += '<option value="' + mm + '"' + (mm === m ? ' selected' : '') + '>' + (mm + 1) + '月</option>';
|
|
47
|
+
monSel += '</select>';
|
|
48
|
+
var html = '<div class="tss-cal-head">'
|
|
49
|
+
+ '<button type="button" class="tss-cal-nav" data-nav="-1" aria-label="前の月">‹</button>'
|
|
50
|
+
+ '<span class="tss-cal-sels">' + yrSel + monSel + '</span>'
|
|
51
|
+
+ '<button type="button" class="tss-cal-nav" data-nav="1" aria-label="次の月">›</button></div>'
|
|
52
|
+
+ '<div class="tss-cal-grid">';
|
|
53
|
+
for (var w = 0; w < 7; w++) {
|
|
54
|
+
var wc = w === 0 ? ' tss-cal-sun' : (w === 6 ? ' tss-cal-sat' : '');
|
|
55
|
+
html += '<div class="tss-cal-wl' + wc + '">' + esc(weekLabels[w]) + '</div>';
|
|
56
|
+
}
|
|
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 = iso(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, sel)) cls += ' tss-cal-sel';
|
|
65
|
+
if (sameDay(d, focus)) cls += ' tss-cal-focus';
|
|
66
|
+
if (disabled(d)) cls += ' tss-cal-disabled';
|
|
67
|
+
html += '<div class="' + cls + '" data-d="' + k + '"' + (nm ? ' title="' + esc(nm) + '"' : '') + '>' + d.getDate() + '</div>';
|
|
68
|
+
d.setDate(d.getDate() + 1);
|
|
69
|
+
}
|
|
70
|
+
pop.innerHTML = html + '</div>';
|
|
71
|
+
}
|
|
72
|
+
function place(anchor) {
|
|
73
|
+
var r = anchor.getBoundingClientRect();
|
|
74
|
+
pop.style.left = Math.max(2, Math.min(r.left, window.innerWidth - pop.offsetWidth - 6)) + 'px';
|
|
75
|
+
var top = r.bottom + 2;
|
|
76
|
+
if (top + pop.offsetHeight > window.innerHeight) top = Math.max(2, r.top - pop.offsetHeight - 2);
|
|
77
|
+
pop.style.top = top + 'px';
|
|
78
|
+
}
|
|
79
|
+
function onClick(e) {
|
|
80
|
+
var nav = e.target.getAttribute && e.target.getAttribute('data-nav');
|
|
81
|
+
if (nav) { view = new Date(view.getFullYear(), view.getMonth() + (+nav), 1); render(); return; }
|
|
82
|
+
var dd = e.target.getAttribute && e.target.getAttribute('data-d');
|
|
83
|
+
if (dd) { var d = parseISO(dd); if (!disabled(d)) pick(d); }
|
|
84
|
+
}
|
|
85
|
+
function onChange(e) {
|
|
86
|
+
var t = e.target;
|
|
87
|
+
if (t.classList && t.classList.contains('tss-cal-sel-yr')) { view = new Date(+t.value, view.getMonth(), 1); render(); }
|
|
88
|
+
else if (t.classList && t.classList.contains('tss-cal-sel-mon')) { view = new Date(view.getFullYear(), +t.value, 1); render(); }
|
|
89
|
+
}
|
|
90
|
+
function onKey(e) {
|
|
91
|
+
if (e.target && e.target.tagName === 'SELECT') return; // 年/月プルダウン操作中は日移動しない
|
|
92
|
+
if (!focus) focus = sel ? new Date(sel) : new Date(view);
|
|
93
|
+
var k = e.key;
|
|
94
|
+
if (k === 'Escape') { e.preventDefault(); cancel(); return; }
|
|
95
|
+
if (k === 'Enter') { e.preventDefault(); if (!disabled(focus)) pick(focus); return; }
|
|
96
|
+
var dx = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[k];
|
|
97
|
+
if (dx != null) { e.preventDefault(); focus = new Date(focus); focus.setDate(focus.getDate() + dx); view = new Date(focus.getFullYear(), focus.getMonth(), 1); render(); return; }
|
|
98
|
+
if (k === 'PageUp' || k === 'PageDown') { e.preventDefault(); view = new Date(view.getFullYear(), view.getMonth() + (k === 'PageUp' ? -1 : 1), 1); render(); }
|
|
99
|
+
}
|
|
100
|
+
function show(anchor, value, onPick, onCancel) {
|
|
101
|
+
done = false; sel = parseISO(value);
|
|
102
|
+
view = sel ? new Date(sel.getFullYear(), sel.getMonth(), 1) : new Date();
|
|
103
|
+
focus = sel ? new Date(sel) : new Date();
|
|
104
|
+
pop = document.createElement('div'); pop.className = 'tss-cal' + (opts.className ? ' ' + opts.className : '');
|
|
105
|
+
pop.tabIndex = -1; document.body.appendChild(pop);
|
|
106
|
+
pop.addEventListener('mousedown', function (e) { if (e.target.tagName !== 'SELECT') e.preventDefault(); }); // フォーカスを奪わない(select は除く)
|
|
107
|
+
pop.addEventListener('click', onClick);
|
|
108
|
+
pop.addEventListener('change', onChange);
|
|
109
|
+
pop.addEventListener('keydown', onKey);
|
|
110
|
+
render(); place(anchor);
|
|
111
|
+
ref = { pick: onPick, cancel: onCancel };
|
|
112
|
+
// 外側クリックで閉じる。ただしアンカー(開いた元セル)は除外=再クリックはグリッド側でトグル処理させる
|
|
113
|
+
outside = function (e) { if (pop && !pop.contains(e.target) && !(anchor && anchor.contains && anchor.contains(e.target))) cancel(); };
|
|
114
|
+
setTimeout(function () { document.addEventListener('mousedown', outside, true); }, 0);
|
|
115
|
+
pop.focus();
|
|
116
|
+
}
|
|
117
|
+
function teardown() { if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; } if (pop) { pop.remove(); pop = null; } }
|
|
118
|
+
function pick(d) { if (done) return; done = true; var f = ref.pick; teardown(); f(iso(d)); }
|
|
119
|
+
function cancel() { if (done) return; done = true; var f = ref.cancel; teardown(); if (f) f(); }
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
// TssGrid カスタムエディタ契約
|
|
123
|
+
open: function (ctx) { show(ctx.td, ctx.value, function (v) { ctx.commit(v); }, function () { ctx.cancel(); }); },
|
|
124
|
+
close: function () { teardown(); },
|
|
125
|
+
icon: opts.icon != null ? opts.icon : '📅', // セル右端に表示する合図(opts.icon:'' で消せる)
|
|
126
|
+
openOnClick: opts.openOnClick !== false, // シングルクリックで開く(既定 ON)
|
|
127
|
+
// 単体ウィジェット: 任意要素にアンカーして開く
|
|
128
|
+
openAt: function (anchorEl, value, onPick) { show(anchorEl, value, function (v) { onPick(v); }, null); },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = TssCalendar;
|
|
132
|
+
else root.TssCalendar = TssCalendar;
|
|
133
|
+
})(typeof self !== 'undefined' ? self : this);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* TssFilter — ヘッダのオートフィルタUI(plugins/tss-filter.js)。色は --tg-accent を流用。 */
|
|
2
|
+
.tssgrid .tg-table thead th .tg-filter-btn {
|
|
3
|
+
display: inline-flex; align-items: center; margin-left: 4px; line-height: 1; cursor: pointer;
|
|
4
|
+
color: #b9bfcc; user-select: none; padding: 1px 2px; border-radius: 3px;
|
|
5
|
+
}
|
|
6
|
+
.tssgrid .tg-table thead th .tg-filter-btn .tg-filter-ico { display: block; }
|
|
7
|
+
.tssgrid .tg-table thead th .tg-filter-btn:hover { color: #fff; background: rgba(255,255,255,.18); }
|
|
8
|
+
.tssgrid .tg-table thead th .tg-filter-btn.on { color: var(--tg-accent, #2f6fed); background: #fff; } /* 適用中はロートがアクセント色+白地で明確 */
|
|
9
|
+
|
|
10
|
+
.tg-filter-pop {
|
|
11
|
+
position: fixed; z-index: 1000; width: 220px; background: #fff; color: #1f2733;
|
|
12
|
+
border: 1px solid #c4ccd6; border-radius: 8px; box-shadow: 0 6px 22px rgba(20,40,80,.22);
|
|
13
|
+
font: 12.5px "Meiryo UI", "Segoe UI", sans-serif; padding: 8px; box-sizing: border-box;
|
|
14
|
+
}
|
|
15
|
+
.tg-filter-pop .tg-filter-search input {
|
|
16
|
+
width: 100%; box-sizing: border-box; padding: 4px 7px; border: 1px solid #cbd2dc; border-radius: 5px; font: inherit;
|
|
17
|
+
}
|
|
18
|
+
.tg-filter-pop .tg-filter-tools { font-size: 11.5px; color: #6b7280; margin: 6px 2px 4px; }
|
|
19
|
+
.tg-filter-pop .tg-filter-tools a { color: var(--tg-accent, #2f6fed); cursor: pointer; text-decoration: none; }
|
|
20
|
+
.tg-filter-pop .tg-filter-tools a:hover { text-decoration: underline; }
|
|
21
|
+
.tg-filter-pop .tg-filter-list { max-height: 200px; overflow: auto; border: 1px solid #eef1f5; border-radius: 5px; padding: 3px; }
|
|
22
|
+
.tg-filter-pop .tg-filter-list label {
|
|
23
|
+
display: flex; align-items: center; gap: 6px; padding: 2px 4px; border-radius: 4px; cursor: pointer; white-space: nowrap;
|
|
24
|
+
}
|
|
25
|
+
.tg-filter-pop .tg-filter-list label:hover { background: #f0f4fb; }
|
|
26
|
+
.tg-filter-pop .tg-filter-list label span { overflow: hidden; text-overflow: ellipsis; }
|
|
27
|
+
.tg-filter-pop .tg-filter-btns { display: flex; gap: 6px; margin-top: 8px; }
|
|
28
|
+
.tg-filter-pop .tg-filter-btns button {
|
|
29
|
+
flex: 1; padding: 5px 0; border: 1px solid #cbd2dc; background: #fff; border-radius: 5px; font: inherit; cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
.tg-filter-pop .tg-filter-btns button[data-act=apply] { background: var(--tg-accent, #2f6fed); border-color: var(--tg-accent, #2f6fed); color: #fff; }
|
|
32
|
+
.tg-filter-pop .tg-filter-btns button:hover { filter: brightness(.97); }
|