@axure/y-prosemirror 1.3.7-fork.2
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/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/src/lib.d.ts +114 -0
- package/dist/src/plugins/cursor-plugin.d.ts +17 -0
- package/dist/src/plugins/keys.d.ts +20 -0
- package/dist/src/plugins/sync-plugin.d.ts +148 -0
- package/dist/src/plugins/undo-plugin.d.ts +33 -0
- package/dist/src/utils.d.ts +1 -0
- package/dist/src/y-prosemirror.d.ts +5 -0
- package/dist/y-prosemirror.cjs +2268 -0
- package/dist/y-prosemirror.cjs.map +1 -0
- package/package.json +89 -0
- package/src/lib.js +440 -0
- package/src/plugins/cursor-plugin.js +267 -0
- package/src/plugins/keys.js +23 -0
- package/src/plugins/sync-plugin.js +1350 -0
- package/src/plugins/undo-plugin.js +126 -0
- package/src/utils.js +20 -0
- package/src/y-prosemirror.js +11 -0
package/src/lib.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { updateYFragment, createNodeFromYElement, yattr2markname, createEmptyMeta } from './plugins/sync-plugin.js' // eslint-disable-line
|
|
2
|
+
import { ySyncPluginKey } from './plugins/keys.js'
|
|
3
|
+
import * as Y from 'yjs'
|
|
4
|
+
import { EditorView } from 'prosemirror-view' // eslint-disable-line
|
|
5
|
+
import { Node, Schema, Fragment } from 'prosemirror-model' // eslint-disable-line
|
|
6
|
+
import * as error from 'lib0/error'
|
|
7
|
+
import * as map from 'lib0/map'
|
|
8
|
+
import * as eventloop from 'lib0/eventloop'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Either a node if type is YXmlElement or an Array of text nodes if YXmlText
|
|
12
|
+
* @typedef {Map<Y.AbstractType, Node | Array<Node>>} ProsemirrorMapping
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Is null if no timeout is in progress.
|
|
17
|
+
* Is defined if a timeout is in progress.
|
|
18
|
+
* Maps from view
|
|
19
|
+
* @type {Map<EditorView, Map<any, any>>|null}
|
|
20
|
+
*/
|
|
21
|
+
let viewsToUpdate = null
|
|
22
|
+
|
|
23
|
+
const updateMetas = () => {
|
|
24
|
+
const ups = /** @type {Map<EditorView, Map<any, any>>} */ (viewsToUpdate)
|
|
25
|
+
viewsToUpdate = null
|
|
26
|
+
ups.forEach((metas, view) => {
|
|
27
|
+
const tr = view.state.tr
|
|
28
|
+
const syncState = ySyncPluginKey.getState(view.state)
|
|
29
|
+
if (syncState && syncState.binding && !syncState.binding.isDestroyed) {
|
|
30
|
+
metas.forEach((val, key) => {
|
|
31
|
+
tr.setMeta(key, val)
|
|
32
|
+
})
|
|
33
|
+
view.dispatch(tr)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const setMeta = (view, key, value) => {
|
|
39
|
+
if (!viewsToUpdate) {
|
|
40
|
+
viewsToUpdate = new Map()
|
|
41
|
+
eventloop.timeout(0, updateMetas)
|
|
42
|
+
}
|
|
43
|
+
map.setIfUndefined(viewsToUpdate, view, map.create).set(key, value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Transforms a Prosemirror based absolute position to a Yjs Cursor (relative position in the Yjs model).
|
|
48
|
+
*
|
|
49
|
+
* @param {number} pos
|
|
50
|
+
* @param {Y.XmlFragment} type
|
|
51
|
+
* @param {ProsemirrorMapping} mapping
|
|
52
|
+
* @return {any} relative position
|
|
53
|
+
*/
|
|
54
|
+
export const absolutePositionToRelativePosition = (pos, type, mapping) => {
|
|
55
|
+
if (pos === 0) {
|
|
56
|
+
// if the type is later populated, we want to retain the 0 position (hence assoc=-1)
|
|
57
|
+
return Y.createRelativePositionFromTypeIndex(type, 0, type.length === 0 ? -1 : 0)
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* @type {any}
|
|
61
|
+
*/
|
|
62
|
+
let n = type._first === null ? null : /** @type {Y.ContentType} */ (type._first.content).type
|
|
63
|
+
while (n !== null && type !== n) {
|
|
64
|
+
if (n instanceof Y.XmlText) {
|
|
65
|
+
if (n._length >= pos) {
|
|
66
|
+
return Y.createRelativePositionFromTypeIndex(n, pos, type.length === 0 ? -1 : 0)
|
|
67
|
+
} else {
|
|
68
|
+
pos -= n._length
|
|
69
|
+
}
|
|
70
|
+
if (n._item !== null && n._item.next !== null) {
|
|
71
|
+
n = /** @type {Y.ContentType} */ (n._item.next.content).type
|
|
72
|
+
} else {
|
|
73
|
+
do {
|
|
74
|
+
n = n._item === null ? null : n._item.parent
|
|
75
|
+
pos--
|
|
76
|
+
} while (n !== type && n !== null && n._item !== null && n._item.next === null)
|
|
77
|
+
if (n !== null && n !== type) {
|
|
78
|
+
// @ts-gnore we know that n.next !== null because of above loop conditition
|
|
79
|
+
n = n._item === null ? null : /** @type {Y.ContentType} */ (/** @type Y.Item */ (n._item.next).content).type
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
const pNodeSize = /** @type {any} */ (mapping.get(n) || { nodeSize: 0 }).nodeSize
|
|
84
|
+
if (n._first !== null && pos < pNodeSize) {
|
|
85
|
+
n = /** @type {Y.ContentType} */ (n._first.content).type
|
|
86
|
+
pos--
|
|
87
|
+
} else {
|
|
88
|
+
if (pos === 1 && n._length === 0 && pNodeSize > 1) {
|
|
89
|
+
// edge case, should end in this paragraph
|
|
90
|
+
return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null)
|
|
91
|
+
}
|
|
92
|
+
pos -= pNodeSize
|
|
93
|
+
if (n._item !== null && n._item.next !== null) {
|
|
94
|
+
n = /** @type {Y.ContentType} */ (n._item.next.content).type
|
|
95
|
+
} else {
|
|
96
|
+
if (pos === 0) {
|
|
97
|
+
// set to end of n.parent
|
|
98
|
+
n = n._item === null ? n : n._item.parent
|
|
99
|
+
return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null)
|
|
100
|
+
}
|
|
101
|
+
do {
|
|
102
|
+
n = /** @type {Y.Item} */ (n._item).parent
|
|
103
|
+
pos--
|
|
104
|
+
} while (n !== type && /** @type {Y.Item} */ (n._item).next === null)
|
|
105
|
+
// if n is null at this point, we have an unexpected case
|
|
106
|
+
if (n !== type) {
|
|
107
|
+
// We know that n._item.next is defined because of above loop condition
|
|
108
|
+
n = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (/** @type {Y.Item} */ (n._item).next).content).type
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (n === null) {
|
|
114
|
+
throw error.unexpectedCase()
|
|
115
|
+
}
|
|
116
|
+
if (pos === 0 && n.constructor !== Y.XmlText && n !== type) { // TODO: set to <= 0
|
|
117
|
+
return createRelativePosition(n._item.parent, n._item)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return Y.createRelativePositionFromTypeIndex(type, type._length, type.length === 0 ? -1 : 0)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const createRelativePosition = (type, item) => {
|
|
124
|
+
let typeid = null
|
|
125
|
+
let tname = null
|
|
126
|
+
if (type._item === null) {
|
|
127
|
+
tname = Y.findRootTypeKey(type)
|
|
128
|
+
} else {
|
|
129
|
+
typeid = Y.createID(type._item.id.client, type._item.id.clock)
|
|
130
|
+
}
|
|
131
|
+
return new Y.RelativePosition(typeid, tname, item.id)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {Y.Doc} y
|
|
136
|
+
* @param {Y.XmlFragment} documentType Top level type that is bound to pView
|
|
137
|
+
* @param {any} relPos Encoded Yjs based relative position
|
|
138
|
+
* @param {ProsemirrorMapping} mapping
|
|
139
|
+
* @return {null|number}
|
|
140
|
+
*/
|
|
141
|
+
export const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) => {
|
|
142
|
+
const decodedPos = Y.createAbsolutePositionFromRelativePosition(relPos, y)
|
|
143
|
+
if (decodedPos === null || (decodedPos.type !== documentType && !Y.isParentOf(documentType, decodedPos.type._item))) {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
let type = decodedPos.type
|
|
147
|
+
let pos = 0
|
|
148
|
+
if (type.constructor === Y.XmlText) {
|
|
149
|
+
pos = decodedPos.index
|
|
150
|
+
} else if (type._item === null || !type._item.deleted) {
|
|
151
|
+
let n = type._first
|
|
152
|
+
let i = 0
|
|
153
|
+
while (i < type._length && i < decodedPos.index && n !== null) {
|
|
154
|
+
if (!n.deleted) {
|
|
155
|
+
const t = /** @type {Y.ContentType} */ (n.content).type
|
|
156
|
+
i++
|
|
157
|
+
if (t instanceof Y.XmlText) {
|
|
158
|
+
pos += t._length
|
|
159
|
+
} else {
|
|
160
|
+
pos += /** @type {any} */ (mapping.get(t)).nodeSize
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
n = /** @type {Y.Item} */ (n.right)
|
|
164
|
+
}
|
|
165
|
+
pos += 1 // increase because we go out of n
|
|
166
|
+
}
|
|
167
|
+
while (type !== documentType && type._item !== null) {
|
|
168
|
+
// @ts-ignore
|
|
169
|
+
const parent = type._item.parent
|
|
170
|
+
// @ts-ignore
|
|
171
|
+
if (parent._item === null || !parent._item.deleted) {
|
|
172
|
+
pos += 1 // the start tag
|
|
173
|
+
let n = /** @type {Y.AbstractType} */ (parent)._first
|
|
174
|
+
// now iterate until we found type
|
|
175
|
+
while (n !== null) {
|
|
176
|
+
const contentType = /** @type {Y.ContentType} */ (n.content).type
|
|
177
|
+
if (contentType === type) {
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
if (!n.deleted) {
|
|
181
|
+
if (contentType instanceof Y.XmlText) {
|
|
182
|
+
pos += contentType._length
|
|
183
|
+
} else {
|
|
184
|
+
pos += /** @type {any} */ (mapping.get(contentType)).nodeSize
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
n = n.right
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
type = /** @type {Y.AbstractType} */ (parent)
|
|
191
|
+
}
|
|
192
|
+
return pos - 1 // we don't count the most outer tag, because it is a fragment
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Utility function for converting an Y.Fragment to a ProseMirror fragment.
|
|
197
|
+
*
|
|
198
|
+
* @param {Y.XmlFragment} yXmlFragment
|
|
199
|
+
* @param {Schema} schema
|
|
200
|
+
*/
|
|
201
|
+
export const yXmlFragmentToProseMirrorFragment = (yXmlFragment, schema) => {
|
|
202
|
+
const fragmentContent = yXmlFragment.toArray().map((t) =>
|
|
203
|
+
createNodeFromYElement(
|
|
204
|
+
/** @type {Y.XmlElement} */ (t),
|
|
205
|
+
schema,
|
|
206
|
+
createEmptyMeta()
|
|
207
|
+
)
|
|
208
|
+
).filter((n) => n !== null)
|
|
209
|
+
return Fragment.fromArray(fragmentContent)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Utility function for converting an Y.Fragment to a ProseMirror node.
|
|
214
|
+
*
|
|
215
|
+
* @param {Y.XmlFragment} yXmlFragment
|
|
216
|
+
* @param {Schema} schema
|
|
217
|
+
*/
|
|
218
|
+
export const yXmlFragmentToProseMirrorRootNode = (yXmlFragment, schema) =>
|
|
219
|
+
schema.topNodeType.create(null, yXmlFragmentToProseMirrorFragment(yXmlFragment, schema))
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The initial ProseMirror content should be supplied by Yjs. This function transforms a Y.Fragment
|
|
223
|
+
* to a ProseMirror Doc node and creates a mapping that is used by the sync plugin.
|
|
224
|
+
*
|
|
225
|
+
* @param {Y.XmlFragment} yXmlFragment
|
|
226
|
+
* @param {Schema} schema
|
|
227
|
+
*
|
|
228
|
+
* @todo deprecate mapping property
|
|
229
|
+
*/
|
|
230
|
+
export const initProseMirrorDoc = (yXmlFragment, schema) => {
|
|
231
|
+
const meta = createEmptyMeta()
|
|
232
|
+
const fragmentContent = yXmlFragment.toArray().map((t) =>
|
|
233
|
+
createNodeFromYElement(
|
|
234
|
+
/** @type {Y.XmlElement} */ (t),
|
|
235
|
+
schema,
|
|
236
|
+
meta
|
|
237
|
+
)
|
|
238
|
+
).filter((n) => n !== null)
|
|
239
|
+
const doc = schema.topNodeType.create(null, Fragment.fromArray(fragmentContent))
|
|
240
|
+
return { doc, meta, mapping: meta.mapping }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Utility method to convert a Prosemirror Doc Node into a Y.Doc.
|
|
245
|
+
*
|
|
246
|
+
* This can be used when importing existing content to Y.Doc for the first time,
|
|
247
|
+
* note that this should not be used to rehydrate a Y.Doc from a database once
|
|
248
|
+
* collaboration has begun as all history will be lost
|
|
249
|
+
*
|
|
250
|
+
* @param {Node} doc
|
|
251
|
+
* @param {string} xmlFragment
|
|
252
|
+
* @return {Y.Doc}
|
|
253
|
+
*/
|
|
254
|
+
export function prosemirrorToYDoc (doc, xmlFragment = 'prosemirror') {
|
|
255
|
+
const ydoc = new Y.Doc()
|
|
256
|
+
const type = /** @type {Y.XmlFragment} */ (ydoc.get(xmlFragment, Y.XmlFragment))
|
|
257
|
+
if (!type.doc) {
|
|
258
|
+
return ydoc
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
prosemirrorToYXmlFragment(doc, type)
|
|
262
|
+
return type.doc
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Utility method to update an empty Y.XmlFragment with content from a Prosemirror Doc Node.
|
|
267
|
+
*
|
|
268
|
+
* This can be used when importing existing content to Y.Doc for the first time,
|
|
269
|
+
* note that this should not be used to rehydrate a Y.Doc from a database once
|
|
270
|
+
* collaboration has begun as all history will be lost
|
|
271
|
+
*
|
|
272
|
+
* Note: The Y.XmlFragment does not need to be part of a Y.Doc document at the time that this
|
|
273
|
+
* method is called, but it must be added before any other operations are performed on it.
|
|
274
|
+
*
|
|
275
|
+
* @param {Node} doc prosemirror document.
|
|
276
|
+
* @param {Y.XmlFragment} [xmlFragment] If supplied, an xml fragment to be
|
|
277
|
+
* populated from the prosemirror state; otherwise a new XmlFragment will be created.
|
|
278
|
+
* @return {Y.XmlFragment}
|
|
279
|
+
*/
|
|
280
|
+
export function prosemirrorToYXmlFragment (doc, xmlFragment) {
|
|
281
|
+
const type = xmlFragment || new Y.XmlFragment()
|
|
282
|
+
const ydoc = type.doc ? type.doc : { transact: (transaction) => transaction(undefined) }
|
|
283
|
+
updateYFragment(ydoc, type, doc, { mapping: new Map(), isOMark: new Map() })
|
|
284
|
+
return type
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Utility method to convert Prosemirror compatible JSON into a Y.Doc.
|
|
289
|
+
*
|
|
290
|
+
* This can be used when importing existing content to Y.Doc for the first time,
|
|
291
|
+
* note that this should not be used to rehydrate a Y.Doc from a database once
|
|
292
|
+
* collaboration has begun as all history will be lost
|
|
293
|
+
*
|
|
294
|
+
* @param {Schema} schema
|
|
295
|
+
* @param {any} state
|
|
296
|
+
* @param {string} xmlFragment
|
|
297
|
+
* @return {Y.Doc}
|
|
298
|
+
*/
|
|
299
|
+
export function prosemirrorJSONToYDoc (schema, state, xmlFragment = 'prosemirror') {
|
|
300
|
+
const doc = Node.fromJSON(schema, state)
|
|
301
|
+
return prosemirrorToYDoc(doc, xmlFragment)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Utility method to convert Prosemirror compatible JSON to a Y.XmlFragment
|
|
306
|
+
*
|
|
307
|
+
* This can be used when importing existing content to Y.Doc for the first time,
|
|
308
|
+
* note that this should not be used to rehydrate a Y.Doc from a database once
|
|
309
|
+
* collaboration has begun as all history will be lost
|
|
310
|
+
*
|
|
311
|
+
* @param {Schema} schema
|
|
312
|
+
* @param {any} state
|
|
313
|
+
* @param {Y.XmlFragment} [xmlFragment] If supplied, an xml fragment to be
|
|
314
|
+
* populated from the prosemirror state; otherwise a new XmlFragment will be created.
|
|
315
|
+
* @return {Y.XmlFragment}
|
|
316
|
+
*/
|
|
317
|
+
export function prosemirrorJSONToYXmlFragment (schema, state, xmlFragment) {
|
|
318
|
+
const doc = Node.fromJSON(schema, state)
|
|
319
|
+
return prosemirrorToYXmlFragment(doc, xmlFragment)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @deprecated Use `yXmlFragmentToProseMirrorRootNode` instead
|
|
324
|
+
*
|
|
325
|
+
* Utility method to convert a Y.Doc to a Prosemirror Doc node.
|
|
326
|
+
*
|
|
327
|
+
* @param {Schema} schema
|
|
328
|
+
* @param {Y.Doc} ydoc
|
|
329
|
+
* @return {Node}
|
|
330
|
+
*/
|
|
331
|
+
export function yDocToProsemirror (schema, ydoc) {
|
|
332
|
+
const state = yDocToProsemirrorJSON(ydoc)
|
|
333
|
+
return Node.fromJSON(schema, state)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
*
|
|
338
|
+
* @deprecated Use `yXmlFragmentToProseMirrorRootNode` instead
|
|
339
|
+
*
|
|
340
|
+
* Utility method to convert a Y.XmlFragment to a Prosemirror Doc node.
|
|
341
|
+
*
|
|
342
|
+
* @param {Schema} schema
|
|
343
|
+
* @param {Y.XmlFragment} xmlFragment
|
|
344
|
+
* @return {Node}
|
|
345
|
+
*/
|
|
346
|
+
export function yXmlFragmentToProsemirror (schema, xmlFragment) {
|
|
347
|
+
const state = yXmlFragmentToProsemirrorJSON(xmlFragment)
|
|
348
|
+
return Node.fromJSON(schema, state)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
*
|
|
353
|
+
* @deprecated Use `yXmlFragmentToProseMirrorRootNode` instead
|
|
354
|
+
*
|
|
355
|
+
* Utility method to convert a Y.Doc to Prosemirror compatible JSON.
|
|
356
|
+
*
|
|
357
|
+
* @param {Y.Doc} ydoc
|
|
358
|
+
* @param {string} xmlFragment
|
|
359
|
+
* @return {Record<string, any>}
|
|
360
|
+
*/
|
|
361
|
+
export function yDocToProsemirrorJSON (
|
|
362
|
+
ydoc,
|
|
363
|
+
xmlFragment = 'prosemirror'
|
|
364
|
+
) {
|
|
365
|
+
return yXmlFragmentToProsemirrorJSON(ydoc.getXmlFragment(xmlFragment))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* @deprecated Use `yXmlFragmentToProseMirrorRootNode` instead
|
|
370
|
+
*
|
|
371
|
+
* Utility method to convert a Y.Doc to Prosemirror compatible JSON.
|
|
372
|
+
*
|
|
373
|
+
* @param {Y.XmlFragment} xmlFragment The fragment, which must be part of a Y.Doc.
|
|
374
|
+
* @return {Record<string, any>}
|
|
375
|
+
*/
|
|
376
|
+
export function yXmlFragmentToProsemirrorJSON (xmlFragment) {
|
|
377
|
+
const items = xmlFragment.toArray()
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* @param {Y.AbstractType} item
|
|
381
|
+
*/
|
|
382
|
+
const serialize = item => {
|
|
383
|
+
/**
|
|
384
|
+
* @type {Object} NodeObject
|
|
385
|
+
* @property {string} NodeObject.type
|
|
386
|
+
* @property {Record<string, string>=} NodeObject.attrs
|
|
387
|
+
* @property {Array<NodeObject>=} NodeObject.content
|
|
388
|
+
*/
|
|
389
|
+
let response
|
|
390
|
+
|
|
391
|
+
// TODO: Must be a better way to detect text nodes than this
|
|
392
|
+
if (item instanceof Y.XmlText) {
|
|
393
|
+
const delta = item.toDelta()
|
|
394
|
+
response = delta.map(/** @param {any} d */ (d) => {
|
|
395
|
+
const text = {
|
|
396
|
+
type: 'text',
|
|
397
|
+
text: d.insert
|
|
398
|
+
}
|
|
399
|
+
if (d.attributes) {
|
|
400
|
+
text.marks = Object.keys(d.attributes).map((type_) => {
|
|
401
|
+
const attrs = d.attributes[type_]
|
|
402
|
+
const type = yattr2markname(type_)
|
|
403
|
+
const mark = {
|
|
404
|
+
type
|
|
405
|
+
}
|
|
406
|
+
if (Object.keys(attrs)) {
|
|
407
|
+
mark.attrs = attrs
|
|
408
|
+
}
|
|
409
|
+
return mark
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
return text
|
|
413
|
+
})
|
|
414
|
+
} else if (item instanceof Y.XmlElement) {
|
|
415
|
+
response = {
|
|
416
|
+
type: item.nodeName
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const attrs = item.getAttributes()
|
|
420
|
+
if (Object.keys(attrs).length) {
|
|
421
|
+
response.attrs = attrs
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const children = item.toArray()
|
|
425
|
+
if (children.length) {
|
|
426
|
+
response.content = children.map(serialize).flat()
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
// expected either Y.XmlElement or Y.XmlText
|
|
430
|
+
error.unexpectedCase()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return response
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
type: 'doc',
|
|
438
|
+
content: items.map(serialize)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import * as Y from 'yjs'
|
|
2
|
+
import { Decoration, DecorationSet } from "prosemirror-view"; // eslint-disable-line
|
|
3
|
+
import { Plugin } from "prosemirror-state"; // eslint-disable-line
|
|
4
|
+
import { Awareness } from "y-protocols/awareness"; // eslint-disable-line
|
|
5
|
+
import {
|
|
6
|
+
absolutePositionToRelativePosition,
|
|
7
|
+
relativePositionToAbsolutePosition,
|
|
8
|
+
setMeta
|
|
9
|
+
} from '../lib.js'
|
|
10
|
+
import { yCursorPluginKey, ySyncPluginKey } from './keys.js'
|
|
11
|
+
|
|
12
|
+
import * as math from 'lib0/math'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default awareness state filter
|
|
16
|
+
*
|
|
17
|
+
* @param {number} currentClientId current client id
|
|
18
|
+
* @param {number} userClientId user client id
|
|
19
|
+
* @param {any} _user user data
|
|
20
|
+
* @return {boolean}
|
|
21
|
+
*/
|
|
22
|
+
export const defaultAwarenessStateFilter = (currentClientId, userClientId, _user) => currentClientId !== userClientId
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default generator for a cursor element
|
|
26
|
+
*
|
|
27
|
+
* @param {any} user user data
|
|
28
|
+
* @return {HTMLElement}
|
|
29
|
+
*/
|
|
30
|
+
export const defaultCursorBuilder = (user) => {
|
|
31
|
+
const cursor = document.createElement('span')
|
|
32
|
+
cursor.classList.add('ProseMirror-yjs-cursor')
|
|
33
|
+
cursor.setAttribute('style', `border-color: ${user.color}`)
|
|
34
|
+
const userDiv = document.createElement('div')
|
|
35
|
+
userDiv.setAttribute('style', `background-color: ${user.color}`)
|
|
36
|
+
userDiv.insertBefore(document.createTextNode(user.name), null)
|
|
37
|
+
const nonbreakingSpace1 = document.createTextNode('\u2060')
|
|
38
|
+
const nonbreakingSpace2 = document.createTextNode('\u2060')
|
|
39
|
+
cursor.insertBefore(nonbreakingSpace1, null)
|
|
40
|
+
cursor.insertBefore(userDiv, null)
|
|
41
|
+
cursor.insertBefore(nonbreakingSpace2, null)
|
|
42
|
+
return cursor
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default generator for the selection attributes
|
|
47
|
+
*
|
|
48
|
+
* @param {any} user user data
|
|
49
|
+
* @return {import('prosemirror-view').DecorationAttrs}
|
|
50
|
+
*/
|
|
51
|
+
export const defaultSelectionBuilder = (user) => {
|
|
52
|
+
return {
|
|
53
|
+
style: `background-color: ${user.color}70`,
|
|
54
|
+
class: 'ProseMirror-yjs-selection'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const rxValidColor = /^#[0-9a-fA-F]{6}$/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {any} state
|
|
62
|
+
* @param {Awareness} awareness
|
|
63
|
+
* @param {function(number, number, any):boolean} awarenessFilter
|
|
64
|
+
* @param {(user: { name: string, color: string }, clientId: number) => Element} createCursor
|
|
65
|
+
* @param {(user: { name: string, color: string }, clientId: number) => import('prosemirror-view').DecorationAttrs} createSelection
|
|
66
|
+
* @return {any} DecorationSet
|
|
67
|
+
*/
|
|
68
|
+
export const createDecorations = (
|
|
69
|
+
state,
|
|
70
|
+
awareness,
|
|
71
|
+
awarenessFilter,
|
|
72
|
+
createCursor,
|
|
73
|
+
createSelection
|
|
74
|
+
) => {
|
|
75
|
+
const ystate = ySyncPluginKey.getState(state)
|
|
76
|
+
const y = ystate.doc
|
|
77
|
+
const decorations = []
|
|
78
|
+
if (
|
|
79
|
+
ystate.snapshot != null || ystate.prevSnapshot != null ||
|
|
80
|
+
ystate.binding.mapping.size === 0
|
|
81
|
+
) {
|
|
82
|
+
// do not render cursors while snapshot is active
|
|
83
|
+
return DecorationSet.create(state.doc, [])
|
|
84
|
+
}
|
|
85
|
+
awareness.getStates().forEach((aw, clientId) => {
|
|
86
|
+
if (!awarenessFilter(y.clientID, clientId, aw)) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (aw.cursor != null) {
|
|
91
|
+
const user = aw.user || {}
|
|
92
|
+
if (user.color == null) {
|
|
93
|
+
user.color = '#ffa500'
|
|
94
|
+
} else if (!rxValidColor.test(user.color)) {
|
|
95
|
+
// We only support 6-digit RGB colors in y-prosemirror
|
|
96
|
+
console.warn('A user uses an unsupported color format', user)
|
|
97
|
+
}
|
|
98
|
+
if (user.name == null) {
|
|
99
|
+
user.name = `User: ${clientId}`
|
|
100
|
+
}
|
|
101
|
+
let anchor = relativePositionToAbsolutePosition(
|
|
102
|
+
y,
|
|
103
|
+
ystate.type,
|
|
104
|
+
Y.createRelativePositionFromJSON(aw.cursor.anchor),
|
|
105
|
+
ystate.binding.mapping
|
|
106
|
+
)
|
|
107
|
+
let head = relativePositionToAbsolutePosition(
|
|
108
|
+
y,
|
|
109
|
+
ystate.type,
|
|
110
|
+
Y.createRelativePositionFromJSON(aw.cursor.head),
|
|
111
|
+
ystate.binding.mapping
|
|
112
|
+
)
|
|
113
|
+
if (anchor !== null && head !== null) {
|
|
114
|
+
const maxsize = math.max(state.doc.content.size - 1, 0)
|
|
115
|
+
anchor = math.min(anchor, maxsize)
|
|
116
|
+
head = math.min(head, maxsize)
|
|
117
|
+
decorations.push(
|
|
118
|
+
Decoration.widget(head, () => createCursor(user, clientId), {
|
|
119
|
+
key: clientId + '',
|
|
120
|
+
side: 10
|
|
121
|
+
})
|
|
122
|
+
)
|
|
123
|
+
const from = math.min(anchor, head)
|
|
124
|
+
const to = math.max(anchor, head)
|
|
125
|
+
decorations.push(
|
|
126
|
+
Decoration.inline(from, to, createSelection(user, clientId), {
|
|
127
|
+
inclusiveEnd: true,
|
|
128
|
+
inclusiveStart: false
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
return DecorationSet.create(state.doc, decorations)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* A prosemirror plugin that listens to awareness information on Yjs.
|
|
139
|
+
* This requires that a `prosemirrorPlugin` is also bound to the prosemirror.
|
|
140
|
+
*
|
|
141
|
+
* @public
|
|
142
|
+
* @param {Awareness} awareness
|
|
143
|
+
* @param {object} opts
|
|
144
|
+
* @param {function(any, any, any):boolean} [opts.awarenessStateFilter]
|
|
145
|
+
* @param {(user: any, clientId: number) => HTMLElement} [opts.cursorBuilder]
|
|
146
|
+
* @param {(user: any, clientId: number) => import('prosemirror-view').DecorationAttrs} [opts.selectionBuilder]
|
|
147
|
+
* @param {function(any):any} [opts.getSelection]
|
|
148
|
+
* @param {string} [cursorStateField] By default all editor bindings use the awareness 'cursor' field to propagate cursor information.
|
|
149
|
+
* @return {any}
|
|
150
|
+
*/
|
|
151
|
+
export const yCursorPlugin = (
|
|
152
|
+
awareness,
|
|
153
|
+
{
|
|
154
|
+
awarenessStateFilter = defaultAwarenessStateFilter,
|
|
155
|
+
cursorBuilder = defaultCursorBuilder,
|
|
156
|
+
selectionBuilder = defaultSelectionBuilder,
|
|
157
|
+
getSelection = (state) => state.selection
|
|
158
|
+
} = {},
|
|
159
|
+
cursorStateField = 'cursor'
|
|
160
|
+
) =>
|
|
161
|
+
new Plugin({
|
|
162
|
+
key: yCursorPluginKey,
|
|
163
|
+
state: {
|
|
164
|
+
init (_, state) {
|
|
165
|
+
return createDecorations(
|
|
166
|
+
state,
|
|
167
|
+
awareness,
|
|
168
|
+
awarenessStateFilter,
|
|
169
|
+
cursorBuilder,
|
|
170
|
+
selectionBuilder
|
|
171
|
+
)
|
|
172
|
+
},
|
|
173
|
+
apply (tr, prevState, _oldState, newState) {
|
|
174
|
+
const ystate = ySyncPluginKey.getState(newState)
|
|
175
|
+
const yCursorState = tr.getMeta(yCursorPluginKey)
|
|
176
|
+
if (
|
|
177
|
+
(ystate && ystate.isChangeOrigin) ||
|
|
178
|
+
(yCursorState && yCursorState.awarenessUpdated)
|
|
179
|
+
) {
|
|
180
|
+
return createDecorations(
|
|
181
|
+
newState,
|
|
182
|
+
awareness,
|
|
183
|
+
awarenessStateFilter,
|
|
184
|
+
cursorBuilder,
|
|
185
|
+
selectionBuilder
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
return prevState.map(tr.mapping, tr.doc)
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
props: {
|
|
192
|
+
decorations: (state) => {
|
|
193
|
+
return yCursorPluginKey.getState(state)
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
view: (view) => {
|
|
197
|
+
const awarenessListener = () => {
|
|
198
|
+
// @ts-ignore
|
|
199
|
+
if (view.docView) {
|
|
200
|
+
setMeta(view, yCursorPluginKey, { awarenessUpdated: true })
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const updateCursorInfo = () => {
|
|
204
|
+
const ystate = ySyncPluginKey.getState(view.state)
|
|
205
|
+
// @note We make implicit checks when checking for the cursor property
|
|
206
|
+
const current = awareness.getLocalState() || {}
|
|
207
|
+
if (view.hasFocus()) {
|
|
208
|
+
const selection = getSelection(view.state)
|
|
209
|
+
/**
|
|
210
|
+
* @type {Y.RelativePosition}
|
|
211
|
+
*/
|
|
212
|
+
const anchor = absolutePositionToRelativePosition(
|
|
213
|
+
selection.anchor,
|
|
214
|
+
ystate.type,
|
|
215
|
+
ystate.binding.mapping
|
|
216
|
+
)
|
|
217
|
+
/**
|
|
218
|
+
* @type {Y.RelativePosition}
|
|
219
|
+
*/
|
|
220
|
+
const head = absolutePositionToRelativePosition(
|
|
221
|
+
selection.head,
|
|
222
|
+
ystate.type,
|
|
223
|
+
ystate.binding.mapping
|
|
224
|
+
)
|
|
225
|
+
if (
|
|
226
|
+
current.cursor == null ||
|
|
227
|
+
!Y.compareRelativePositions(
|
|
228
|
+
Y.createRelativePositionFromJSON(current.cursor.anchor),
|
|
229
|
+
anchor
|
|
230
|
+
) ||
|
|
231
|
+
!Y.compareRelativePositions(
|
|
232
|
+
Y.createRelativePositionFromJSON(current.cursor.head),
|
|
233
|
+
head
|
|
234
|
+
)
|
|
235
|
+
) {
|
|
236
|
+
awareness.setLocalStateField(cursorStateField, {
|
|
237
|
+
anchor,
|
|
238
|
+
head
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
} else if (
|
|
242
|
+
current.cursor != null &&
|
|
243
|
+
relativePositionToAbsolutePosition(
|
|
244
|
+
ystate.doc,
|
|
245
|
+
ystate.type,
|
|
246
|
+
Y.createRelativePositionFromJSON(current.cursor.anchor),
|
|
247
|
+
ystate.binding.mapping
|
|
248
|
+
) !== null
|
|
249
|
+
) {
|
|
250
|
+
// delete cursor information if current cursor information is owned by this editor binding
|
|
251
|
+
awareness.setLocalStateField(cursorStateField, null)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
awareness.on('change', awarenessListener)
|
|
255
|
+
view.dom.addEventListener('focusin', updateCursorInfo)
|
|
256
|
+
view.dom.addEventListener('focusout', updateCursorInfo)
|
|
257
|
+
return {
|
|
258
|
+
update: updateCursorInfo,
|
|
259
|
+
destroy: () => {
|
|
260
|
+
view.dom.removeEventListener('focusin', updateCursorInfo)
|
|
261
|
+
view.dom.removeEventListener('focusout', updateCursorInfo)
|
|
262
|
+
awareness.off('change', awarenessListener)
|
|
263
|
+
awareness.setLocalStateField(cursorStateField, null)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
})
|