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.
- package/dist-lib/assets/{worker-CHtzL1YW.js → worker-BIFTSRF2.js} +7043 -7021
- package/dist-lib/assets/{worker-_GAJUHf5.js → worker-DO8U44KQ.js} +1 -0
- package/dist-lib/{browser-BxYyzpLt.js → browser-xuRja-pP.js} +2 -2
- package/dist-lib/builder.js +7 -7
- package/dist-lib/changeAggregator-ymKDXGA8.js +376 -0
- package/dist-lib/{editorApp-7-JzxxVn.js → editorApp-zIpjgEaZ.js} +9026 -8276
- package/dist-lib/events.js +119 -43
- package/dist-lib/export.js +1742 -1724
- package/dist-lib/generate-toc.js +5 -5
- package/dist-lib/import.js +2 -2
- package/dist-lib/index-Dd7gMK7l.js +12857 -0
- package/dist-lib/{kindy-editor-DKDwpt09.js → kindy-editor-DuN692fi.js} +133 -83
- package/dist-lib/kindy-editor.js +9 -6
- package/dist-lib/{lists-DpmgbDyt.js → lists-BVCy5dMa.js} +2 -2
- package/dist-lib/measure.js +3 -3
- package/dist-lib/{measureHost-C2NOdNkI.js → measureHost-y37gLU6M.js} +2 -2
- package/dist-lib/{metrics-DHgtBP-R.js → metrics-CdfHD-8y.js} +3 -3
- package/dist-lib/{paintStyle-Cjk9egel.js → paintStyle-Cb7IGKPW.js} +51 -51
- package/dist-lib/{parse-D4RpPrd7.js → parse-DJZd-2A7.js} +20 -20
- package/dist-lib/{pipeline-DhhBcLvV.js → pipeline-DKgBMmw1.js} +301 -301
- package/dist-lib/recalc-docx.js +2 -2
- package/dist-lib/{recalcToc-n50vq4nC.js → recalcToc-DCykIt4o.js} +80 -80
- package/dist-lib/{styleProps-OePkgWnD.js → styleProps-Dg3Wf-ZJ.js} +3 -3
- package/dist-lib/{stylesheet-E5bu1xch.js → stylesheet-EQFx9l5c.js} +3 -3
- package/dist-lib/{toc-DSwkNKls.js → toc-BIj8uQ-W.js} +417 -388
- package/dist-lib/{webmcp-KVT__0GH.js → webmcp-QA5rVg1F.js} +2 -2
- package/docs/http-event-sink.md +7 -0
- package/docs/public-events.md +103 -0
- package/docs/template-variables.md +310 -0
- package/docs/track-changes-events.md +551 -0
- package/package.json +2 -2
- package/types/events.d.ts +67 -1
- package/types/kindy-editor.d.ts +126 -3
- package/dist-lib/eventHub-CZFpTVqe.js +0 -71
- package/dist-lib/events-D_WEQDa6.js +0 -104
- package/dist-lib/index-Brv5TV-9.js +0 -11822
- package/docs/.vitepress/cache/deps/@theme_index.js +0 -275
- package/docs/.vitepress/cache/deps/@theme_index.js.map +0 -7
- package/docs/.vitepress/cache/deps/_metadata.json +0 -40
- package/docs/.vitepress/cache/deps/chunk-FNDFPMRQ.js +0 -13131
- package/docs/.vitepress/cache/deps/chunk-FNDFPMRQ.js.map +0 -7
- package/docs/.vitepress/cache/deps/chunk-IOFLAFGL.js +0 -9719
- package/docs/.vitepress/cache/deps/chunk-IOFLAFGL.js.map +0 -7
- package/docs/.vitepress/cache/deps/package.json +0 -3
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4483
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -583
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
- package/docs/.vitepress/cache/deps/vue.js +0 -347
- package/docs/.vitepress/cache/deps/vue.js.map +0 -7
- package/docs/.vitepress/config.ts +0 -30
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
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({
|
package/docs/http-event-sink.md
CHANGED
|
@@ -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,
|
package/docs/public-events.md
CHANGED
|
@@ -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
|
+
```
|