kindy-editor 3.2.5 → 3.2.6

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.
@@ -2,7 +2,7 @@ import "./qrcode-efBlPzzh.js";
2
2
  import { m as x } from "./stylesheet-BT1DGhnx.js";
3
3
  import { H as m, A as f, C as v, b as y, I as w, T as C, F as T, d as S, e as L, E as D, D as P } from "./paintStyle-Cb7IGKPW.js";
4
4
  import { E as A } from "./changeAggregator-ZI959nPP.js";
5
- const z = 1.1, B = 0.25, R = 5, M = 36, p = {
5
+ const z = 1.1, B = 0.25, R = 5, M = 36, c = {
6
6
  canvasBackground: "#e8eaed",
7
7
  grid: P,
8
8
  gridMesh: "rgba(0,0,0,0.08)",
@@ -37,10 +37,10 @@ const z = 1.1, B = 0.25, R = 5, M = 36, p = {
37
37
  };
38
38
  function E(r) {
39
39
  return r ? {
40
- ...p,
40
+ ...c,
41
41
  ...l(r),
42
- ruler: { ...p.ruler, ...r.ruler ? l(r.ruler) : {} }
43
- } : p;
42
+ ruler: { ...c.ruler, ...r.ruler ? l(r.ruler) : {} }
43
+ } : c;
44
44
  }
45
45
  function F(r) {
46
46
  return r ? { ...g, ...l(r) } : g;
@@ -77,10 +77,10 @@ function O(r) {
77
77
  }
78
78
  function l(r) {
79
79
  const e = {};
80
- for (const [t, n] of Object.entries(r)) n !== void 0 && (e[t] = n);
80
+ for (const [t, o] of Object.entries(r)) o !== void 0 && (e[t] = o);
81
81
  return e;
82
82
  }
83
- const N = "kindy-editor-styles", H = `
83
+ const H = "kindy-editor-styles", N = `
84
84
  /* ===== Design tokens (Office 365 / Fluent-inspired palette) ========= */
85
85
  .kindy-editor-root {
86
86
  /* Brand blues */
@@ -783,39 +783,39 @@ function h(r, e) {
783
783
  t.id = r, t.textContent = e, document.head.appendChild(t);
784
784
  }
785
785
  function J() {
786
- h(N, H);
786
+ h(H, N);
787
787
  }
788
788
  const U = `
789
789
  .ked-drag-handle{cursor:move;user-select:none;}
790
790
  .ked-drag-handle::before{content:"⠿";color:#b0b4ba;font-size:14px;line-height:1;margin-right:4px;flex:0 0 auto;}`;
791
791
  function Z(r) {
792
792
  h("ked-drag-handle-styles", U);
793
- const { backdrop: e, modal: t, handle: n, signal: o } = r;
794
- e.style.pointerEvents = "none", e.style.background = "transparent", e.style.display = "block", t.style.position = "fixed", t.style.margin = "0", t.style.pointerEvents = "auto", n.classList.add("ked-drag-handle");
793
+ const { backdrop: e, modal: t, handle: o, signal: n } = r;
794
+ e.style.pointerEvents = "none", e.style.background = "transparent", e.style.display = "block", t.style.position = "fixed", t.style.margin = "0", t.style.pointerEvents = "auto", o.classList.add("ked-drag-handle");
795
795
  const d = (a, s) => {
796
796
  const b = Math.max(4, window.innerWidth - t.offsetWidth - 6), k = Math.max(4, window.innerHeight - 44);
797
797
  t.style.left = `${Math.min(Math.max(4, a), b)}px`, t.style.top = `${Math.min(Math.max(4, s), k)}px`;
798
798
  };
799
799
  d(window.innerWidth - t.offsetWidth - 24, 72);
800
800
  let i = null;
801
- n.addEventListener(
801
+ o.addEventListener(
802
802
  "mousedown",
803
803
  (a) => {
804
804
  if (r.noDrag && a.target.closest(r.noDrag)) return;
805
805
  const s = t.getBoundingClientRect();
806
806
  i = { dx: a.clientX - s.left, dy: a.clientY - s.top }, a.preventDefault();
807
807
  },
808
- { signal: o }
808
+ { signal: n }
809
809
  ), window.addEventListener("mousemove", (a) => {
810
810
  i && d(a.clientX - i.dx, a.clientY - i.dy);
811
- }, { signal: o }), window.addEventListener("mouseup", () => {
811
+ }, { signal: n }), window.addEventListener("mouseup", () => {
812
812
  i = null;
813
- }, { signal: o });
813
+ }, { signal: n });
814
814
  }
815
- const c = 0.15;
815
+ const p = 0.15;
816
816
  function Y(r, e) {
817
817
  const t = e > 0 ? r / e : 1;
818
- return { phase: "fonts", loaded: r, total: e, percent: c + (1 - c) * t };
818
+ return { phase: "fonts", loaded: r, total: e, percent: p + (1 - p) * t };
819
819
  }
820
820
  const u = {
821
821
  common: {
@@ -1298,7 +1298,7 @@ const u = {
1298
1298
  submit: "Comment",
1299
1299
  replyPlaceholder: "Reply…"
1300
1300
  }
1301
- }, K = {
1301
+ }, V = {
1302
1302
  common: {
1303
1303
  ok: "Đồng ý",
1304
1304
  cancel: "Hủy",
@@ -1779,13 +1779,13 @@ const u = {
1779
1779
  submit: "Nhận xét",
1780
1780
  replyPlaceholder: "Trả lời…"
1781
1781
  }
1782
- }, V = {
1782
+ }, K = {
1783
1783
  en: u,
1784
- vi: K
1784
+ vi: V
1785
1785
  };
1786
1786
  function j(r) {
1787
1787
  const t = (r ?? (typeof navigator < "u" ? navigator.language : "en") ?? "en").split(/[-_]/)[0].toLowerCase();
1788
- return V[t] ?? u;
1788
+ return K[t] ?? u;
1789
1789
  }
1790
1790
  class $ {
1791
1791
  /** Namespaced, typed public integration events. Legacy on()/off() remains. */
@@ -1796,7 +1796,7 @@ class $ {
1796
1796
  handlers = /* @__PURE__ */ new Map();
1797
1797
  constructor(e) {
1798
1798
  this.pendingQRCode = e.qrCode, this.events = new A(e.events), this.ready = new Promise((t) => {
1799
- const n = {
1799
+ const o = {
1800
1800
  container: e.container,
1801
1801
  backendUrl: e.backendUrl,
1802
1802
  // `docId` is the canonical name; fall back to the deprecated `collabId`.
@@ -1828,27 +1828,27 @@ class $ {
1828
1828
  onVariableClick: e.onVariableClick,
1829
1829
  showVariablePopover: e.showVariablePopover,
1830
1830
  qrCode: e.qrCode,
1831
- onReady: (o) => {
1832
- this.handle = o, this.pendingQRCode !== void 0 && o.setQRCode(this.pendingQRCode), t(o);
1831
+ onReady: (n) => {
1832
+ this.handle = n, this.pendingQRCode !== void 0 && n.setQRCode(this.pendingQRCode), t(n);
1833
1833
  },
1834
- onEvent: (o) => this.emit(o),
1835
- onPublicEvent: (o) => this.events.dispatch(o)
1834
+ onEvent: (n) => this.emit(n),
1835
+ onPublicEvent: (n) => this.events.dispatch(n)
1836
1836
  };
1837
- e.onLoadProgress?.({ phase: "bundle", percent: 0, loaded: 0, total: 0 }), import("./editorApp-B6qDC4lD.js").then((o) => o.e).then((o) => (e.onLoadProgress?.({ phase: "bundle", percent: c, loaded: 0, total: 0 }), o.mountEditorApp(n)));
1837
+ e.onLoadProgress?.({ phase: "bundle", percent: 0, loaded: 0, total: 0 }), import("./editorApp-Bu3dbUIU.js").then((n) => n.e).then((n) => (e.onLoadProgress?.({ phase: "bundle", percent: p, loaded: 0, total: 0 }), n.mountEditorApp(o)));
1838
1838
  });
1839
1839
  }
1840
1840
  /** Subscribe to a collaboration event. Returns an unsubscribe function. */
1841
1841
  on(e, t) {
1842
- let n = this.handlers.get(e);
1843
- return n || this.handlers.set(e, n = /* @__PURE__ */ new Set()), n.add(t), () => this.off(e, t);
1842
+ let o = this.handlers.get(e);
1843
+ return o || this.handlers.set(e, o = /* @__PURE__ */ new Set()), o.add(t), () => this.off(e, t);
1844
1844
  }
1845
1845
  /** Unsubscribe a handler previously passed to on(). */
1846
1846
  off(e, t) {
1847
1847
  this.handlers.get(e)?.delete(t);
1848
1848
  }
1849
1849
  emit(e) {
1850
- const { type: t, ...n } = e;
1851
- for (const o of this.handlers.get(t) ?? []) o(n);
1850
+ const { type: t, ...o } = e;
1851
+ for (const n of this.handlers.get(t) ?? []) n(o);
1852
1852
  }
1853
1853
  /** Resolves once the editor is mounted and ready. */
1854
1854
  whenReady() {
@@ -1882,20 +1882,20 @@ class $ {
1882
1882
  * is ready are buffered and flush on ready. (mountEditor() requires the editor
1883
1883
  * to be ready — await whenReady() first.) */
1884
1884
  createChild() {
1885
- let e = null, t = null, n = !1;
1886
- return this.ready.then((o) => {
1887
- n || (e = o.createChild(), t?.(), t = null);
1885
+ let e = null, t = null, o = !1;
1886
+ return this.ready.then((n) => {
1887
+ o || (e = n.createChild(), t?.(), t = null);
1888
1888
  }), {
1889
- render: (o, d, i) => {
1890
- const a = () => e?.render(o, d, i);
1889
+ render: (n, d, i) => {
1890
+ const a = () => e?.render(n, d, i);
1891
1891
  e ? a() : t = a;
1892
1892
  },
1893
- mountEditor: (o, d, i) => {
1893
+ mountEditor: (n, d, i) => {
1894
1894
  if (!e) throw new Error("KindyEditor.createChild(): await whenReady() before mountEditor()");
1895
- return e.mountEditor(o, d, i);
1895
+ return e.mountEditor(n, d, i);
1896
1896
  },
1897
1897
  destroy: () => {
1898
- n = !0, e?.destroy(), e = null;
1898
+ o = !0, e?.destroy(), e = null;
1899
1899
  }
1900
1900
  };
1901
1901
  }
@@ -1981,8 +1981,8 @@ class $ {
1981
1981
  async addComment(e, t) {
1982
1982
  return (await this.ready).addComment(e, t);
1983
1983
  }
1984
- async replyToComment(e, t, n) {
1985
- (await this.ready).replyToComment(e, t, n);
1984
+ async replyToComment(e, t, o) {
1985
+ (await this.ready).replyToComment(e, t, o);
1986
1986
  }
1987
1987
  async startComment() {
1988
1988
  (await this.ready).startComment();
@@ -1990,8 +1990,8 @@ class $ {
1990
1990
  async openCommentThread(e) {
1991
1991
  (await this.ready).openCommentThread(e);
1992
1992
  }
1993
- async editComment(e, t, n, o) {
1994
- (await this.ready).editComment(e, t, n, o);
1993
+ async editComment(e, t, o, n) {
1994
+ (await this.ready).editComment(e, t, o, n);
1995
1995
  }
1996
1996
  async deleteComment(e, t) {
1997
1997
  (await this.ready).deleteComment(e, t);
@@ -2023,6 +2023,26 @@ class $ {
2023
2023
  async executeTableAction(e) {
2024
2024
  return (await this.ready).executeTableAction(e);
2025
2025
  }
2026
+ /** Check if a table action can be executed by host with optional selection. */
2027
+ canExecuteHostTableAction(e, t) {
2028
+ return this.handle?.canExecuteHostTableAction(e, t) ?? !1;
2029
+ }
2030
+ /** Execute a table action from host/code, optionally targeting a specific selection or table path/ID. */
2031
+ async executeHostTableAction(e, t) {
2032
+ return (await this.ready).executeHostTableAction(e, t);
2033
+ }
2034
+ /** Update the text value of a single cell in a table. */
2035
+ async setTableCellValue(e, t, o, n) {
2036
+ return (await this.ready).setTableCellValue(e, t, o, n);
2037
+ }
2038
+ /** Replace data rows in a table in a single atomic batch update. */
2039
+ async replaceTableRows(e, t, o) {
2040
+ return (await this.ready).replaceTableRows(e, t, o);
2041
+ }
2042
+ /** Get summaries of all tables in the document. */
2043
+ getTables() {
2044
+ return this.handle?.getTables() ?? [];
2045
+ }
2026
2046
  getDocId() {
2027
2047
  return this.handle?.getDocId() ?? null;
2028
2048
  }
@@ -2042,14 +2062,18 @@ class $ {
2042
2062
  async revealRange(e, t) {
2043
2063
  (await this.ready).revealRange(e, t);
2044
2064
  }
2065
+ /** Scroll the viewport directly to the specified 1-based page number. */
2066
+ async goToPage(e, t) {
2067
+ return (await this.ready).goToPage(e, t);
2068
+ }
2045
2069
  /** Live-update {{KEY}} template placeholders in the document without full reload,
2046
2070
  * preserving zoom, scroll position, and selection. */
2047
2071
  async setTemplateVariables(e, t) {
2048
2072
  (await this.ready).setTemplateVariables(e, t);
2049
2073
  }
2050
2074
  /** Revert a template variable to an earlier value recorded in its audit history. */
2051
- async revertTemplateVariable(e, t, n) {
2052
- return (await this.ready).revertTemplateVariable(e, t, n);
2075
+ async revertTemplateVariable(e, t, o) {
2076
+ return (await this.ready).revertTemplateVariable(e, t, o);
2053
2077
  }
2054
2078
  /** Insert an atomic template variable token at current caret/selection. */
2055
2079
  async insertTemplateVariable(e, t) {
@@ -2108,8 +2132,8 @@ class $ {
2108
2132
  (await this.ready).markSigningArea(e);
2109
2133
  }
2110
2134
  /** Thay đổi kích thước (Width x Height) của một ô ký */
2111
- async resizeSignatureBox(e, t, n) {
2112
- (await this.ready).resizeSignatureBox(e, t, n);
2135
+ async resizeSignatureBox(e, t, o) {
2136
+ (await this.ready).resizeSignatureBox(e, t, o);
2113
2137
  }
2114
2138
  /** Cập nhật thông tin và kích thước của một ô ký */
2115
2139
  async updateSignatureBox(e, t) {
@@ -2132,7 +2156,7 @@ class $ {
2132
2156
  }
2133
2157
  }
2134
2158
  export {
2135
- p as D,
2159
+ c as D,
2136
2160
  $ as K,
2137
2161
  B as Z,
2138
2162
  R as a,
@@ -1,4 +1,4 @@
1
- import { K as e, d as o, m as r } from "./kindy-editor-CT14IO-Y.js";
1
+ import { K as e, d as o, m as r } from "./kindy-editor-CToKn_gP.js";
2
2
  import { C as i, a as m, E as n, c as g } from "./changeAggregator-ZI959nPP.js";
3
3
  export {
4
4
  i as ChangeAggregator,
@@ -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-B6qDC4lD.js").then((m) => m.c);
335
+ const { insertText: d } = await import("./editorApp-Bu3dbUIU.js").then((m) => m.c);
336
336
  return e.dispatch(d(t)), i && e.searchClear(), g("inserted");
337
337
  }
338
338
  }), a({
@@ -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. Lịch sử Cập nhật (Update Log)
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`).
@@ -26,7 +26,103 @@ 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
- ## API tích hợp
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
+ Để 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`:
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 API thao tác bảng từ Host
46
+
47
+ #### 1. Lấy danh sách bảng (`getTables`)
48
+ ```ts
49
+ const tables = editor.getTables();
50
+ // [
51
+ // {
52
+ // tableId: "table_1",
53
+ // tablePath: { story: { kind: "body" }, ancestors: [], tableId: "table_1" },
54
+ // rows: 5,
55
+ // cols: 3,
56
+ // header: ["STT", "Tên hàng", "Thành tiền"],
57
+ // hasMergedCells: false
58
+ // }
59
+ // ]
60
+ ```
61
+
62
+ #### 2. Thực thi Table Action với selection trực tiếp (`executeHostTableAction`)
63
+ Không cần người dùng click canvas. Host truyền trực tiếp `TableAction` cùng selection:
64
+
65
+ ```ts
66
+ // Thêm 1 dòng dưới dòng index 1
67
+ await editor.executeHostTableAction("insertRowBelow", { tableId: "table_1", row: 1 });
68
+
69
+ // Thêm 1 cột bên phải cột index 0
70
+ await editor.executeHostTableAction("insertColumnRight", { tableId: "table_1", col: 0 });
71
+
72
+ // Xóa dòng index 2
73
+ await editor.executeHostTableAction("deleteRow", { tableId: "table_1", row: 2 });
74
+
75
+ // Merge các ô từ (1, 0) đến (1, 2)
76
+ await editor.executeHostTableAction("mergeCells", {
77
+ tableId: "table_1",
78
+ fromRow: 1,
79
+ toRow: 1,
80
+ fromCol: 0,
81
+ toCol: 2
82
+ });
83
+
84
+ // Xóa bảng
85
+ await editor.executeHostTableAction("deleteTable", "table_1");
86
+ ```
87
+
88
+ Kiểm tra action có khả thi trước khi thực thi:
89
+ ```ts
90
+ if (editor.canExecuteHostTableAction("deleteRow", { tableId: "table_1", row: 1 })) {
91
+ await editor.executeHostTableAction("deleteRow", { tableId: "table_1", row: 1 });
92
+ }
93
+ ```
94
+
95
+ #### 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:
97
+
98
+ ```ts
99
+ await editor.setTableCellValue("table_1", 1, 2, "250.000 đ");
100
+ ```
101
+
102
+ #### 4. Thay thế nhiều dòng dữ liệu cùng lúc (`replaceTableRows`)
103
+ Cập nhật hàng loạt dòng dữ liệu trong một transaction nguyên tử (atomic update).
104
+ - Tự động giữ format từ dòng mẫu (`sampleRowIndex`).
105
+ - Giữ nguyên header (`headerRows`) và footer (`footerRows`).
106
+ - Tự động sinh ID duy nhất cho cell/paragraph mới.
107
+ - Validate cấu trúc bảng và render canvas đúng một lần.
108
+
109
+ ```ts
110
+ await editor.replaceTableRows(
111
+ "table_1",
112
+ [
113
+ ["1", "Sản phẩm A", "100.000"],
114
+ ["2", "Sản phẩm B", "200.000"],
115
+ ["3", "Sản phẩm C", "300.000"],
116
+ ],
117
+ {
118
+ headerRows: 1, // Giữ nguyên dòng tiêu đề
119
+ footerRows: 1, // Giữ nguyên dòng tổng cộng
120
+ sampleRowIndex: 1, // Lấy định dạng font/style từ dòng mẫu 1
121
+ }
122
+ );
123
+ ```
124
+
125
+ ## API tương tác canvas thông thường
30
126
 
31
127
  ```ts
32
128
  import type { TablePath, TableSelection } from "kindy-editor";
@@ -533,7 +533,94 @@ Thanh công cụ Ribbon của Editor có độ rộng mặc định tương đ
533
533
 
534
534
  ---
535
535
 
536
- ## 10. Trải Nghiệm Trang Demo Trực Quan
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
+ ```ts
543
+ const editor = new KindyEditor({
544
+ container: document.getElementById("editor-container"),
545
+ mode: "view",
546
+ permissions: {
547
+ hostTableMutation: true, // Cho phép host thao tác bảng trong View Mode
548
+ },
549
+ });
550
+ ```
551
+
552
+ ### 10.2 Lấy danh sách bảng (`editor.getTables()`)
553
+
554
+ ```ts
555
+ const tables = editor.getTables();
556
+ // tables[0] -> {
557
+ // tableId: "tbl_invoice",
558
+ // rows: 4,
559
+ // cols: 5,
560
+ // header: ["STT", "Mã", "Tên dịch vụ", "Số lượng", "Đơn giá"],
561
+ // hasMergedCells: false
562
+ // }
563
+ ```
564
+
565
+ ### 10.3 Cập nhật giá trị ô (`editor.setTableCellValue()`)
566
+
567
+ ```ts
568
+ // Cập nhật ô dòng 1, cột 2 (0-indexed)
569
+ await editor.setTableCellValue("tbl_invoice", 1, 2, "Khám tổng quát");
570
+ ```
571
+
572
+ ### 10.4 Thay thế nhiều dòng dữ liệu một lần (`editor.replaceTableRows()`)
573
+
574
+ Cập nhật hàng loạt dòng dữ liệu trong một transaction nguyên tử:
575
+ - Giữ nguyên số dòng header (`headerRows`) và footer (`footerRows`).
576
+ - Giữ format font, shading, border từ dòng mẫu (`sampleRowIndex`).
577
+ - Tự động sinh ID hợp lệ duy nhất cho từng cell và paragraph.
578
+ - Validate bảng và render canvas đúng một lần.
579
+
580
+ ```ts
581
+ const rowsFromForm = [
582
+ ["1", "DV01", "Siêu âm màu", "1", "150.000"],
583
+ ["2", "DV02", "Xét nghiệm máu", "1", "200.000"],
584
+ ["3", "DV03", "Chụp X-quang", "2", "300.000"],
585
+ ];
586
+
587
+ await editor.replaceTableRows("tbl_invoice", rowsFromForm, {
588
+ headerRows: 1,
589
+ footerRows: 1,
590
+ sampleRowIndex: 1,
591
+ });
592
+ ```
593
+
594
+ ### 10.5 Chạy Table Action từ FE (`editor.executeHostTableAction()`)
595
+
596
+ Tái sử dụng toàn bộ 9 Table Action mà không phụ thuộc con trỏ chuột:
597
+
598
+ ```ts
599
+ // Thêm 1 dòng dưới dòng 1
600
+ await editor.executeHostTableAction("insertRowBelow", { tableId: "tbl_invoice", row: 1 });
601
+
602
+ // Thêm cột bên phải cột 2
603
+ await editor.executeHostTableAction("insertColumnRight", { tableId: "tbl_invoice", col: 2 });
604
+
605
+ // Xóa dòng 3
606
+ await editor.executeHostTableAction("deleteRow", { tableId: "tbl_invoice", row: 3 });
607
+
608
+ // Merge ô từ dòng 1 cột 0 đến dòng 1 cột 2
609
+ await editor.executeHostTableAction("mergeCells", {
610
+ tableId: "tbl_invoice",
611
+ fromRow: 1,
612
+ toRow: 1,
613
+ fromCol: 0,
614
+ toCol: 2,
615
+ });
616
+
617
+ // Xóa bảng
618
+ await editor.executeHostTableAction("deleteTable", "tbl_invoice");
619
+ ```
620
+
621
+ ---
622
+
623
+ ## 11. Trải Nghiệm Trang Demo Trực Quan
537
624
 
538
625
  Kindy Editor cung cấp sẵn trang demo tương tác đầy đủ các tính năng trên:
539
626
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kindy-editor",
3
- "version": "3.2.5",
3
+ "version": "3.2.6",
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",
@@ -196,6 +196,7 @@ export interface EditorPermissions {
196
196
  copy: boolean;
197
197
  createComment: boolean;
198
198
  viewComment: boolean;
199
+ hostTableMutation?: boolean;
199
200
  }
200
201
 
201
202
  /** A selection/caret in the document (offsets are UTF-16 code units into a
@@ -227,6 +228,73 @@ export type TableAction =
227
228
  | "deleteRow" | "deleteColumn" | "deleteTable"
228
229
  | "mergeCells" | "unmergeCell";
229
230
 
231
+ export type TableCellValue =
232
+ | string
233
+ | number
234
+ | boolean
235
+ | null
236
+ | undefined
237
+ | { text?: string; value?: unknown };
238
+
239
+ export type HostTableSelection =
240
+ | TableSelection
241
+ | {
242
+ kind?: "cell";
243
+ table?: string | TablePath;
244
+ tableId?: string;
245
+ anchor: TableGridPoint;
246
+ focus?: TableGridPoint;
247
+ }
248
+ | {
249
+ kind: "row";
250
+ table?: string | TablePath;
251
+ tableId?: string;
252
+ from: number;
253
+ to?: number;
254
+ }
255
+ | {
256
+ kind: "column";
257
+ table?: string | TablePath;
258
+ tableId?: string;
259
+ from: number;
260
+ to?: number;
261
+ }
262
+ | {
263
+ kind: "table";
264
+ table?: string | TablePath;
265
+ tableId?: string;
266
+ }
267
+ | {
268
+ tableId: string;
269
+ row?: number;
270
+ col?: number;
271
+ fromRow?: number;
272
+ toRow?: number;
273
+ fromCol?: number;
274
+ toCol?: number;
275
+ }
276
+ | string;
277
+
278
+ export interface TableSummary {
279
+ tableId: string;
280
+ tablePath: TablePath;
281
+ path: TablePath;
282
+ rows: number;
283
+ cols: number;
284
+ header: string[];
285
+ headers?: string[][];
286
+ hasMergedCells: boolean;
287
+ }
288
+
289
+ export interface ReplaceTableRowsOptions {
290
+ /** Number of leading rows to preserve as header (defaults to leading repeatHeader rows, or 1 if table has >= 2 rows). */
291
+ headerRows?: number;
292
+ /** Number of trailing rows to preserve as footer (defaults to 0). */
293
+ footerRows?: number;
294
+ /** Index of the row to use as formatting template (defaults to first data row after header, or row 0). */
295
+ sampleRowIndex?: number;
296
+ }
297
+
230
298
  // ===== Review layer (track changes + comments) =============================
231
299
  // An attributed, anchored overlay on top of the core document. `getReview()`
232
300
  // returns a snapshot; the `reviewChanged` event carries the full layer.
@@ -908,6 +976,15 @@ export interface EditorHandle {
908
976
  setTableSelection(selection: TableSelection | null): void;
909
977
  canExecuteTableAction(action: TableAction): boolean;
910
978
  executeTableAction(action: TableAction): boolean;
979
+ canExecuteHostTableAction(action: TableAction, selection?: HostTableSelection | TableSelection): boolean;
980
+ executeHostTableAction(action: TableAction, selection?: HostTableSelection | TableSelection): boolean;
981
+ setTableCellValue(table: string | TablePath, row: number, column: number, value: TableCellValue): boolean;
982
+ replaceTableRows(
983
+ table: string | TablePath,
984
+ rows: Array<Array<TableCellValue> | Record<string, unknown>>,
985
+ options?: ReplaceTableRowsOptions,
986
+ ): boolean;
987
+ getTables(): TableSummary[];
911
988
  // ---- review layer (track changes + comments) ----------------------------
912
989
  /** The current editor mode. */
913
990
  getMode(): EditMode;
@@ -944,6 +1021,11 @@ export interface EditorHandle {
944
1021
  clearExternalAnnotations(): void;
945
1022
  /** Scroll the viewport to reveal a document range with smooth animation. */
946
1023
  revealRange(range: ExternalRange, options?: RevealRangeOptions): void;
1024
+ /** Scroll the viewport directly to the specified 1-based page number. */
1025
+ goToPage(
1026
+ pageNumber: number,
1027
+ options?: { behavior?: "auto" | "smooth" },
1028
+ ): Promise<boolean>;
947
1029
  /** Live-update {{KEY}} template placeholders in the document without full reload,
948
1030
  * preserving zoom, scroll position, and selection. */
949
1031
  setTemplateVariables(values: TemplateVariableValues, options?: SetTemplateVariablesOptions): void;
@@ -1066,6 +1148,15 @@ export declare class KindyEditor {
1066
1148
  setTableSelection(selection: TableSelection | null): Promise<void>;
1067
1149
  canExecuteTableAction(action: TableAction): boolean;
1068
1150
  executeTableAction(action: TableAction): Promise<boolean>;
1151
+ canExecuteHostTableAction(action: TableAction, selection?: HostTableSelection | TableSelection): boolean;
1152
+ executeHostTableAction(action: TableAction, selection?: HostTableSelection | TableSelection): Promise<boolean>;
1153
+ setTableCellValue(table: string | TablePath, row: number, column: number, value: TableCellValue): Promise<boolean>;
1154
+ replaceTableRows(
1155
+ table: string | TablePath,
1156
+ rows: Array<Array<TableCellValue> | Record<string, unknown>>,
1157
+ options?: ReplaceTableRowsOptions,
1158
+ ): Promise<boolean>;
1159
+ getTables(): TableSummary[];
1069
1160
  getDocId(): string | null;
1070
1161
  getShareLink(): string | null;
1071
1162
  /** Render ephemeral highlight decorations for external comments/annotations.
@@ -1075,6 +1166,11 @@ export declare class KindyEditor {
1075
1166
  clearExternalAnnotations(): Promise<void>;
1076
1167
  /** Scroll the viewport to reveal a document range with smooth animation. */
1077
1168
  revealRange(range: ExternalRange, options?: RevealRangeOptions): Promise<void>;
1169
+ /** Scroll the viewport directly to the specified 1-based page number. */
1170
+ goToPage(
1171
+ pageNumber: number,
1172
+ options?: { behavior?: "auto" | "smooth" },
1173
+ ): Promise<boolean>;
1078
1174
  /** Live-update {{KEY}} template placeholders in the document without full reload,
1079
1175
  * preserving zoom, scroll position, and selection. */
1080
1176
  setTemplateVariables(values: TemplateVariableValues, options?: SetTemplateVariablesOptions): Promise<void>;