@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/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",
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
+ })
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@ap666/office-word-yjs-server",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "start": "node index.js"
7
+ },
8
+ "dependencies": {
9
+ "lib0": "^0.2.102",
10
+ "ws": "^8.18.0",
11
+ "y-protocols": "^1.0.6",
12
+ "yjs": "^13.6.30"
13
+ }
14
+ }