@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/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
+ })