@threestarsoftware/tssgrid 0.1.7 → 0.1.8
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 +8 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/plugins/tss-cascade.css +15 -0
- package/plugins/tss-cascade.js +139 -0
- package/src/tssgrid.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
TssGrid の各リリースの変更点。日付は JST。形式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) 準拠(ゆるめ)。
|
|
4
4
|
数値は特記なき限り**自社調べ**(headless Chrome / Windows 11・環境差あり)。
|
|
5
5
|
|
|
6
|
+
## [0.1.8] - 2026-09-02
|
|
7
|
+
|
|
8
|
+
### 追加
|
|
9
|
+
- **多段(フライアウト)プルダウン プラグイン `tss-cascade`**(プラグイン・無依存・コア無改変)— メニューのように「大分類 ▸ 中分類 ▸ 小分類」とフライアウトで開き、**葉を選ぶとセルに確定**するカスタムエディタ(`columns[c].editor` 契約に乗る)。`options` は入れ子配列 `[{ label, value?, children? }]`、保存は葉の `value`(未指定なら label)、表示は **`TssCascade.pathFormat(options)`** で「大 / 中 / 小」のパスに。クリック展開+キーボード(↑↓/→←/Enter/Esc)+画面端フリップ。動く例 `examples/cascade-dropdown.html`。
|
|
10
|
+
|
|
11
|
+
### 修正
|
|
12
|
+
- **右クリックメニューのカスタム項目が `key`(組込キー)だけ・`callback` 無しで無反応だったのを修正**(コア)— カスタム項目 `{ name, key:'row_above' }` は `callback` を実行する仕様で、`key` だけ書いても組込アクションが起動せず**何も起きなかった**(位置に依らず/組込文字列 `'row_above'` は最下行でも正常)。**`callback` 無しで `key` が組込項目を指す時はその組込アクションを実行**するようにし、「**組込動作のまま `name`/`disabled` だけカスタムしたい**」を可能に。`callback` 付き・未知 `key`・組込文字列・`disabled` は不変(後方互換)。
|
|
13
|
+
|
|
6
14
|
## [0.1.7] - 2026-08-01
|
|
7
15
|
|
|
8
16
|
### 追加
|
package/README.md
CHANGED
|
@@ -1214,6 +1214,7 @@ new TssGrid(el, {
|
|
|
1214
1214
|
|
|
1215
1215
|
- **組込キー**: `row_above` / `row_below` / `remove_row` / `col_left` / `col_right` / `remove_col` / `copy` / `cut` / `paste` / `clear` / `undo` / `redo`、区切りは `'---------'`(ダッシュのみ)。
|
|
1216
1216
|
- **カスタム項目**: `{ name, callback, disabled?, hidden?, key?, submenu? }`。`name`/`disabled`/`hidden` は**関数**も可=引数は `(range, ctx)`(`range`=選択範囲 `{r0,c0,r1,c1}`)。`callback` の `this` はグリッド、引数は `{ range, key, ctx }`。
|
|
1217
|
+
- **組込動作のまま name/disabled だけ変えたい**: カスタム項目に `callback` を**書かず** `key` に組込キー(`row_above` 等)を指定すると、**その組込アクションを実行**します(`disabled`/`name` はカスタム側を尊重)。例: `{ name: '行を上に追加', key: 'row_above', disabled: (s) => … }`=ラベルと活性条件だけ差し替え、挿入動作は組込のまま。
|
|
1217
1218
|
- **`ctx`=右クリックの対象** `{ on, r, c }`。`on` は `'col'`(列ヘッダ)/`'row'`(行ヘッダ)/`'cell'`(データセル)/`'corner'`(隅)。例: **列ヘッダを右クリックした時だけ項目を出す**なら `hidden: (range, ctx) => ctx.on !== 'col'`。
|
|
1218
1219
|
- `submenu` に配列を渡すとフライアウトの**サブメニュー**になります(同じ組込キー/カスタム項目が使えます)。
|
|
1219
1220
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threestarsoftware/tssgrid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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,15 @@
|
|
|
1
|
+
/* TssCascade — 多段(フライアウト)プルダウン(plugins/tss-cascade.js)。色は --tg-accent を流用。 */
|
|
2
|
+
.tg-cascade {
|
|
3
|
+
position: fixed; z-index: 1000; min-width: 150px; max-height: 60vh; overflow: auto;
|
|
4
|
+
background: #fff; color: #1f2733; border: 1px solid #c4ccd6; border-radius: 8px;
|
|
5
|
+
box-shadow: 0 6px 22px rgba(20, 40, 80, .22);
|
|
6
|
+
font: 13px "Meiryo UI", "Segoe UI", sans-serif; padding: 4px; box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
.tg-cascade .tg-casc-item {
|
|
9
|
+
display: flex; align-items: center; gap: 10px; padding: 5px 10px; border-radius: 5px;
|
|
10
|
+
cursor: pointer; white-space: nowrap; outline: none;
|
|
11
|
+
}
|
|
12
|
+
.tg-cascade .tg-casc-item:hover,
|
|
13
|
+
.tg-cascade .tg-casc-item.active { background: var(--tg-accent, #1573c0); color: #fff; }
|
|
14
|
+
.tg-cascade .tg-casc-label { flex: 1; }
|
|
15
|
+
.tg-cascade .tg-casc-arrow { font-size: 10px; opacity: .7; margin-left: 6px; }
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/*! TssCascade — 多段(フライアウト)プルダウン。TssGrid カスタムエディタ(ポップアップ型)。
|
|
2
|
+
*
|
|
3
|
+
* メニューのように「大分類 ▸ 中分類 ▸ 小分類」とフライアウトで開き、葉を選ぶとセルに確定。
|
|
4
|
+
* 保存はその葉の value(未指定なら label)。表示は TssCascade.pathFormat で「大 / 中 / 小」のパスにできる。
|
|
5
|
+
*
|
|
6
|
+
* 使い方:
|
|
7
|
+
* const OPTS = [
|
|
8
|
+
* { label:'営業', children:[
|
|
9
|
+
* { label:'国内', children:[ {label:'東日本', value:'sales.jp.east'}, {label:'西日本', value:'sales.jp.west'} ] },
|
|
10
|
+
* { label:'海外', value:'sales.global' },
|
|
11
|
+
* ] },
|
|
12
|
+
* { label:'開発', children:[ … ] },
|
|
13
|
+
* ];
|
|
14
|
+
* columns: [
|
|
15
|
+
* { data:'category', editor: TssCascade({ options: OPTS }),
|
|
16
|
+
* format: TssCascade.pathFormat(OPTS) }, // 表示を「営業 / 国内 / 東日本」に(任意)
|
|
17
|
+
* ]
|
|
18
|
+
*
|
|
19
|
+
* opts:
|
|
20
|
+
* options : 入れ子配列 [{ label, value?, children? }]。必須。value 未指定の葉は label を保存。
|
|
21
|
+
* openOnClick : 既定 true(プルダウン同様シングルクリックで開く)。
|
|
22
|
+
* className : ポップアップに付ける任意クラス。
|
|
23
|
+
* 静的:
|
|
24
|
+
* TssCascade.pathFormat(options, { separator }) => (value)=>string // 列 format 用
|
|
25
|
+
* TssCascade.pathOf(options, value, sep?) => string
|
|
26
|
+
*/
|
|
27
|
+
(function (root) {
|
|
28
|
+
'use strict';
|
|
29
|
+
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|
30
|
+
function leafVal(n) { return n.value !== undefined ? n.value : n.label; }
|
|
31
|
+
|
|
32
|
+
// 値 → ラベルのパス配列([大, 中, 小])。見つからなければ null。
|
|
33
|
+
function findPath(nodes, value, trail) {
|
|
34
|
+
trail = trail || [];
|
|
35
|
+
for (var i = 0; i < (nodes || []).length; i++) {
|
|
36
|
+
var n = nodes[i], t = trail.concat(n.label);
|
|
37
|
+
if (n.children && n.children.length) { var r = findPath(n.children, value, t); if (r) return r; }
|
|
38
|
+
else if (String(leafVal(n)) === String(value)) return t;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function TssCascade(opts) {
|
|
44
|
+
opts = opts || {};
|
|
45
|
+
var roots = opts.options || [];
|
|
46
|
+
var panels = []; // 開いているパネル(div)。panels[0]=root, [1]=sub, …
|
|
47
|
+
var ctxRef = null, done = false, onDoc = null, onKey = null;
|
|
48
|
+
|
|
49
|
+
function teardown() {
|
|
50
|
+
panels.forEach(function (p) { if (p.parentNode) p.parentNode.removeChild(p); });
|
|
51
|
+
panels = [];
|
|
52
|
+
if (onDoc) document.removeEventListener('mousedown', onDoc, true);
|
|
53
|
+
if (onKey) document.removeEventListener('keydown', onKey, true);
|
|
54
|
+
onDoc = onKey = null;
|
|
55
|
+
}
|
|
56
|
+
function closeFrom(depth) { while (panels.length > depth) { var p = panels.pop(); if (p.parentNode) p.parentNode.removeChild(p); } }
|
|
57
|
+
function commitLeaf(n) { if (done) return; done = true; var c = ctxRef; teardown(); c.commit(leafVal(n)); }
|
|
58
|
+
function doCancel() { if (done) return; done = true; var c = ctxRef; teardown(); c.cancel(); }
|
|
59
|
+
|
|
60
|
+
function position(pop, anchor, depth) {
|
|
61
|
+
var pw = pop.offsetWidth, ph = pop.offsetHeight, vw = window.innerWidth, vh = window.innerHeight, left, top;
|
|
62
|
+
if (depth === 0) { left = anchor.left; top = anchor.bottom + 2; }
|
|
63
|
+
else { left = anchor.right - 2; top = anchor.top - 4; } // 親アイテムの右へフライアウト
|
|
64
|
+
if (left + pw > vw - 6) left = depth === 0 ? Math.max(6, vw - pw - 6) : Math.max(6, anchor.left - pw + 2); // 右端で左へフリップ
|
|
65
|
+
if (top + ph > vh - 6) top = Math.max(6, vh - ph - 6);
|
|
66
|
+
pop.style.left = Math.max(6, left) + 'px'; pop.style.top = top + 'px';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function openPanel(nodes, depth, anchor) {
|
|
70
|
+
closeFrom(depth);
|
|
71
|
+
var pop = document.createElement('div');
|
|
72
|
+
pop.className = 'tg-cascade' + (opts.className ? ' ' + opts.className : '');
|
|
73
|
+
pop.setAttribute('role', 'menu');
|
|
74
|
+
pop.innerHTML = nodes.map(function (n, i) {
|
|
75
|
+
var branch = n.children && n.children.length;
|
|
76
|
+
return '<div class="tg-casc-item' + (branch ? ' branch' : '') + '" data-i="' + i + '" role="menuitem" tabindex="-1">' +
|
|
77
|
+
'<span class="tg-casc-label">' + esc(n.label) + '</span>' + (branch ? '<span class="tg-casc-arrow">▸</span>' : '') + '</div>';
|
|
78
|
+
}).join('');
|
|
79
|
+
document.body.appendChild(pop);
|
|
80
|
+
panels[depth] = pop;
|
|
81
|
+
position(pop, anchor, depth);
|
|
82
|
+
|
|
83
|
+
var items = pop.querySelectorAll('.tg-casc-item');
|
|
84
|
+
pop._items = items; pop._active = null; pop._depth = depth;
|
|
85
|
+
pop._activate = function (idx) { Array.prototype.forEach.call(items, function (el, k) { el.classList.toggle('active', k === idx); }); pop._active = idx; };
|
|
86
|
+
pop._enter = function (idx, viaKey) { // 分岐なら submenu を開く(ホバー/→)。葉なら深いパネルを閉じる。
|
|
87
|
+
var n = nodes[idx]; if (!n) return; pop._activate(idx);
|
|
88
|
+
if (n.children && n.children.length) {
|
|
89
|
+
openPanel(n.children, depth + 1, items[idx].getBoundingClientRect());
|
|
90
|
+
if (viaKey) { var sub = panels[depth + 1], f = sub._items[0]; if (f) { sub._activate(0); f.focus(); } }
|
|
91
|
+
} else closeFrom(depth + 1);
|
|
92
|
+
};
|
|
93
|
+
pop._choose = function (idx) { // クリック/Enter:分岐→開く、葉→確定
|
|
94
|
+
var n = nodes[idx]; if (!n) return;
|
|
95
|
+
if (n.children && n.children.length) pop._enter(idx, true); else commitLeaf(n);
|
|
96
|
+
};
|
|
97
|
+
Array.prototype.forEach.call(items, function (el) {
|
|
98
|
+
var idx = +el.dataset.i;
|
|
99
|
+
el.addEventListener('mouseenter', function () { pop._enter(idx, false); });
|
|
100
|
+
el.addEventListener('click', function (e) { e.stopPropagation(); pop._choose(idx); });
|
|
101
|
+
});
|
|
102
|
+
return pop;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleKey(e) {
|
|
106
|
+
var pop = panels[panels.length - 1]; if (!pop) return;
|
|
107
|
+
var items = pop._items, idx = pop._active == null ? -1 : pop._active, n;
|
|
108
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); n = (idx + 1) % items.length; pop._activate(n); items[n].focus(); }
|
|
109
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); n = (idx - 1 + items.length) % items.length; pop._activate(n); items[n].focus(); }
|
|
110
|
+
else if (e.key === 'ArrowRight') { e.preventDefault(); if (idx >= 0) pop._enter(idx, true); }
|
|
111
|
+
else if (e.key === 'ArrowLeft') { e.preventDefault(); if (panels.length > 1) { closeFrom(panels.length - 1); var pp = panels[panels.length - 1]; if (pp._active != null) pp._items[pp._active].focus(); } }
|
|
112
|
+
else if (e.key === 'Enter') { e.preventDefault(); if (idx >= 0) pop._choose(idx); }
|
|
113
|
+
else if (e.key === 'Escape') { e.preventDefault(); doCancel(); }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
openOnClick: opts.openOnClick !== false,
|
|
118
|
+
open: function (ctx) {
|
|
119
|
+
ctxRef = ctx; done = false;
|
|
120
|
+
openPanel(roots, 0, ctx.td.getBoundingClientRect());
|
|
121
|
+
onDoc = function (e) { for (var i = 0; i < panels.length; i++) if (panels[i].contains(e.target)) return; doCancel(); };
|
|
122
|
+
onKey = handleKey;
|
|
123
|
+
document.addEventListener('mousedown', onDoc, true);
|
|
124
|
+
document.addEventListener('keydown', onKey, true);
|
|
125
|
+
var f = panels[0]._items[0]; if (f) { panels[0]._activate(0); f.focus(); }
|
|
126
|
+
},
|
|
127
|
+
close: function () { teardown(); },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
TssCascade.pathFormat = function (options, o) {
|
|
132
|
+
o = o || {}; var sep = o.separator || ' / ';
|
|
133
|
+
return function (value) { if (value === '' || value == null) return ''; var p = findPath(options || [], value); return p ? p.join(sep) : String(value); };
|
|
134
|
+
};
|
|
135
|
+
TssCascade.pathOf = function (options, value, sep) { var p = findPath(options || [], value); return p ? p.join(sep || ' / ') : (value == null ? '' : String(value)); };
|
|
136
|
+
|
|
137
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = TssCascade;
|
|
138
|
+
else root.TssCascade = TssCascade;
|
|
139
|
+
})(typeof self !== 'undefined' ? self : this);
|
package/src/tssgrid.js
CHANGED
|
@@ -2545,7 +2545,10 @@
|
|
|
2545
2545
|
const disabled = typeof entry.disabled === 'function' ? !!entry.disabled.call(this, s, ctx) : !!entry.disabled;
|
|
2546
2546
|
const it = { label: String(name == null ? '' : name), disabled, danger: !!entry.danger };
|
|
2547
2547
|
if (Array.isArray(entry.submenu)) it.submenu = entry.submenu;
|
|
2548
|
-
else
|
|
2548
|
+
else if (typeof entry.callback === 'function') it.act = () => { try { entry.callback.call(this, { range: { ...s }, key: entry.key, ctx }); } catch (_) {} };
|
|
2549
|
+
// callback 無しで key が組込項目(row_above 等)を指すなら、その組込アクションを実行=
|
|
2550
|
+
// 「組込動作のまま name/disabled だけカスタムしたい」を可能に(key だけ書いて無反応だった不具合の解消)。
|
|
2551
|
+
else if (entry.key) { const bi = this._builtinItem(entry.key, s); if (bi && typeof bi.act === 'function') it.act = bi.act; }
|
|
2549
2552
|
items.push(it);
|
|
2550
2553
|
}
|
|
2551
2554
|
return items;
|