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.
- package/dist-lib/{editorApp-B6qDC4lD.js → editorApp-Bu3dbUIU.js} +8582 -8270
- package/dist-lib/{kindy-editor-CT14IO-Y.js → kindy-editor-CToKn_gP.js} +70 -46
- package/dist-lib/kindy-editor.js +1 -1
- package/dist-lib/{webmcp-BcWSo1v9.js → webmcp-ClHEa6Gq.js} +1 -1
- package/docs/qrcode-integration-guide.md +36 -1
- package/docs/table-editing.md +97 -1
- package/docs/table-form-integration.md +88 -1
- package/package.json +1 -1
- package/types/kindy-editor.d.ts +96 -0
|
@@ -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,
|
|
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
|
-
...
|
|
40
|
+
...c,
|
|
41
41
|
...l(r),
|
|
42
|
-
ruler: { ...
|
|
43
|
-
} :
|
|
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,
|
|
80
|
+
for (const [t, o] of Object.entries(r)) o !== void 0 && (e[t] = o);
|
|
81
81
|
return e;
|
|
82
82
|
}
|
|
83
|
-
const
|
|
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(
|
|
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:
|
|
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",
|
|
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
|
-
|
|
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:
|
|
808
|
+
{ signal: n }
|
|
809
809
|
), window.addEventListener("mousemove", (a) => {
|
|
810
810
|
i && d(a.clientX - i.dx, a.clientY - i.dy);
|
|
811
|
-
}, { signal:
|
|
811
|
+
}, { signal: n }), window.addEventListener("mouseup", () => {
|
|
812
812
|
i = null;
|
|
813
|
-
}, { signal:
|
|
813
|
+
}, { signal: n });
|
|
814
814
|
}
|
|
815
|
-
const
|
|
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:
|
|
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
|
-
},
|
|
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
|
-
},
|
|
1782
|
+
}, K = {
|
|
1783
1783
|
en: u,
|
|
1784
|
-
vi:
|
|
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
|
|
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
|
|
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: (
|
|
1832
|
-
this.handle =
|
|
1831
|
+
onReady: (n) => {
|
|
1832
|
+
this.handle = n, this.pendingQRCode !== void 0 && n.setQRCode(this.pendingQRCode), t(n);
|
|
1833
1833
|
},
|
|
1834
|
-
onEvent: (
|
|
1835
|
-
onPublicEvent: (
|
|
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-
|
|
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
|
|
1843
|
-
return
|
|
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, ...
|
|
1851
|
-
for (const
|
|
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,
|
|
1886
|
-
return this.ready.then((
|
|
1887
|
-
|
|
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: (
|
|
1890
|
-
const a = () => e?.render(
|
|
1889
|
+
render: (n, d, i) => {
|
|
1890
|
+
const a = () => e?.render(n, d, i);
|
|
1891
1891
|
e ? a() : t = a;
|
|
1892
1892
|
},
|
|
1893
|
-
mountEditor: (
|
|
1893
|
+
mountEditor: (n, d, i) => {
|
|
1894
1894
|
if (!e) throw new Error("KindyEditor.createChild(): await whenReady() before mountEditor()");
|
|
1895
|
-
return e.mountEditor(
|
|
1895
|
+
return e.mountEditor(n, d, i);
|
|
1896
1896
|
},
|
|
1897
1897
|
destroy: () => {
|
|
1898
|
-
|
|
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,
|
|
1985
|
-
(await this.ready).replyToComment(e, t,
|
|
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,
|
|
1994
|
-
(await this.ready).editComment(e, t,
|
|
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,
|
|
2052
|
-
return (await this.ready).revertTemplateVariable(e, t,
|
|
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,
|
|
2112
|
-
(await this.ready).resizeSignatureBox(e, t,
|
|
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
|
-
|
|
2159
|
+
c as D,
|
|
2136
2160
|
$ as K,
|
|
2137
2161
|
B as Z,
|
|
2138
2162
|
R as a,
|
package/dist-lib/kindy-editor.js
CHANGED
|
@@ -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-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.
|
|
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`).
|
package/docs/table-editing.md
CHANGED
|
@@ -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
|
-
##
|
|
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.
|
|
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
package/types/kindy-editor.d.ts
CHANGED
|
@@ -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>;
|