kindy-editor 1.0.1 → 2.0.0
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/CAPABILITIES.md +39 -0
- package/CHANGELOG.md +56 -0
- package/GUIDE.md +190 -0
- package/MIGRATION.md +46 -0
- package/README.md +198 -253
- package/README.zh-CN.md +41 -192
- package/dist/assets/docx.worker-9ZRUe-2j.js +243 -0
- package/dist/kindy-core.cjs +786 -0
- package/dist/kindy-core.js +738 -0
- package/dist/kindy-editor.cjs +421 -76
- package/dist/kindy-editor.css +1 -1
- package/dist/kindy-editor.d.ts +165 -169
- package/dist/kindy-editor.js +55210 -30323
- package/dist/types/core/adapters/legacy.d.ts +9 -0
- package/dist/types/core/adapters/memory.d.ts +87 -0
- package/dist/types/core/adapters/rest.d.ts +27 -0
- package/dist/types/core/client.d.ts +47 -0
- package/dist/types/core/collaboration.d.ts +30 -0
- package/dist/types/core/editor-state.d.ts +47 -0
- package/dist/types/core/emitter.d.ts +19 -0
- package/dist/types/core/errors.d.ts +13 -0
- package/dist/types/core/index.d.ts +10 -0
- package/dist/types/core/state.d.ts +9 -0
- package/dist/types/core/types.d.ts +235 -0
- package/docs/README.md +15 -0
- package/docs/api-reference.md +510 -0
- package/docs/completion-roadmap.md +62 -0
- package/docs/getting-started.md +180 -0
- package/docs/performance.md +149 -0
- package/docs/plugin-development.md +100 -0
- package/docs/ui-engine.md +242 -0
- package/openapi/document-api.yaml +267 -0
- package/package.json +59 -9
- package/dist/kindy-editor.iife.js +0 -148
package/README.md
CHANGED
|
@@ -1,333 +1,278 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
1
|
+
# Kindy Editor v2
|
|
2
|
+
|
|
3
|
+
Kindy Editor là SDK Document Library chuyên cho DOCX, xây dựng bằng Vue 3 và Tiptap/ProseMirror. Thư viện cung cấp giao diện quản lý tài liệu, editor trên web, lịch sử phiên bản, DOCX codec và hợp đồng API để ứng dụng chủ kết nối backend của mình.
|
|
4
|
+
|
|
5
|
+
Kindy Editor **không** cung cấp backend, database, authentication, object storage hoặc nghiệp vụ hợp đồng. JSON `KindyDocumentState` là trạng thái chỉnh sửa chuẩn; DOCX gốc và DOCX export là artifact do server ứng dụng chủ lưu.
|
|
6
|
+
|
|
7
|
+
## Phạm vi v2.0
|
|
8
|
+
|
|
9
|
+
- Import DOCX trong Web Worker, kiểm tra ZIP/OOXML và trả `CompatibilityReport`.
|
|
10
|
+
- Chỉnh sửa nội dung bằng Tiptap/ProseMirror với preset `Contract`: toolbar compact, page-only và chỉ giữ công cụ cần cho hợp đồng.
|
|
11
|
+
- Explorer theo thư mục, tìm kiếm, tạo tài liệu trống và tạo từ template.
|
|
12
|
+
- Autosave có debounce, cancellation và optimistic concurrency.
|
|
13
|
+
- Lưu thủ công tạo version, xem read-only và khôi phục version.
|
|
14
|
+
- Export DOCX OOXML thật bằng package `docx`.
|
|
15
|
+
- In hoặc Save as PDF qua print dialog của trình duyệt.
|
|
16
|
+
- UI engine có shell responsive, theme tokens, locale messages, slots và typed hooks.
|
|
17
|
+
|
|
18
|
+
Khả năng round-trip DOCX chỉ được cam kết trong [Kindy DOCX Compatibility Profile](./CAPABILITIES.md). Kindy không phải Microsoft Word layout engine và không cam kết giữ nguyên mọi tính năng OOXML.
|
|
19
|
+
|
|
20
|
+
Ngoại lệ duy nhất có thể bảo đảm byte-for-byte là tài liệu vừa import và **chưa có revision chỉnh sửa**: workspace tải lại artifact `original-docx` thay vì serialize JSON. Sau lần chỉnh sửa/save đầu tiên, DOCX mới được dựng từ `KindyDocumentState` và chỉ có cam kết theo compatibility profile.
|
|
21
|
+
|
|
22
|
+
## Kiến trúc
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
Ứng dụng chủ
|
|
26
|
+
├─ KindyDocumentLibrary
|
|
27
|
+
│ ├─ DocumentLibraryShell layout/responsive/theme
|
|
28
|
+
│ ├─ KindyDocumentExplorer list/search/folder/import/template
|
|
29
|
+
│ ├─ KindyEditor Tiptap/ProseMirror editor
|
|
30
|
+
│ └─ KindyVersionPanel preview/restore
|
|
31
|
+
├─ DocumentLibraryClient autosave/conflict/events
|
|
32
|
+
├─ DOCX codecs / browser print
|
|
33
|
+
└─ DocumentApiAdapter
|
|
34
|
+
├─ RestDocumentAdapter
|
|
35
|
+
├─ MemoryDocumentAdapter
|
|
36
|
+
└─ adapter riêng của ứng dụng
|
|
37
|
+
```
|
|
18
38
|
|
|
19
|
-
|
|
39
|
+
UI không gọi URL cố định và không biết token. Mọi IO đi qua `DocumentApiAdapter`.
|
|
20
40
|
|
|
21
|
-
##
|
|
41
|
+
## Cài đặt
|
|
22
42
|
|
|
23
43
|
```bash
|
|
24
44
|
npm install kindy-editor
|
|
25
|
-
# hoặc
|
|
26
|
-
pnpm add kindy-editor
|
|
27
|
-
# hoặc
|
|
28
|
-
yarn add kindy-editor
|
|
29
45
|
```
|
|
30
46
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
```ts
|
|
48
|
+
import 'kindy-editor/style'
|
|
49
|
+
```
|
|
34
50
|
|
|
35
|
-
|
|
51
|
+
Vue phải được cài trong ứng dụng chủ và đáp ứng peer dependency `vue ^3.5.0`.
|
|
36
52
|
|
|
37
|
-
|
|
53
|
+
## Quick start với REST API
|
|
38
54
|
|
|
39
55
|
```vue
|
|
40
56
|
<template>
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
<KindyDocumentLibrary
|
|
58
|
+
ref="workspace"
|
|
59
|
+
:adapter="adapter"
|
|
60
|
+
:autosave="{ enabled: true, delay: 5000 }"
|
|
61
|
+
:ui="{ explorerWidth: '320px', versionsWidth: '300px' }"
|
|
62
|
+
:theme="{ '--kindy-library-primary': '#0b74de' }"
|
|
63
|
+
locale="vi-VN"
|
|
64
|
+
style="height: 100vh"
|
|
65
|
+
@saved="onSaved"
|
|
66
|
+
@error="onError"
|
|
67
|
+
/>
|
|
44
68
|
</template>
|
|
45
69
|
|
|
46
|
-
<script setup>
|
|
70
|
+
<script setup lang="ts">
|
|
47
71
|
import { ref } from 'vue'
|
|
48
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
KindyDocumentLibrary,
|
|
74
|
+
createRestDocumentAdapter,
|
|
75
|
+
} from 'kindy-editor'
|
|
49
76
|
import 'kindy-editor/style'
|
|
50
77
|
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
console.log('Đã lưu:', content)
|
|
60
|
-
},
|
|
78
|
+
const workspace = ref()
|
|
79
|
+
const adapter = createRestDocumentAdapter({
|
|
80
|
+
baseUrl: '/document-api',
|
|
81
|
+
transport: (url, init) => fetch(url, {
|
|
82
|
+
...init,
|
|
83
|
+
credentials: 'include',
|
|
84
|
+
headers: { ...init?.headers, 'x-tenant-id': 'acme' },
|
|
85
|
+
}),
|
|
61
86
|
})
|
|
87
|
+
|
|
88
|
+
function onSaved(result: unknown) {
|
|
89
|
+
console.info('Saved', result)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onError(error: unknown) {
|
|
93
|
+
console.error(error)
|
|
94
|
+
}
|
|
62
95
|
</script>
|
|
63
96
|
```
|
|
64
97
|
|
|
65
|
-
|
|
98
|
+
Server phải triển khai contract trong [`openapi/document-api.yaml`](./openapi/document-api.yaml). OpenAPI không bắt buộc cơ chế auth; cookie, bearer token hoặc API gateway do ứng dụng chủ quyết định.
|
|
66
99
|
|
|
67
|
-
|
|
68
|
-
import { createApp } from 'vue'
|
|
69
|
-
import App from './App.vue'
|
|
70
|
-
import { useKindyEditor } from 'kindy-editor'
|
|
71
|
-
import 'kindy-editor/style'
|
|
100
|
+
### UI hợp đồng mặc định
|
|
72
101
|
|
|
73
|
-
|
|
102
|
+
`KindyDocumentLibrary` tự dùng `CONTRACT_EDITOR_OPTIONS`: toolbar một hàng theo hướng Google Docs, chỉ có Định dạng/Chèn/Bảng/Trang/Xuất, không có Web view, Mermaid, biểu đồ, media web hoặc chuyển đổi số tiếng Trung. Nhóm Xuất chỉ gọi pipeline an toàn của Document Library cho Import DOCX, Export DOCX và In/PDF. Thanh trạng thái chỉ còn `Trang X / Y`, số ký tự, zoom và ngôn ngữ.
|
|
74
103
|
|
|
75
|
-
|
|
76
|
-
app.use(useKindyEditor, {
|
|
77
|
-
locale: 'vi-VN',
|
|
78
|
-
theme: 'light',
|
|
79
|
-
})
|
|
104
|
+
Ứng dụng chủ vẫn có thể override rõ ràng qua `editorOptions`:
|
|
80
105
|
|
|
81
|
-
|
|
106
|
+
```vue
|
|
107
|
+
<KindyDocumentLibrary
|
|
108
|
+
:adapter="adapter"
|
|
109
|
+
:editor-options="{
|
|
110
|
+
toolbar: { menus: ['base', 'insert', 'table', 'page', 'export'] },
|
|
111
|
+
statusbar: { showLocale: false },
|
|
112
|
+
}"
|
|
113
|
+
/>
|
|
82
114
|
```
|
|
83
115
|
|
|
84
|
-
|
|
116
|
+
Để dùng preset với `KindyEditor` độc lập:
|
|
85
117
|
|
|
86
|
-
|
|
118
|
+
```ts
|
|
119
|
+
import { createContractEditorOptions } from 'kindy-editor'
|
|
87
120
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
import { mountKindyEditor } from 'kindy-editor'
|
|
91
|
-
import 'kindy-editor/style'
|
|
121
|
+
const editorOptions = createContractEditorOptions({ locale: 'vi-VN' })
|
|
122
|
+
```
|
|
92
123
|
|
|
93
|
-
|
|
94
|
-
const containerRef = useRef(null)
|
|
124
|
+
## Chạy demo không cần backend
|
|
95
125
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const instance = mountKindyEditor(containerRef.current, props)
|
|
99
|
-
return () => instance.unmount()
|
|
100
|
-
}, [])
|
|
126
|
+
```ts
|
|
127
|
+
import { createMemoryDocumentAdapter } from 'kindy-editor'
|
|
101
128
|
|
|
102
|
-
|
|
103
|
-
}
|
|
129
|
+
const adapter = createMemoryDocumentAdapter()
|
|
104
130
|
```
|
|
105
131
|
|
|
106
|
-
|
|
132
|
+
`MemoryDocumentAdapter` chỉ dành cho demo và test; dữ liệu mất khi reload trang.
|
|
107
133
|
|
|
108
|
-
|
|
134
|
+
Trong repository:
|
|
109
135
|
|
|
110
|
-
```
|
|
111
|
-
|
|
136
|
+
```bash
|
|
137
|
+
npm install
|
|
138
|
+
npm run dev
|
|
139
|
+
```
|
|
112
140
|
|
|
113
|
-
|
|
141
|
+
Mở `http://localhost:9000/kindy-editor`.
|
|
114
142
|
|
|
115
|
-
|
|
116
|
-
() => import('./KindyEditorReact').then((mod) => mod.KindyEditorReact),
|
|
117
|
-
{ ssr: false }
|
|
118
|
-
)
|
|
143
|
+
## Dùng editor độc lập
|
|
119
144
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
145
|
+
```vue
|
|
146
|
+
<KindyEditor
|
|
147
|
+
ref="editor"
|
|
148
|
+
:document="{ content: initialState.content, assets: initialState.assets }"
|
|
149
|
+
:page="initialState.page"
|
|
150
|
+
/>
|
|
127
151
|
```
|
|
128
152
|
|
|
129
|
-
|
|
153
|
+
```ts
|
|
154
|
+
const state = editor.value.getState()
|
|
155
|
+
editor.value.setContent(nextContent)
|
|
156
|
+
editor.value.setReadOnly(true)
|
|
157
|
+
const printable = await editor.value.preparePrint()
|
|
158
|
+
editor.value.print()
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Vanilla/React/Angular/Svelte có thể dùng mount helper:
|
|
130
162
|
|
|
131
|
-
```
|
|
132
|
-
import {
|
|
133
|
-
import { mountKindyEditor } from 'kindy-editor';
|
|
134
|
-
import 'kindy-editor/style';
|
|
163
|
+
```ts
|
|
164
|
+
import { mountKindyEditor } from 'kindy-editor'
|
|
135
165
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
166
|
+
const mounted = mountKindyEditor('#editor', {
|
|
167
|
+
locale: 'vi-VN',
|
|
168
|
+
document: { content: initialState.content },
|
|
139
169
|
})
|
|
140
|
-
export class KindyEditorComponent implements AfterViewInit, OnDestroy {
|
|
141
|
-
@ViewChild('editorContainer') editorContainer!: ElementRef;
|
|
142
|
-
@Input() locale: string = 'vi-VN';
|
|
143
|
-
private instance: any;
|
|
144
|
-
|
|
145
|
-
ngAfterViewInit() {
|
|
146
|
-
this.instance = mountKindyEditor(this.editorContainer.nativeElement, {
|
|
147
|
-
locale: this.locale,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
170
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
171
|
+
mounted.instance.setContent(nextState.content)
|
|
172
|
+
mounted.instance.print()
|
|
173
|
+
mounted.unmount()
|
|
157
174
|
```
|
|
158
175
|
|
|
159
|
-
|
|
176
|
+
V2.0 chưa có native React component; wrapper dùng Vue mount helper.
|
|
160
177
|
|
|
161
|
-
|
|
162
|
-
<script>
|
|
163
|
-
import { onMount, onDestroy } from 'svelte';
|
|
164
|
-
import { mountKindyEditor } from 'kindy-editor';
|
|
165
|
-
import 'kindy-editor/style';
|
|
178
|
+
## Dùng headless client
|
|
166
179
|
|
|
167
|
-
|
|
168
|
-
let container;
|
|
169
|
-
let instance;
|
|
180
|
+
Subpath `kindy-editor/core` không tải Vue UI:
|
|
170
181
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
182
|
+
```ts
|
|
183
|
+
import {
|
|
184
|
+
createDocumentLibrary,
|
|
185
|
+
createRestDocumentAdapter,
|
|
186
|
+
} from 'kindy-editor/core'
|
|
174
187
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
188
|
+
const client = createDocumentLibrary({
|
|
189
|
+
adapter: createRestDocumentAdapter({ baseUrl: '/document-api' }),
|
|
190
|
+
autosave: { enabled: true, delay: 5000 },
|
|
191
|
+
})
|
|
179
192
|
|
|
180
|
-
|
|
193
|
+
const snapshot = await client.open('document-123')
|
|
194
|
+
client.updateState(snapshot.state)
|
|
195
|
+
await client.save('manual')
|
|
181
196
|
```
|
|
182
197
|
|
|
183
|
-
|
|
198
|
+
`VERSION_CONFLICT` dừng autosave. SDK không tự merge hoặc ghi đè revision mới hơn:
|
|
184
199
|
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
200
|
+
```ts
|
|
201
|
+
client.on('save-failed', (error) => {
|
|
202
|
+
if (error.code === 'VERSION_CONFLICT') {
|
|
203
|
+
// Hiển thị lựa chọn reload hoặc lưu thành bản sao.
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
```
|
|
189
207
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
let instance;
|
|
208
|
+
`kindy-editor/core` dùng được trong Node/SSR. Entry `kindy-editor` chứa Vue UI và
|
|
209
|
+
các công cụ browser như cropper/print, vì vậy chỉ import nó trong bundle phía client.
|
|
193
210
|
|
|
194
|
-
|
|
195
|
-
instance = mountKindyEditor(containerRef, {
|
|
196
|
-
locale: 'vi-VN',
|
|
197
|
-
...props,
|
|
198
|
-
});
|
|
199
|
-
});
|
|
211
|
+
## Import và export DOCX
|
|
200
212
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
213
|
+
```ts
|
|
214
|
+
import { importDocxInWorker, exportDocx } from 'kindy-editor'
|
|
204
215
|
|
|
205
|
-
|
|
216
|
+
const imported = await importDocxInWorker(file, {
|
|
217
|
+
mode: 'best-effort',
|
|
218
|
+
profile: 'kindy-docx-v2.0',
|
|
219
|
+
})
|
|
220
|
+
if (imported.report.issues.length) {
|
|
221
|
+
// Hiển thị warning trước khi tiếp tục.
|
|
206
222
|
}
|
|
207
|
-
```
|
|
208
223
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
```html
|
|
212
|
-
<!DOCTYPE html>
|
|
213
|
-
<html>
|
|
214
|
-
<head>
|
|
215
|
-
<link rel="stylesheet" href="https://unpkg.com/kindy-editor/dist/kindy-editor.css">
|
|
216
|
-
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
217
|
-
<script src="https://unpkg.com/kindy-editor/dist/kindy-editor.iife.js"></script>
|
|
218
|
-
</head>
|
|
219
|
-
<body>
|
|
220
|
-
<div id="editor-container" style="height: 100vh;"></div>
|
|
221
|
-
|
|
222
|
-
<script>
|
|
223
|
-
const { mountKindyEditor } = KindyEditor;
|
|
224
|
-
mountKindyEditor('#editor-container', {
|
|
225
|
-
locale: 'vi-VN',
|
|
226
|
-
});
|
|
227
|
-
</script>
|
|
228
|
-
</body>
|
|
229
|
-
</html>
|
|
224
|
+
const output = await exportDocx(imported.state) // strict mặc định
|
|
230
225
|
```
|
|
231
226
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
227
|
+
UI workspace tự xử lý luồng xác nhận best-effort khi import hoặc download. API programmatic mặc định strict và ném `DOCX_UNSUPPORTED` nếu state có feature ngoài profile.
|
|
228
|
+
Mặc định là v2.0. Chọn `kindy-docx-v2.1` cho sections/header/footer hoặc
|
|
229
|
+
`kindy-docx-v2.2` cho comments/Track Changes sau khi integration đã chạy corpus tương ứng.
|
|
235
230
|
|
|
236
|
-
|
|
237
|
-
|---|---|---|---|
|
|
238
|
-
| `locale` | `string` | `'vi-VN'` | Ngôn ngữ giao diện (`vi-VN`, `en-US`, `zh-CN`) |
|
|
239
|
-
| `theme` | `string` | `'light'` | Chế độ giao diện (`light`, `dark`, `auto`) |
|
|
240
|
-
| `skin` | `string` | `'default'` | Phong cách thanh công cụ (`default`, `modern`) |
|
|
241
|
-
| `height` | `string` | `'100%'` | Chiều cao khung biên tập |
|
|
242
|
-
| `toolbar.mode` | `string` | `'ribbon'` | Chế độ thanh công cụ (`ribbon` hoặc `classic`) |
|
|
243
|
-
| `document.title` | `string` | `''` | Tiêu đề tài liệu mặc định |
|
|
244
|
-
| `document.content` | `string` \| `object` | `''` | Nội dung HTML hoặc JSON ban đầu |
|
|
245
|
-
| `document.readOnly` | `boolean` | `false` | Chế độ chỉ đọc (Chỉ xem) |
|
|
246
|
-
| `document.autoSave` | `object` | `{ enabled: false }` | Tự động lưu (`enabled: true, interval: 30000`) |
|
|
231
|
+
`POST /documents/import` phải lưu blob gốc và trả `DocumentRecord.originalSource` gồm `artifactId`, `revisionId`, `format: 'original-docx'` và `fileName`. Nhờ đó `KindyDocumentLibrary.downloadDocx()` trả đúng file gốc khi revision chưa đổi. URL artifact của REST backend phải là URL tải được (thường là signed URL); Memory adapter trả trực tiếp `Blob`.
|
|
247
232
|
|
|
248
|
-
|
|
233
|
+
## Public components
|
|
249
234
|
|
|
250
|
-
|
|
235
|
+
| Export | Mục đích |
|
|
236
|
+
|---|---|
|
|
237
|
+
| `KindyDocumentLibrary` | Workspace hoàn chỉnh, kết nối adapter và điều phối state |
|
|
238
|
+
| `KindyDocumentLibraryShell` | Layout ba vùng độc lập cho UI tùy biến |
|
|
239
|
+
| `KindyDocumentExplorer` | List/search/folder/import/template độc lập |
|
|
240
|
+
| `KindyEditor` | Editor độc lập |
|
|
241
|
+
| `KindyVersionPanel` | Lịch sử version độc lập |
|
|
251
242
|
|
|
252
|
-
|
|
243
|
+
## Events chuẩn
|
|
253
244
|
|
|
254
|
-
|
|
255
|
-
// 1. Lấy nội dung HTML
|
|
256
|
-
const html = editorRef.value.getContent('html')
|
|
245
|
+
`ready`, `opened`, `changed`, `save-started`, `saved`, `save-failed`, `created`, `imported`, `compatibility-warning`, `version-restored`, `printed`, `error`.
|
|
257
246
|
|
|
258
|
-
|
|
259
|
-
const json = editorRef.value.getContent('json')
|
|
247
|
+
## Error codes
|
|
260
248
|
|
|
261
|
-
|
|
262
|
-
editorRef.value.setContent('<h1>Nội dung mới</h1>')
|
|
249
|
+
`DOCX_INVALID`, `DOCX_UNSUPPORTED`, `IMPORT_FAILED`, `EXPORT_FAILED`, `ADAPTER_ERROR`, `NETWORK_ERROR`, `VERSION_CONFLICT`, `DOCUMENT_NOT_FOUND`, `OPERATION_CANCELLED`.
|
|
263
250
|
|
|
264
|
-
|
|
265
|
-
editorRef.value.insertContent('<p>Đoạn văn mới</p>')
|
|
251
|
+
## Hiệu năng tài liệu dài
|
|
266
252
|
|
|
267
|
-
|
|
268
|
-
await editorRef.value.exportPdf('tai-lieu.pdf')
|
|
269
|
-
|
|
270
|
-
// 6. Xuất trang thành hình ảnh
|
|
271
|
-
await editorRef.value.exportImage('png', 'tai-lieu.png')
|
|
272
|
-
|
|
273
|
-
// 7. In tài liệu
|
|
274
|
-
editorRef.value.print()
|
|
275
|
-
|
|
276
|
-
// 8. Đổi giao diện tối / sáng
|
|
277
|
-
editorRef.value.setTheme('dark')
|
|
278
|
-
```
|
|
253
|
+
Import parsing đã chạy ngoài main thread. Editor vẫn dùng một ProseMirror instance liên tục; pagination cache phép đo block và workspace gom state sync. Trên production preview cục bộ (Chrome 151, Apple M2/24GB, 40 mẫu), corpus 100 trang text mở khoảng 0,49 giây với typing p95 21,5ms; mixed mở khoảng 0,85 giây với typing p95 36,5ms. Pagination cached của hai corpus lần lượt khoảng 2,8ms và 3,7ms. Đây là regression baseline, không phải SLA cho mọi thiết bị. Corpus mixed 200 trang vẫn chỉ được xem là stress limit. Xem [Performance guide](./docs/performance.md) để biết phương pháp đo và giới hạn.
|
|
279
254
|
|
|
280
|
-
|
|
255
|
+
## Tài liệu
|
|
281
256
|
|
|
282
|
-
|
|
257
|
+
- [Bắt đầu tích hợp](./docs/getting-started.md)
|
|
258
|
+
- [UI engine, theme, responsive và slots](./docs/ui-engine.md)
|
|
259
|
+
- [API reference](./docs/api-reference.md)
|
|
260
|
+
- [Backend, security và adapter](./GUIDE.md)
|
|
261
|
+
- [DOCX capability matrix](./CAPABILITIES.md)
|
|
262
|
+
- [Performance và tài liệu 100 trang](./docs/performance.md)
|
|
263
|
+
- [Migration v1 → v2](./MIGRATION.md)
|
|
264
|
+
- [REST OpenAPI](./openapi/document-api.yaml)
|
|
283
265
|
|
|
284
|
-
|
|
285
|
-
Bình luận được lưu trực tiếp dưới dạng thẻ `span` chứa thuộc tính `data-comment` và `data-thread` trong tài liệu HTML/JSON:
|
|
286
|
-
|
|
287
|
-
```html
|
|
288
|
-
<span
|
|
289
|
-
data-comment="c1a2b3"
|
|
290
|
-
data-color="rgba(255, 213, 79, 0.4)"
|
|
291
|
-
data-thread='{"id":"c1a2b3","text":"Cần chỉnh sửa","replies":[]}'
|
|
292
|
-
class="kindy-comment"
|
|
293
|
-
>
|
|
294
|
-
Đoạn văn bản được bình luận
|
|
295
|
-
</span>
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### Lưu trữ Cơ sở dữ liệu (Database Integration):
|
|
299
|
-
- **Cách 1 (Khuyên dùng)**: Lưu toàn bộ chuỗi HTML chứa thẻ `<span data-comment>` vào 1 cột `content TEXT` trong Database. Khi mở lại, bình luận tự động khôi phục 100%.
|
|
300
|
-
- **Cách 2**: Lưu riêng danh sách mảng object comment qua API `editorRef.value.getComments()`.
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## 💻 Dự án Mẫu (Examples)
|
|
305
|
-
|
|
306
|
-
Thư mục `examples/` chứa các ứng dụng mẫu đầy đủ:
|
|
307
|
-
- ⚛️ **[examples/react-demo](./examples/react-demo)**: React.js + IndexedDB Database Persistence.
|
|
308
|
-
|
|
309
|
-
---
|
|
310
|
-
|
|
311
|
-
## 🛠️ Lệnh phát triển (Development Commands)
|
|
266
|
+
## Phát triển và kiểm thử
|
|
312
267
|
|
|
313
268
|
```bash
|
|
314
|
-
|
|
315
|
-
npm
|
|
316
|
-
|
|
317
|
-
#
|
|
318
|
-
npm run
|
|
319
|
-
|
|
320
|
-
# Đóng gói sản phẩm (Production build ra thư mục dist/)
|
|
269
|
+
npm test
|
|
270
|
+
npm run test:e2e
|
|
271
|
+
npm run test:package
|
|
272
|
+
npm run test:libreoffice # cần có soffice; CI tự cài LibreOffice
|
|
273
|
+
npm run typecheck
|
|
274
|
+
npm run lint:check
|
|
321
275
|
npm run build
|
|
322
|
-
|
|
323
|
-
# Định dạng code
|
|
324
|
-
npm run format
|
|
325
276
|
```
|
|
326
277
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
## 📄 License & Credits
|
|
330
|
-
|
|
331
|
-
- Phát hành theo giấy phép [MIT License](./LICENSE).
|
|
332
|
-
- Dựa trên mã nguồn gốc [Umo Editor](https://github.com/umodoc/editor) bởi đội ngũ umodoc.
|
|
333
|
-
- Bản quyền thuộc về **Kindy** ([GitHub](https://github.com/hotienky)).
|
|
278
|
+
License: MIT.
|