kindy-editor 3.2.1 → 3.2.3

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.
Files changed (51) hide show
  1. package/dist-lib/assets/{worker-CHtzL1YW.js → worker-BIFTSRF2.js} +7043 -7021
  2. package/dist-lib/assets/{worker-_GAJUHf5.js → worker-DO8U44KQ.js} +1 -0
  3. package/dist-lib/{browser-BxYyzpLt.js → browser-xuRja-pP.js} +2 -2
  4. package/dist-lib/builder.js +7 -7
  5. package/dist-lib/changeAggregator-ymKDXGA8.js +376 -0
  6. package/dist-lib/{editorApp-7-JzxxVn.js → editorApp-zIpjgEaZ.js} +9026 -8276
  7. package/dist-lib/events.js +119 -43
  8. package/dist-lib/export.js +1742 -1724
  9. package/dist-lib/generate-toc.js +5 -5
  10. package/dist-lib/import.js +2 -2
  11. package/dist-lib/index-Dd7gMK7l.js +12857 -0
  12. package/dist-lib/{kindy-editor-DKDwpt09.js → kindy-editor-DuN692fi.js} +133 -83
  13. package/dist-lib/kindy-editor.js +9 -6
  14. package/dist-lib/{lists-DpmgbDyt.js → lists-BVCy5dMa.js} +2 -2
  15. package/dist-lib/measure.js +3 -3
  16. package/dist-lib/{measureHost-C2NOdNkI.js → measureHost-y37gLU6M.js} +2 -2
  17. package/dist-lib/{metrics-DHgtBP-R.js → metrics-CdfHD-8y.js} +3 -3
  18. package/dist-lib/{paintStyle-Cjk9egel.js → paintStyle-Cb7IGKPW.js} +51 -51
  19. package/dist-lib/{parse-D4RpPrd7.js → parse-DJZd-2A7.js} +20 -20
  20. package/dist-lib/{pipeline-DhhBcLvV.js → pipeline-DKgBMmw1.js} +301 -301
  21. package/dist-lib/recalc-docx.js +2 -2
  22. package/dist-lib/{recalcToc-n50vq4nC.js → recalcToc-DCykIt4o.js} +80 -80
  23. package/dist-lib/{styleProps-OePkgWnD.js → styleProps-Dg3Wf-ZJ.js} +3 -3
  24. package/dist-lib/{stylesheet-E5bu1xch.js → stylesheet-EQFx9l5c.js} +3 -3
  25. package/dist-lib/{toc-DSwkNKls.js → toc-BIj8uQ-W.js} +417 -388
  26. package/dist-lib/{webmcp-KVT__0GH.js → webmcp-QA5rVg1F.js} +2 -2
  27. package/docs/http-event-sink.md +7 -0
  28. package/docs/public-events.md +103 -0
  29. package/docs/template-variables.md +310 -0
  30. package/docs/track-changes-events.md +551 -0
  31. package/package.json +2 -2
  32. package/types/events.d.ts +67 -1
  33. package/types/kindy-editor.d.ts +126 -3
  34. package/dist-lib/eventHub-CZFpTVqe.js +0 -71
  35. package/dist-lib/events-D_WEQDa6.js +0 -104
  36. package/dist-lib/index-Brv5TV-9.js +0 -11822
  37. package/docs/.vitepress/cache/deps/@theme_index.js +0 -275
  38. package/docs/.vitepress/cache/deps/@theme_index.js.map +0 -7
  39. package/docs/.vitepress/cache/deps/_metadata.json +0 -40
  40. package/docs/.vitepress/cache/deps/chunk-FNDFPMRQ.js +0 -13131
  41. package/docs/.vitepress/cache/deps/chunk-FNDFPMRQ.js.map +0 -7
  42. package/docs/.vitepress/cache/deps/chunk-IOFLAFGL.js +0 -9719
  43. package/docs/.vitepress/cache/deps/chunk-IOFLAFGL.js.map +0 -7
  44. package/docs/.vitepress/cache/deps/package.json +0 -3
  45. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4483
  46. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
  47. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -583
  48. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
  49. package/docs/.vitepress/cache/deps/vue.js +0 -347
  50. package/docs/.vitepress/cache/deps/vue.js.map +0 -7
  51. package/docs/.vitepress/config.ts +0 -30
@@ -1,4 +1,4 @@
1
- import { a as j, j as _, A as F, t as k, D as L } from "./toc-DSwkNKls.js";
1
+ import { p as j, c as _, e as F, t as k, D as L } from "./toc-BIj8uQ-W.js";
2
2
  const s = (e) => Math.round(e * 100) / 100;
3
3
  function A(e) {
4
4
  const o = { font: e.fontFamily, sizePx: e.fontSizePx };
@@ -332,7 +332,7 @@ function H(e, o = {}, r = {}) {
332
332
  if (i && e.search(i).total === 0)
333
333
  return e.searchClear(), f(`text not found: ${i}`);
334
334
  if (!e.getSelection()) return f("no selection (pass 'find' or select_range first)");
335
- const { insertText: d } = await import("./editorApp-7-JzxxVn.js").then((m) => m.c);
335
+ const { insertText: d } = await import("./editorApp-zIpjgEaZ.js").then((m) => m.c);
336
336
  return e.dispatch(d(t)), i && e.searchClear(), g("inserted");
337
337
  }
338
338
  }), a({
@@ -13,6 +13,13 @@ const sink = createHttpEventSink({
13
13
  headers: async () => ({
14
14
  Authorization: `Bearer ${await session.accessToken()}`,
15
15
  }),
16
+ // Transmit ONLY clean semantic committed events + review/export lifecycle:
17
+ eventTypes: [
18
+ "document.change.committed",
19
+ "document.export.completed",
20
+ "review.comment.added",
21
+ "review.comment.deleted",
22
+ ],
16
23
  batchSize: 25,
17
24
  flushIntervalMs: 1000,
18
25
  maxAttempts: 4,
@@ -98,6 +98,7 @@ general-purpose analytics services.
98
98
  | Export | `document.export.*` |
99
99
  | Changes | `document.change.applied`, `document.change.committed`, `document.change.rejected`, `document.remoteChange.applied` |
100
100
  | Review | `review.operation.applied`, `review.suggestion.*`, `review.thread.*`, `review.comment.*` |
101
+ | Template & Metadata | `template.variable.clicked`, `template.variable.changed`, `template.displayMode.changed` |
101
102
  | Collaboration | `collaboration.connecting`, `collaboration.connected`, `collaboration.disconnected`, `collaboration.user.*`, `collaboration.presence.changed` |
102
103
  | Other | `document.shared`, `selection.changed`, `custom` |
103
104
 
@@ -109,6 +110,108 @@ includes `newlyMentionedUserIds`, and tombstoning emits
109
110
  `review.comment.deleted`. See [Review comments and @mentions](./review-comments.md)
110
111
  for the integration and notification flow.
111
112
 
113
+ ## Document change events: Raw vs Semantic layer
114
+
115
+ Kindy Editor divides content changes into two distinct tiers:
116
+
117
+ ```text
118
+ User Input / Keystrokes
119
+ │
120
+ ▼
121
+ [ RAW LAYER ] document.change.applied (keystroke mutations, internal / collab / debug)
122
+ │
123
+ ▼
124
+ ChangeAggregator (Session Buffer, IME, continuous typing merge, no-op filter)
125
+ │
126
+ ├────── CommitPolicy (idle debounce ~1s, blur, save, block jump, enter)
127
+ │
128
+ ▼
129
+ [ SEMANTIC LAYER ] document.change.committed (clean diffs for CRM, Audit, AI, Outbox)
130
+ ```
131
+
132
+ ### 1. Raw layer: `document.change.applied`
133
+ Fires immediately on every editor mutation (keystrokes, backspace, paste, undo/redo).
134
+ Best used for: internal extensions, presence sync, local undo history, or low-level operation debugging.
135
+
136
+ Payload (`DocumentChangeEventData`):
137
+ ```ts
138
+ interface DocumentChangeEventData {
139
+ origin: "typing" | "command" | "paste" | "undo" | "redo";
140
+ operationCount: number;
141
+ affectedBlockIds: string[];
142
+ operations?: Op[]; // Present when detail !== "metadata"
143
+ textDiffs?: TextDiffSummary[]; // Discrete changes computed against pre-mutation shadow state
144
+ selectionBefore?: DocSelection | null;
145
+ selectionAfter?: DocSelection | null;
146
+ change?: Change;
147
+ }
148
+ ```
149
+
150
+ ### 2. Semantic layer: `document.change.committed`
151
+ Fires only when a logical editing session finishes across a commit boundary (idle debounce, blur, save, block switch).
152
+ Best used for: **external integrations (CRM, Audit Log, AI processors, Webhook sinks, Notifications)**.
153
+
154
+ Payload (`DocumentChangeCommittedData`):
155
+ ```ts
156
+ interface DocumentChangeCommittedData {
157
+ transactionId: string;
158
+ changeGroupId: string; // Stable group id tying all transactions in this session
159
+ beforeVersion: number; // Document baseline version before session began
160
+ afterVersion: number; // Document version after session completed
161
+ origin: "typing" | "command" | "paste" | "undo" | "redo";
162
+ textDiffs: TextDiffSummary[]; // Clean, aggregated text diffs (no-ops stripped)
163
+ intent?: string;
164
+ affectedBlockIds?: string[];
165
+ }
166
+ ```
167
+
168
+ ### `TextDiffSummary` structure
169
+ ```ts
170
+ interface TextDiffSummary {
171
+ action: "insert" | "delete" | "replace";
172
+ blockId: string;
173
+ offset: number;
174
+ fromText: string;
175
+ toText: string;
176
+ }
177
+ ```
178
+
179
+ ### How specific scenarios are handled:
180
+
181
+ 1. **Continuous Typing Session (Gõ liên tục):**
182
+ - User types characters one by one.
183
+ - Raw `document.change.applied` events fire on each keystroke for immediate UI responsiveness.
184
+ - `ChangeAggregator` buffers the keystrokes without spamming downstream sinks.
185
+ - When the user stops typing for ~1000ms (idle) or clicks outside (blur), a single `document.change.committed` is emitted with the consolidated change (e.g., `action: "insert"`, `fromText: ""`, `toText: "Đà Nẵng"`).
186
+
187
+ 2. **Selected Range Replacement (Bôi đen ghi đè):**
188
+ - User selects `"Hà Nội"` and types `"Đà Nẵng"`.
189
+ - The editor runs `deleteRange` followed by `insertText`.
190
+ - Shadow state evaluation and `mergeRelatedChanges` combine them into:
191
+ `{ action: "replace", blockId: "p-5", offset: 13, fromText: "Hà Nội", toText: "Đà Nẵng" }`.
192
+
193
+ 3. **Multiple Independent Edits in One Transaction / Session (Nhiều sửa đổi độc lập):**
194
+ - E.g. Editing `"50"` -> `"75"` at offset 5 and `"30"` -> `"45"` at offset 25.
195
+ - `textDiffs` contains two separate items, preserving exact offsets without string concatenation corruption:
196
+ ```json
197
+ [
198
+ { "action": "replace", "blockId": "p-5", "offset": 5, "fromText": "50", "toText": "75" },
199
+ { "action": "replace", "blockId": "p-5", "offset": 25, "fromText": "30", "toText": "45" }
200
+ ]
201
+ ```
202
+
203
+ 4. **No-op Edits (Gõ rồi xóa về nguyên bản):**
204
+ - User types `" thêm"` and immediately backspaces it back to original text.
205
+ - When session boundary triggers, `ChangeAggregator` compares baseline vs current text.
206
+ - Since text is identical, **no event is emitted**, preventing dirty garbage data from reaching downstream databases.
207
+
208
+ 5. **Batch Commands & Paste (Dán hoặc chạy command):**
209
+ - Paste, cut, or formatting commands commit immediately (`CommitBoundaryReason = "explicit"`).
210
+ - Any prior typing session is automatically flushed before the new action begins.
211
+
212
+ 6. **Cursor Jump / Block Switch (Chuyển khối hoặc nhảy chuột):**
213
+ - If user edits block `p-1`, then clicks block `p-2` or moves caret far away, `CommitPolicy` detects a boundary (`"selection-jump"` / `"block-switch"`), flushes block `p-1`'s session, and opens a new session for `p-2`.
214
+
112
215
  ## What each layer owns
113
216
 
114
217
  The editor library owns content changes, review operations, editor state,
@@ -0,0 +1,310 @@
1
+ # Template Variables & Contract Metadata API
2
+
3
+ Kindy Editor hỗ trợ quản lý các **biến mẫu (Template Variables)** và **Metadata Hợp đồng** trực tiếp trong mô hình tài liệu (AST level).
4
+
5
+ Các biến mẫu được liên kết chặt chẽ với văn bản thông qua thuộc tính `CharStyle.variableKey`, đảm bảo tính toàn vẹn (atomic tokens), hỗ trợ định dạng tự động theo kiểu dữ liệu (`TEXT`, `NUMBER`, `DATE`, `CURRENCY`, `SELECT`), và cung cấp hệ thống event phong phú để host application (React, Vue, Angular, v.v.) tự do dựng giao diện popover/drawer theo design system riêng.
6
+
7
+ ---
8
+
9
+ ## 1. Khởi tạo & Cấu hình (Options)
10
+
11
+ Khi khởi tạo `KindyEditor`, bạn có thể cấu hình các tùy chọn liên quan đến biến mẫu:
12
+
13
+ ```ts
14
+ import { KindyEditor } from "kindy-editor";
15
+ import type { VariableClickInfo } from "kindy-editor";
16
+
17
+ const editor = new KindyEditor({
18
+ container: document.getElementById("editor-container")!,
19
+
20
+ // Callback khi người dùng click vào một biến mẫu trong văn bản
21
+ onVariableClick: (info: VariableClickInfo) => {
22
+ console.log("Clicked variable:", info.key, info.def);
23
+ // info.clientRect chứa tọa độ viewport { top, left, width, height }
24
+ // để host app hiển thị Popover / Drawer / Modal tùy biến
25
+ },
26
+
27
+ // Có hiển thị Popover mặc định của editor không (mặc định: false)
28
+ // Nếu đặt là false, host app toàn quyền hiển thị UI qua onVariableClick
29
+ showVariablePopover: false,
30
+ });
31
+ ```
32
+
33
+ ---
34
+
35
+ ## 2. Các phương thức của Editor API (`EditorHandle` / `KindyEditor`)
36
+
37
+ Tất cả các phương thức sau có thể gọi thông qua instance của `KindyEditor` hoặc thông qua `handle` nhận được từ `onReady` / `editor.ready`:
38
+
39
+ ### 2.1. `setTemplateVariables(values, options?)`
40
+ Cập nhật giá trị hàng loạt biến mà không cần reload tài liệu. Trạng thái con trỏ, lịch sử undo/redo và vị trí scroll đều được bảo toàn.
41
+
42
+ ```ts
43
+ await editor.setTemplateVariables(
44
+ {
45
+ contract_code: "HD-2026/001",
46
+ contract_value: 25000000,
47
+ signing_date: "2026-10-04",
48
+ customer_name: "Công ty Cổ phần Công nghệ ABC",
49
+ },
50
+ {
51
+ author: { id: "user-1", firstName: "Hùng", lastName: "Ngô" },
52
+ source: "sidebar", // "api" | "editor" | "sidebar" | "inline" | "import" | "palette"
53
+ }
54
+ );
55
+ ```
56
+
57
+ ### 2.2. `insertTemplateVariable(item, author?)`
58
+ Chèn một token biến mẫu atomic tại vị trí con trỏ hiện tại (hoặc thay thế vùng chọn hiện tại).
59
+
60
+ ```ts
61
+ await editor.insertTemplateVariable(
62
+ {
63
+ key: "contract_value",
64
+ label: "Giá trị hợp đồng",
65
+ dataType: "CURRENCY",
66
+ value: 50000000,
67
+ defaultValue: "{{contract_value}}",
68
+ group: "TAI_CHINH",
69
+ metadataId: 102,
70
+ },
71
+ { id: "user-1", firstName: "Hùng", lastName: "Ngô" }
72
+ );
73
+ ```
74
+
75
+ ### 2.3. `setVariableDisplayMode(mode)` & `getVariableDisplayMode()`
76
+ Thay đổi chế độ hiển thị biến trên toàn bộ văn bản:
77
+ * `'code'` / `'placeholder'`: Hiển thị mã placeholder, ví dụ `{{contract_code}}`. Thích hợp khi soạn thảo mẫu hợp đồng (Template Editor).
78
+ * `'value'`: Hiển thị giá trị thực tế sau khi đã format theo `dataType`, ví dụ `HD-2026/001` hoặc `25.000.000 VNĐ`. Nếu biến chưa có giá trị, tự động fallback về mã placeholder.
79
+ * `'label'`: Hiển thị nhãn tiếng Việt của biến.
80
+
81
+ ```ts
82
+ // Chuyển sang hiển thị giá trị thực tế
83
+ await editor.setVariableDisplayMode("value");
84
+
85
+ // Đọc chế độ hiện tại
86
+ const currentMode = await editor.getVariableDisplayMode(); // "value" | "code"
87
+ ```
88
+
89
+ ### 2.4. `getTemplateVariables()` & `getTemplateVariable(key)`
90
+ Truy vấn danh sách hoặc chi tiết các biến đã đăng ký trong tài liệu.
91
+
92
+ ```ts
93
+ // Lấy toàn bộ biến đã đăng ký
94
+ const allVariables = await editor.getTemplateVariables();
95
+ console.log(allVariables["contract_code"]);
96
+
97
+ // Lấy 1 biến cụ thể
98
+ const codeVar = await editor.getTemplateVariable("contract_code");
99
+ ```
100
+
101
+ ### 2.5. `getUsedMetadataIds()`
102
+ Trả về danh sách các `metadataId` (ID số định danh từ cơ sở dữ liệu backend) hiện đang thực sự được nhúng trong tài liệu. Rất hữu ích cho backend khi cần kiểm tra những metadata nào đang được văn bản sử dụng.
103
+
104
+ ```ts
105
+ const usedIds = await editor.getUsedMetadataIds();
106
+ // Ví dụ: [101, 102, 105]
107
+ ```
108
+
109
+ ### 2.6. `setMetadataCatalog(catalog)` & `openMetadataPalette()`
110
+ Cung cấp danh mục các biến từ backend cho tính năng gợi ý autocomplete (`@`), hoặc chủ động mở thanh tìm kiếm biến tại vị trí con trỏ.
111
+
112
+ ```ts
113
+ // Cung cấp danh mục biến cho autocomplete
114
+ await editor.setMetadataCatalog([
115
+ {
116
+ key: "contract_code",
117
+ label: "Mã hợp đồng",
118
+ dataType: "TEXT",
119
+ group: "THONG_TIN_CHUNG",
120
+ metadataId: 1,
121
+ },
122
+ {
123
+ key: "contract_value",
124
+ label: "Giá trị hợp đồng",
125
+ dataType: "CURRENCY",
126
+ group: "TAI_CHINH",
127
+ metadataId: 2,
128
+ },
129
+ {
130
+ key: "effective_date",
131
+ label: "Ngày hiệu lực",
132
+ dataType: "DATE",
133
+ dateFormat: "dd/MM/yyyy",
134
+ group: "THOI_HAN",
135
+ metadataId: 3,
136
+ },
137
+ ]);
138
+
139
+ // Chủ động mở bảng chọn @ tại vị trí con trỏ
140
+ await editor.openMetadataPalette();
141
+ ```
142
+
143
+ ---
144
+
145
+ ## 3. Public Events API
146
+
147
+ Lắng nghe các sự kiện liên quan đến Template Variables thông qua `editor.on(...)`:
148
+
149
+ ### 3.1. `template.variable.clicked`
150
+ Kích hoạt khi người dùng click vào một biến mẫu trong văn bản:
151
+
152
+ ```ts
153
+ editor.on("template.variable.clicked", (event) => {
154
+ const { key, def, position, rect, clientRect } = event.data;
155
+
156
+ console.log(`Clicked variable: ${key}`);
157
+ console.log(`Current value:`, def.value);
158
+ console.log(`Bounding Box trên màn hình:`, clientRect);
159
+ // clientRect: { left, top, right, bottom, width, height }
160
+ // Dùng để định vị floating popover/tooltip cạnh biến vừa click
161
+ });
162
+ ```
163
+
164
+ ### 3.2. `template.variable.changed`
165
+ Kích hoạt khi một biến được tạo mới, cập nhật giá trị hoặc hoàn tác:
166
+
167
+ ```ts
168
+ editor.on("template.variable.changed", (event) => {
169
+ const { key, newValue, previousValue, action, source, def } = event.data;
170
+ console.log(`Variable ${key} was ${action} from source ${source}`);
171
+ console.log(`Old: ${previousValue} -> New: ${newValue}`);
172
+ });
173
+ ```
174
+
175
+ ### 3.3. `template.displayMode.changed`
176
+ Kích hoạt khi chế độ hiển thị biến thay đổi:
177
+
178
+ ```ts
179
+ editor.on("template.displayMode.changed", (event) => {
180
+ const { mode, previousMode } = event.data;
181
+ console.log(`Display mode changed from ${previousMode} to ${mode}`);
182
+ });
183
+ ```
184
+
185
+ ---
186
+
187
+ ## 4. Định dạng tự động theo Kiểu dữ liệu (`MetadataDataType`)
188
+
189
+ Khi ở chế độ `value`, hàm định dạng tự động chuyển đổi giá trị dựa vào trường `dataType`:
190
+
191
+ | `dataType` | Ví dụ đầu vào | Kết quả hiển thị |
192
+ | :--- | :--- | :--- |
193
+ | `TEXT` | `"Hà Nội"` | `Hà Nội` |
194
+ | `NUMBER` | `1500000` | `1.500.000` (định dạng theo locale vi-VN) |
195
+ | `CURRENCY` | `25000000` | `25.000.000 VNĐ` |
196
+ | `DATE` | `"2026-10-04"` | `04/10/2026` (hỗ trợ `dateFormat: "dd/MM/yyyy"` hoặc `"yyyy-MM-dd"`) |
197
+ | `SELECT` | `"Đã ký"` | `Đã ký` |
198
+
199
+ ---
200
+
201
+ ## 5. Tương tác Người dùng (UX & Hành vi gõ phím)
202
+
203
+ 1. **Kích hoạt Autocomplete qua `@`**:
204
+ - Khi gõ ký tự `@` ở đầu dòng hoặc sau một khoảng trắng, bảng **Metadata Palette** sẽ tự động mở ra.
205
+ - Người dùng có thể dùng các phím mũi tên `↑` `↓` để duyệt, `Enter` hoặc `Tab` để chèn, và `Escape` để đóng.
206
+ - Gõ email (ví dụ: `sales@gmail.com`) sẽ **không** bị kích hoạt bảng chọn vì trước `@` không phải là khoảng trắng.
207
+
208
+ 2. **Tính chất Atomic Token**:
209
+ - Khi bấm phím `Backspace` hoặc `Delete` tại biên của biến, toàn bộ token biến sẽ được xóa sạch trong một thao tác (thay vì xóa từng ký tự placeholder).
210
+ - Khi gõ phím tiếp theo ngay sau một biến, văn bản mới sẽ tự động nhận style thông thường và không bị dính thuộc tính `variableKey`.
211
+ - Khi click vào biến, văn bản của biến sẽ tự động được chọn toàn bộ.
212
+
213
+ ---
214
+
215
+ ## 6. Ví dụ Tích hợp với React
216
+
217
+ ```tsx
218
+ import React, { useEffect, useRef, useState } from "react";
219
+ import { KindyEditor, type VariableClickInfo } from "kindy-editor";
220
+ import "kindy-editor/style";
221
+
222
+ export function ContractEditor() {
223
+ const containerRef = useRef<HTMLDivElement>(null);
224
+ const editorRef = useRef<KindyEditor | null>(null);
225
+ const [selectedVar, setSelectedVar] = useState<VariableClickInfo | null>(null);
226
+
227
+ useEffect(() => {
228
+ if (!containerRef.current) return;
229
+
230
+ const editor = new KindyEditor({
231
+ container: containerRef.current,
232
+ showVariablePopover: false, // tắt popup mặc định để dùng React Popover
233
+ onVariableClick: (info) => {
234
+ setSelectedVar(info);
235
+ },
236
+ });
237
+
238
+ editorRef.current = editor;
239
+
240
+ // Lắng nghe sự kiện click ra ngoài để đóng popover
241
+ const handleMouseDown = (e: MouseEvent) => {
242
+ if (!(e.target as HTMLElement).closest(".custom-variable-popover")) {
243
+ setSelectedVar(null);
244
+ }
245
+ };
246
+ window.addEventListener("mousedown", handleMouseDown);
247
+
248
+ return () => {
249
+ window.removeEventListener("mousedown", handleMouseDown);
250
+ editor.destroy();
251
+ };
252
+ }, []);
253
+
254
+ return (
255
+ <div style={{ position: "relative", width: "100%", height: "100vh" }}>
256
+ {/* Editor Canvas Container */}
257
+ <div ref={containerRef} style={{ width: "100%", height: "100%" }} />
258
+
259
+ {/* Custom React Floating Popover */}
260
+ {selectedVar && selectedVar.clientRect && (
261
+ <div
262
+ className="custom-variable-popover"
263
+ style={{
264
+ position: "fixed",
265
+ left: `${selectedVar.clientRect.left}px`,
266
+ top: `${selectedVar.clientRect.bottom + 8}px`,
267
+ background: "#ffffff",
268
+ border: "1px solid #e2e8f0",
269
+ borderRadius: "8px",
270
+ boxShadow: "0 10px 15px -3px rgba(0,0,0,0.1)",
271
+ padding: "12px 16px",
272
+ zIndex: 1000,
273
+ minWidth: "260px",
274
+ }}
275
+ >
276
+ <div style={{ fontWeight: "bold", color: "#0284c7" }}>
277
+ {selectedVar.def.label} ({`{{${selectedVar.key}}}`})
278
+ </div>
279
+ <div style={{ fontSize: "12px", color: "#64748b", marginTop: "4px" }}>
280
+ Loại: {selectedVar.def.dataType}
281
+ </div>
282
+ <div style={{ fontSize: "14px", marginTop: "6px" }}>
283
+ Giá trị: <strong>{String(selectedVar.def.value ?? "Chưa có")}</strong>
284
+ </div>
285
+ <button
286
+ onClick={() => {
287
+ const nextVal = prompt("Nhập giá trị mới:", String(selectedVar.def.value ?? ""));
288
+ if (nextVal !== null) {
289
+ editorRef.current?.setTemplateVariables({ [selectedVar.key]: nextVal });
290
+ setSelectedVar(null);
291
+ }
292
+ }}
293
+ style={{
294
+ marginTop: "8px",
295
+ padding: "4px 8px",
296
+ background: "#0284c7",
297
+ color: "#fff",
298
+ border: "none",
299
+ borderRadius: "4px",
300
+ cursor: "pointer",
301
+ }}
302
+ >
303
+ Chỉnh sửa giá trị
304
+ </button>
305
+ </div>
306
+ )}
307
+ </div>
308
+ );
309
+ }
310
+ ```