@ap666/office-word 0.1.3 → 0.1.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/README.md +117 -1
- package/dist/components/LocalFileBlockView.vue.d.ts +3 -0
- package/dist/components/OutlinePanel.vue.d.ts +1 -0
- package/dist/components/RichTextEditor.vue.d.ts +10 -1
- package/dist/composables/useRichTextEditor.d.ts +8 -0
- package/dist/extensions/local-file-block.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4429 -4178
- package/dist/index.umd.cjs +396 -218
- package/dist/style.css +1 -1
- package/dist/types.d.ts +40 -0
- package/docs/usage.md +49 -0
- package/package.json +8 -2
- package/yjs/README.md +63 -0
- package/yjs/index.js +224 -0
- package/yjs/package.json +14 -0
package/dist/types.d.ts
CHANGED
|
@@ -2,7 +2,39 @@ import { JSONContent } from '@tiptap/core';
|
|
|
2
2
|
export type RichTextEditorProps = {
|
|
3
3
|
modelValue?: JSONContent | null;
|
|
4
4
|
editable?: boolean;
|
|
5
|
+
mode?: 'edit' | 'preview';
|
|
6
|
+
outlinePlacement?: 'left' | 'right';
|
|
7
|
+
enabledExportItems?: RichTextEditorExportItemKey[] | null;
|
|
8
|
+
enabledInsertMenuItems?: RichTextEditorInsertMenuItemKey[] | null;
|
|
9
|
+
enabledToolbarActions?: RichTextEditorToolbarActionKey[] | null;
|
|
5
10
|
placeholder?: string;
|
|
11
|
+
collaboration?: RichTextEditorCollaborationOptions | null;
|
|
12
|
+
};
|
|
13
|
+
export type RichTextEditorExportItemKey = 'pdf' | 'html' | 'image' | 'print';
|
|
14
|
+
export type RichTextEditorInsertMenuItemKey = 'image' | 'video' | 'table' | 'local-file' | 'columns' | 'highlight-block' | 'date' | 'code-block' | 'formula' | 'blockquote' | 'emoji' | 'link' | 'divider' | 'countdown' | 'markdown-import';
|
|
15
|
+
export type RichTextEditorToolbarActionKey = 'blockquote';
|
|
16
|
+
export type RichTextEditorCollaborationDocument = {
|
|
17
|
+
getXmlFragment: (field: string) => unknown;
|
|
18
|
+
};
|
|
19
|
+
export type RichTextEditorCollaborationAwareness = {
|
|
20
|
+
states: Map<number, unknown>;
|
|
21
|
+
on: (event: 'update', callback: (...args: unknown[]) => void) => void;
|
|
22
|
+
setLocalStateField: (field: string, value: unknown) => void;
|
|
23
|
+
};
|
|
24
|
+
export type RichTextEditorCollaborationProvider = {
|
|
25
|
+
doc?: unknown;
|
|
26
|
+
awareness: RichTextEditorCollaborationAwareness;
|
|
27
|
+
};
|
|
28
|
+
export type RichTextEditorCollaborationUser = {
|
|
29
|
+
name: string;
|
|
30
|
+
color: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
export type RichTextEditorCollaborationOptions = {
|
|
34
|
+
document: RichTextEditorCollaborationDocument;
|
|
35
|
+
field?: string;
|
|
36
|
+
provider?: RichTextEditorCollaborationProvider | null;
|
|
37
|
+
user?: RichTextEditorCollaborationUser | null;
|
|
6
38
|
};
|
|
7
39
|
export type RichTextEditorAlign = 'left' | 'center' | 'right';
|
|
8
40
|
export type RichTextEditorImagePayload = {
|
|
@@ -30,6 +62,12 @@ export type RichTextEditorFilePayload = {
|
|
|
30
62
|
widthPercent?: number;
|
|
31
63
|
height?: number;
|
|
32
64
|
};
|
|
65
|
+
export type RichTextEditorLocalFilePayload = {
|
|
66
|
+
url: string;
|
|
67
|
+
name: string;
|
|
68
|
+
size?: number;
|
|
69
|
+
mimeType?: string;
|
|
70
|
+
};
|
|
33
71
|
export type RichTextEditorImageExportOptions = {
|
|
34
72
|
type?: 'image/png' | 'image/jpeg';
|
|
35
73
|
quality?: number;
|
|
@@ -41,6 +79,8 @@ export type RichTextEditorInstance = {
|
|
|
41
79
|
insertImage: (payload: RichTextEditorImagePayload | RichTextEditorImagePayload[]) => boolean;
|
|
42
80
|
insertVideo: (payload: RichTextEditorVideoPayload) => boolean;
|
|
43
81
|
insertFile: (payload: RichTextEditorFilePayload) => boolean;
|
|
82
|
+
insertLocalFile: (payload: RichTextEditorLocalFilePayload) => boolean;
|
|
83
|
+
openLocalFilePicker: () => void;
|
|
44
84
|
focus: () => void;
|
|
45
85
|
getJSON: () => JSONContent | null;
|
|
46
86
|
};
|
package/docs/usage.md
CHANGED
|
@@ -13,7 +13,56 @@ import { RichTextEditor } from '@ap666/office-word'
|
|
|
13
13
|
## Main Capabilities
|
|
14
14
|
|
|
15
15
|
- rich text editing with a built-in toolbar UI
|
|
16
|
+
- optional Yjs collaboration mode with the same component API
|
|
16
17
|
- export to PDF, image, and HTML through instance methods
|
|
17
18
|
- external upload integration for image, video, and file insertion
|
|
18
19
|
- typed public instance API for Vue `ref` usage
|
|
19
20
|
|
|
21
|
+
## Collaboration
|
|
22
|
+
|
|
23
|
+
Use the optional `collaboration` prop to bind the editor to a Yjs document.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import * as Y from 'yjs'
|
|
27
|
+
import { WebsocketProvider } from 'y-websocket'
|
|
28
|
+
|
|
29
|
+
const ydoc = new Y.Doc()
|
|
30
|
+
const provider = new WebsocketProvider('ws://localhost:1234', 'office-word-demo', ydoc)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```vue
|
|
34
|
+
<RichTextEditor
|
|
35
|
+
v-model="content"
|
|
36
|
+
:collaboration="{
|
|
37
|
+
document: ydoc,
|
|
38
|
+
field: 'content',
|
|
39
|
+
provider,
|
|
40
|
+
user: {
|
|
41
|
+
name: '张三',
|
|
42
|
+
color: '#3b82f6',
|
|
43
|
+
},
|
|
44
|
+
}"
|
|
45
|
+
/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
When `collaboration` is omitted, the editor stays in normal single-user mode. When `provider + user` are passed, the editor also renders remote cursors and remote selections.
|
|
49
|
+
|
|
50
|
+
## Bundled Yjs Server
|
|
51
|
+
|
|
52
|
+
The npm package also ships a minimal websocket server example in `yjs/`.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
cd node_modules/@ap666/office-word/yjs
|
|
56
|
+
npm install
|
|
57
|
+
npm run start
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Client side:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import * as Y from 'yjs'
|
|
64
|
+
import { WebsocketProvider } from 'y-websocket'
|
|
65
|
+
|
|
66
|
+
const ydoc = new Y.Doc()
|
|
67
|
+
const provider = new WebsocketProvider('ws://127.0.0.1:1234', 'office-word-demo', ydoc)
|
|
68
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ap666/office-word",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "A reusable Vue 3 rich text editor component built with Tiptap 3.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.umd.cjs",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dist",
|
|
19
19
|
"README.md",
|
|
20
20
|
"docs",
|
|
21
|
+
"yjs",
|
|
21
22
|
"LICENSE"
|
|
22
23
|
],
|
|
23
24
|
"sideEffects": [
|
|
@@ -39,6 +40,8 @@
|
|
|
39
40
|
"@tiptap/core": "^3.22.1",
|
|
40
41
|
"@tiptap/extension-code-block": "^3.22.1",
|
|
41
42
|
"@tiptap/extension-code-block-lowlight": "^3.22.1",
|
|
43
|
+
"@tiptap/extension-collaboration": "^3.22.2",
|
|
44
|
+
"@tiptap/extension-collaboration-caret": "^3.22.2",
|
|
42
45
|
"@tiptap/extension-font-family": "^3.22.1",
|
|
43
46
|
"@tiptap/extension-placeholder": "^3.22.1",
|
|
44
47
|
"@tiptap/extension-subscript": "^3.22.1",
|
|
@@ -54,8 +57,11 @@
|
|
|
54
57
|
"@tiptap/pm": "^3.22.1",
|
|
55
58
|
"@tiptap/starter-kit": "^3.22.1",
|
|
56
59
|
"@tiptap/vue-3": "^3.22.1",
|
|
60
|
+
"@tiptap/y-tiptap": "^3.0.2",
|
|
57
61
|
"lowlight": "^3.3.0",
|
|
58
|
-
"vue": "^3.5.0"
|
|
62
|
+
"vue": "^3.5.0",
|
|
63
|
+
"y-prosemirror": "^1.2.6",
|
|
64
|
+
"yjs": "^13.6.30"
|
|
59
65
|
},
|
|
60
66
|
"dependencies": {
|
|
61
67
|
"html2canvas": "^1.4.1",
|
package/yjs/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Yjs Server Example
|
|
2
|
+
|
|
3
|
+
This folder is shipped with the npm package so consumers can run a minimal `y-websocket`-compatible Yjs server without adding another backend dependency.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `index.js`: minimal websocket server compatible with `y-websocket`
|
|
8
|
+
- `package.json`: runtime dependencies and `npm run start`
|
|
9
|
+
|
|
10
|
+
## Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cd yjs
|
|
14
|
+
npm install
|
|
15
|
+
npm run start
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
By default the server listens on `ws://0.0.0.0:1234`.
|
|
19
|
+
|
|
20
|
+
To override host or port:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
HOST=0.0.0.0 PORT=1234 npm run start
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
PowerShell:
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
$env:HOST="0.0.0.0"
|
|
30
|
+
$env:PORT="1234"
|
|
31
|
+
npm run start
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Client Link Example
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import * as Y from 'yjs'
|
|
38
|
+
import { WebsocketProvider } from 'y-websocket'
|
|
39
|
+
|
|
40
|
+
const ydoc = new Y.Doc()
|
|
41
|
+
const provider = new WebsocketProvider('ws://127.0.0.1:1234', 'office-word-demo', ydoc)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<RichTextEditor
|
|
46
|
+
v-model="content"
|
|
47
|
+
:collaboration="{
|
|
48
|
+
document: ydoc,
|
|
49
|
+
field: 'content',
|
|
50
|
+
provider,
|
|
51
|
+
user: {
|
|
52
|
+
name: '张三',
|
|
53
|
+
color: '#3b82f6',
|
|
54
|
+
},
|
|
55
|
+
}"
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Notes
|
|
60
|
+
|
|
61
|
+
- all collaborators must use the same websocket address and the same room name
|
|
62
|
+
- if clients are on different machines, do not use `127.0.0.1`; use the server's real LAN or public address
|
|
63
|
+
- this sample server is in-memory only; document state is lost after restart
|
package/yjs/index.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
|
|
3
|
+
import * as decoding from 'lib0/decoding'
|
|
4
|
+
import * as encoding from 'lib0/encoding'
|
|
5
|
+
import * as awarenessProtocol from 'y-protocols/awareness'
|
|
6
|
+
import * as syncProtocol from 'y-protocols/sync'
|
|
7
|
+
import WebSocket, { WebSocketServer } from 'ws'
|
|
8
|
+
import * as Y from 'yjs'
|
|
9
|
+
|
|
10
|
+
const host = process.env.HOST || '0.0.0.0'
|
|
11
|
+
const port = Number(process.env.PORT || 1234)
|
|
12
|
+
|
|
13
|
+
const messageSync = 0
|
|
14
|
+
const messageAwareness = 1
|
|
15
|
+
const pingTimeout = 30000
|
|
16
|
+
|
|
17
|
+
const docs = new Map()
|
|
18
|
+
|
|
19
|
+
function getDoc(name) {
|
|
20
|
+
const existing = docs.get(name)
|
|
21
|
+
if (existing) {
|
|
22
|
+
return existing
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const doc = new Y.Doc()
|
|
26
|
+
const awareness = new awarenessProtocol.Awareness(doc)
|
|
27
|
+
const conns = new Map()
|
|
28
|
+
|
|
29
|
+
awareness.setLocalState(null)
|
|
30
|
+
|
|
31
|
+
doc.on('update', (update) => {
|
|
32
|
+
const encoder = encoding.createEncoder()
|
|
33
|
+
encoding.writeVarUint(encoder, messageSync)
|
|
34
|
+
syncProtocol.writeUpdate(encoder, update)
|
|
35
|
+
const message = encoding.toUint8Array(encoder)
|
|
36
|
+
|
|
37
|
+
conns.forEach((_, conn) => {
|
|
38
|
+
send(conn, message)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
awareness.on('update', ({ added, updated, removed }, conn) => {
|
|
43
|
+
const changedClients = added.concat(updated, removed)
|
|
44
|
+
|
|
45
|
+
if (conn) {
|
|
46
|
+
const controlledIds = conns.get(conn)
|
|
47
|
+
if (controlledIds) {
|
|
48
|
+
added.forEach((id) => controlledIds.add(id))
|
|
49
|
+
removed.forEach((id) => controlledIds.delete(id))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const encoder = encoding.createEncoder()
|
|
54
|
+
encoding.writeVarUint(encoder, messageAwareness)
|
|
55
|
+
encoding.writeVarUint8Array(
|
|
56
|
+
encoder,
|
|
57
|
+
awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const message = encoding.toUint8Array(encoder)
|
|
61
|
+
conns.forEach((_, ws) => {
|
|
62
|
+
send(ws, message)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const entry = { name, doc, awareness, conns }
|
|
67
|
+
docs.set(name, entry)
|
|
68
|
+
return entry
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function removeDocIfEmpty(entry) {
|
|
72
|
+
if (entry.conns.size === 0) {
|
|
73
|
+
entry.doc.destroy()
|
|
74
|
+
docs.delete(entry.name)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function closeConn(entry, conn) {
|
|
79
|
+
const controlledIds = entry.conns.get(conn)
|
|
80
|
+
|
|
81
|
+
if (controlledIds) {
|
|
82
|
+
entry.conns.delete(conn)
|
|
83
|
+
awarenessProtocol.removeAwarenessStates(
|
|
84
|
+
entry.awareness,
|
|
85
|
+
Array.from(controlledIds),
|
|
86
|
+
null,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
conn.close()
|
|
92
|
+
} catch {
|
|
93
|
+
// ignore close failures
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
removeDocIfEmpty(entry)
|
|
97
|
+
console.log(`[disconnect] room=${entry.name} clients=${entry.conns.size}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function send(conn, message) {
|
|
101
|
+
if (conn.readyState !== WebSocket.OPEN && conn.readyState !== WebSocket.CONNECTING) {
|
|
102
|
+
try {
|
|
103
|
+
conn.close()
|
|
104
|
+
} catch {
|
|
105
|
+
// ignore close failures
|
|
106
|
+
}
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
conn.send(message, (error) => {
|
|
111
|
+
if (error) {
|
|
112
|
+
try {
|
|
113
|
+
conn.close()
|
|
114
|
+
} catch {
|
|
115
|
+
// ignore close failures
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function setupWSConnection(conn, req) {
|
|
122
|
+
const docName = (req.url || '/').slice(1).split('?')[0] || 'default'
|
|
123
|
+
const entry = getDoc(docName)
|
|
124
|
+
|
|
125
|
+
entry.conns.set(conn, new Set())
|
|
126
|
+
conn.binaryType = 'arraybuffer'
|
|
127
|
+
|
|
128
|
+
conn.on('message', (message) => {
|
|
129
|
+
try {
|
|
130
|
+
const decoder = decoding.createDecoder(new Uint8Array(message))
|
|
131
|
+
const encoder = encoding.createEncoder()
|
|
132
|
+
const messageType = decoding.readVarUint(decoder)
|
|
133
|
+
|
|
134
|
+
switch (messageType) {
|
|
135
|
+
case messageSync:
|
|
136
|
+
encoding.writeVarUint(encoder, messageSync)
|
|
137
|
+
syncProtocol.readSyncMessage(decoder, encoder, entry.doc, conn)
|
|
138
|
+
if (encoding.length(encoder) > 1) {
|
|
139
|
+
send(conn, encoding.toUint8Array(encoder))
|
|
140
|
+
}
|
|
141
|
+
break
|
|
142
|
+
case messageAwareness:
|
|
143
|
+
awarenessProtocol.applyAwarenessUpdate(
|
|
144
|
+
entry.awareness,
|
|
145
|
+
decoding.readVarUint8Array(decoder),
|
|
146
|
+
conn,
|
|
147
|
+
)
|
|
148
|
+
break
|
|
149
|
+
default:
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('[message-error]', error)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
let pongReceived = true
|
|
158
|
+
const pingInterval = setInterval(() => {
|
|
159
|
+
if (!pongReceived) {
|
|
160
|
+
clearInterval(pingInterval)
|
|
161
|
+
closeConn(entry, conn)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
pongReceived = false
|
|
166
|
+
try {
|
|
167
|
+
conn.ping()
|
|
168
|
+
} catch {
|
|
169
|
+
clearInterval(pingInterval)
|
|
170
|
+
closeConn(entry, conn)
|
|
171
|
+
}
|
|
172
|
+
}, pingTimeout)
|
|
173
|
+
|
|
174
|
+
conn.on('pong', () => {
|
|
175
|
+
pongReceived = true
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
conn.on('close', () => {
|
|
179
|
+
clearInterval(pingInterval)
|
|
180
|
+
closeConn(entry, conn)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
conn.on('error', (error) => {
|
|
184
|
+
console.error('[socket-error]', error)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const syncEncoder = encoding.createEncoder()
|
|
188
|
+
encoding.writeVarUint(syncEncoder, messageSync)
|
|
189
|
+
syncProtocol.writeSyncStep1(syncEncoder, entry.doc)
|
|
190
|
+
send(conn, encoding.toUint8Array(syncEncoder))
|
|
191
|
+
|
|
192
|
+
const awarenessStates = entry.awareness.getStates()
|
|
193
|
+
if (awarenessStates.size > 0) {
|
|
194
|
+
const awarenessEncoder = encoding.createEncoder()
|
|
195
|
+
encoding.writeVarUint(awarenessEncoder, messageAwareness)
|
|
196
|
+
encoding.writeVarUint8Array(
|
|
197
|
+
awarenessEncoder,
|
|
198
|
+
awarenessProtocol.encodeAwarenessUpdate(
|
|
199
|
+
entry.awareness,
|
|
200
|
+
Array.from(awarenessStates.keys()),
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
send(conn, encoding.toUint8Array(awarenessEncoder))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(`[connect] room=${docName} clients=${entry.conns.size}`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const server = http.createServer((_req, res) => {
|
|
210
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
211
|
+
res.end('ok')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const wss = new WebSocketServer({ noServer: true })
|
|
215
|
+
|
|
216
|
+
server.on('upgrade', (request, socket, head) => {
|
|
217
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
218
|
+
setupWSConnection(ws, request)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
server.listen(port, host, () => {
|
|
223
|
+
console.log(`yjs websocket server running at ws://${host}:${port}`)
|
|
224
|
+
})
|
package/yjs/package.json
ADDED