@threestarsoftware/tssgrid 0.1.32 → 0.1.34
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 +16 -0
- package/README.md +8 -5
- package/package.json +1 -1
- package/plugins/tss-hyperlink.d.ts +18 -5
- package/plugins/tss-hyperlink.js +64 -17
- package/plugins/tss-toggle.css +1 -1
- package/plugins/tss-toggle.js +9 -7
- package/src/tssgrid.css +2 -0
- package/src/tssgrid.d.ts +4 -0
- package/src/tssgrid.js +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.34] - 2026-11-05
|
|
7
|
+
|
|
8
|
+
### 改善(プラグイン)
|
|
9
|
+
- **`tss-toggle` を 2択から N択(3つ以上)へ拡張**。これまで内部で選択肢を2つに切り詰めていた(`slice(0,2)`)のを撤廃し、`columns: { ans: ['未回答','出席','欠席'] }` のように **3つ以上の択一**を「並んだセグメント=ラジオ的に全選択肢が常に見える」形で置けるようにした。クリック確定(`setValue`=検証・Undo を通る)・許可値以外の validator 拒否・アクティブセルで **Space は次の値へ循環**(2択なら従来どおり反対側)は据え置き。値は dropdown 同様 **value を保存**。**短いラベルなら dropdown と同程度の幅**で収まり、タップ数は「開く→選ぶ」の 2 から **1** に。用途: 出欠(未回答/出席/欠席)・優先度(高/中/低)など少数の短い択一。選択肢が多い/ラベルが長い/列幅が狭い場合は従来どおり `type:'dropdown'` が省スペース。例 `examples/toggle.html` に出欠3択パネルを追加。**後方互換**(2択の既存指定・`TssToggle.format()` 直指定は不変)。
|
|
10
|
+
|
|
11
|
+
## [0.1.33] - 2026-11-05
|
|
12
|
+
|
|
13
|
+
### 追加
|
|
14
|
+
- **`ellipsis`(溢れた文字を … で省略+ホバーで全文)を追加**(コア・`wordWrap` の兄弟)。既定は従来どおり `overflow:hidden` で切るだけ(… は出ない)。`ellipsis:true`(グリッド全体)/`columns[c].ellipsis:true`(列単位)で**末尾を省略記号**にし、**溢れているセルだけホバーで全文を `title`**(ネイティブツールチップ)表示。溢れているセルだけ初回ホバーで1回測るので軽い(未使用なら即 return=ゼロコスト)。**見え方だけ**=値・コピー・編集は全文のまま(コピーは表示文字でなく元の値)。折り返し(`wordWrap`)と同時なら折り返しが優先。例 `examples/ellipsis.html`。
|
|
15
|
+
|
|
16
|
+
### 修正
|
|
17
|
+
- **`html:true` 列の編集がフォーマット後の生HTMLになる不具合を修正**(コア)— 編集開始値を作る `_editText` が、date/number 以外では `_displayValue`(format 適用後)を返しており、`html:true`+`format` 列だと `<a>`/`<span>` の**生HTML**が編集欄に入って崩れていた(その後タグのままリンク化して二重に崩れる)。**`html:true` 列は「生の値」を編集する**ように修正(表示の装飾=format と、編集対象=値、を分離)。html でない format 列は従来どおり表示形を編集(回帰なし)。
|
|
18
|
+
|
|
19
|
+
### 改善(プラグイン)
|
|
20
|
+
- **`tss-hyperlink` に「モードB(値=文字だけ・URLはシステムが付与)」を追加** — 従来は値 `"表示\|URL"` に URL を埋める必要があった(モードA)。`url` オプション(文字列 or `(value,ctx,row)=>string`)を渡すと、**値は表示テキストだけ**になり URL・装飾をシステム側で付与できる(入力・コピー・編集は文字だけ/Tabulator の `link` フォーマッタ同様)。安全な URL を返せば `<a>`、`null`/`''`/不正なら `<span>`(href 無し=**SPA クリックをホストが拾う**用)、`false` で素テキスト。`className` は関数化(行ごとに出し分け)、`attrs`(`data-*`/`aria-*`/`title`/`id`/`role` のみ・値はエスケープ・`null` は出さない)で `data-id` 等を安全に付与。本文は常に自動エスケープ=手書き format の XSS 罠を回避。**`safeUrl` がサイト内相対パス(`/path`・`./`・`../`・`#`・`?`)をそのまま許可**(SPA ルーティング用)、**プロトコル相対 `//host` は拒否**(不意の外部遷移回避)。`TssHyperlink()`(値="text\|url")は**完全後方互換**。
|
|
21
|
+
|
|
6
22
|
## [0.1.32] - 2026-11-03
|
|
7
23
|
|
|
8
24
|
### 改善(プラグイン)
|
package/README.md
CHANGED
|
@@ -771,6 +771,7 @@ columns: [
|
|
|
771
771
|
```
|
|
772
772
|
- **★安全URL**: `http`/`https`/`mailto`/`tel` のみリンク化。**`javascript:`・`data:` 等は素のテキストに落とす**(XSS 無効化)。スキーム無しは `https://` 補完、表示テキスト・href は HTML エスケープ、`target=_blank` に `rel="noopener noreferrer"` を付与。
|
|
773
773
|
- `separator`(既定 `'|'`) / `target`(既定 `'_blank'`) / `className`(既定 `'tss-link'`)。`TssHyperlink.safeUrl(u)` で URL 安全化だけ単体利用も可(許可外は `null`)。
|
|
774
|
+
- **モードB(値=文字だけ・URL はシステムが付与)**: `url` オプション(文字列 or `(value,ctx,row)=>string`)を渡すと値は表示テキストだけになり、**入力・コピー・編集も文字だけ**(Tabulator の `link` フォーマッタ同様)。安全な URL を返せば `<a>`、`null`/`''`/不正なら `<span>`(href 無し=**SPA クリックをホストが拾う**用)、`false` で素テキスト。`className` は関数化(行ごとに出し分け)、`attrs`(`data-*`/`aria-*`/`title`/`id`/`role` のみ・値はエスケープ・`null` は出さない)で `data-id` 等を安全付与。`label` で表示差し替え。サイト内相対パス(`/x`・`./`・`../`・`#`・`?`)は許可、`//host` は拒否。例: `format: TssHyperlink({ url:(v,{r},row)=>'/members/'+row.id })`。
|
|
774
775
|
- 数値しか SVG に入れない=安全。`formula`/`html-cells` と同じ仕組みで、**有料グリッドの "セル内グラフ" を無料で**。
|
|
775
776
|
|
|
776
777
|
### 同梱プラグイン: セル内 横棒グラフ(bar・データバー)
|
|
@@ -816,9 +817,9 @@ new TssGrid(el, { columns, data, onAfterChange: () => { chart.data = toChartData
|
|
|
816
817
|
```
|
|
817
818
|
- **おすすめ(いずれも `<script>`/CDN で no-build)**: **Chart.js**(MIT・定番・バランス◎)/ **ApexCharts**(MIT・モダンで映え)/ **Apache ECharts**(Apache-2.0・多機能)/ **uPlot**(MIT・超軽量)。Highcharts/AG Charts は商用有料なので MIT 方針なら避ける。
|
|
818
819
|
|
|
819
|
-
### 同梱プラグイン: 2
|
|
820
|
+
### 同梱プラグイン: 択一トグル/ラジオ(toggle・2択以上)
|
|
820
821
|
|
|
821
|
-
`plugins/tss-toggle.js`+`.css`=
|
|
822
|
+
`plugins/tss-toggle.js`+`.css`= 択一(**2つ以上**)を**並んだセグメント(有効|無効/未回答|出席|欠席)**で表示、クリックで切替。ラジオボタンの「**全選択肢が常に見える・1クリックで確定**」をセル内で実現。値は dropdown 同様 **value を保存**。動く例: [`examples/toggle.html`](https://tssgrid.threestarsoftware.co.jp/examples/toggle.html)。
|
|
822
823
|
|
|
823
824
|
```html
|
|
824
825
|
<link rel="stylesheet" href="plugins/tss-toggle.css">
|
|
@@ -826,12 +827,13 @@ new TssGrid(el, { columns, data, onAfterChange: () => { chart.data = toChartData
|
|
|
826
827
|
```
|
|
827
828
|
```js
|
|
828
829
|
plugins: [ TssToggle.plugin({ columns: {
|
|
829
|
-
status: ['有効', '無効'], //
|
|
830
|
+
status: ['有効', '無効'], // 文字列(value=label)。2つ以上でOK
|
|
831
|
+
ans: ['未回答', '出席', '欠席'], // 3択以上も可
|
|
830
832
|
pub: [{ value:'1', label:'公開' }, { value:'0', label:'非公開' }], // value/label 分離
|
|
831
833
|
} }) ]
|
|
832
834
|
```
|
|
833
|
-
-
|
|
834
|
-
- 単純な ON/OFF 1個なら組込 `type:'checkbox'
|
|
835
|
+
- クリックでその値に確定(`setValue`=**検証・Undo を通る**)。アクティブセルで **Space** で次の値へ循環(2択なら反対側)。許可値以外の打ち込みは自動 validator で拒否。
|
|
836
|
+
- 単純な ON/OFF 1個なら組込 `type:'checkbox'`。**短いラベルの少数択一**(出欠・高/中/低 等)に向き、短ければ dropdown と同程度の幅で収まる。**選択肢が多い/ラベルが長い/列幅が狭い**なら `type:'dropdown'` が省スペース。見た目は `--tg-toggle-accent` 等で。
|
|
835
837
|
|
|
836
838
|
### 同梱プラグイン: セル結合(merge・横/縦/矩形)
|
|
837
839
|
|
|
@@ -976,6 +978,7 @@ new TssGrid(el, { hiddenColumns: [1] }); // 初期で隠す
|
|
|
976
978
|
| `rowHeaders` / `colHeaders` | `boolean` | `true` | 行番号列 / ヘッダ行の表示。`false` で隠す |
|
|
977
979
|
| `hiddenColumns` | `number[]` | `[]` | 初期で隠す列インデックス。実行時は `hideColumn`/`showColumn`/`toggleColumn`(→ [列の表示/非表示](#列の表示非表示)) |
|
|
978
980
|
| `wordWrap` | `boolean` | `false` | セル内で折り返し(行高は内容に合わせ伸びる)。`columns[c].wordWrap` で列単位も可 |
|
|
981
|
+
| `ellipsis` | `boolean` | `false` | 溢れた文字を **… で省略**し、溢れたセルは**ホバーで全文(`title`)**。`columns[c].ellipsis` で列単位も可。見え方だけ=値・コピー・編集は全文のまま。`wordWrap` と同時なら折り返しが優先 |
|
|
979
982
|
| `placeholder` | `string` | — | 空セルに薄いヒント文字(`getData` には入らない)。`columns[c].placeholder` で列単位も可 |
|
|
980
983
|
| `className` | `string` | — | コンテナ(`.tssgrid`)に付ける任意クラス(テーマ付け) |
|
|
981
984
|
| `fillHandle` | `boolean \| {direction,autoInsertRow}` | `true` | **`false`でフィル完全OFF**(ハンドルを出さない=ドラッグでの消去/上書きなし。フィルの起点はハンドルのみなので fill 自体が無効に)。`{direction:'vertical'\|'horizontal'\|'both', autoInsertRow:true}`で詳細指定(下へドラッグで行追加)。設定/表示専用グリッドで誤フィルを防ぐのに有効 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threestarsoftware/tssgrid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
|
|
@@ -2,17 +2,30 @@
|
|
|
2
2
|
export = TssHyperlink;
|
|
3
3
|
export as namespace TssHyperlink;
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/** セルの値をリンク HTML に変換する format 関数を返す。
|
|
6
|
+
* モードA(既定): 値 "表示テキスト|URL"(または URL 単体)。値に URL が入る。
|
|
7
|
+
* モードB(`url` 指定): 値=表示テキストだけ・URL/装飾はオプションで付与(入力/コピー/編集は文字だけ)。 */
|
|
8
|
+
declare function TssHyperlink(opts?: TssHyperlink.Options): (value: any, ctx?: TssHyperlink.Ctx, row?: any) => string;
|
|
7
9
|
|
|
8
10
|
declare namespace TssHyperlink {
|
|
11
|
+
interface Ctx { r: number; c: number; }
|
|
12
|
+
/** (value, ctx, row) を受けて値を返す関数(className/url/label/attrs 用)。row はその行の値配列。 */
|
|
13
|
+
type Fn<T> = (value: any, ctx: Ctx, row: any) => T;
|
|
14
|
+
|
|
9
15
|
interface Options {
|
|
10
|
-
/**
|
|
16
|
+
/** (モードA)表示テキストと URL の区切り(既定 '|')。 */
|
|
11
17
|
separator?: string;
|
|
12
18
|
/** リンクの target(既定 '_blank')。 */
|
|
13
19
|
target?: string;
|
|
14
|
-
/** <a>
|
|
15
|
-
className?: string
|
|
20
|
+
/** <a>/<span> に付けるクラス。文字列 or 関数(行ごとに出し分け可。既定 'tss-link')。 */
|
|
21
|
+
className?: string | Fn<string | null | undefined>;
|
|
22
|
+
/** 指定するとモードB(値=表示テキストだけ)。URL を文字列 or 関数で付与。
|
|
23
|
+
* 安全な URL を返せば <a>、null/''/不正なら <span>(href 無し=SPA クリック用)、false で素テキスト。 */
|
|
24
|
+
url?: string | Fn<string | null | false | undefined>;
|
|
25
|
+
/** (モードB・任意)表示テキストの差し替え(既定は値そのもの)。 */
|
|
26
|
+
label?: Fn<any>;
|
|
27
|
+
/** 追加属性 (value, ctx, row)=>({key:val})。data- / aria- / title / id / role のみ・値はエスケープ・null は出さない。 */
|
|
28
|
+
attrs?: Fn<Record<string, any>>;
|
|
16
29
|
}
|
|
17
30
|
/** URL を安全化(http/https/mailto/tel のみ許可・スキーム無しは https 補完・許可外は null)。 */
|
|
18
31
|
function safeUrl(u: any): string | null;
|
package/plugins/tss-hyperlink.js
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
/*! TssHyperlink — セル内ハイパーリンク(format ヘルパ)。SVG/HTML 自前描画・無依存・コア無改変。
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 2つのモード(Tabulator の link フォーマッタ同様、URL を値に埋めても/システムが付けてもよい):
|
|
4
|
+
* ◆モードA(既定・後方互換): 値 = "表示テキスト|URL" または URL 単体。値に URL が入っている。
|
|
5
|
+
* format: TssHyperlink()
|
|
6
|
+
* ◆モードB(`url` オプション指定時): 値 = 表示テキストだけ。URL・装飾はシステム側(関数/文字列)で付与。
|
|
7
|
+
* 入力・コピー・編集は「文字だけ」。本文は常に自動エスケープ=手書き format の XSS 罠を回避。
|
|
8
|
+
* format: TssHyperlink({ url:(val,{r},row)=>'/x/'+row.id }) // 本物リンク(URLを生成)
|
|
9
|
+
* format: TssHyperlink({ url:()=>null, className:'my-link', // href 無し=<span>(SPA クリックをホストが拾う)
|
|
10
|
+
* attrs:(val,{r})=>({ 'data-id': ids[r] }) })
|
|
5
11
|
*
|
|
6
|
-
* ★セキュリティ: URL
|
|
7
|
-
* -
|
|
8
|
-
* - スキーム無し(example.com/path)は https:// を補完。表示テキスト・href
|
|
12
|
+
* ★セキュリティ: URL も attrs も利用者データになり得る=XSS を最初から潰す。
|
|
13
|
+
* - URL のプロトコルは http/https/mailto/tel のみ許可(javascript: data: vbscript: 等は拒否=ただのテキスト/素の span に落とす)。
|
|
14
|
+
* - スキーム無し(example.com/path)は https:// を補完。表示テキスト・href・属性値は必ず HTML エスケープ(" ' も)。
|
|
15
|
+
* - attrs は data-* / aria-* / title / id / role のキーだけ許可(on* や style, href, class は弾く)。値が null のキーは出さない。
|
|
9
16
|
* - target=_blank には rel="noopener noreferrer" を付与。
|
|
10
17
|
*
|
|
11
|
-
*
|
|
18
|
+
* 使い方(列は html:true が要る=format が HTML を返すため。編集はコアが「生の値」を出すので文字だけ編集になる):
|
|
12
19
|
* columns: [
|
|
13
|
-
* { data:'site', title:'リンク', html:true, readOnly:true, format: TssHyperlink() },
|
|
20
|
+
* { data:'site', title:'リンク', html:true, readOnly:true, format: TssHyperlink() }, // モードA
|
|
21
|
+
* { data:'name', title:'名前', html:true, format: TssHyperlink({ url:(v,{r})=>'/u/'+ids[r] }) }, // モードB
|
|
14
22
|
* ]
|
|
15
|
-
* // 値: "GitHub|https://github.com" / "https://example.com" / "example.com" / "問合せ|mailto:a@b.jp"
|
|
16
23
|
*
|
|
17
24
|
* opts:
|
|
18
|
-
* separator :
|
|
25
|
+
* separator : (モードA)表示テキストと URL の区切り(既定 '|')
|
|
26
|
+
* url : (指定でモードB)URL。文字列 or (value, ctx, row)=>string。安全な URL を返せば <a>、
|
|
27
|
+
* null/''/不正なら <span>(href 無し=SPA クリック用)、false を返すと素テキスト(装飾なし)。
|
|
28
|
+
* label : (モードB・任意)表示テキストを差し替える (value, ctx, row)=>string(既定は値そのもの)
|
|
29
|
+
* attrs : (任意)追加属性 (value, ctx, row)=>({key:val})。data- / aria- / title / id / role のみ・値はエスケープ。
|
|
19
30
|
* target : リンクの target(既定 '_blank'。'_self' 等も可)
|
|
20
|
-
* className
|
|
31
|
+
* className : <a>/<span> に付けるクラス。文字列 or (value, ctx, row)=>string(行ごとに出し分け可。既定 'tss-link')
|
|
21
32
|
*/
|
|
22
33
|
(function (root) {
|
|
23
34
|
'use strict';
|
|
@@ -31,27 +42,63 @@
|
|
|
31
42
|
u = String(u == null ? '' : u).trim();
|
|
32
43
|
if (u === '') return null;
|
|
33
44
|
if (/^(https?:|mailto:|tel:)/i.test(u)) return u; // whitelist したスキーム
|
|
45
|
+
if (/^\/\//.test(u)) return null; // プロトコル相対 //host は拒否(不意の外部遷移/phishing 回避)
|
|
46
|
+
if (/^(\/|\.\/|\.\.\/|[#?])/.test(u)) return u; // サイト内相対(/path ./x ../x #frag ?q)はそのまま(SPA ルーティング用)
|
|
34
47
|
if (!/[:]/.test(u) && /^[\w./?#=&%+\-~@!$*,;()]+$/.test(u)) return 'https://' + u; // スキーム無し=ドメイン/パス→https 補完
|
|
35
48
|
return null; // javascript: data: vbscript: 等=拒否
|
|
36
49
|
}
|
|
50
|
+
// attrs を安全な属性文字列に。data-*/aria-*/title/id/role のみ・値はエスケープ・null は出さない。
|
|
51
|
+
function attrStr(fn, v, ctx, row) {
|
|
52
|
+
if (typeof fn !== 'function') return '';
|
|
53
|
+
var o; try { o = fn(v, ctx, row) || {}; } catch (_) { return ''; }
|
|
54
|
+
var s = '';
|
|
55
|
+
for (var k in o) {
|
|
56
|
+
if (!Object.prototype.hasOwnProperty.call(o, k)) continue;
|
|
57
|
+
var val = o[k];
|
|
58
|
+
if (val == null) continue;
|
|
59
|
+
if (!/^(data-[a-z0-9-]+|aria-[a-z0-9-]+|title|id|role)$/i.test(k)) continue; // 安全キーのみ
|
|
60
|
+
s += ' ' + k.toLowerCase() + '="' + esc(val) + '"';
|
|
61
|
+
}
|
|
62
|
+
return s;
|
|
63
|
+
}
|
|
37
64
|
|
|
38
65
|
function TssHyperlink(opts) {
|
|
39
66
|
opts = opts || {};
|
|
40
67
|
var sep = opts.separator != null ? opts.separator : '|';
|
|
41
68
|
var target = opts.target || '_blank';
|
|
42
|
-
var
|
|
43
|
-
|
|
69
|
+
var defCls = opts.className != null ? opts.className : 'tss-link';
|
|
70
|
+
var modeB = ('url' in opts); // url 指定=モードB(値は文字だけ)
|
|
71
|
+
function clsAttrFor(v, ctx, row) {
|
|
72
|
+
var c = (typeof defCls === 'function') ? defCls(v, ctx, row) : defCls;
|
|
73
|
+
c = (c == null ? '' : String(c));
|
|
74
|
+
return c ? ' class="' + esc(c) + '"' : '';
|
|
75
|
+
}
|
|
76
|
+
return function (value, ctx, row) {
|
|
44
77
|
var v = String(value == null ? '' : value);
|
|
45
78
|
if (v === '') return '';
|
|
79
|
+
var clsAttr = clsAttrFor(v, ctx, row);
|
|
80
|
+
var extra = attrStr(opts.attrs, v, ctx, row);
|
|
81
|
+
var rel = (target === '_blank') ? ' rel="noopener noreferrer"' : '';
|
|
82
|
+
|
|
83
|
+
if (modeB) {
|
|
84
|
+
// 値=表示テキスト。URL は opts.url(文字列/関数)で付与。本文は常にエスケープ。
|
|
85
|
+
var label0 = (typeof opts.label === 'function') ? opts.label(v, ctx, row) : v;
|
|
86
|
+
var label = esc(String(label0 == null ? '' : label0));
|
|
87
|
+
var u = (typeof opts.url === 'function') ? opts.url(v, ctx, row) : opts.url;
|
|
88
|
+
if (u === false) return label; // 明示的にリンクにしない=素テキスト
|
|
89
|
+
var safeB = safeUrl(u);
|
|
90
|
+
if (safeB) return '<a href="' + esc(safeB) + '" target="' + esc(target) + '"' + rel + clsAttr + extra + '>' + label + '</a>';
|
|
91
|
+
return '<span' + clsAttr + extra + '>' + label + '</span>'; // href 無し=SPA クリック用
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// モードA(後方互換): 値 "表示|URL" / "URL"
|
|
46
95
|
var i = v.indexOf(sep);
|
|
47
96
|
var text = i >= 0 ? v.slice(0, i) : v;
|
|
48
97
|
var url = i >= 0 ? v.slice(i + 1) : v;
|
|
49
98
|
var safe = safeUrl(url);
|
|
50
|
-
var
|
|
51
|
-
if (!safe) return
|
|
52
|
-
|
|
53
|
-
return '<a href="' + esc(safe) + '" target="' + esc(target) + '"' + rel +
|
|
54
|
-
(cls ? ' class="' + esc(cls) + '"' : '') + '>' + label + '</a>';
|
|
99
|
+
var labelA = esc((text.trim() || url.trim()));
|
|
100
|
+
if (!safe) return labelA; // 不正/未許可 URL=リンクにせずテキスト表示(=安全)
|
|
101
|
+
return '<a href="' + esc(safe) + '" target="' + esc(target) + '"' + rel + clsAttr + extra + '>' + labelA + '</a>';
|
|
55
102
|
};
|
|
56
103
|
}
|
|
57
104
|
// URL の安全化だけ外から使いたい時用(カスタム format 内など)。許可外は null。
|
package/plugins/tss-toggle.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* TssToggle — 2
|
|
1
|
+
/* TssToggle — 2択以上(N択)のセグメント。色は変数で上書き可。横幅ぶんピルが並ぶ。 */
|
|
2
2
|
.tg-toggle { display: inline-flex; border: 1px solid var(--tg-toggle-border, #cfd7e0); border-radius: 999px; overflow: hidden; font-size: .92em; line-height: 1.7; vertical-align: middle; }
|
|
3
3
|
.tg-toggle .tg-seg { padding: 0 11px; cursor: pointer; color: var(--tg-toggle-fg, #64748b); user-select: none; transition: background .12s; }
|
|
4
4
|
.tg-toggle .tg-seg + .tg-seg { border-left: 1px solid var(--tg-toggle-border, #cfd7e0); }
|
package/plugins/tss-toggle.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
/*! TssToggle — 2
|
|
1
|
+
/*! TssToggle — 2択以上(N択)のセグメント・トグル/ラジオ(有効|無効・未回答|出席|欠席 …)。
|
|
2
2
|
*
|
|
3
|
-
*
|
|
3
|
+
* 択一(2つ以上)を「並んだピル(セグメント)」で表示し、クリックで切替。値は dropdown と同じく
|
|
4
4
|
* 選んだ option の value を保存(getData も value)。html:true+format で描画、クリックは委譲で拾う。
|
|
5
|
+
* ラジオボタンの「全選択肢が常に見える・1クリックで確定」をセル内で実現する用途(出欠・優先度 高/中/低 等)。
|
|
5
6
|
*
|
|
6
7
|
* 使い方:
|
|
7
8
|
* plugins: [ TssToggle.plugin({ columns: {
|
|
8
|
-
* status: ['有効', '無効'], //
|
|
9
|
+
* status: ['有効', '無効'], // 文字列(value=label)。2つ以上でOK
|
|
10
|
+
* att: ['未回答', '出席', '欠席'], // 3択以上も可
|
|
9
11
|
* pub: [{value:'1',label:'公開'},{value:'0',label:'非公開'}] // value/label 分離も可
|
|
10
12
|
* } }) ]
|
|
11
13
|
* // 列定義側で直接: { data:'status', html:true, format: TssToggle.format(['有効','無効']) } + plugins:[TssToggle.plugin()]
|
|
12
14
|
*
|
|
13
|
-
* opts(plugin): columns({dataキー: 2
|
|
15
|
+
* opts(plugin): columns({dataキー: 2要素以上の配列}) / accent(選択側の色・CSS変数 --tg-toggle-accent でも)。
|
|
14
16
|
* 挙動: クリックでその側に確定(setValue=検証/Undo を通る)。アクティブセルで Space は反対側へトグル。
|
|
15
17
|
* 他値を打ち込めないよう、対象列に「2値のみ許可」の validator を自動付与。
|
|
16
18
|
*/
|
|
17
19
|
(function (root) {
|
|
18
20
|
'use strict';
|
|
19
21
|
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|
20
|
-
function normOpts(arr) { return (arr || []).
|
|
22
|
+
function normOpts(arr) { return (arr || []).map(function (o) { return (o && typeof o === 'object') ? { value: String(o.value), label: String(o.label) } : { value: String(o), label: String(o) }; }); }
|
|
21
23
|
|
|
22
24
|
function makeFormat(options) {
|
|
23
25
|
var opts = normOpts(options);
|
|
@@ -45,7 +47,7 @@
|
|
|
45
47
|
cfg.html = true;
|
|
46
48
|
if (typeof cfg.format !== 'function') cfg.format = makeFormat(o);
|
|
47
49
|
var vals = o.map(function (x) { return x.value; });
|
|
48
|
-
if (typeof cfg.validator !== 'function') cfg.validator = function (v) { return v === '' || vals.indexOf(v) >= 0; }; //
|
|
50
|
+
if (typeof cfg.validator !== 'function') cfg.validator = function (v) { return v === '' || vals.indexOf(v) >= 0; }; // 許可値以外を弾く
|
|
49
51
|
byCol[ci] = vals;
|
|
50
52
|
});
|
|
51
53
|
// それ自体は無効でも format 直指定の列をクリックで拾えるよう、format 由来の列も拾う
|
|
@@ -68,7 +70,7 @@
|
|
|
68
70
|
if (!vals || !editable(r, c) || grid.mode === 'edit') return;
|
|
69
71
|
e.preventDefault();
|
|
70
72
|
var cur = grid.getValue(r, c), i = vals.indexOf(String(cur));
|
|
71
|
-
grid.setValue(r, c, vals[(i + 1) % vals.length]); //
|
|
73
|
+
grid.setValue(r, c, vals[(i + 1) % vals.length]); // 次の値へ循環(2択なら反対側)
|
|
72
74
|
}
|
|
73
75
|
grid.wrap.addEventListener('click', onClick);
|
|
74
76
|
grid.editor.addEventListener('keydown', onKey);
|
package/src/tssgrid.css
CHANGED
|
@@ -97,6 +97,8 @@
|
|
|
97
97
|
.tssgrid .tg-table td.tg-placeholder { color: var(--tg-placeholder-fg); }
|
|
98
98
|
/* セル内折り返し: グリッド全体(.tg-wordwrap) or 列単位(td.tg-wrap-cell)。行高は中身に合わせて伸びる */
|
|
99
99
|
.tssgrid.tg-wordwrap .tg-table td, .tssgrid .tg-table td.tg-wrap-cell { white-space: normal; word-break: break-word; }
|
|
100
|
+
/* 溢れた文字を … で省略: グリッド全体(.tg-ellipsis) or 列単位(td.tg-ellip-cell)。既定は overflow:hidden で切るだけ=…は出ない。ホバーで全文を title 表示(JS)。折り返し(wordWrap)が同時指定なら折り返しが優先(white-space:normal で text-overflow 無効) */
|
|
101
|
+
.tssgrid.tg-ellipsis .tg-table td, .tssgrid .tg-table td.tg-ellip-cell { text-overflow: ellipsis; }
|
|
100
102
|
/* 読み取り専用セル: 淡色+既定カーソル(編集不可・全変更経路で拒否) */
|
|
101
103
|
.tssgrid .tg-table td.tg-readonly { background: var(--tg-readonly-bg); color: var(--tg-readonly-fg); cursor: default; }
|
|
102
104
|
.tssgrid .tg-table td.tg-readonly.sel { background: #e7edf5; }
|
package/src/tssgrid.d.ts
CHANGED
|
@@ -89,6 +89,8 @@ export interface ColumnDef {
|
|
|
89
89
|
html?: boolean;
|
|
90
90
|
/** 列単位の折り返し。 */
|
|
91
91
|
wordWrap?: boolean;
|
|
92
|
+
/** 列単位の … 省略(溢れた文字を省略記号で切る/ホバーで全文を title 表示)。値・コピー・編集は不変=見え方だけ。折り返し(wordWrap)と同時なら折り返しが優先。 */
|
|
93
|
+
ellipsis?: boolean;
|
|
92
94
|
/** 複数行(改行)入力を有効化。編集は隠し textarea(IME 直打ち対応)、表示は改行保持(pre-wrap)。行高は自動で伸びないので rowHeights 等で確保する。 */
|
|
93
95
|
multiline?: boolean;
|
|
94
96
|
/** 空セルの薄いヒント文字。 */
|
|
@@ -171,6 +173,8 @@ export interface TssGridOptions {
|
|
|
171
173
|
colHeaders?: boolean;
|
|
172
174
|
hiddenColumns?: number[];
|
|
173
175
|
wordWrap?: boolean;
|
|
176
|
+
/** グリッド全体の … 省略(溢れた文字を省略記号で切る/ホバーで全文を title 表示)。列単位は columns[c].ellipsis。見え方だけ=値・コピー・編集は不変。 */
|
|
177
|
+
ellipsis?: boolean;
|
|
174
178
|
/** multiline 列の Enter 挙動。'commit'(既定・Excel風): Enter=確定 / Alt+Enter=改行。'newline'(メモ帳風): Enter=改行 / Ctrl+Enter=確定。 */
|
|
175
179
|
multilineEnter?: 'commit' | 'newline';
|
|
176
180
|
/** multiline セル右上の「複数行」ヒント表示(既定 true)。色は CSS 変数 --tg-ml-mark。false で非表示。 */
|
package/src/tssgrid.js
CHANGED
|
@@ -214,6 +214,8 @@
|
|
|
214
214
|
this.stretchH = opts.stretchH || 'none';
|
|
215
215
|
// セル内折り返し(既定 nowrap)。grid 全体 or columns[c].wordWrap。
|
|
216
216
|
this.wordWrap = !!opts.wordWrap;
|
|
217
|
+
// 溢れた文字を … で省略(既定は切るだけ)。grid 全体 or columns[c].ellipsis。ホバーで全文を title 表示(見え方だけ・値/コピー/編集は不変)。
|
|
218
|
+
this.ellipsis = !!opts.ellipsis;
|
|
217
219
|
// 複数行(改行)編集の Enter 挙動: 'commit'(既定・Excel風: Enter=確定 / Alt+Enter=改行) | 'newline'(メモ帳風: Enter=改行 / Ctrl+Enter=確定)。列は columns[c].multiline:true で有効化。
|
|
218
220
|
this.multilineEnter = opts.multilineEnter === 'newline' ? 'newline' : 'commit';
|
|
219
221
|
// multiline セル右上の「複数行」ヒント(既定 ON)。色は CSS 変数 --tg-ml-mark で。false で消す。
|
|
@@ -858,6 +860,7 @@
|
|
|
858
860
|
this.root = q('.tssgrid');
|
|
859
861
|
if (this.className) this.root.className += ' ' + this.className; // 任意クラス(テーマ付け)
|
|
860
862
|
if (this.wordWrap) this.root.classList.add('tg-wordwrap'); // グリッド全体の折り返し
|
|
863
|
+
if (this.ellipsis) this.root.classList.add('tg-ellipsis'); // グリッド全体の … 省略
|
|
861
864
|
if (!this.multilineMark) this.root.classList.add('tg-no-ml-mark'); // multiline の右上ヒントを消す
|
|
862
865
|
if (!this.cursor) this.root.classList.add('tg-no-cursor'); // 表示専用(選択枠なし+セルカーソル既定)
|
|
863
866
|
if (this.width) {
|
|
@@ -1195,6 +1198,8 @@
|
|
|
1195
1198
|
_editText(r, c) {
|
|
1196
1199
|
if (this._nativeInputType(c)) return this.data[r][c];
|
|
1197
1200
|
if (this.colType(c) === 'number') return this.data[r][c];
|
|
1201
|
+
// html:true は format の出力が markup(<a>/<span> 等)=編集対象は生の値。整形後を入れるとタグを編集して崩れる。
|
|
1202
|
+
if (this.colCfg(c).html) return this.data[r][c];
|
|
1198
1203
|
return this._displayValue(r, c);
|
|
1199
1204
|
}
|
|
1200
1205
|
// td の中身を型に応じて描画。data は確定済み前提。空セルは placeholder(あれば)を表示。
|
|
@@ -1485,6 +1490,7 @@
|
|
|
1485
1490
|
if (this.colType(c) === 'number') cls.push('tg-num'); // 数値は右寄せ
|
|
1486
1491
|
if (this._isReadOnly(r, c)) cls.push('tg-readonly'); // 読み取り専用(淡色+カーソル既定)
|
|
1487
1492
|
if (this.colCfg(c).wordWrap) cls.push('tg-wrap-cell'); // 列単位の折り返し
|
|
1493
|
+
if (this.colCfg(c).ellipsis) cls.push('tg-ellip-cell'); // 列単位の … 省略
|
|
1488
1494
|
if (this.colCfg(c).multiline) cls.push('tg-multiline-cell'); // 複数行(改行保持=pre-wrap)
|
|
1489
1495
|
if (this.data[r][c] === '' && this._placeholder(c) != null) cls.push('tg-placeholder'); // 空セルのヒント
|
|
1490
1496
|
const al = this._cellAlign(r, c); // 整列(列既定+セル上書き)
|
|
@@ -3164,6 +3170,15 @@
|
|
|
3164
3170
|
this.setActive(+td.dataset.r, +td.dataset.c); this._beginEdit(this.active.r, this.active.c);
|
|
3165
3171
|
});
|
|
3166
3172
|
this.fillhandle.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); this.filling = { src: this.rectRange() }; this._fillRow = null; });
|
|
3173
|
+
// 溢れて … で切れたセルは、ホバー時に全文を title(ネイティブツールチップ)で出す。
|
|
3174
|
+
// 対象セルだけを初回ホバーで1回測るso軽い(redraw で td が作り直され _ellChk も消える)。ellipsis 未使用なら即 return=ゼロコスト。
|
|
3175
|
+
this.table.addEventListener('mouseover', (e) => {
|
|
3176
|
+
const td = e.target.closest && e.target.closest('td');
|
|
3177
|
+
if (!td || td._ellChk) return;
|
|
3178
|
+
if (!this.ellipsis && !td.classList.contains('tg-ellip-cell')) return;
|
|
3179
|
+
td._ellChk = 1;
|
|
3180
|
+
if (td.scrollWidth > td.clientWidth + 1 && !td.hasAttribute('title')) td.title = td.textContent;
|
|
3181
|
+
});
|
|
3167
3182
|
// 右クリックメニュー(行/列の挿入・削除)
|
|
3168
3183
|
this.table.addEventListener('contextmenu', (e) => this._onContextMenu(e));
|
|
3169
3184
|
this.menuEl.addEventListener('mousedown', (e) => {
|