@kerebron/extension-yjs 0.5.2 → 0.5.4

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.
Files changed (67) hide show
  1. package/README.md +3 -89
  2. package/esm/ExtensionYjs.d.ts +10 -1
  3. package/esm/ExtensionYjs.d.ts.map +1 -1
  4. package/esm/ExtensionYjs.js +47 -6
  5. package/esm/ExtensionYjs.js.map +1 -1
  6. package/esm/MarkYChange.d.ts +7 -0
  7. package/esm/MarkYChange.d.ts.map +1 -0
  8. package/esm/MarkYChange.js +21 -0
  9. package/esm/MarkYChange.js.map +1 -0
  10. package/esm/ProsemirrorBinding.d.ts +60 -0
  11. package/esm/ProsemirrorBinding.d.ts.map +1 -0
  12. package/esm/ProsemirrorBinding.js +405 -0
  13. package/esm/ProsemirrorBinding.js.map +1 -0
  14. package/esm/createNodeFromYElement.d.ts +10 -0
  15. package/esm/createNodeFromYElement.d.ts.map +1 -0
  16. package/esm/createNodeFromYElement.js +123 -0
  17. package/esm/createNodeFromYElement.js.map +1 -0
  18. package/esm/debug.d.ts +13 -0
  19. package/esm/debug.d.ts.map +1 -0
  20. package/esm/debug.js +147 -0
  21. package/esm/debug.js.map +1 -0
  22. package/esm/keys.d.ts +5 -8
  23. package/esm/keys.d.ts.map +1 -1
  24. package/esm/keys.js +1 -6
  25. package/esm/keys.js.map +1 -1
  26. package/esm/lib.d.ts +1 -2
  27. package/esm/lib.d.ts.map +1 -1
  28. package/esm/lib.js +12 -2
  29. package/esm/lib.js.map +1 -1
  30. package/esm/updateYFragment.d.ts +17 -0
  31. package/esm/updateYFragment.d.ts.map +1 -0
  32. package/esm/updateYFragment.js +333 -0
  33. package/esm/updateYFragment.js.map +1 -0
  34. package/esm/utils.d.ts +2 -0
  35. package/esm/utils.d.ts.map +1 -1
  36. package/esm/utils.js +4 -0
  37. package/esm/utils.js.map +1 -1
  38. package/esm/yPositionPlugin.d.ts +12 -4
  39. package/esm/yPositionPlugin.d.ts.map +1 -1
  40. package/esm/yPositionPlugin.js +114 -61
  41. package/esm/yPositionPlugin.js.map +1 -1
  42. package/esm/ySyncPlugin.d.ts +16 -78
  43. package/esm/ySyncPlugin.d.ts.map +1 -1
  44. package/esm/ySyncPlugin.js +81 -848
  45. package/esm/ySyncPlugin.js.map +1 -1
  46. package/esm/yUndoPlugin.d.ts +1 -1
  47. package/esm/yUndoPlugin.d.ts.map +1 -1
  48. package/esm/yUndoPlugin.js +1 -1
  49. package/esm/yUndoPlugin.js.map +1 -1
  50. package/package.json +9 -3
  51. package/src/ExtensionYjs.ts +65 -9
  52. package/src/MarkYChange.ts +23 -0
  53. package/src/ProsemirrorBinding.ts +607 -0
  54. package/src/createNodeFromYElement.ts +175 -0
  55. package/src/debug.ts +218 -0
  56. package/src/keys.ts +9 -9
  57. package/src/lib.ts +11 -3
  58. package/src/updateYFragment.ts +439 -0
  59. package/src/utils.ts +6 -0
  60. package/src/yPositionPlugin.ts +167 -92
  61. package/src/ySyncPlugin.ts +135 -1193
  62. package/src/yUndoPlugin.ts +1 -1
  63. package/esm/convertUtils.d.ts +0 -59
  64. package/esm/convertUtils.d.ts.map +0 -1
  65. package/esm/convertUtils.js +0 -89
  66. package/esm/convertUtils.js.map +0 -1
  67. package/src/convertUtils.ts +0 -143
@@ -1,99 +1,97 @@
1
- // deno-lint-ignore-file no-window
2
1
  import * as Y from 'yjs';
3
- import { createMutex } from 'lib0/mutex';
4
- import * as PModel from 'prosemirror-model';
5
- import { AllSelection, NodeSelection, Plugin, TextSelection, } from 'prosemirror-state';
6
- import * as math from 'lib0/math';
7
- import * as object from 'lib0/object';
8
- import * as set from 'lib0/set';
9
- import { simpleDiff } from 'lib0/diff';
10
- import * as error from 'lib0/error';
11
- import * as random from 'lib0/random';
12
- import * as environment from 'lib0/environment';
13
- import * as dom from 'lib0/dom';
14
- import * as eventloop from 'lib0/eventloop';
15
- import * as map from 'lib0/map';
16
- import { remoteSelectionPluginKey } from '@kerebron/extension-basic-editor/ExtensionRemoteSelection';
2
+ import { Plugin } from 'prosemirror-state';
17
3
  import { ySyncPluginKey, yUndoPluginKey } from './keys.js';
18
- import * as utils from './utils.js';
19
- import { absolutePositionToRelativePosition, relativePositionToAbsolutePosition, } from './lib.js';
20
- export const isVisible = (item, snapshot) => snapshot === undefined
21
- ? !item.deleted
22
- : (snapshot.sv.has(item.id.client) && /** @type {number} */
23
- (snapshot.sv.get(item.id.client)) > item.id.clock &&
24
- !Y.isDeleted(snapshot.ds, item.id));
25
- const defaultColors = [{
26
- light: '#ecd44433',
27
- dark: '#ecd444',
28
- }];
29
- const getUserColor = (colorMapping, colors, user) => {
30
- // @todo do not hit the same color twice if possible
31
- if (!colorMapping.has(user)) {
32
- if (colorMapping.size < colors.length) {
33
- const usedColors = set.create();
34
- colorMapping.forEach((color) => usedColors.add(color));
35
- colors = colors.filter((color) => !usedColors.has(color));
36
- }
37
- colorMapping.set(user, random.oneOf(colors));
38
- }
39
- return colorMapping.get(user) || defaultColors[0];
40
- };
4
+ import { defaultColors, ProsemirrorBinding } from './ProsemirrorBinding.js';
41
5
  /**
42
6
  * This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
43
7
  *
44
8
  * This plugin also keeps references to the type and the shared document so other plugins can access it.
45
9
  */
46
- export const ySyncPlugin = (yXmlFragment, { colors = defaultColors, colorMapping = new Map(), permanentUserData = null, onFirstRender = () => {
47
- }, mapping, } = {}) => {
10
+ export const ySyncPlugin = (schema, createWsProvider, { colors = defaultColors, colorMapping = new Map(), onFirstRender = () => {
11
+ }, } = {}) => {
48
12
  let initialContentChanged = false;
49
- const binding = new ProsemirrorBinding(yXmlFragment, mapping);
50
13
  const plugin = new Plugin({
51
14
  props: {
52
- editable: (state) => {
53
- const syncState = ySyncPluginKey.getState(state);
54
- return syncState.snapshot == null && syncState.prevSnapshot == null;
55
- },
15
+ // editable: (state) => {
16
+ // const syncState = ySyncPluginKey.getState(state)!;
17
+ // return syncState.snapshot && syncState.prevSnapshot;
18
+ // },
56
19
  },
57
20
  key: ySyncPluginKey,
58
21
  state: {
59
- /**
60
- * @returns {any}
61
- */
62
- init: (_initargs, _state) => {
22
+ init: (_initargs, state) => {
23
+ const ydoc = new Y.Doc();
24
+ const yXmlFragment = ydoc.getXmlFragment('prosemirror');
25
+ const binding = new ProsemirrorBinding(yXmlFragment, new Map());
63
26
  return {
64
- type: yXmlFragment,
65
- doc: yXmlFragment.doc,
27
+ roomId: '',
28
+ provider: undefined,
66
29
  binding,
67
- snapshot: null,
68
- prevSnapshot: null,
30
+ type: yXmlFragment,
31
+ ydoc: ydoc,
32
+ snapshot: undefined,
33
+ prevSnapshot: undefined,
69
34
  isChangeOrigin: false,
70
35
  isUndoRedoOperation: false,
71
36
  addToHistory: true,
37
+ restore: undefined,
72
38
  colors,
73
39
  colorMapping,
74
- permanentUserData,
40
+ permanentUserData: undefined,
75
41
  };
76
42
  },
77
43
  apply: (tr, pluginState) => {
78
44
  const change = tr.getMeta(ySyncPluginKey);
79
45
  if (change !== undefined) {
80
- pluginState = Object.assign({}, pluginState);
81
- for (const key in change) {
82
- pluginState[key] = change[key];
46
+ pluginState = {
47
+ ...pluginState,
48
+ ...change,
49
+ };
50
+ if ('roomId' in change) {
51
+ if (change.roomId) {
52
+ const [provider, ydoc] = createWsProvider(change.roomId);
53
+ pluginState.provider = provider;
54
+ const yXmlFragment = ydoc.getXmlFragment('prosemirror');
55
+ pluginState.type = yXmlFragment, pluginState.ydoc = ydoc;
56
+ if (pluginState.provider) {
57
+ if (pluginState.binding.prosemirrorView) {
58
+ const view = pluginState.binding.prosemirrorView;
59
+ const tr = view.state.tr.setMeta('yjs:awareness', pluginState.provider.awareness);
60
+ view.dispatch(tr);
61
+ }
62
+ }
63
+ }
64
+ else {
65
+ const ydoc = new Y.Doc();
66
+ const yXmlFragment = ydoc.getXmlFragment('prosemirror');
67
+ pluginState.type = yXmlFragment, pluginState.ydoc = ydoc;
68
+ pluginState.provider = undefined;
69
+ }
70
+ pluginState.snapshot = undefined;
71
+ pluginState.prevSnapshot = undefined;
72
+ pluginState.isChangeOrigin = false;
73
+ pluginState.isUndoRedoOperation = false;
74
+ pluginState.addToHistory = true;
75
+ pluginState.restore = undefined;
76
+ initialContentChanged = false;
77
+ pluginState.binding.changeRoom(pluginState.type);
78
+ setTimeout(() => {
79
+ pluginState.binding._forceRerender();
80
+ }, 0);
81
+ return pluginState;
83
82
  }
84
83
  }
85
84
  pluginState.addToHistory = tr.getMeta('addToHistory') !== false;
86
85
  // always set isChangeOrigin. If undefined, this is not change origin.
87
- pluginState.isChangeOrigin = change !== undefined &&
88
- !!change.isChangeOrigin;
89
- pluginState.isUndoRedoOperation = change !== undefined &&
90
- !!change.isChangeOrigin && !!change.isUndoRedoOperation;
91
- if (binding.prosemirrorView !== null) {
92
- if (change !== undefined &&
93
- (change.snapshot != null || change.prevSnapshot != null)) {
86
+ pluginState.isChangeOrigin = !!change?.isChangeOrigin;
87
+ pluginState.isUndoRedoOperation = !!change?.isChangeOrigin &&
88
+ !!change?.isUndoRedoOperation;
89
+ const binding = pluginState.binding;
90
+ if (binding?.prosemirrorView) {
91
+ if (change?.snapshot || change?.prevSnapshot) {
94
92
  // snapshot changed, rerender next
95
93
  setTimeout(() => {
96
- if (binding.prosemirrorView == null) {
94
+ if (!binding.prosemirrorView) {
97
95
  return;
98
96
  }
99
97
  if (change.restore == null) {
@@ -105,8 +103,12 @@ export const ySyncPlugin = (yXmlFragment, { colors = defaultColors, colorMapping
105
103
  delete pluginState.restore;
106
104
  delete pluginState.snapshot;
107
105
  delete pluginState.prevSnapshot;
106
+ initialContentChanged = false;
108
107
  binding.mux(() => {
109
- binding._prosemirrorChanged(binding.prosemirrorView.state.doc);
108
+ if (!binding.prosemirrorView) {
109
+ return;
110
+ }
111
+ binding.prosemirrorChanged(binding.prosemirrorView.state.doc);
110
112
  });
111
113
  }
112
114
  }, 0);
@@ -116,8 +118,10 @@ export const ySyncPlugin = (yXmlFragment, { colors = defaultColors, colorMapping
116
118
  },
117
119
  },
118
120
  view: (view) => {
121
+ const pluginState = ySyncPluginKey.getState(view.state);
122
+ const binding = pluginState.binding;
119
123
  binding.initView(view);
120
- if (mapping == null) {
124
+ if (binding.mapping.size === 0) {
121
125
  // force rerender to update the bindings mapping
122
126
  binding._forceRerender();
123
127
  }
@@ -125,7 +129,11 @@ export const ySyncPlugin = (yXmlFragment, { colors = defaultColors, colorMapping
125
129
  return {
126
130
  update: () => {
127
131
  const pluginState = plugin.getState(view.state);
128
- if (pluginState.snapshot == null && pluginState.prevSnapshot == null) {
132
+ if (!pluginState) {
133
+ return;
134
+ }
135
+ const binding = pluginState.binding;
136
+ if (!pluginState.snapshot && !pluginState.prevSnapshot) {
129
137
  if (
130
138
  // If the content doesn't change initially, we don't render anything to Yjs
131
139
  // If the content was cleared by a user action, we want to catch the change and
@@ -141,9 +149,12 @@ export const ySyncPlugin = (yXmlFragment, { colors = defaultColors, colorMapping
141
149
  }
142
150
  }
143
151
  binding.mux(() => {
144
- pluginState.doc.transact((tr) => {
152
+ if (!pluginState.ydoc) {
153
+ return;
154
+ }
155
+ pluginState.ydoc.transact((tr) => {
145
156
  tr.meta.set('addToHistory', pluginState.addToHistory);
146
- binding._prosemirrorChanged(view.state.doc);
157
+ binding.prosemirrorChanged(view.state.doc);
147
158
  }, ySyncPluginKey);
148
159
  });
149
160
  }
@@ -157,782 +168,4 @@ export const ySyncPlugin = (yXmlFragment, { colors = defaultColors, colorMapping
157
168
  });
158
169
  return plugin;
159
170
  };
160
- const restoreRelativeSelection = (tr, relSel, binding) => {
161
- if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
162
- if (relSel.type === 'all') {
163
- tr.setSelection(new AllSelection(tr.doc));
164
- }
165
- else if (relSel.type === 'node') {
166
- const anchor = relativePositionToAbsolutePosition(binding.ydoc, binding.type, relSel.anchor, binding.mapping);
167
- tr.setSelection(NodeSelection.create(tr.doc, anchor));
168
- }
169
- else {
170
- const anchor = relativePositionToAbsolutePosition(binding.ydoc, binding.type, relSel.anchor, binding.mapping);
171
- const head = relativePositionToAbsolutePosition(binding.ydoc, binding.type, relSel.head, binding.mapping);
172
- if (anchor !== null && head !== null) {
173
- const sel = TextSelection.between(tr.doc.resolve(anchor), tr.doc.resolve(head));
174
- tr.setSelection(sel);
175
- }
176
- }
177
- }
178
- };
179
- function getSelectionType(selection) {
180
- if (selection instanceof TextSelection) {
181
- return 'text';
182
- }
183
- if (selection instanceof AllSelection) {
184
- return 'all';
185
- }
186
- if (selection instanceof NodeSelection) {
187
- return 'node';
188
- }
189
- return 'other_selection';
190
- }
191
- export const getRelativeSelection = (pmbinding, state) => ({
192
- type: getSelectionType(state.selection),
193
- anchor: absolutePositionToRelativePosition(state.selection.anchor, pmbinding.type, pmbinding.mapping),
194
- head: absolutePositionToRelativePosition(state.selection.head, pmbinding.type, pmbinding.mapping),
195
- });
196
- /**
197
- * Binding for prosemirror.
198
- *
199
- * @protected
200
- */
201
- export class ProsemirrorBinding {
202
- mapping;
203
- ydoc;
204
- isOMark;
205
- type;
206
- mux;
207
- prosemirrorView;
208
- _beforeTransactionSelection;
209
- beforeAllTransactions;
210
- afterAllTransactions;
211
- _observeFunction;
212
- _domSelectionInView = false;
213
- get beforeTransactionSelection() {
214
- return this._beforeTransactionSelection;
215
- }
216
- set beforeTransactionSelection(value) {
217
- this._beforeTransactionSelection = value;
218
- }
219
- constructor(yXmlFragment, mapping = new Map()) {
220
- this.mapping = mapping;
221
- this.type = yXmlFragment;
222
- this.prosemirrorView = null;
223
- this.mux = createMutex();
224
- /**
225
- * Is overlapping mark - i.e. mark does not exclude itself.
226
- */
227
- this.isOMark = new Map();
228
- this._observeFunction = (event, transaction) => this.yXmlChanged(event, transaction);
229
- this.ydoc = yXmlFragment.doc;
230
- /**
231
- * current selection as relative positions in the Yjs model
232
- */
233
- this._beforeTransactionSelection = null;
234
- this.beforeAllTransactions = () => {
235
- if (this._beforeTransactionSelection === null &&
236
- this.prosemirrorView != null) {
237
- this._beforeTransactionSelection = getRelativeSelection(this, this.prosemirrorView.state);
238
- }
239
- };
240
- this.afterAllTransactions = () => {
241
- this._beforeTransactionSelection = null;
242
- };
243
- this._domSelectionInView = false;
244
- }
245
- debug(msg = 'ydoc.prosemirror') {
246
- console.log(msg, this.type.toString());
247
- }
248
- _isLocalCursorInView() {
249
- if (!this.prosemirrorView?.hasFocus())
250
- return false;
251
- if (environment.isBrowser && this._domSelectionInView === false) {
252
- // Calculate the domSelectionInView and clear by next tick after all events are finished
253
- eventloop.timeout(0, () => {
254
- this._domSelectionInView = false;
255
- });
256
- this._domSelectionInView = this._isDomSelectionInView();
257
- }
258
- return this._domSelectionInView;
259
- }
260
- _isDomSelectionInView() {
261
- const selection = this.prosemirrorView?.root?.getSelection(); // https://stackoverflow.com/questions/62054839/shadowroot-getselection
262
- if (!selection || selection.anchorNode == null)
263
- return false;
264
- const range = dom.doc.createRange(); // https://github.com/yjs/y-prosemirror/pull/193
265
- range.setStart(selection.anchorNode, selection.anchorOffset);
266
- range.setEnd(selection.focusNode, selection.focusOffset);
267
- // This is a workaround for an edgecase where getBoundingClientRect will
268
- // return zero values if the selection is collapsed at the start of a newline
269
- // see reference here: https://stackoverflow.com/a/59780954
270
- const rects = range.getClientRects();
271
- if (rects.length === 0) {
272
- // probably buggy newline behavior, explicitly select the node contents
273
- if (range.startContainer && range.collapsed) {
274
- range.selectNodeContents(range.startContainer);
275
- }
276
- }
277
- const bounding = range.getBoundingClientRect();
278
- const documentElement = dom.doc.documentElement;
279
- return bounding.bottom >= 0 && bounding.right >= 0 &&
280
- bounding.left <=
281
- (globalThis.innerWidth || documentElement.clientWidth || 0) &&
282
- bounding.top <= (globalThis.innerHeight || documentElement.clientHeight || 0);
283
- }
284
- renderSnapshot(snapshot, prevSnapshot) {
285
- if (!prevSnapshot) {
286
- prevSnapshot = Y.createSnapshot(Y.createDeleteSet(), new Map());
287
- }
288
- if (this.prosemirrorView) {
289
- const _tr = this.prosemirrorView.state.tr.setMeta('addToHistory', false);
290
- this.prosemirrorView.dispatch(_tr.setMeta(ySyncPluginKey, { snapshot, prevSnapshot }));
291
- }
292
- }
293
- unrenderSnapshot() {
294
- this.mapping.clear();
295
- this.mux(() => {
296
- if (!this.prosemirrorView) {
297
- return;
298
- }
299
- const state = this.prosemirrorView.state;
300
- const fragmentContent = this.type.toArray().map((t) => createNodeFromYElement(t, state.schema, this)).filter((n) => n !== null);
301
- const _tr = state.tr.setMeta('addToHistory', false);
302
- const tr = _tr.replace(0, state.doc.content.size, new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0));
303
- tr.setMeta(ySyncPluginKey, { snapshot: null, prevSnapshot: null });
304
- this.prosemirrorView.dispatch(tr);
305
- });
306
- }
307
- _forceRerender() {
308
- this.mapping.clear();
309
- this.mux(() => {
310
- if (!this.prosemirrorView) {
311
- return;
312
- }
313
- const state = this.prosemirrorView.state;
314
- // If this is a forced rerender, this might neither happen as a pm change nor within a Yjs
315
- // transaction. Then the "before selection" doesn't exist. In this case, we need to create a
316
- // relative position before replacing content. Fixes #126
317
- const sel = this._beforeTransactionSelection !== null
318
- ? null
319
- : state.selection;
320
- const fragmentContent = this.type.toArray().map((t) => createNodeFromYElement(
321
- /** @type {Y.XmlElement} */ (t), state.schema, this)).filter((n) => n !== null);
322
- const _tr = state.tr.setMeta('addToHistory', false);
323
- const tr = _tr.replace(0, state.doc.content.size, new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0));
324
- if (sel) {
325
- /**
326
- * If the Prosemirror document we just created from this.type is
327
- * smaller than the previous document, the selection might be
328
- * out of bound, which would make Prosemirror throw an error.
329
- */
330
- const clampedAnchor = math.min(math.max(sel.anchor, 0), tr.doc.content.size);
331
- const clampedHead = math.min(math.max(sel.head, 0), tr.doc.content.size);
332
- tr.setSelection(TextSelection.create(tr.doc, clampedAnchor, clampedHead));
333
- }
334
- this.prosemirrorView.dispatch(tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, binding: this })
335
- .setMeta(remoteSelectionPluginKey, { isChangeOrigin: true }));
336
- });
337
- }
338
- _renderSnapshot(snapshot, prevSnapshot, pluginState) {
339
- /**
340
- * The document that contains the full history of this document.
341
- */
342
- let historyDoc = this.ydoc;
343
- let historyType = this.type;
344
- if (!snapshot) {
345
- snapshot = Y.snapshot(this.ydoc);
346
- }
347
- if (snapshot instanceof Uint8Array || prevSnapshot instanceof Uint8Array) {
348
- if (!(snapshot instanceof Uint8Array) ||
349
- !(prevSnapshot instanceof Uint8Array)) {
350
- // expected both snapshots to be v2 updates
351
- error.unexpectedCase();
352
- }
353
- historyDoc = new Y.Doc({ gc: false });
354
- Y.applyUpdateV2(historyDoc, prevSnapshot);
355
- prevSnapshot = Y.snapshot(historyDoc);
356
- Y.applyUpdateV2(historyDoc, snapshot);
357
- snapshot = Y.snapshot(historyDoc);
358
- if (historyType._item === null) {
359
- /**
360
- * If is a root type, we need to find the root key in the initial document
361
- * and use it to get the history type.
362
- */
363
- const rootKey = Array.from(this.ydoc.share.keys()).find((key) => this.ydoc.share.get(key) === this.type);
364
- historyType = historyDoc.getXmlFragment(rootKey);
365
- }
366
- else {
367
- /**
368
- * If it is a sub type, we use the item id to find the history type.
369
- */
370
- const historyStructs = historyDoc.store.clients.get(historyType._item.id.client) ?? [];
371
- const itemIndex = Y.findIndexSS(historyStructs, historyType._item.id.clock);
372
- const item = /** @type {Y.Item} */ (historyStructs[itemIndex]);
373
- const content = /** @type {Y.ContentType} */ (item.content);
374
- historyType = /** @type {Y.XmlFragment} */ (content.type);
375
- }
376
- }
377
- // clear mapping because we are going to rerender
378
- this.mapping.clear();
379
- this.mux(() => {
380
- historyDoc.transact((transaction) => {
381
- // before rendering, we are going to sanitize ops and split deleted ops
382
- // if they were deleted by seperate users.
383
- /**
384
- * @type {Y.PermanentUserData}
385
- */
386
- const pud = pluginState.permanentUserData;
387
- if (pud) {
388
- pud.dss.forEach((ds) => {
389
- Y.iterateDeletedStructs(transaction, ds, (_item) => { });
390
- });
391
- }
392
- const computeYChange = (type, id) => {
393
- const user = type === 'added'
394
- ? pud.getUserByClientId(id.client)
395
- : pud.getUserByDeletedId(id);
396
- return {
397
- user,
398
- type,
399
- color: getUserColor(pluginState.colorMapping, pluginState.colors, user),
400
- };
401
- };
402
- // Create document fragment and render
403
- const fragmentContent = Y.typeListToArraySnapshot(historyType, new Y.Snapshot(prevSnapshot.ds, snapshot.sv)).map((t) => {
404
- if (!this.prosemirrorView) {
405
- return null;
406
- }
407
- if (!t._item.deleted || isVisible(t._item, snapshot) ||
408
- isVisible(t._item, prevSnapshot)) {
409
- return createNodeFromYElement(t, this.prosemirrorView.state.schema, { mapping: new Map(), isOMark: new Map() }, snapshot, prevSnapshot, computeYChange);
410
- }
411
- else {
412
- // No need to render elements that are not visible by either snapshot.
413
- // If a client adds and deletes content in the same snapshot the element is not visible by either snapshot.
414
- return null;
415
- }
416
- }).filter((n) => n !== null);
417
- if (this.prosemirrorView) {
418
- const _tr = this.prosemirrorView.state.tr.setMeta('addToHistory', false);
419
- const tr = _tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0));
420
- this.prosemirrorView.dispatch(tr.setMeta(ySyncPluginKey, { isChangeOrigin: true }));
421
- }
422
- }, ySyncPluginKey);
423
- });
424
- }
425
- yXmlChanged(events, transaction) {
426
- if (this.prosemirrorView == null)
427
- return;
428
- const syncState = ySyncPluginKey.getState(this.prosemirrorView.state);
429
- if (events.length === 0 || syncState.snapshot != null ||
430
- syncState.prevSnapshot != null) {
431
- // drop out if snapshot is active
432
- this.renderSnapshot(syncState.snapshot, syncState.prevSnapshot);
433
- return;
434
- }
435
- this.mux(() => {
436
- const delType = (_, type) => this.mapping.delete(type);
437
- Y.iterateDeletedStructs(transaction, transaction.deleteSet, (struct) => {
438
- if (struct.constructor === Y.Item) {
439
- const type =
440
- /** @type {Y.ContentType} */ ( /** @type {Y.Item} */(struct)
441
- .content).type;
442
- type && this.mapping.delete(type);
443
- }
444
- });
445
- transaction.changed.forEach(delType);
446
- transaction.changedParentTypes.forEach(delType);
447
- if (!this.prosemirrorView) {
448
- return;
449
- }
450
- const state = this.prosemirrorView.state;
451
- const fragmentContent = this.type.toArray().map((t) => createNodeIfNotExists(
452
- /** @type {Y.XmlElement | Y.XmlHook} */ (t), state.schema, this)).filter((n) => n !== null);
453
- const _tr = state.tr.setMeta('addToHistory', false);
454
- let tr = _tr.replace(0, state.doc.content.size, new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0));
455
- try {
456
- restoreRelativeSelection(tr, this._beforeTransactionSelection, this);
457
- }
458
- catch (err) {
459
- console.warn(err);
460
- }
461
- tr = tr.setMeta(ySyncPluginKey, {
462
- isChangeOrigin: true,
463
- isUndoRedoOperation: transaction.origin instanceof Y.UndoManager,
464
- });
465
- if (this._beforeTransactionSelection !== null && this._isLocalCursorInView()) {
466
- tr.scrollIntoView();
467
- }
468
- this.prosemirrorView.dispatch(tr);
469
- });
470
- }
471
- _prosemirrorChanged(doc) {
472
- this.ydoc.transact(() => {
473
- updateYFragment(this.ydoc, this.type, doc, this);
474
- this._beforeTransactionSelection = getRelativeSelection(this, this.prosemirrorView.state);
475
- }, ySyncPluginKey);
476
- }
477
- /**
478
- * View is ready to listen to changes. Register observers.
479
- */
480
- initView(prosemirrorView) {
481
- if (this.prosemirrorView != null)
482
- this.destroy();
483
- this.prosemirrorView = prosemirrorView;
484
- this.ydoc.on('beforeAllTransactions', this.beforeAllTransactions);
485
- this.ydoc.on('afterAllTransactions', this.afterAllTransactions);
486
- this.type.observeDeep(this._observeFunction);
487
- }
488
- destroy() {
489
- if (this.prosemirrorView == null)
490
- return;
491
- this.prosemirrorView = null;
492
- this.type.unobserveDeep(this._observeFunction);
493
- this.ydoc.off('beforeAllTransactions', this.beforeAllTransactions);
494
- this.ydoc.off('afterAllTransactions', this.afterAllTransactions);
495
- }
496
- }
497
- const createNodeIfNotExists = (el, schema, meta, snapshot, prevSnapshot, computeYChange) => {
498
- const node = meta.mapping.get(el);
499
- if (node === undefined) {
500
- if (el instanceof Y.XmlElement) {
501
- return createNodeFromYElement(el, schema, meta, snapshot, prevSnapshot, computeYChange);
502
- }
503
- else {
504
- throw error.methodUnimplemented(); // we are currently not handling hooks
505
- }
506
- }
507
- return node;
508
- };
509
- export const createNodeFromYElement = (el, schema, meta, snapshot, prevSnapshot, computeYChange) => {
510
- const children = [];
511
- const createChildren = (type) => {
512
- if (type instanceof Y.XmlElement) {
513
- const n = createNodeIfNotExists(type, schema, meta, snapshot, prevSnapshot, computeYChange);
514
- if (n !== null) {
515
- children.push(n);
516
- }
517
- }
518
- else {
519
- // If the next ytext exists and was created by us, move the content to the current ytext.
520
- // This is a fix for #160 -- duplication of characters when two Y.Text exist next to each
521
- // other.
522
- const nextytext = /** @type {Y.ContentType} */ (type._item.right?.content)
523
- ?.type;
524
- if (nextytext instanceof Y.Text && !nextytext._item.deleted &&
525
- nextytext._item.id.client === nextytext.doc.clientID) {
526
- type.applyDelta([
527
- { retain: type.length },
528
- ...nextytext.toDelta(),
529
- ]);
530
- nextytext.doc.transact((tr) => {
531
- nextytext._item.delete(tr);
532
- });
533
- }
534
- // now create the prosemirror text nodes
535
- const ns = createTextNodesFromYText(type, schema, meta, snapshot, prevSnapshot, computeYChange);
536
- if (ns !== null) {
537
- ns.forEach((textchild) => {
538
- if (textchild !== null) {
539
- children.push(textchild);
540
- }
541
- });
542
- }
543
- }
544
- };
545
- if (snapshot === undefined || prevSnapshot === undefined) {
546
- el.toArray().forEach(createChildren);
547
- }
548
- else {
549
- Y.typeListToArraySnapshot(el, new Y.Snapshot(prevSnapshot.ds, snapshot.sv))
550
- .forEach(createChildren);
551
- }
552
- try {
553
- const attrs = el.getAttributes(snapshot);
554
- if (snapshot !== undefined) {
555
- if (!isVisible(/** @type {Y.Item} */ (el._item), snapshot)) {
556
- attrs.ychange = computeYChange
557
- ? computeYChange('removed', /** @type {Y.Item} */ (el._item).id)
558
- : { type: 'removed' };
559
- }
560
- else if (!isVisible(/** @type {Y.Item} */ (el._item), prevSnapshot)) {
561
- attrs.ychange = computeYChange
562
- ? computeYChange('added', /** @type {Y.Item} */ (el._item).id)
563
- : { type: 'added' };
564
- }
565
- }
566
- const node = schema.node(el.nodeName, attrs, children);
567
- meta.mapping.set(el, node);
568
- return node;
569
- }
570
- catch (e) {
571
- // an error occured while creating the node. This is probably a result of a concurrent action.
572
- /** @type {Y.Doc} */ (el.doc).transact((transaction) => {
573
- /** @type {Y.Item} */ (el._item).delete(transaction);
574
- }, ySyncPluginKey);
575
- meta.mapping.delete(el);
576
- return null;
577
- }
578
- };
579
- /**
580
- * @private
581
- */
582
- const createTextNodesFromYText = (text, schema, _meta, snapshot, prevSnapshot, computeYChange) => {
583
- const nodes = [];
584
- const deltas = text.toDelta(snapshot, prevSnapshot, computeYChange);
585
- try {
586
- for (let i = 0; i < deltas.length; i++) {
587
- const delta = deltas[i];
588
- nodes.push(schema.text(delta.insert, attributesToMarks(delta.attributes, schema)));
589
- }
590
- }
591
- catch (e) {
592
- // an error occured while creating the node. This is probably a result of a concurrent action.
593
- /** @type {Y.Doc} */ (text.doc).transact((transaction) => {
594
- /** @type {Y.Item} */ (text._item).delete(transaction);
595
- }, ySyncPluginKey);
596
- return null;
597
- }
598
- return nodes;
599
- };
600
- /**
601
- * @private
602
- */
603
- const createTypeFromTextNodes = (nodes, meta) => {
604
- const type = new Y.XmlText();
605
- const delta = nodes.map((node) => ({
606
- insert: node.text,
607
- attributes: marksToAttributes(node.marks, meta),
608
- }));
609
- type.applyDelta(delta);
610
- meta.mapping.set(type, nodes);
611
- return type;
612
- };
613
- /**
614
- * @private
615
- */
616
- const createTypeFromElementNode = (node, meta) => {
617
- const type = new Y.XmlElement(node.type.name);
618
- for (const key in node.attrs) {
619
- const val = node.attrs[key];
620
- if (val !== null && key !== 'ychange') {
621
- type.setAttribute(key, val);
622
- }
623
- }
624
- type.insert(0, normalizePNodeContent(node).map((n) => createTypeFromTextOrElementNode(n, meta)));
625
- meta.mapping.set(type, node);
626
- return type;
627
- };
628
- /**
629
- * @private
630
- */
631
- const createTypeFromTextOrElementNode = (node, meta) => node instanceof Array
632
- ? createTypeFromTextNodes(node, meta)
633
- : createTypeFromElementNode(node, meta);
634
- const isObject = (val) => typeof val === 'object' && val !== null;
635
- const equalAttrs = (pattrs, yattrs) => {
636
- const keys = Object.keys(pattrs).filter((key) => pattrs[key] !== null);
637
- let eq = keys.length ===
638
- (yattrs == null
639
- ? 0
640
- : Object.keys(yattrs).filter((key) => yattrs[key] !== null).length);
641
- for (let i = 0; i < keys.length && eq; i++) {
642
- const key = keys[i];
643
- const l = pattrs[key];
644
- const r = yattrs[key];
645
- eq = key === 'ychange' || l === r ||
646
- (isObject(l) && isObject(r) && equalAttrs(l, r));
647
- }
648
- return eq;
649
- };
650
- const normalizePNodeContent = (pnode) => {
651
- const c = pnode.content.content;
652
- const res = [];
653
- for (let i = 0; i < c.length; i++) {
654
- const n = c[i];
655
- if (n.isText) {
656
- const textNodes = [];
657
- for (let tnode = c[i]; i < c.length && tnode.isText; tnode = c[++i]) {
658
- textNodes.push(tnode);
659
- }
660
- i--;
661
- res.push(textNodes);
662
- }
663
- else {
664
- res.push(n);
665
- }
666
- }
667
- return res;
668
- };
669
- const equalYTextPText = (ytext, ptexts) => {
670
- const delta = ytext.toDelta();
671
- return delta.length === ptexts.length &&
672
- delta.every((d, i) => d.insert === /** @type {any} */ (ptexts[i]).text &&
673
- object.keys(d.attributes || {}).length === ptexts[i].marks.length &&
674
- object.every(d.attributes, (attr, yattrname) => {
675
- const markname = yattr2markname(yattrname);
676
- const pmarks = ptexts[i].marks;
677
- return equalAttrs(attr, pmarks.find((mark) => mark.type.name === markname)?.attrs);
678
- }));
679
- };
680
- const equalYTypePNode = (ytype, pnode) => {
681
- if (ytype instanceof Y.XmlElement && !(pnode instanceof Array) &&
682
- matchNodeName(ytype, pnode)) {
683
- const normalizedContent = normalizePNodeContent(pnode);
684
- return ytype._length === normalizedContent.length &&
685
- equalAttrs(ytype.getAttributes(), pnode.attrs) &&
686
- ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, normalizedContent[i]));
687
- }
688
- return ytype instanceof Y.XmlText && pnode instanceof Array &&
689
- equalYTextPText(ytype, pnode);
690
- };
691
- const mappedIdentity = (mapped, pcontent) => mapped === pcontent ||
692
- (mapped instanceof Array && pcontent instanceof Array &&
693
- mapped.length === pcontent.length &&
694
- mapped.every((a, i) => pcontent[i] === a));
695
- const computeChildEqualityFactor = (ytype, pnode, meta) => {
696
- const yChildren = ytype.toArray();
697
- const pChildren = normalizePNodeContent(pnode);
698
- const pChildCnt = pChildren.length;
699
- const yChildCnt = yChildren.length;
700
- const minCnt = math.min(yChildCnt, pChildCnt);
701
- let left = 0;
702
- let right = 0;
703
- let foundMappedChild = false;
704
- for (; left < minCnt; left++) {
705
- const leftY = yChildren[left];
706
- const leftP = pChildren[left];
707
- if (mappedIdentity(meta.mapping.get(leftY), leftP)) {
708
- foundMappedChild = true; // definite (good) match!
709
- }
710
- else if (!equalYTypePNode(leftY, leftP)) {
711
- break;
712
- }
713
- }
714
- for (; left + right < minCnt; right++) {
715
- const rightY = yChildren[yChildCnt - right - 1];
716
- const rightP = pChildren[pChildCnt - right - 1];
717
- if (mappedIdentity(meta.mapping.get(rightY), rightP)) {
718
- foundMappedChild = true;
719
- }
720
- else if (!equalYTypePNode(rightY, rightP)) {
721
- break;
722
- }
723
- }
724
- return {
725
- equalityFactor: left + right,
726
- foundMappedChild,
727
- };
728
- };
729
- const ytextTrans = (ytext) => {
730
- let str = '';
731
- let n = ytext._start;
732
- const nAttrs = {};
733
- while (n !== null) {
734
- if (!n.deleted) {
735
- if (n.countable && n.content instanceof Y.ContentString) {
736
- str += n.content.str;
737
- }
738
- else if (n.content instanceof Y.ContentFormat) {
739
- nAttrs[n.content.key] = null;
740
- }
741
- }
742
- n = n.right;
743
- }
744
- return {
745
- str,
746
- nAttrs,
747
- };
748
- };
749
- /**
750
- * @todo test this more
751
- */
752
- const updateYText = (ytext, ptexts, meta) => {
753
- meta.mapping.set(ytext, ptexts);
754
- const { nAttrs, str } = ytextTrans(ytext);
755
- const content = ptexts.map((p) => ({
756
- insert: /** @type {any} */ (p).text,
757
- attributes: Object.assign({}, nAttrs, marksToAttributes(p.marks, meta)),
758
- }));
759
- const { insert, remove, index } = simpleDiff(str, content.map((c) => c.insert).join(''));
760
- ytext.delete(index, remove);
761
- ytext.insert(index, insert);
762
- ytext.applyDelta(content.map((c) => ({ retain: c.insert.length, attributes: c.attributes })));
763
- };
764
- const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/;
765
- export const yattr2markname = (attrName) => hashedMarkNameRegex.exec(attrName)?.[1] ?? attrName;
766
- /**
767
- * @todo move this to markstoattributes
768
- */
769
- export const attributesToMarks = (attrs, schema) => {
770
- const marks = [];
771
- for (const markName in attrs) {
772
- // remove hashes if necessary
773
- marks.push(schema.mark(yattr2markname(markName), attrs[markName]));
774
- }
775
- return marks;
776
- };
777
- const marksToAttributes = (marks, meta) => {
778
- const pattrs = {};
779
- marks.forEach((mark) => {
780
- if (mark.type.name !== 'ychange') {
781
- const isOverlapping = map.setIfUndefined(meta.isOMark, mark.type, () => !mark.type.excludes(mark.type));
782
- pattrs[isOverlapping
783
- ? `${mark.type.name}--${utils.hashOfJSON(mark.toJSON())}`
784
- : mark.type.name] = mark.attrs;
785
- }
786
- });
787
- return pattrs;
788
- };
789
- /**
790
- * Update a yDom node by syncing the current content of the prosemirror node.
791
- *
792
- * This is a y-prosemirror internal feature that you can use at your own risk.
793
- *
794
- * @private
795
- * @unstable
796
- */
797
- export const updateYFragment = (y, yDomFragment, pNode, meta) => {
798
- if (yDomFragment instanceof Y.XmlElement &&
799
- yDomFragment.nodeName !== pNode.type.name) {
800
- throw new Error('node name mismatch!');
801
- }
802
- meta.mapping.set(yDomFragment, pNode);
803
- // update attributes
804
- if (yDomFragment instanceof Y.XmlElement) {
805
- const yDomAttrs = yDomFragment.getAttributes();
806
- const pAttrs = pNode.attrs;
807
- for (const key in pAttrs) {
808
- if (pAttrs[key] !== null) {
809
- if (yDomAttrs[key] !== pAttrs[key] && key !== 'ychange') {
810
- yDomFragment.setAttribute(key, pAttrs[key]);
811
- }
812
- }
813
- else {
814
- yDomFragment.removeAttribute(key);
815
- }
816
- }
817
- // remove all keys that are no longer in pAttrs
818
- for (const key in yDomAttrs) {
819
- if (pAttrs[key] === undefined) {
820
- yDomFragment.removeAttribute(key);
821
- }
822
- }
823
- }
824
- // update children
825
- const pChildren = normalizePNodeContent(pNode);
826
- const pChildCnt = pChildren.length;
827
- const yChildren = yDomFragment.toArray();
828
- const yChildCnt = yChildren.length;
829
- const minCnt = math.min(pChildCnt, yChildCnt);
830
- let left = 0;
831
- let right = 0;
832
- // find number of matching elements from left
833
- for (; left < minCnt; left++) {
834
- const leftY = yChildren[left];
835
- const leftP = pChildren[left];
836
- if (!mappedIdentity(meta.mapping.get(leftY), leftP)) {
837
- if (equalYTypePNode(leftY, leftP)) {
838
- // update mapping
839
- meta.mapping.set(leftY, leftP);
840
- }
841
- else {
842
- break;
843
- }
844
- }
845
- }
846
- // find number of matching elements from right
847
- for (; right + left < minCnt; right++) {
848
- const rightY = yChildren[yChildCnt - right - 1];
849
- const rightP = pChildren[pChildCnt - right - 1];
850
- if (!mappedIdentity(meta.mapping.get(rightY), rightP)) {
851
- if (equalYTypePNode(rightY, rightP)) {
852
- // update mapping
853
- meta.mapping.set(rightY, rightP);
854
- }
855
- else {
856
- break;
857
- }
858
- }
859
- }
860
- y.transact(() => {
861
- // try to compare and update
862
- while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
863
- const leftY = yChildren[left];
864
- const leftP = pChildren[left];
865
- const rightY = yChildren[yChildCnt - right - 1];
866
- const rightP = pChildren[pChildCnt - right - 1];
867
- if (leftY instanceof Y.XmlText && leftP instanceof Array) {
868
- if (!equalYTextPText(leftY, leftP)) {
869
- updateYText(leftY, leftP, meta);
870
- }
871
- left += 1;
872
- }
873
- else {
874
- let updateLeft = leftY instanceof Y.XmlElement &&
875
- matchNodeName(leftY, leftP);
876
- let updateRight = rightY instanceof Y.XmlElement &&
877
- matchNodeName(rightY, rightP);
878
- if (updateLeft && updateRight) {
879
- // decide which element to update
880
- const equalityLeft = computeChildEqualityFactor(leftY, leftP, meta);
881
- const equalityRight = computeChildEqualityFactor(rightY, rightP, meta);
882
- if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) {
883
- updateRight = false;
884
- }
885
- else if (!equalityLeft.foundMappedChild && equalityRight.foundMappedChild) {
886
- updateLeft = false;
887
- }
888
- else if (equalityLeft.equalityFactor < equalityRight.equalityFactor) {
889
- updateLeft = false;
890
- }
891
- else {
892
- updateRight = false;
893
- }
894
- }
895
- if (updateLeft) {
896
- updateYFragment(y,
897
- /** @type {Y.XmlFragment} */ (leftY),
898
- /** @type {PModel.Node} */ (leftP), meta);
899
- left += 1;
900
- }
901
- else if (updateRight) {
902
- updateYFragment(y,
903
- /** @type {Y.XmlFragment} */ (rightY),
904
- /** @type {PModel.Node} */ (rightP), meta);
905
- right += 1;
906
- }
907
- else {
908
- meta.mapping.delete(yDomFragment.get(left));
909
- yDomFragment.delete(left, 1);
910
- yDomFragment.insert(left, [
911
- createTypeFromTextOrElementNode(leftP, meta),
912
- ]);
913
- left += 1;
914
- }
915
- }
916
- }
917
- const yDelLen = yChildCnt - left - right;
918
- if (yChildCnt === 1 && pChildCnt === 0 && yChildren[0] instanceof Y.XmlText) {
919
- meta.mapping.delete(yChildren[0]);
920
- // Edge case handling https://github.com/yjs/y-prosemirror/issues/108
921
- // Only delete the content of the Y.Text to retain remote changes on the same Y.Text object
922
- yChildren[0].delete(0, yChildren[0].length);
923
- }
924
- else if (yDelLen > 0) {
925
- yDomFragment.slice(left, left + yDelLen).forEach((type) => meta.mapping.delete(type));
926
- yDomFragment.delete(left, yDelLen);
927
- }
928
- if (left + right < pChildCnt) {
929
- const ins = [];
930
- for (let i = left; i < pChildCnt - right; i++) {
931
- ins.push(createTypeFromTextOrElementNode(pChildren[i], meta));
932
- }
933
- yDomFragment.insert(left, ins);
934
- }
935
- }, ySyncPluginKey);
936
- };
937
- const matchNodeName = (yElement, pNode) => !(pNode instanceof Array) && yElement.nodeName === pNode.type.name;
938
171
  //# sourceMappingURL=ySyncPlugin.js.map