kindy-editor 3.2.6 → 3.2.8

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 (31) hide show
  1. package/dist-lib/assets/{worker-B7ALyZOw.js → worker-DjZBUOhB.js} +1009 -997
  2. package/dist-lib/assets/{worker-z-dyEi8F.js → worker-Du8hmveE.js} +2234 -2222
  3. package/dist-lib/builder.js +4 -4
  4. package/dist-lib/events.js +9 -120
  5. package/dist-lib/export.js +9 -11
  6. package/dist-lib/generate-toc.js +3 -3
  7. package/dist-lib/import.js +2 -2
  8. package/dist-lib/index-D5dkvB0x.js +490 -0
  9. package/dist-lib/{editorApp-Bu3dbUIU.js → kindy-editor-T1YCQdwF.js} +13483 -11185
  10. package/dist-lib/kindy-editor.js +4 -4
  11. package/dist-lib/measure.js +3 -4
  12. package/dist-lib/{measureHost-CpqJsipA.js → measureHost-BG9FPivu.js} +17 -18
  13. package/dist-lib/{paintStyle-Cb7IGKPW.js → metrics-D7Ma2xlj.js} +175 -148
  14. package/dist-lib/{parse-CKw-ZDr9.js → parse-BsUkqk-n.js} +3 -3
  15. package/dist-lib/{pipeline-BkC7G8gU.js → pipeline-CX_Hc5BD.js} +850 -837
  16. package/dist-lib/{qrcode-oNwKusWD.js → qrcode--5MhVNEZ.js} +1 -1
  17. package/dist-lib/{qrcode-efBlPzzh.js → qrcode-CR8i1-w0.js} +11 -11
  18. package/dist-lib/recalc-docx.js +1 -1
  19. package/dist-lib/{recalcToc-Bc8V9Ecn.js → recalcToc-zAFS4S6S.js} +755 -742
  20. package/dist-lib/{stylesheet-BT1DGhnx.js → stylesheet-BfxJTGA5.js} +1 -1
  21. package/dist-lib/{webmcp-ClHEa6Gq.js → webmcp-Ds469lXP.js} +2 -2
  22. package/docs/find-and-replace.md +224 -0
  23. package/docs/security-review.md +26 -0
  24. package/docs/table-editing.md +17 -2
  25. package/docs/table-form-integration.md +53 -1
  26. package/package.json +6 -6
  27. package/types/import.d.ts +7 -0
  28. package/types/kindy-editor.d.ts +30 -0
  29. package/dist-lib/changeAggregator-ZI959nPP.js +0 -376
  30. package/dist-lib/kindy-editor-CToKn_gP.js +0 -2171
  31. package/dist-lib/metrics-CdfHD-8y.js +0 -31
@@ -1,4 +1,4 @@
1
- import { p as u } from "./qrcode-efBlPzzh.js";
1
+ import { p as u } from "./qrcode-CR8i1-w0.js";
2
2
  function p(e) {
3
3
  return e.type ?? "paragraph";
4
4
  }
@@ -1,4 +1,4 @@
1
- import { p as j, c as _, e as F, t as k, D as L } from "./qrcode-efBlPzzh.js";
1
+ import { p as j, c as _, e as F, t as k, D as L } from "./qrcode-CR8i1-w0.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-Bu3dbUIU.js").then((m) => m.c);
335
+ const { insertText: d } = await import("./kindy-editor-T1YCQdwF.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()` | - |
@@ -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.
@@ -30,7 +30,7 @@ Mọi structural command là một transaction duy nhất, nên Undo/Redo không
30
30
 
31
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
32
 
33
- Để cho phép ứng dụng FE (form ngoài, modal, wizard) thao tác và cập nhật bảng bằng code mà người dùng vẫn ở View Mode, cấp quyền `hostTableMutation: true`:
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
34
 
35
35
  ```ts
36
36
  const editor = new KindyEditor({
@@ -42,6 +42,8 @@ const editor = new KindyEditor({
42
42
  });
43
43
  ```
44
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
+
45
47
  ### Các API thao tác bảng từ Host
46
48
 
47
49
  #### 1. Lấy danh sách bảng (`getTables`)
@@ -93,11 +95,13 @@ if (editor.canExecuteHostTableAction("deleteRow", { tableId: "table_1", row: 1 }
93
95
  ```
94
96
 
95
97
  #### 3. Nhập giá trị vào 1 ô (`setTableCellValue`)
96
- 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:
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), tăng `revision`, giải phóng layout cache của bảng, tự động phân trang lại khi kích thước hàng thay đổi và lên lịch repaint mượt mà qua `requestAnimationFrame`:
97
99
 
98
100
  ```ts
99
101
  await editor.setTableCellValue("table_1", 1, 2, "250.000 đ");
100
102
  ```
103
+ - **Không cần gọi `setDocument()`**: Thay đổi được áp dụng ngay lập tức mà không phải rebuild lại DOM editor.
104
+ - **Giữ nguyên trạng thái**: Bảo toàn `mode="view"`, vị trí scroll, zoom và vùng chọn ô (`cellSelection`).
101
105
 
102
106
  #### 4. Thay thế nhiều dòng dữ liệu cùng lúc (`replaceTableRows`)
103
107
  Cập nhật hàng loạt dòng dữ liệu trong một transaction nguyên tử (atomic update).
@@ -122,6 +126,17 @@ await editor.replaceTableRows(
122
126
  );
123
127
  ```
124
128
 
129
+ #### 5. Giải phóng Cache Layout thủ công (`invalidateLayout`)
130
+ Ép buộc giải phóng cache đo đạc layout của bảng hoặc toàn bộ tài liệu:
131
+
132
+ ```ts
133
+ // Giải phóng cache bảng và repaint
134
+ await editor.invalidateLayout("table_1");
135
+
136
+ // Hoặc giải phóng toàn bộ cache
137
+ await editor.invalidateLayout();
138
+ ```
139
+
125
140
  ## API tương tác canvas thông thường
126
141
 
127
142
  ```ts
@@ -539,6 +539,8 @@ Nếu sản phẩm yêu cầu Kindy chạy ở chế độ **View Mode** (ngư
539
539
 
540
540
  ### 10.1 Cấp quyền `hostTableMutation`
541
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
+
542
544
  ```ts
543
545
  const editor = new KindyEditor({
544
546
  container: document.getElementById("editor-container"),
@@ -549,6 +551,8 @@ const editor = new KindyEditor({
549
551
  });
550
552
  ```
551
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
+
552
556
  ### 10.2 Lấy danh sách bảng (`editor.getTables()`)
553
557
 
554
558
  ```ts
@@ -564,11 +568,19 @@ const tables = editor.getTables();
564
568
 
565
569
  ### 10.3 Cập nhật giá trị ô (`editor.setTableCellValue()`)
566
570
 
571
+ Cập nhật nội dung một ô theo tọa độ `(row, column)`. Tự động tăng `revision`, giải phóng layout cache (`linesCache`, `tableCache`), tự động phân trang lại nếu chiều cao hàng thay đổi, và lên lịch repaint mượt mà qua `requestAnimationFrame`:
572
+
567
573
  ```ts
568
574
  // Cập nhật ô dòng 1, cột 2 (0-indexed)
569
575
  await editor.setTableCellValue("tbl_invoice", 1, 2, "Khám tổng quát");
570
576
  ```
571
577
 
578
+ ::: tip Tự động Repaint & Giữ Nguyên Viewport
579
+ - **Không cần gọi `setDocument()`**: Canvas sẽ tự động tính lại layout và repaint ngay sau khi gọi `setTableCellValue()`.
580
+ - **Bảo toàn trạng thái**: Giữ nguyên `mode="view"`, vị trí scroll, tỷ lệ zoom và vùng chọn ô (`cellSelection`).
581
+ - **Bảng tách nhiều trang (Multi-page Fragments)**: Nếu nội dung dài làm tăng chiều cao hàng và đẩy các hàng sang trang sau, hệ thống tự động phân trang lại và repaint tất cả các trang/fragment bị ảnh hưởng.
582
+ :::
583
+
572
584
  ### 10.4 Thay thế nhiều dòng dữ liệu một lần (`editor.replaceTableRows()`)
573
585
 
574
586
  Cập nhật hàng loạt dòng dữ liệu trong một transaction nguyên tử:
@@ -618,9 +630,49 @@ await editor.executeHostTableAction("mergeCells", {
618
630
  await editor.executeHostTableAction("deleteTable", "tbl_invoice");
619
631
  ```
620
632
 
633
+ ### 10.6 Giải phóng Cache Layout thủ công (`editor.invalidateLayout()`)
634
+
635
+ Nếu cần ép buộc làm mới cache đo đạc và vẽ lại một bảng hoặc toàn bộ tài liệu:
636
+
637
+ ```ts
638
+ // Xóa cache đo đạc của bảng cụ thể và repaint
639
+ await editor.invalidateLayout("tbl_invoice");
640
+
641
+ // Hoặc làm mới toàn bộ layout cache
642
+ await editor.invalidateLayout();
643
+ ```
644
+
645
+ ---
646
+
647
+ ## 11. Tối Ưu Hiệu Năng: Nhập Liệu Liên Tục Không Gây Giật
648
+
649
+ ::: warning Tuyệt đối không gọi `setDocument()` khi người dùng đang nhập Form
650
+ Phương thức `setDocument()` thực hiện hủy và tái tạo toàn bộ DOM của Editor (`editor.destroy()` + mount lại). Nếu gọi `setDocument()` trên mỗi sự kiện `input` / `change` của form, canvas sẽ bị nhấp nháy trắng, tụt FPS và nhảy vị trí cuộn.
651
+
652
+ **Hãy dùng `setTableCellValue()` kết hợp gom frame bằng `requestAnimationFrame`:**
653
+ :::
654
+
655
+ ```ts
656
+ let pendingRaf: number | null = null;
657
+ let pendingValue = "";
658
+
659
+ function onFormCellInput(newValue: string) {
660
+ pendingValue = newValue;
661
+ if (pendingRaf !== null) return;
662
+
663
+ pendingRaf = requestAnimationFrame(async () => {
664
+ pendingRaf = null;
665
+ await editor.setTableCellValue("tbl_invoice", 1, 2, pendingValue);
666
+ });
667
+ }
668
+ ```
669
+ Nhờ cơ chế này:
670
+ - Trình duyệt chỉ cập nhật cell và repaint 1 lần mỗi khung hình (60/120 FPS).
671
+ - Trải nghiệm nhập liệu ở Form ngoài sang Canvas đạt độ mượt mà tối đa.
672
+
621
673
  ---
622
674
 
623
- ## 11. Trải Nghiệm Trang Demo Trực Quan
675
+ ## 12. Trải Nghiệm Trang Demo Trực Quan
624
676
 
625
677
  Kindy Editor cung cấp sẵn trang demo tương tác đầy đủ các tính năng trên:
626
678
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kindy-editor",
3
- "version": "3.2.6",
3
+ "version": "3.2.8",
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
- "vue": "^3.5.29"
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;
@@ -26,6 +26,12 @@ export interface Participant {
26
26
  user?: UserInfo;
27
27
  }
28
28
 
29
+ /** Search and replace state. */
30
+ export interface SearchState {
31
+ index: number;
32
+ total: number;
33
+ }
34
+
29
35
  /** A WeSign signing box with laid-out geometry. `page` is 1-based for WeSign
30
36
  * `PositionInfoRequest.page`; `pageIndex` is the 0-based layout index. */
31
37
  export interface SignatureBox {
@@ -985,6 +991,8 @@ export interface EditorHandle {
985
991
  options?: ReplaceTableRowsOptions,
986
992
  ): boolean;
987
993
  getTables(): TableSummary[];
994
+ /** Invalidate layout cache (for a specific table or full relayout) and repaint. */
995
+ invalidateLayout(blockId?: string): void;
988
996
  // ---- review layer (track changes + comments) ----------------------------
989
997
  /** The current editor mode. */
990
998
  getMode(): EditMode;
@@ -1026,6 +1034,16 @@ export interface EditorHandle {
1026
1034
  pageNumber: number,
1027
1035
  options?: { behavior?: "auto" | "smooth" },
1028
1036
  ): Promise<boolean>;
1037
+ /** Find & replace. search() highlights all matches, focuses first match, and returns state. */
1038
+ search(query: string, opts?: { matchCase?: boolean; wholeWord?: boolean }): SearchState;
1039
+ /** Navigate between search matches (-1 for previous, 1 for next) and scroll focus to it. */
1040
+ searchNav(dir: 1 | -1): SearchState;
1041
+ /** Replace the currently focused match with text and scroll focus to the next match. */
1042
+ searchReplaceCurrent(replacement: string): SearchState;
1043
+ /** Replace all matches in one atomic transaction. */
1044
+ searchReplaceAll(replacement: string): number;
1045
+ /** Clear search highlights and reset search state. */
1046
+ searchClear(): void;
1029
1047
  /** Live-update {{KEY}} template placeholders in the document without full reload,
1030
1048
  * preserving zoom, scroll position, and selection. */
1031
1049
  setTemplateVariables(values: TemplateVariableValues, options?: SetTemplateVariablesOptions): void;
@@ -1157,6 +1175,8 @@ export declare class KindyEditor {
1157
1175
  options?: ReplaceTableRowsOptions,
1158
1176
  ): Promise<boolean>;
1159
1177
  getTables(): TableSummary[];
1178
+ /** Invalidate layout cache (for a specific table or full relayout) and repaint. */
1179
+ invalidateLayout(blockId?: string): Promise<void>;
1160
1180
  getDocId(): string | null;
1161
1181
  getShareLink(): string | null;
1162
1182
  /** Render ephemeral highlight decorations for external comments/annotations.
@@ -1218,5 +1238,15 @@ export declare class KindyEditor {
1218
1238
  setQRCode(config: QRCodeConfig | null): void;
1219
1239
  /** Get current header/footer QR code configuration. */
1220
1240
  getQRCode(): QRCodeConfig | undefined;
1241
+ /** Find & replace. search() highlights all matches, focuses first match, and returns state. */
1242
+ search(query: string, opts?: { matchCase?: boolean; wholeWord?: boolean }): Promise<SearchState>;
1243
+ /** Navigate between search matches (-1 for previous, 1 for next) and scroll focus to it. */
1244
+ searchNav(dir: 1 | -1): Promise<SearchState>;
1245
+ /** Replace the currently focused match with text and scroll focus to the next match. */
1246
+ searchReplaceCurrent(replacement: string): Promise<SearchState>;
1247
+ /** Replace all search matches in document in a single atomic undoable transaction. */
1248
+ searchReplaceAll(replacement: string): Promise<number>;
1249
+ /** Clear search highlights and reset search state. */
1250
+ searchClear(): Promise<void>;
1221
1251
  destroy(): void;
1222
1252
  }