kindy-editor 3.2.5 → 3.2.7
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-B7ALyZOw.js → worker-DjZBUOhB.js} +1009 -997
- package/dist-lib/assets/{worker-z-dyEi8F.js → worker-pUzyeoiR.js} +2 -4
- package/dist-lib/builder.js +4 -4
- package/dist-lib/{changeAggregator-ZI959nPP.js → changeAggregator-DEuynIcV.js} +1 -1
- package/dist-lib/{editorApp-B6qDC4lD.js → editorApp-BtDzRfJq.js} +9246 -8819
- package/dist-lib/events.js +2 -2
- package/dist-lib/export.js +7 -9
- package/dist-lib/generate-toc.js +3 -3
- package/dist-lib/import.js +2 -2
- package/dist-lib/{kindy-editor-CT14IO-Y.js → kindy-editor---oVGhZU.js} +99 -56
- package/dist-lib/kindy-editor.js +2 -2
- package/dist-lib/{parse-CKw-ZDr9.js → parse-CpGoMQ_v.js} +3 -3
- package/dist-lib/{pipeline-BkC7G8gU.js → pipeline-CCwWMjhw.js} +850 -837
- package/dist-lib/{qrcode-efBlPzzh.js → qrcode-B-JKix2T.js} +5 -5
- package/dist-lib/{qrcode-oNwKusWD.js → qrcode-BCaJLV1G.js} +1 -1
- package/dist-lib/recalc-docx.js +1 -1
- package/dist-lib/{recalcToc-Bc8V9Ecn.js → recalcToc-CaiTn2IJ.js} +1 -1
- package/dist-lib/{stylesheet-BT1DGhnx.js → stylesheet-BPIJJMlK.js} +1 -1
- package/dist-lib/{webmcp-BcWSo1v9.js → webmcp-B0swfp0T.js} +2 -2
- package/docs/find-and-replace.md +224 -0
- package/docs/qrcode-integration-guide.md +36 -1
- package/docs/security-review.md +26 -0
- package/docs/table-editing.md +99 -1
- package/docs/table-form-integration.md +92 -1
- package/package.json +6 -6
- package/types/import.d.ts +7 -0
- package/types/kindy-editor.d.ts +122 -0
|
@@ -2288,13 +2288,13 @@ export {
|
|
|
2288
2288
|
Ue as i,
|
|
2289
2289
|
$e as j,
|
|
2290
2290
|
qe as k,
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2291
|
+
lt as l,
|
|
2292
|
+
Ct as m,
|
|
2293
|
+
pt as n,
|
|
2294
|
+
U as o,
|
|
2295
2295
|
jt as p,
|
|
2296
2296
|
z as q,
|
|
2297
|
-
|
|
2297
|
+
Ke as r,
|
|
2298
2298
|
Ut as s,
|
|
2299
2299
|
X as t,
|
|
2300
2300
|
He as u,
|
package/dist-lib/recalc-docx.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { u, s as f, a as m, z as g } from "./browser-ODPzT3Lm.js";
|
|
2
2
|
import { f as h } from "./contentTypes-ChqPfcVI.js";
|
|
3
|
-
import { p as b, c as k } from "./recalcToc-
|
|
3
|
+
import { p as b, c as k } from "./recalcToc-CaiTn2IJ.js";
|
|
4
4
|
function w(s, r) {
|
|
5
5
|
const n = b(r), a = {};
|
|
6
6
|
for (const [o, i] of Object.entries(s.bookmarks ?? {})) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as Ln, m as Ls, t as Os, s as Ps } from "./qrcode-B-JKix2T.js";
|
|
2
2
|
import { c as Is, f as Ze } from "./lists-BVCy5dMa.js";
|
|
3
3
|
import { m as he, f as Ue, c as le } from "./metrics-CdfHD-8y.js";
|
|
4
4
|
import { c as vs, f as On, M as nt, g as Rs, P as Mt, w as Ws, s as Ts } from "./paintStyle-Cb7IGKPW.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { p as j, c as _, e as F, t as k, D as L } from "./qrcode-
|
|
1
|
+
import { p as j, c as _, e as F, t as k, D as L } from "./qrcode-B-JKix2T.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-BtDzRfJq.js").then((m) => m.c);
|
|
336
336
|
return e.dispatch(d(t)), i && e.searchClear(), g("inserted");
|
|
337
337
|
}
|
|
338
338
|
}), a({
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Tìm kiếm, Thay thế & Cuộn Focus (Find & Replace API)
|
|
2
|
+
|
|
3
|
+
Kindy Editor cung cấp hệ thống Tìm kiếm & Thay thế (Find & Replace) mạnh mẽ trên nền Canvas, hỗ trợ tính toán tọa độ ký tự chính xác, tự động bôi đen (selection) và **tự động cuộn màn hình (scroll focus)** đến vị trí văn bản cần thay thế.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Cơ chế hoạt động (How it works)
|
|
8
|
+
|
|
9
|
+
Khi tìm kiếm hoặc thay thế, editor thực hiện các bước sau:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
[search(query)]
|
|
13
|
+
│
|
|
14
|
+
├── 1. Quét AST text của các block trong Document
|
|
15
|
+
├── 2. Highlight tất cả các matches trên Canvas overlay
|
|
16
|
+
└── 3. gotoMatch(0):
|
|
17
|
+
├── setSelection({ anchor, focus }) --> Bôi đen match
|
|
18
|
+
├── caretRect(tree, position) --> Tính tọa độ pixel trên trang
|
|
19
|
+
└── paint.ensureVisible(rect) --> TỰ ĐỘNG CUỘN VIEWPORT ĐẾN MATCH
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Khi thực hiện thay thế (`searchReplaceCurrent`):
|
|
23
|
+
1. Match đang chọn được thay thế bằng chuỗi mới thông qua một **Transaction nguyên tử** (Atomic Op Group: `deleteRange` + `insertText`).
|
|
24
|
+
2. Editor tự động quét lại các match còn lại.
|
|
25
|
+
3. Tự động chuyển con trỏ và **cuộn màn hình đến match kế tiếp** ngay lập tức.
|
|
26
|
+
4. Toàn bộ thao tác có thể **Undo (`Ctrl+Z`)** hoàn hảo.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Danh sách API (API Reference)
|
|
31
|
+
|
|
32
|
+
Các API này có thể được gọi trực tiếp từ đối tượng editor (`KindyEditorElement` hoặc `EditorHandle`):
|
|
33
|
+
|
|
34
|
+
### `search(query, opts?): Promise<SearchState>`
|
|
35
|
+
Tìm kiếm văn bản trong tài liệu, đánh dấu tất cả các vị trí trùng khớp và tự động cuộn focus đến match đầu tiên.
|
|
36
|
+
- **Tham số**:
|
|
37
|
+
- `query: string`: Từ khóa cần tìm.
|
|
38
|
+
- `opts?: { matchCase?: boolean; wholeWord?: boolean }`:
|
|
39
|
+
- `matchCase`: Khớp chính xác chữ hoa/chữ thường (mặc định: `false`).
|
|
40
|
+
- `wholeWord`: Chỉ khớp từ nguyên vẹn (mặc định: `false`).
|
|
41
|
+
- **Trả về**: `Promise<SearchState>` gồm `{ index: number, total: number }` (1-based, ví dụ `1/12`).
|
|
42
|
+
|
|
43
|
+
### `searchNav(dir): Promise<SearchState>`
|
|
44
|
+
Điều hướng giữa các kết quả tìm kiếm và tự động cuộn màn hình đến kết quả đó.
|
|
45
|
+
- **Tham số**:
|
|
46
|
+
- `dir: 1 | -1`: `1` để chuyển đến match kế tiếp, `-1` để quay lại match trước.
|
|
47
|
+
- **Trả về**: `Promise<SearchState>`.
|
|
48
|
+
|
|
49
|
+
### `searchReplaceCurrent(replacement): Promise<SearchState>`
|
|
50
|
+
Thay thế kết quả đang được chọn bằng nội dung mới, sau đó **tự động nhảy và cuộn focus đến match kế tiếp**.
|
|
51
|
+
- **Tham số**:
|
|
52
|
+
- `replacement: string`: Chuỗi văn bản thay thế.
|
|
53
|
+
- **Trả về**: `Promise<SearchState>` biểu thị vị trí match tiếp theo.
|
|
54
|
+
|
|
55
|
+
### `searchReplaceAll(replacement): Promise<number>`
|
|
56
|
+
Thay thế toàn bộ tất cả các kết quả tìm kiếm trong tài liệu trong một transaction duy nhất (1 lần Undo).
|
|
57
|
+
- **Tham số**:
|
|
58
|
+
- `replacement: string`: Chuỗi văn bản thay thế.
|
|
59
|
+
- **Trả về**: `Promise<number>`: Số lượng đoạn văn bản đã được thay thế.
|
|
60
|
+
|
|
61
|
+
### `searchClear(): Promise<void>`
|
|
62
|
+
Xóa tất cả highlight tìm kiếm và đóng trạng thái tìm kiếm hiện tại.
|
|
63
|
+
|
|
64
|
+
### `revealRange(range, options?): Promise<void>`
|
|
65
|
+
Cuộn viewport đến bất kỳ vị trí text range nào trong văn bản (hữu ích cho AI suggestion hoặc spellcheck).
|
|
66
|
+
- **Tham số**:
|
|
67
|
+
- `range: ExternalRange`: Gồm `{ start: { blockId, offset }, end: { blockId, offset } }`.
|
|
68
|
+
- `options?: RevealRangeOptions`:
|
|
69
|
+
- `behavior?: "auto" | "smooth"`: Hiệu ứng cuộn mượt (mặc định `"smooth"`).
|
|
70
|
+
- `align?: "nearest" | "center"`: Vị trí canh lề (mặc định `"center"`).
|
|
71
|
+
- `durationMs?: number`: Thời gian animation cuộn (mặc định `280ms`).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 3. Hướng dẫn tích hợp hệ thống (System Integration Guide)
|
|
76
|
+
|
|
77
|
+
### Cách 1: Sử dụng UI Tìm kiếm & Thay thế có sẵn (Built-in UI)
|
|
78
|
+
|
|
79
|
+
Kindy Editor đã tích hợp sẵn thanh công cụ Find & Replace:
|
|
80
|
+
- **Phím tắt**: Nhấn `Ctrl + F` (hoặc `Cmd + F` trên macOS).
|
|
81
|
+
- **Ribbon**: Chọn tab **Trang chủ** / **Home** -> nút **Tìm kiếm / Thay thế**.
|
|
82
|
+
- Giao diện có sẵn:
|
|
83
|
+
- Ô nhập từ khóa tìm kiếm và ô nhập nội dung thay thế.
|
|
84
|
+
- Tùy chọn khớp hoa/thường (`Aa`) và khớp nguyên từ (`[W]`).
|
|
85
|
+
- Nút điều hướng trước/sau (`‹`, `›`) với phím tắt `Enter` / `Shift+Enter`.
|
|
86
|
+
- Nút `Replace` và `Replace All`.
|
|
87
|
+
- Bộ đếm `1/5` hiển thị số lượng match trực quan.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### Cách 2: Tích hợp Custom Search & Replace Toolbar từ Host Application
|
|
92
|
+
|
|
93
|
+
Nếu hệ thống của bạn muốn xây dựng thanh tìm kiếm riêng bên ngoài khung editor (ví dụ: thanh công cụ header của ứng dụng web React / Vue / Angular):
|
|
94
|
+
|
|
95
|
+
#### Mã nguồn tích hợp JavaScript / TypeScript:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import type { KindyEditorElement, SearchState } from "kindy-editor";
|
|
99
|
+
|
|
100
|
+
// 1. Lấy tham chiếu đến web component kindy-editor
|
|
101
|
+
const editor = document.querySelector("kindy-editor") as KindyEditorElement;
|
|
102
|
+
|
|
103
|
+
// 2. Thực hiện tìm kiếm
|
|
104
|
+
async function handleSearch(keyword: string) {
|
|
105
|
+
if (!keyword.trim()) {
|
|
106
|
+
await editor.searchClear();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// search() tự động highlight VÀ cuộn focus đến kết quả đầu tiên
|
|
111
|
+
const state: SearchState = await editor.search(keyword, {
|
|
112
|
+
matchCase: false, // Không phân biệt hoa/thường
|
|
113
|
+
wholeWord: false, // Không bắt buộc nguyên từ
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(`Tìm thấy ${state.total} kết quả. Đang ở vị trí ${state.index}`);
|
|
117
|
+
updateMatchUI(state);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 3. Chuyển sang kết quả tiếp theo (Next match)
|
|
121
|
+
async function handleNext() {
|
|
122
|
+
const state = await editor.searchNav(1);
|
|
123
|
+
updateMatchUI(state);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 4. Quay lại kết quả trước (Previous match)
|
|
127
|
+
async function handlePrev() {
|
|
128
|
+
const state = await editor.searchNav(-1);
|
|
129
|
+
updateMatchUI(state);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 5. Thay thế kết quả hiện tại và tự động cuộn đến kết quả tiếp theo
|
|
133
|
+
async function handleReplace(newText: string) {
|
|
134
|
+
const state = await editor.searchReplaceCurrent(newText);
|
|
135
|
+
console.log(`Đã thay thế. Chuyển đến kết quả tiếp theo: ${state.index}/${state.total}`);
|
|
136
|
+
updateMatchUI(state);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 6. Thay thế tất cả (Replace All)
|
|
140
|
+
async function handleReplaceAll(newText: string) {
|
|
141
|
+
const count = await editor.searchReplaceAll(newText);
|
|
142
|
+
alert(`Đã thay thế thành công ${count} vị trí.`);
|
|
143
|
+
await editor.searchClear();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 7. Đóng tìm kiếm
|
|
147
|
+
async function handleCloseSearch() {
|
|
148
|
+
await editor.searchClear();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function updateMatchUI(state: SearchState) {
|
|
152
|
+
const badge = document.getElementById("search-counter");
|
|
153
|
+
if (badge) {
|
|
154
|
+
badge.innerText = state.total > 0 ? `${state.index}/${state.total}` : "0/0";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### Cách 3: Thay thế tại vị trí cụ thể và Scroll Focus (AI Suggestion / Linter)
|
|
162
|
+
|
|
163
|
+
Khi hệ thống backend (AI rà soát hợp đồng, kiểm tra chính tả) trả về một vị trí lỗi cụ thể và bạn muốn người dùng duyệt qua từng lỗi, cuộn màn hình đến đoạn đó và thay thế:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
interface ReplacementSuggestion {
|
|
167
|
+
range: {
|
|
168
|
+
start: { blockId: string; offset: number };
|
|
169
|
+
end: { blockId: string; offset: number };
|
|
170
|
+
};
|
|
171
|
+
originalText: string;
|
|
172
|
+
suggestedText: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function reviewAndApplySuggestion(suggestion: ReplacementSuggestion) {
|
|
176
|
+
// 1. Cuộn màn hình canvas đến vị trí cần sửa với animation mượt
|
|
177
|
+
await editor.revealRange(suggestion.range, {
|
|
178
|
+
behavior: "smooth",
|
|
179
|
+
align: "center",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 2. Tùy chọn: Chọn đoạn văn bản đó để người dùng thấy rõ
|
|
183
|
+
// (Nếu host có hỗ trợ setSelection)
|
|
184
|
+
|
|
185
|
+
// 3. Khi người dùng bấm "Chấp nhận thay thế":
|
|
186
|
+
// Nếu dùng search theo cụm từ:
|
|
187
|
+
const state = await editor.search(suggestion.originalText);
|
|
188
|
+
if (state.total > 0) {
|
|
189
|
+
await editor.searchReplaceCurrent(suggestion.suggestedText);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### Cách 4: Phân biệt với Thay thế Biến mẫu hợp đồng (`setTemplateVariables`)
|
|
197
|
+
|
|
198
|
+
Nếu mục đích của bạn là **điền dữ liệu hợp đồng tự động** (ví dụ thay thế `{{HO_TEN}}`, `{{SO_CCCD}}`, `{{NGAY_KY}}`):
|
|
199
|
+
|
|
200
|
+
👉 **Nên dùng `setTemplateVariables`** thay vì `searchReplaceCurrent`:
|
|
201
|
+
- `setTemplateVariables` cập nhật nguyên tử, bảo toàn format chữ, không phá vỡ cấu trúc và có lịch sử Audit Trail.
|
|
202
|
+
- `searchReplaceCurrent` phù hợp cho tìm kiếm / thay thế văn bản thông thường của người dùng.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// Điền tự động các trường dữ liệu hợp đồng
|
|
206
|
+
await editor.setTemplateVariables({
|
|
207
|
+
HO_TEN: "Nguyễn Văn A",
|
|
208
|
+
SO_CCCD: "0123456789",
|
|
209
|
+
NGAY_KY: "10/10/2026"
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 4. Bảng tóm tắt các tính năng
|
|
216
|
+
|
|
217
|
+
| Nhu cầu | Hàm đề xuất | Tự động Scroll & Focus? |
|
|
218
|
+
| :--- | :--- | :---: |
|
|
219
|
+
| Tìm từ khóa và nhảy đến vị trí đó | `editor.search(query, opts)` | Có (`gotoMatch` -> `paint.ensureVisible`) |
|
|
220
|
+
| Xem kết quả kế tiếp / trước đó | `editor.searchNav(1)` / `(-1)` | Có |
|
|
221
|
+
| Thay thế từ đang chọn và nhảy đến từ kế tiếp | `editor.searchReplaceCurrent(newText)` | Có |
|
|
222
|
+
| Thay thế toàn bộ tài liệu | `editor.searchReplaceAll(newText)` | Giữ nguyên selection |
|
|
223
|
+
| Cuộn đến một đoạn bất kỳ theo Range | `editor.revealRange(range, options)` | Có (Smooth animation) |
|
|
224
|
+
| Xóa highlight tìm kiếm | `editor.searchClear()` | - |
|
|
@@ -514,8 +514,43 @@ Mỗi khi cấu hình mã QR được cập nhật (`editor.setQRCode(...)`), h
|
|
|
514
514
|
|
|
515
515
|
---
|
|
516
516
|
|
|
517
|
-
## 11.
|
|
517
|
+
## 11. Xử lý Điều Hướng Trang Khi Quét QR (`goToPage`)
|
|
518
518
|
|
|
519
|
+
Khi người dùng quét mã QR trên bản in hoặc văn bản điện tử, đường dẫn mở ra sẽ chứa tham số trang:
|
|
520
|
+
```text
|
|
521
|
+
https://my-hospital.vn/verify/BA-2026-0012?page=3
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Ứng dụng Host FE chỉ cần đọc tham số `?page=N` từ URL và gọi `goToPage` trên `KindyEditor` hoặc `EditorHandle`:
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
528
|
+
const pageParam = urlParams.get("page");
|
|
529
|
+
|
|
530
|
+
if (pageParam) {
|
|
531
|
+
const page = parseInt(pageParam, 10);
|
|
532
|
+
// Tự động chờ layout render xong, tự scroll chuẩn xác container Kindy
|
|
533
|
+
const success = await editor.goToPage(page, { behavior: "smooth" });
|
|
534
|
+
if (!success) {
|
|
535
|
+
console.warn("Không thể chuyển tới trang:", page);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Các ưu điểm nổi bật của `goToPage`:
|
|
541
|
+
- **`pageNumber` bắt đầu từ 1** (1-based).
|
|
542
|
+
- **Tự động chờ layout hoàn tất**: Tự đợi layout engine tính toán và DOM mount xong trước khi scroll, giải quyết triệt để lỗi scroll hụt khi vừa mở tài liệu.
|
|
543
|
+
- **Tự scroll đúng container Kindy**: Cuộn chính xác thanh cuộn `.ked-app` của editor, không làm lệch hay cuộn trang web bên ngoài của host.
|
|
544
|
+
- **Tự clamp trang**: Nếu số trang truyền vào vượt quá tổng số trang tài liệu (ví dụ trang 10 nhưng tài liệu chỉ có 4 trang), editor tự động cuộn đến trang cuối cùng.
|
|
545
|
+
- **Kiểm tra hợp lệ an toàn**: Trả về `false` nếu `pageNumber` không hợp lệ (`<= 0`, `NaN`, chuỗi không phải số).
|
|
546
|
+
- **Hoạt động đa nền tảng & đa chế độ**: Hoạt động hoàn hảo trong mọi chế độ (`view`, `comment`, `edit`), chế độ toàn màn hình (fullscreen) cũng như giao diện thu gọn trên điện thoại di động (mobile).
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## 12. Lịch sử Cập nhật (Update Log)
|
|
551
|
+
|
|
552
|
+
* **v3.2.5**:
|
|
553
|
+
- ✅ **Bổ sung `goToPage(pageNumber, options)`**: Hỗ trợ cuộn mượt hoặc tức thì tới trang chỉ định theo tham số `?page=N` từ URL khi quét mã QR, tự động chờ layout render và tự clamp trang.
|
|
519
554
|
* **v3.2.4**:
|
|
520
555
|
- ✅ **Tự động sinh `?page=N` (`autoPageParam`)**: Chỉ cần truyền URL gốc (ví dụ: `https://my-hospital.vn/verify/{docId}`), thư viện sẽ tự động phân tích và sinh thêm `?page=1`, `?page=2`... cho từng trang. Tự động dùng `&page=N` nếu URL đã có sẵn tham số.
|
|
521
556
|
- ✅ **Chuẩn hóa URL Parameter**: Đổi từ `?page={page}/{totalPages}` sang chuẩn web `?page={page}` (ví dụ: `?page=1`, `?page=2`).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Security review — editor hardening
|
|
2
|
+
|
|
3
|
+
**Reviewed:** 2026-10-09
|
|
4
|
+
**Scope:** editor repository changes based on `c1aef59f`, covering signature demo DOM rendering, host table mutation permissions and input limits, DOCX archive import, QR configuration logging, and JavaScript development/build dependencies. This is a scoped code review, not a penetration test or product-wide compliance assessment.
|
|
5
|
+
|
|
6
|
+
## Changes made
|
|
7
|
+
|
|
8
|
+
- Signature metadata from imported documents is written with DOM `textContent`; the demo's dynamic log messages are text nodes. Fixed placeholder HTML was replaced with DOM construction as well.
|
|
9
|
+
- Host table mutation remains available in normal edit mode. It is off in view/comment modes and turns off when `editContent: false` unless the host explicitly enables `hostTableMutation`. Hosts still need to authorize users and documents at their own trusted boundary.
|
|
10
|
+
- Host table APIs reject over-budget tables and replacements before allocating new cells: 10,000 rows, 256 columns, 50,000 cells, 1,000,000 text characters, 100,000 characters or 1,024 paragraphs per cell, 20,000 paragraphs, and 8 nested table levels.
|
|
11
|
+
- DOCX import rejects archives over 64 MiB compressed, more than 4,096 entries, an entry over 32 MiB expanded, or more than 128 MiB expanded in total. The file/Blob size is checked before reading it into memory.
|
|
12
|
+
- QR configuration values are no longer printed to the developer console. `.env*` files are ignored, with `.env.example` permitted.
|
|
13
|
+
- Updated Vite to 6.4.4, Vitest to 4.1.11, and Vue to 3.5.43. A workspace override keeps VitePress and its Vite plugins on Vite 6.4.4, which meets their published peer ranges.
|
|
14
|
+
|
|
15
|
+
## Verification
|
|
16
|
+
|
|
17
|
+
- Focused regression/security tests: **PASS**, 36 tests across table mutation, permissions, DOCX archive limits, and signature rendering, using Vitest 4.1.11.
|
|
18
|
+
- Production library build: **PASS**, Vite 6.4.4.
|
|
19
|
+
- Production dependency audit (`npm audit --omit=dev`): **PASS**, 0 reported findings.
|
|
20
|
+
- Typecheck: **FAIL**. The repository has existing errors in demo/test fixtures, including model shape mismatches and stale editor APIs. This scoped change also revealed no retained `defaultReviewAccessCan` missing-import error; full typecheck remains blocked by the listed broader issues.
|
|
21
|
+
- `npm audit`: **12 findings remain**: 4 high, 2 moderate, and 6 low. High findings are in the `patch-package` → `find-yarn-workspace-root` → `micromatch`/`braces` install tooling chain; npm offers only a major downgrade to patch-package 6.0.7. Remaining moderate findings are `pbkdf2` and `qs`; remaining low findings are in the `vite-plugin-node-polyfills`/`node-stdlib-browser` compatibility chain, whose npm fix also requires a major downgrade. These were not changed automatically because doing so would alter the patch workflow or browser polyfill support. Review and replace these toolchain components before production release if their install/build risk is unacceptable.
|
|
22
|
+
- Secret-pattern scan: **PASS for the checked patterns only** across tracked files and this reviewed change (0 matches); it does not prove the entire Git history is free of secrets.
|
|
23
|
+
|
|
24
|
+
## Remaining limits and risk
|
|
25
|
+
|
|
26
|
+
This review does not verify server authentication/authorization, hosting headers/CSP, CORS, CSRF, production secrets, external API rate limits, or runtime behavior in every host integration. The DOCX byte budgets bound archive expansion; malformed files and parser complexity still need normal failure handling. No browser exploit exercise or complete Git-history secrets scan was run.
|
package/docs/table-editing.md
CHANGED
|
@@ -26,7 +26,105 @@ Vì vậy thao tác chèn/xóa hàng hoặc cột, merge/unmerge và undo đều
|
|
|
26
26
|
|
|
27
27
|
Mọi structural command là một transaction duy nhất, nên Undo/Redo không để lại bảng ở trạng thái trung gian.
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Thao tác bảng từ Host Application trong View Mode (`mode: "view"`)
|
|
30
|
+
|
|
31
|
+
Khi ứng dụng chạy ở chế độ xem (`mode: "view"`), người dùng **không thể click hoặc chỉnh sửa canvas trực tiếp**. Toolbar, thanh hover rail và context menu chỉnh sửa hoàn toàn bị khóa.
|
|
32
|
+
|
|
33
|
+
Trong `edit` mode, các thao tác bảng sẵn có được bật như trước. Nếu đặt `editContent: false`, thao tác bảng từ host sẽ tự tắt trừ khi ứng dụng cấp rõ ràng `hostTableMutation: true` sau khi xác thực actor và tài liệu tại lớp host/backend. Trong `view`/`comment` mode, quyền này mặc định tắt:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const editor = new KindyEditor({
|
|
37
|
+
container: document.getElementById("editor"),
|
|
38
|
+
mode: "view",
|
|
39
|
+
permissions: {
|
|
40
|
+
hostTableMutation: true,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Các giới hạn để tránh payload làm nghẽn giao diện: tối đa 10.000 hàng đầu vào, 256 cột mỗi hàng, 50.000 ô trong bảng sau khi thay thế, 1.000.000 ký tự tổng, 100.000 ký tự và 1.024 đoạn trong một ô, 20.000 paragraph và 8 tầng bảng lồng. API trả `false` khi vượt giới hạn; bảng giữ nguyên.
|
|
46
|
+
|
|
47
|
+
### Các API thao tác bảng từ Host
|
|
48
|
+
|
|
49
|
+
#### 1. Lấy danh sách bảng (`getTables`)
|
|
50
|
+
```ts
|
|
51
|
+
const tables = editor.getTables();
|
|
52
|
+
// [
|
|
53
|
+
// {
|
|
54
|
+
// tableId: "table_1",
|
|
55
|
+
// tablePath: { story: { kind: "body" }, ancestors: [], tableId: "table_1" },
|
|
56
|
+
// rows: 5,
|
|
57
|
+
// cols: 3,
|
|
58
|
+
// header: ["STT", "Tên hàng", "Thành tiền"],
|
|
59
|
+
// hasMergedCells: false
|
|
60
|
+
// }
|
|
61
|
+
// ]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### 2. Thực thi Table Action với selection trực tiếp (`executeHostTableAction`)
|
|
65
|
+
Không cần người dùng click canvas. Host truyền trực tiếp `TableAction` cùng selection:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// Thêm 1 dòng dưới dòng index 1
|
|
69
|
+
await editor.executeHostTableAction("insertRowBelow", { tableId: "table_1", row: 1 });
|
|
70
|
+
|
|
71
|
+
// Thêm 1 cột bên phải cột index 0
|
|
72
|
+
await editor.executeHostTableAction("insertColumnRight", { tableId: "table_1", col: 0 });
|
|
73
|
+
|
|
74
|
+
// Xóa dòng index 2
|
|
75
|
+
await editor.executeHostTableAction("deleteRow", { tableId: "table_1", row: 2 });
|
|
76
|
+
|
|
77
|
+
// Merge các ô từ (1, 0) đến (1, 2)
|
|
78
|
+
await editor.executeHostTableAction("mergeCells", {
|
|
79
|
+
tableId: "table_1",
|
|
80
|
+
fromRow: 1,
|
|
81
|
+
toRow: 1,
|
|
82
|
+
fromCol: 0,
|
|
83
|
+
toCol: 2
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Xóa bảng
|
|
87
|
+
await editor.executeHostTableAction("deleteTable", "table_1");
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Kiểm tra action có khả thi trước khi thực thi:
|
|
91
|
+
```ts
|
|
92
|
+
if (editor.canExecuteHostTableAction("deleteRow", { tableId: "table_1", row: 1 })) {
|
|
93
|
+
await editor.executeHostTableAction("deleteRow", { tableId: "table_1", row: 1 });
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### 3. Nhập giá trị vào 1 ô (`setTableCellValue`)
|
|
98
|
+
Cập nhật nội dung một ô theo tọa độ `(row, column)`. Tự động giữ nguyên định dạng cell/paragraph (font, kích thước, căn lề, màu sắc) và hỗ trợ xuống dòng:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
await editor.setTableCellValue("table_1", 1, 2, "250.000 đ");
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### 4. Thay thế nhiều dòng dữ liệu cùng lúc (`replaceTableRows`)
|
|
105
|
+
Cập nhật hàng loạt dòng dữ liệu trong một transaction nguyên tử (atomic update).
|
|
106
|
+
- Tự động giữ format từ dòng mẫu (`sampleRowIndex`).
|
|
107
|
+
- Giữ nguyên header (`headerRows`) và footer (`footerRows`).
|
|
108
|
+
- Tự động sinh ID duy nhất cho cell/paragraph mới.
|
|
109
|
+
- Validate cấu trúc bảng và render canvas đúng một lần.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
await editor.replaceTableRows(
|
|
113
|
+
"table_1",
|
|
114
|
+
[
|
|
115
|
+
["1", "Sản phẩm A", "100.000"],
|
|
116
|
+
["2", "Sản phẩm B", "200.000"],
|
|
117
|
+
["3", "Sản phẩm C", "300.000"],
|
|
118
|
+
],
|
|
119
|
+
{
|
|
120
|
+
headerRows: 1, // Giữ nguyên dòng tiêu đề
|
|
121
|
+
footerRows: 1, // Giữ nguyên dòng tổng cộng
|
|
122
|
+
sampleRowIndex: 1, // Lấy định dạng font/style từ dòng mẫu 1
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## API tương tác canvas thông thường
|
|
30
128
|
|
|
31
129
|
```ts
|
|
32
130
|
import type { TablePath, TableSelection } from "kindy-editor";
|
|
@@ -533,7 +533,98 @@ Thanh công cụ Ribbon của Editor có độ rộng mặc định tương đ
|
|
|
533
533
|
|
|
534
534
|
---
|
|
535
535
|
|
|
536
|
-
## 10.
|
|
536
|
+
## 10. Thao Tác Table Native Trong View Mode (`mode: "view"`)
|
|
537
|
+
|
|
538
|
+
Nếu sản phẩm yêu cầu Kindy chạy ở chế độ **View Mode** (người dùng không thể gõ hoặc sửa trực tiếp trên canvas, toolbar/context menu/hover rail bị khóa), FE có thể dùng các API table native tích hợp sẵn:
|
|
539
|
+
|
|
540
|
+
### 10.1 Cấp quyền `hostTableMutation`
|
|
541
|
+
|
|
542
|
+
Quyền này mặc định tắt trong `view`/`comment` mode. Ở `edit` mode, thao tác bảng tích hợp vẫn được bật. Nếu đặt `editContent: false`, thao tác bảng từ host tự tắt trừ khi ứng dụng cấp rõ ràng `hostTableMutation: true` sau khi xác thực quyền người dùng và tài liệu. Đây là quyền ngoại lệ ở API client, không thay thế kiểm tra quyền backend.
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
const editor = new KindyEditor({
|
|
546
|
+
container: document.getElementById("editor-container"),
|
|
547
|
+
mode: "view",
|
|
548
|
+
permissions: {
|
|
549
|
+
hostTableMutation: true, // Cho phép host thao tác bảng trong View Mode
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Giới hạn đầu vào `replaceTableRows`: 10.000 hàng, 256 cột mỗi hàng, tối đa 50.000 ô sau khi thay thế, 1.000.000 ký tự tổng, 100.000 ký tự/1.024 paragraph mỗi ô, tối đa 20.000 paragraph và 8 tầng bảng lồng. API trả `false` nếu vượt ngưỡng và không ghi thay đổi.
|
|
555
|
+
|
|
556
|
+
### 10.2 Lấy danh sách bảng (`editor.getTables()`)
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
const tables = editor.getTables();
|
|
560
|
+
// tables[0] -> {
|
|
561
|
+
// tableId: "tbl_invoice",
|
|
562
|
+
// rows: 4,
|
|
563
|
+
// cols: 5,
|
|
564
|
+
// header: ["STT", "Mã", "Tên dịch vụ", "Số lượng", "Đơn giá"],
|
|
565
|
+
// hasMergedCells: false
|
|
566
|
+
// }
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### 10.3 Cập nhật giá trị ô (`editor.setTableCellValue()`)
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
// Cập nhật ô dòng 1, cột 2 (0-indexed)
|
|
573
|
+
await editor.setTableCellValue("tbl_invoice", 1, 2, "Khám tổng quát");
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### 10.4 Thay thế nhiều dòng dữ liệu một lần (`editor.replaceTableRows()`)
|
|
577
|
+
|
|
578
|
+
Cập nhật hàng loạt dòng dữ liệu trong một transaction nguyên tử:
|
|
579
|
+
- Giữ nguyên số dòng header (`headerRows`) và footer (`footerRows`).
|
|
580
|
+
- Giữ format font, shading, border từ dòng mẫu (`sampleRowIndex`).
|
|
581
|
+
- Tự động sinh ID hợp lệ duy nhất cho từng cell và paragraph.
|
|
582
|
+
- Validate bảng và render canvas đúng một lần.
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
const rowsFromForm = [
|
|
586
|
+
["1", "DV01", "Siêu âm màu", "1", "150.000"],
|
|
587
|
+
["2", "DV02", "Xét nghiệm máu", "1", "200.000"],
|
|
588
|
+
["3", "DV03", "Chụp X-quang", "2", "300.000"],
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
await editor.replaceTableRows("tbl_invoice", rowsFromForm, {
|
|
592
|
+
headerRows: 1,
|
|
593
|
+
footerRows: 1,
|
|
594
|
+
sampleRowIndex: 1,
|
|
595
|
+
});
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### 10.5 Chạy Table Action từ FE (`editor.executeHostTableAction()`)
|
|
599
|
+
|
|
600
|
+
Tái sử dụng toàn bộ 9 Table Action mà không phụ thuộc con trỏ chuột:
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
// Thêm 1 dòng dưới dòng 1
|
|
604
|
+
await editor.executeHostTableAction("insertRowBelow", { tableId: "tbl_invoice", row: 1 });
|
|
605
|
+
|
|
606
|
+
// Thêm cột bên phải cột 2
|
|
607
|
+
await editor.executeHostTableAction("insertColumnRight", { tableId: "tbl_invoice", col: 2 });
|
|
608
|
+
|
|
609
|
+
// Xóa dòng 3
|
|
610
|
+
await editor.executeHostTableAction("deleteRow", { tableId: "tbl_invoice", row: 3 });
|
|
611
|
+
|
|
612
|
+
// Merge ô từ dòng 1 cột 0 đến dòng 1 cột 2
|
|
613
|
+
await editor.executeHostTableAction("mergeCells", {
|
|
614
|
+
tableId: "tbl_invoice",
|
|
615
|
+
fromRow: 1,
|
|
616
|
+
toRow: 1,
|
|
617
|
+
fromCol: 0,
|
|
618
|
+
toCol: 2,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Xóa bảng
|
|
622
|
+
await editor.executeHostTableAction("deleteTable", "tbl_invoice");
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## 11. Trải Nghiệm Trang Demo Trực Quan
|
|
537
628
|
|
|
538
629
|
Kindy Editor cung cấp sẵn trang demo tương tác đầy đủ các tính năng trên:
|
|
539
630
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kindy-editor",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Trình soạn thảo tài liệu Word chính xác, vẽ bằng canvas",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
"fontkit": "^2.0.4",
|
|
60
60
|
"pdfkit": "^0.19.0",
|
|
61
61
|
"txml": "^6.0.0",
|
|
62
|
-
"vite": "^6.0.0",
|
|
63
|
-
"vite-plugin-node-polyfills": "^0.28.0",
|
|
64
|
-
"vitest": "^3.0.0",
|
|
65
62
|
"typescript": "^5.9.2",
|
|
66
|
-
"
|
|
63
|
+
"vite": "^6.4.4",
|
|
64
|
+
"vite-plugin-node-polyfills": "^0.28.0",
|
|
65
|
+
"vitest": "^4.1.11",
|
|
66
|
+
"vue": "^3.5.43"
|
|
67
67
|
}
|
|
68
|
-
}
|
|
68
|
+
}
|
package/types/import.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export type { Document } from "./model";
|
|
|
10
10
|
|
|
11
11
|
export type ImportPhase = "unzip" | "styles" | "parse" | "map";
|
|
12
12
|
|
|
13
|
+
export type ImportErrorCode = "NOT_ZIP" | "ENCRYPTED" | "NO_DOCUMENT_PART" | "MALFORMED_XML" | "ARCHIVE_LIMIT";
|
|
14
|
+
|
|
15
|
+
/** Error raised when a DOCX is malformed, encrypted, missing its main part, or exceeds safe import limits. */
|
|
16
|
+
export declare class ImportError extends Error {
|
|
17
|
+
readonly code: ImportErrorCode;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
/** A lossy-mapping decision — surfaced, not swallowed (deduplicated by code). */
|
|
14
21
|
export interface ImportWarning {
|
|
15
22
|
code: string;
|