@threestarsoftware/tssgrid 0.1.29 → 0.1.30-beta.0
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 +5 -0
- package/README.md +25 -1
- package/package.json +5 -1
- package/plugins/tss-comment.css +47 -0
- package/plugins/tss-comment.d.ts +90 -0
- package/plugins/tss-comment.js +253 -0
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-comment`(セルの注釈・Excel のコメント/メモ風)を追加** — コメントのあるセルは**右上に赤い三角**、**ホバーで吹き出し**、**右クリック or Shift+F2 で挿入・編集・削除**(編集中 Ctrl+Enter 保存/Esc 取消)。**他プラグインと干渉しない設計**=セルのフック(`cellClass`/`cellStyle`/`editor`/`format`)を一切奪わず、コメント済みの `<td>` に自前でクラスを足すだけ(再描画追従は `MutationObserver`)。条件付き書式・heatmap・cascade/calendar 等と**同じセルで共存**できる。キーは `rowId(row)`(既定は行index/`row.detailId` 等を返せば**ソート/フィルタ/再読込に追従**)+列 `data` キーで持ち、r/c ではズレない。永続化は持たず `onCommentChange({id,dataKey,text,action})` で通知するだけ(保存はホストへ委譲)。`contextMenu` に `cmt.menuItems()` を混ぜる/`readOnly` で閲覧専用。例 `examples/comment.html`。
|
|
10
|
+
|
|
6
11
|
## [0.1.29] - 2026-10-25
|
|
7
12
|
|
|
8
13
|
### 追加(プラグイン)
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ import '@threestarsoftware/tssgrid/tssgrid.css';
|
|
|
59
59
|
**プラグインの型(TypeScript)**: エディタ系プラグインは **`.d.ts` 同梱で `import` するだけで型が効きます**(`.js` 付きで import):
|
|
60
60
|
```ts
|
|
61
61
|
import TssCombo from '@threestarsoftware/tssgrid/plugins/tss-combo.js'; // ← 型が効く
|
|
62
|
-
// 対応: tss-combo / tss-autocomplete / tss-cascade / tss-calendar / tss-datetime / tss-time
|
|
62
|
+
// 対応: tss-combo / tss-autocomplete / tss-cascade / tss-calendar / tss-datetime / tss-time / tss-comment
|
|
63
63
|
columns: [{ data: 'pay', editor: TssCombo({ options: [{ code: '01', label: '現金' }], other: { code: '99', label: 'その他' }, codeField: 'payCode' }) }];
|
|
64
64
|
```
|
|
65
65
|
それ以外のプラグイン(`usePlugin` 系・`format` ヘルパ系)は型定義がまだ無いので、必要なら5行のアンビエント宣言で補えます(`editor.open/close` を持つ戻り値は `EditorDef`):
|
|
@@ -545,6 +545,30 @@ new TssGrid(el, { columns: [{ type: 'time', editor: t }] });
|
|
|
545
545
|
- `opts`: `step`(分刻み, 既定1) / `hour12`(AM/PM 表示, 既定false) / `icon`(既定 `'🕐'`) / `openOnClick`(既定true) / `className`。
|
|
546
546
|
- 🕐 を**シングルクリックで開閉**、**現在**/**決定**ボタン、キーボード(Enter 決定 / Esc 取消)、外側クリックで採用、フィルで下へコピー。見た目は `--time-*` 変数と `tss-time-*` クラスで自由に。
|
|
547
547
|
|
|
548
|
+
### 同梱プラグイン: セルのコメント(tss-comment・Excel のメモ風)
|
|
549
|
+
|
|
550
|
+
`plugins/tss-comment.js`= セルに注釈を付ける。コメントのあるセルは**右上に赤い三角**、**ホバーで吹き出し**、**右クリック or Shift+F2 で挿入・編集・削除**。動く例: [`examples/comment.html`](https://tssgrid.threestarsoftware.co.jp/examples/comment.html)。
|
|
551
|
+
|
|
552
|
+
**★他プラグインと干渉しません**:セルのフック(`cellClass`/`cellStyle`/`editor`/`format`)を一切奪わず、コメント済みの `<td>` に自前でクラスを足すだけ(再描画追従は `MutationObserver`)。条件付き書式・heatmap・cascade/calendar と**同じセルで共存**できます。
|
|
553
|
+
|
|
554
|
+
```html
|
|
555
|
+
<link rel="stylesheet" href="plugins/tss-comment.css">
|
|
556
|
+
<script src="plugins/tss-comment.js"></script>
|
|
557
|
+
```
|
|
558
|
+
```js
|
|
559
|
+
const menu = []; // contextMenu は参照渡し(下で項目を足す)
|
|
560
|
+
const grid = new TssGrid(el, { data, columns, contextMenu: menu });
|
|
561
|
+
const cmt = TssComment(grid, {
|
|
562
|
+
rowId: (row) => row.id, // 安定キー(ソート/フィルタ/再読込に追従)。既定は行index
|
|
563
|
+
comments: [{ id: 101, dataKey: 'score', text: '要確認' }], // 初期(保存済み)
|
|
564
|
+
onCommentChange: ({ id, dataKey, text, action }) => save(id, dataKey, text, action), // 永続化はホストへ
|
|
565
|
+
});
|
|
566
|
+
menu.push(...cmt.menuItems()); // 右クリックに「挿入/編集・削除」を追加
|
|
567
|
+
```
|
|
568
|
+
- キーは **`rowId(row)` + 列 `data` キー**で持つ(r/c では持たない=ソート/フィルタ/行削除でズレない)。永続化は持たず `onCommentChange` で通知するだけ(保存はホストが担当)。
|
|
569
|
+
- `opts`: `rowId` / `comments` / `onCommentChange` / `canComment(r,c)` / `readOnly`(閲覧専用) / `labels` / `className`。API: `set/get/remove/open/getAll/menuItems/refresh/destroy`。
|
|
570
|
+
- グリッドを作り直すホストは、作り直すたびに `TssComment(grid, { comments: cmt.getAll(), … })` で貼り直す。見た目は `--cmt-*` 変数と `tg-comment-*` クラスで自由に。
|
|
571
|
+
|
|
548
572
|
### 同梱プラグイン: 派生列(formula / computed column)
|
|
549
573
|
|
|
550
574
|
`plugins/tss-formula.js`=「その行の値だけから計算する列」を `columns[c].formula` 関数で書けるようにする。金額=数量×単価、税込=金額×1.1 のような**派生列**を宣言的に。動く例: [`examples/formula.html`](https://tssgrid.threestarsoftware.co.jp/examples/formula.html)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threestarsoftware/tssgrid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30-beta.0",
|
|
4
4
|
"description": "軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ThreeStarSoftware (https://www.threestarsoftware.co.jp/)",
|
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
"types": "./plugins/tss-hyperlink.d.ts",
|
|
64
64
|
"default": "./plugins/tss-hyperlink.js"
|
|
65
65
|
},
|
|
66
|
+
"./plugins/tss-comment.js": {
|
|
67
|
+
"types": "./plugins/tss-comment.d.ts",
|
|
68
|
+
"default": "./plugins/tss-comment.js"
|
|
69
|
+
},
|
|
66
70
|
"./plugins/*": "./plugins/*",
|
|
67
71
|
"./package.json": "./package.json"
|
|
68
72
|
},
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* TssComment — セル注釈(マーカー+吹き出し+編集)。業務側は下の CSS 変数で色/角を調整。
|
|
2
|
+
例: .tssgrid { --cmt-mark:#e8590c; } / 吹き出し: .tg-comment-bubble { --cmt-bubble-bg:#fff; } */
|
|
3
|
+
|
|
4
|
+
/* ▼ マーカー(セル右上の小さな三角=コメント有り)。セルフックを使わず td に付けるだけ=他の装飾と共存。 */
|
|
5
|
+
.tssgrid .tg-table td.tg-comment-mark { position: relative; } /* ::after の土台(コメント済みセルだけに付与) */
|
|
6
|
+
.tssgrid .tg-table td.tg-comment-mark::after {
|
|
7
|
+
content: ''; position: absolute; top: 0; right: 0; width: 0; height: 0;
|
|
8
|
+
border-style: solid; border-width: 0 6px 6px 0;
|
|
9
|
+
border-color: transparent var(--cmt-mark, #e03131) transparent transparent;
|
|
10
|
+
pointer-events: none; /* クリックはセルに通す(編集/選択の邪魔をしない) */
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* ▼ ホバーの吹き出し(読み取り表示)。Excel のメモ風=淡色の付箋。 */
|
|
14
|
+
.tg-comment-bubble {
|
|
15
|
+
--cmt-bubble-bg: #fffbe6; --cmt-bubble-border: #e0c200; --cmt-bubble-fg: #333;
|
|
16
|
+
position: fixed; z-index: 10001; max-width: 280px;
|
|
17
|
+
background: var(--cmt-bubble-bg); color: var(--cmt-bubble-fg);
|
|
18
|
+
border: 1px solid var(--cmt-bubble-border); border-radius: 6px;
|
|
19
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, .18);
|
|
20
|
+
padding: 6px 9px; font: 12px/1.5 "Segoe UI", "Meiryo", sans-serif;
|
|
21
|
+
white-space: pre-wrap; word-break: break-word; pointer-events: none; /* 表示専用=マウスを吸わない */
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ▼ 編集ポップアップ(挿入/編集)。 */
|
|
25
|
+
.tg-comment-editor {
|
|
26
|
+
--cmt-bg: #fff; --cmt-border: #c3ccd6; --cmt-accent: #2aa0d8;
|
|
27
|
+
position: fixed; z-index: 10002;
|
|
28
|
+
background: var(--cmt-bg); border: 1px solid var(--cmt-border); border-radius: 8px;
|
|
29
|
+
box-shadow: 0 6px 24px rgba(0, 0, 0, .2); padding: 8px;
|
|
30
|
+
font: 13px "Segoe UI", "Meiryo", sans-serif;
|
|
31
|
+
}
|
|
32
|
+
.tg-comment-editor .tg-comment-ta {
|
|
33
|
+
display: block; width: 240px; min-height: 64px; resize: vertical; box-sizing: border-box;
|
|
34
|
+
font: inherit; color: #222; padding: 6px 8px;
|
|
35
|
+
border: 1px solid var(--cmt-border); border-radius: 6px; outline: none;
|
|
36
|
+
}
|
|
37
|
+
.tg-comment-editor .tg-comment-ta:focus { border-color: var(--cmt-accent); box-shadow: 0 0 0 2px rgba(42, 160, 216, .25); }
|
|
38
|
+
.tg-comment-btns { display: flex; justify-content: flex-end; gap: 6px; margin-top: 7px; }
|
|
39
|
+
.tg-comment-btn {
|
|
40
|
+
font: inherit; cursor: pointer; padding: 4px 12px;
|
|
41
|
+
border: 1px solid var(--cmt-border); border-radius: 6px; background: #f5f7fa; color: #222;
|
|
42
|
+
}
|
|
43
|
+
.tg-comment-btn:hover { background: #eef4ff; }
|
|
44
|
+
.tg-comment-btn[data-act="save"] { background: var(--cmt-accent); border-color: var(--cmt-accent); color: #fff; }
|
|
45
|
+
.tg-comment-btn[data-act="save"]:hover { filter: brightness(1.05); }
|
|
46
|
+
.tg-comment-btn.danger { color: #c0392b; }
|
|
47
|
+
.tg-comment-btn.danger:hover { background: #fdecea; }
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// TssComment — セルの注釈(Excel のコメント/メモ風)。マーカー+ホバー吹き出し+編集。
|
|
2
|
+
// ★セルのフック(cellClass/cellStyle/editor/format)を奪わない=他プラグイン・条件付き書式と共存。
|
|
3
|
+
// 永続化は持たず onCommentChange で通知するだけ(保存/読込はホストが持つ)。
|
|
4
|
+
import { TssGrid } from '../src/tssgrid';
|
|
5
|
+
|
|
6
|
+
export = TssComment;
|
|
7
|
+
export as namespace TssComment;
|
|
8
|
+
|
|
9
|
+
declare function TssComment(grid: TssGrid, opts?: TssComment.Options): TssComment.Controller;
|
|
10
|
+
|
|
11
|
+
declare namespace TssComment {
|
|
12
|
+
/** 保存済み/初期コメント1件(永続化の単位)。 */
|
|
13
|
+
interface CommentRecord {
|
|
14
|
+
/** 行の安定ID(`rowId` が返す値)。 */
|
|
15
|
+
id: string | number;
|
|
16
|
+
/** 列の `data` キー。 */
|
|
17
|
+
dataKey: string;
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** onCommentChange に渡る変更情報。 */
|
|
22
|
+
interface ChangeInfo {
|
|
23
|
+
id: string;
|
|
24
|
+
dataKey: string;
|
|
25
|
+
/** 変更時点の表示上の行・列インデックス(参考)。 */
|
|
26
|
+
r: number;
|
|
27
|
+
c: number;
|
|
28
|
+
/** 削除時は ''。 */
|
|
29
|
+
text: string;
|
|
30
|
+
action: 'set' | 'delete';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Labels {
|
|
34
|
+
add?: string;
|
|
35
|
+
edit?: string;
|
|
36
|
+
remove?: string;
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
save?: string;
|
|
39
|
+
del?: string;
|
|
40
|
+
cancel?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface Options {
|
|
44
|
+
/** 初期(保存済み)コメント。 */
|
|
45
|
+
comments?: CommentRecord[];
|
|
46
|
+
/**
|
|
47
|
+
* 行の安定ID。ソート/フィルタ/行削除/再読込に追従させるため、行を一意に指す値を返す
|
|
48
|
+
* (例: `(row) => row.detailId`)。省略時は行インデックス(=並べ替えると位置基準になる)。
|
|
49
|
+
* `row` は `grid.getRows()[r]`(オブジェクト)。
|
|
50
|
+
*/
|
|
51
|
+
rowId?: (row: Record<string, unknown>, r: number) => string | number;
|
|
52
|
+
/** コメントが変わるたびに呼ぶ(永続化はホストへ委譲)。 */
|
|
53
|
+
onCommentChange?: (info: ChangeInfo) => void;
|
|
54
|
+
/** そのセルにコメントして良いか(既定 true)。保存済み行のロック等に。 */
|
|
55
|
+
canComment?: (r: number, c: number) => boolean;
|
|
56
|
+
/** 閲覧専用(マーカー+ホバー表示のみ・編集/削除・メニュー・Shift+F2 を出さない)。 */
|
|
57
|
+
readOnly?: boolean;
|
|
58
|
+
/** ボタン/メニューの文言。 */
|
|
59
|
+
labels?: Labels;
|
|
60
|
+
/** 吹き出し/編集ポップアップに付ける任意クラス。 */
|
|
61
|
+
className?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** contextMenu に混ぜる項目(挿入/編集・削除)。 */
|
|
65
|
+
interface MenuItem {
|
|
66
|
+
name: string | ((arg: unknown) => string);
|
|
67
|
+
key: string;
|
|
68
|
+
disabled?: (arg: unknown) => boolean;
|
|
69
|
+
callback?: (arg: unknown) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface Controller {
|
|
73
|
+
/** セルにコメントを設定(空文字なら削除扱い)。 */
|
|
74
|
+
set(r: number, c: number, text: string): void;
|
|
75
|
+
/** セルのコメント文字列(無ければ null)。 */
|
|
76
|
+
get(r: number, c: number): string | null;
|
|
77
|
+
/** セルのコメントを削除。 */
|
|
78
|
+
remove(r: number, c: number): void;
|
|
79
|
+
/** 編集ポップアップを開く。 */
|
|
80
|
+
open(r: number, c: number): void;
|
|
81
|
+
/** 保存済み一覧(ホストの永続化・グリッド作り直し時の貼り直し用)。 */
|
|
82
|
+
getAll(): CommentRecord[];
|
|
83
|
+
/** グリッドの `contextMenu` に混ぜる項目(挿入/編集・削除)。readOnly では空配列。 */
|
|
84
|
+
menuItems(): MenuItem[];
|
|
85
|
+
/** マーカーを貼り直す。 */
|
|
86
|
+
refresh(): void;
|
|
87
|
+
/** observer / リスナ / ポップアップを解除。 */
|
|
88
|
+
destroy(): void;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/*! TssComment — セルの注釈(Excel のコメント/メモ風)。マーカー+ホバー吹き出し+編集。
|
|
2
|
+
*
|
|
3
|
+
* ★他プラグインと干渉しない設計:セルのフック(cellClass / cellStyle / editor / format)を
|
|
4
|
+
* 一切奪わない。コメント済みセルの <td> に自前でクラス(tg-comment-mark)を足し直すだけ=
|
|
5
|
+
* 条件付き書式・heatmap(cellStyle)・cascade/calendar(editor) 等と同じセルに共存できる。
|
|
6
|
+
* 再描画への追従は MutationObserver(グリッドの <table> を監視)で行う(クラス付与は属性変更=
|
|
7
|
+
* childList 監視をループさせない)。吹き出し/編集ポップアップは body 直下にアンカー(calendar と同型)。
|
|
8
|
+
*
|
|
9
|
+
* 使い方:
|
|
10
|
+
* const cmt = TssComment(grid, {
|
|
11
|
+
* rowId: (row, r) => row.detailId ?? r, // 安定キー(ソート/フィルタ/再読込に追従)。既定は行index
|
|
12
|
+
* comments: [{ id: 12, dataKey: 'amount', text: '要確認' }], // 初期(保存済み)
|
|
13
|
+
* onCommentChange: ({ id, dataKey, text, action }) => save(...), // 永続化はホストへ委譲
|
|
14
|
+
* });
|
|
15
|
+
* // 右クリックに項目を出す(ホストの contextMenu に混ぜる):
|
|
16
|
+
* new TssGrid(el, { contextMenu: [ ...cmt.menuItems(), ...others ] })
|
|
17
|
+
* // ※グリッドを作り直すホストは、作り直すたびに TssComment(grid, {comments: cmt.getAll(), ...}) で貼り直す。
|
|
18
|
+
*
|
|
19
|
+
* キー = rowId(行) + 列の data キー(r/c では持たない=ソート/フィルタ/行削除でズレないため)。
|
|
20
|
+
* 永続化はしない(onCommentChange で通知するだけ)。保存/読込はホスト(kintone 等)が持つ。
|
|
21
|
+
*/
|
|
22
|
+
(function (root) {
|
|
23
|
+
'use strict';
|
|
24
|
+
var SEP = '\u0000';
|
|
25
|
+
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|
26
|
+
|
|
27
|
+
// 要素をアンカー(セル)に合わせて配置(画面外にはみ出さない)。calendar と同じ考え方。
|
|
28
|
+
function place(el, anchor) {
|
|
29
|
+
var r = anchor.getBoundingClientRect();
|
|
30
|
+
el.style.left = Math.max(2, Math.min(r.left, window.innerWidth - el.offsetWidth - 6)) + 'px';
|
|
31
|
+
var top = r.bottom + 2;
|
|
32
|
+
if (top + el.offsetHeight > window.innerHeight) top = Math.max(2, r.top - el.offsetHeight - 2);
|
|
33
|
+
el.style.top = top + 'px';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function TssComment(grid, opts) {
|
|
37
|
+
opts = opts || {};
|
|
38
|
+
if (!grid || !grid.table) throw new Error('TssComment: grid が不正です');
|
|
39
|
+
var customRowId = typeof opts.rowId === 'function' ? opts.rowId : null;
|
|
40
|
+
var onChange = typeof opts.onCommentChange === 'function' ? opts.onCommentChange : null;
|
|
41
|
+
var canComment = typeof opts.canComment === 'function' ? opts.canComment : function () { return true; };
|
|
42
|
+
var readOnly = !!opts.readOnly;
|
|
43
|
+
var L = Object.assign({
|
|
44
|
+
add: 'コメントを挿入', edit: 'コメントを編集', remove: 'コメントを削除',
|
|
45
|
+
placeholder: 'コメントを入力…', save: '保存', del: '削除', cancel: '取消',
|
|
46
|
+
}, opts.labels || {});
|
|
47
|
+
|
|
48
|
+
var host = grid.root || (grid.table.closest && grid.table.closest('.tssgrid')) || grid.table;
|
|
49
|
+
var store = new Map(); // key(id\0dataKey) -> text
|
|
50
|
+
var bubble = null, editor = null, edCell = null, outside = null, hideT = 0, rafId = 0, dead = false;
|
|
51
|
+
|
|
52
|
+
// 初期コメント(保存済み)を取り込む
|
|
53
|
+
(opts.comments || []).forEach(function (cm) {
|
|
54
|
+
if (cm && cm.id != null && cm.dataKey != null && cm.text != null) store.set(cm.id + SEP + cm.dataKey, String(cm.text));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ── キー計算 ───────────────────────────────────────────
|
|
58
|
+
function dataKeyOf(c) { var col = (grid.columns && grid.columns[c]) || {}; return col.data != null ? String(col.data) : ('#' + c); }
|
|
59
|
+
function colOf(dataKey) {
|
|
60
|
+
var cols = grid.columns || [];
|
|
61
|
+
for (var c = 0; c < cols.length; c++) if (dataKeyOf(c) === dataKey) return c;
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
// 行 r の安定ID。customRowId が無ければ行index。rows はキャッシュ(1操作内で使い回す)。
|
|
65
|
+
var _rowsCache = null;
|
|
66
|
+
function rowsNow() { if (!_rowsCache) _rowsCache = grid.getRows(); return _rowsCache; }
|
|
67
|
+
function clearRowsCache() { _rowsCache = null; }
|
|
68
|
+
function idAt(r) { if (!customRowId) return String(r); var row = rowsNow()[r]; return String(customRowId(row, r)); }
|
|
69
|
+
function keyAt(r, c) { return idAt(r) + SEP + dataKeyOf(c); }
|
|
70
|
+
|
|
71
|
+
// ── マーカー再描画(セルフックを使わず td にクラスを足すだけ)───────────
|
|
72
|
+
function decorate() {
|
|
73
|
+
rafId = 0;
|
|
74
|
+
if (dead || !grid.table) return;
|
|
75
|
+
var marked = grid.table.querySelectorAll('td.tg-comment-mark');
|
|
76
|
+
for (var i = 0; i < marked.length; i++) marked[i].classList.remove('tg-comment-mark');
|
|
77
|
+
if (store.size === 0) return;
|
|
78
|
+
// id -> 現在の表示行index(読み込み済みの行だけ)
|
|
79
|
+
clearRowsCache();
|
|
80
|
+
var rows = customRowId ? rowsNow() : null;
|
|
81
|
+
var idToRow = new Map();
|
|
82
|
+
var n = customRowId ? rows.length : grid.getData().length;
|
|
83
|
+
for (var r = 0; r < n; r++) idToRow.set(customRowId ? String(customRowId(rows[r], r)) : String(r), r);
|
|
84
|
+
store.forEach(function (text, key) {
|
|
85
|
+
var sp = key.indexOf(SEP), id = key.slice(0, sp), dk = key.slice(sp + 1);
|
|
86
|
+
var rr = idToRow.get(id); if (rr == null) return;
|
|
87
|
+
var cc = colOf(dk); if (cc < 0) return;
|
|
88
|
+
var td = grid.cellEl(rr, cc); if (td) td.classList.add('tg-comment-mark');
|
|
89
|
+
});
|
|
90
|
+
clearRowsCache();
|
|
91
|
+
}
|
|
92
|
+
function scheduleDecorate() { if (!rafId) rafId = (window.requestAnimationFrame || window.setTimeout)(decorate, 0); }
|
|
93
|
+
|
|
94
|
+
// 再描画に追従:table の子が作り直されたら貼り直す(クラス付与は属性変更=childList 監視はループしない)
|
|
95
|
+
var mo = new MutationObserver(scheduleDecorate);
|
|
96
|
+
mo.observe(grid.table, { childList: true, subtree: true });
|
|
97
|
+
|
|
98
|
+
// ── ホバーの吹き出し(読み取り専用の表示)───────────────
|
|
99
|
+
function showBubble(td, text) {
|
|
100
|
+
hideBubble();
|
|
101
|
+
bubble = document.createElement('div');
|
|
102
|
+
bubble.className = 'tg-comment-bubble' + (opts.className ? ' ' + opts.className : '');
|
|
103
|
+
bubble.textContent = text;
|
|
104
|
+
document.body.appendChild(bubble);
|
|
105
|
+
place(bubble, td);
|
|
106
|
+
}
|
|
107
|
+
function hideBubble() { if (bubble) { bubble.remove(); bubble = null; } }
|
|
108
|
+
function onOver(e) {
|
|
109
|
+
var td = e.target.closest && e.target.closest('td.tg-comment-mark');
|
|
110
|
+
if (!td || !grid.table.contains(td)) return;
|
|
111
|
+
if (hideT) { clearTimeout(hideT); hideT = 0; }
|
|
112
|
+
var r = +td.getAttribute('data-r'), c = +td.getAttribute('data-c');
|
|
113
|
+
var text = getText(r, c);
|
|
114
|
+
if (text != null) showBubble(td, text);
|
|
115
|
+
}
|
|
116
|
+
function onOut(e) {
|
|
117
|
+
var td = e.target.closest && e.target.closest('td.tg-comment-mark');
|
|
118
|
+
if (!td) return;
|
|
119
|
+
if (e.relatedTarget && td.contains(e.relatedTarget)) return; // セル内の移動は無視
|
|
120
|
+
hideT = setTimeout(hideBubble, 60);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── 編集ポップアップ ───────────────────────────────────
|
|
124
|
+
function openEditor(r, c) {
|
|
125
|
+
if (readOnly || dead) return;
|
|
126
|
+
if (!canComment(r, c)) return;
|
|
127
|
+
closeEditor(); hideBubble();
|
|
128
|
+
var td = grid.cellEl(r, c); if (!td) return;
|
|
129
|
+
edCell = { r: r, c: c };
|
|
130
|
+
editor = document.createElement('div');
|
|
131
|
+
editor.className = 'tg-comment-editor' + (opts.className ? ' ' + opts.className : '');
|
|
132
|
+
var cur = getText(r, c) || '';
|
|
133
|
+
editor.innerHTML =
|
|
134
|
+
'<textarea class="tg-comment-ta" placeholder="' + esc(L.placeholder) + '"></textarea>' +
|
|
135
|
+
'<div class="tg-comment-btns">' +
|
|
136
|
+
'<button type="button" class="tg-comment-btn" data-act="save">' + esc(L.save) + '</button>' +
|
|
137
|
+
(cur ? '<button type="button" class="tg-comment-btn danger" data-act="del">' + esc(L.del) + '</button>' : '') +
|
|
138
|
+
'<button type="button" class="tg-comment-btn" data-act="cancel">' + esc(L.cancel) + '</button>' +
|
|
139
|
+
'</div>';
|
|
140
|
+
document.body.appendChild(editor);
|
|
141
|
+
var ta = editor.querySelector('.tg-comment-ta');
|
|
142
|
+
ta.value = cur;
|
|
143
|
+
place(editor, td);
|
|
144
|
+
ta.focus(); ta.setSelectionRange(cur.length, cur.length);
|
|
145
|
+
editor.addEventListener('click', function (e) {
|
|
146
|
+
var act = e.target.getAttribute && e.target.getAttribute('data-act');
|
|
147
|
+
if (act === 'save') commitEditor();
|
|
148
|
+
else if (act === 'del') { removeAt(r, c); closeEditor(); }
|
|
149
|
+
else if (act === 'cancel') closeEditor();
|
|
150
|
+
});
|
|
151
|
+
ta.addEventListener('keydown', function (e) {
|
|
152
|
+
if (e.key === 'Escape') { e.preventDefault(); closeEditor(); }
|
|
153
|
+
else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); commitEditor(); }
|
|
154
|
+
});
|
|
155
|
+
outside = function (e) { if (editor && !editor.contains(e.target)) closeEditor(); };
|
|
156
|
+
setTimeout(function () { document.addEventListener('mousedown', outside, true); }, 0);
|
|
157
|
+
}
|
|
158
|
+
function commitEditor() {
|
|
159
|
+
if (!editor || !edCell) return;
|
|
160
|
+
var ta = editor.querySelector('.tg-comment-ta'), text = ta ? ta.value.trim() : '';
|
|
161
|
+
var cell = edCell;
|
|
162
|
+
closeEditor();
|
|
163
|
+
if (text) setText(cell.r, cell.c, text); else removeAt(cell.r, cell.c);
|
|
164
|
+
}
|
|
165
|
+
function closeEditor() {
|
|
166
|
+
if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; }
|
|
167
|
+
if (editor) { editor.remove(); editor = null; }
|
|
168
|
+
edCell = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── データ操作(+onCommentChange 通知・マーカー更新)───────
|
|
172
|
+
function getText(r, c) { var v = store.get(keyAt(r, c)); return v == null ? null : v; }
|
|
173
|
+
function fire(r, c, text, action) {
|
|
174
|
+
if (!onChange) return;
|
|
175
|
+
try { onChange({ id: idAt(r), dataKey: dataKeyOf(c), r: r, c: c, text: text, action: action }); } catch (e) { /* noop */ }
|
|
176
|
+
}
|
|
177
|
+
function setText(r, c, text) {
|
|
178
|
+
text = String(text == null ? '' : text);
|
|
179
|
+
if (!text) { removeAt(r, c); return; }
|
|
180
|
+
store.set(keyAt(r, c), text); clearRowsCache();
|
|
181
|
+
fire(r, c, text, 'set'); decorate();
|
|
182
|
+
}
|
|
183
|
+
function removeAt(r, c) {
|
|
184
|
+
var k = keyAt(r, c);
|
|
185
|
+
if (!store.has(k)) { clearRowsCache(); return; }
|
|
186
|
+
store.delete(k); clearRowsCache();
|
|
187
|
+
fire(r, c, '', 'delete'); decorate();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── キーボード(Shift+F2=Excel 流でアクティブセルに挿入/編集)────
|
|
191
|
+
function onKey(e) {
|
|
192
|
+
if (readOnly) return;
|
|
193
|
+
if (e.key === 'F2' && e.shiftKey) {
|
|
194
|
+
var a = grid.active; if (!a) return;
|
|
195
|
+
e.preventDefault(); e.stopPropagation(); openEditor(a.r, a.c);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
grid.table.addEventListener('mouseover', onOver);
|
|
200
|
+
grid.table.addEventListener('mouseout', onOut);
|
|
201
|
+
host.addEventListener('keydown', onKey, true);
|
|
202
|
+
scheduleDecorate();
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
/** セルにコメントを設定(空なら削除扱い)。 */
|
|
206
|
+
set: function (r, c, text) { setText(r, c, text); },
|
|
207
|
+
/** セルのコメント文字列(無ければ null)。 */
|
|
208
|
+
get: function (r, c) { var v = getText(r, c); clearRowsCache(); return v; },
|
|
209
|
+
/** セルのコメントを削除。 */
|
|
210
|
+
remove: function (r, c) { removeAt(r, c); },
|
|
211
|
+
/** 編集ポップアップを開く。 */
|
|
212
|
+
open: function (r, c) { openEditor(r, c); },
|
|
213
|
+
/** 保存済み一覧 [{id, dataKey, text}](ホストの永続化・貼り直し用)。 */
|
|
214
|
+
getAll: function () {
|
|
215
|
+
var out = []; store.forEach(function (text, key) { var sp = key.indexOf(SEP); out.push({ id: key.slice(0, sp), dataKey: key.slice(sp + 1), text: text }); }); return out;
|
|
216
|
+
},
|
|
217
|
+
/** グリッドの contextMenu に混ぜる項目(挿入/編集・削除)。セル右クリック時のみ表示。
|
|
218
|
+
* 本体契約: name/disabled/hidden は (s, ctx)、callback は { range, key, ctx }。ctx={on,r,c}。 */
|
|
219
|
+
menuItems: function () {
|
|
220
|
+
if (readOnly) return [];
|
|
221
|
+
var cellFromCtx = function (ctx) {
|
|
222
|
+
if (ctx && ctx.on === 'cell' && ctx.r != null) return { r: ctx.r, c: ctx.c };
|
|
223
|
+
var a = grid.active; return a ? { r: a.r, c: a.c } : null;
|
|
224
|
+
};
|
|
225
|
+
var onlyCell = function (s, ctx) { return !(ctx && ctx.on === 'cell'); }; // hidden: セル以外(列/行/隅)では出さない
|
|
226
|
+
return [
|
|
227
|
+
{ name: function (s, ctx) { var cc = cellFromCtx(ctx); return cc && getText(cc.r, cc.c) != null ? L.edit : L.add; },
|
|
228
|
+
key: 'tss-comment-edit', hidden: onlyCell,
|
|
229
|
+
disabled: function (s, ctx) { var cc = cellFromCtx(ctx); return !cc || !canComment(cc.r, cc.c); },
|
|
230
|
+
callback: function (arg) { var cc = cellFromCtx(arg && arg.ctx); if (cc) openEditor(cc.r, cc.c); } },
|
|
231
|
+
{ name: L.remove, key: 'tss-comment-remove', hidden: onlyCell,
|
|
232
|
+
disabled: function (s, ctx) { var cc = cellFromCtx(ctx); return !cc || getText(cc.r, cc.c) == null; },
|
|
233
|
+
callback: function (arg) { var cc = cellFromCtx(arg && arg.ctx); if (cc) removeAt(cc.r, cc.c); } },
|
|
234
|
+
];
|
|
235
|
+
},
|
|
236
|
+
/** マーカーを貼り直す(ホストが構造を変えた後などに手動で)。 */
|
|
237
|
+
refresh: function () { scheduleDecorate(); },
|
|
238
|
+
/** 後片付け(observer / リスナ / 開いているポップアップを解除)。 */
|
|
239
|
+
destroy: function () {
|
|
240
|
+
dead = true;
|
|
241
|
+
try { mo.disconnect(); } catch (e) { /* noop */ }
|
|
242
|
+
grid.table && grid.table.removeEventListener('mouseover', onOver);
|
|
243
|
+
grid.table && grid.table.removeEventListener('mouseout', onOut);
|
|
244
|
+
host.removeEventListener('keydown', onKey, true);
|
|
245
|
+
if (outside) { document.removeEventListener('mousedown', outside, true); outside = null; }
|
|
246
|
+
hideBubble(); closeEditor();
|
|
247
|
+
if (grid.table) { var m = grid.table.querySelectorAll('td.tg-comment-mark'); for (var i = 0; i < m.length; i++) m[i].classList.remove('tg-comment-mark'); }
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = TssComment;
|
|
252
|
+
else root.TssComment = TssComment;
|
|
253
|
+
})(typeof self !== 'undefined' ? self : this);
|