@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.
- package/README.md +3 -89
- package/esm/ExtensionYjs.d.ts +10 -1
- package/esm/ExtensionYjs.d.ts.map +1 -1
- package/esm/ExtensionYjs.js +47 -6
- package/esm/ExtensionYjs.js.map +1 -1
- package/esm/MarkYChange.d.ts +7 -0
- package/esm/MarkYChange.d.ts.map +1 -0
- package/esm/MarkYChange.js +21 -0
- package/esm/MarkYChange.js.map +1 -0
- package/esm/ProsemirrorBinding.d.ts +60 -0
- package/esm/ProsemirrorBinding.d.ts.map +1 -0
- package/esm/ProsemirrorBinding.js +405 -0
- package/esm/ProsemirrorBinding.js.map +1 -0
- package/esm/createNodeFromYElement.d.ts +10 -0
- package/esm/createNodeFromYElement.d.ts.map +1 -0
- package/esm/createNodeFromYElement.js +123 -0
- package/esm/createNodeFromYElement.js.map +1 -0
- package/esm/debug.d.ts +13 -0
- package/esm/debug.d.ts.map +1 -0
- package/esm/debug.js +147 -0
- package/esm/debug.js.map +1 -0
- package/esm/keys.d.ts +5 -8
- package/esm/keys.d.ts.map +1 -1
- package/esm/keys.js +1 -6
- package/esm/keys.js.map +1 -1
- package/esm/lib.d.ts +1 -2
- package/esm/lib.d.ts.map +1 -1
- package/esm/lib.js +12 -2
- package/esm/lib.js.map +1 -1
- package/esm/updateYFragment.d.ts +17 -0
- package/esm/updateYFragment.d.ts.map +1 -0
- package/esm/updateYFragment.js +333 -0
- package/esm/updateYFragment.js.map +1 -0
- package/esm/utils.d.ts +2 -0
- package/esm/utils.d.ts.map +1 -1
- package/esm/utils.js +4 -0
- package/esm/utils.js.map +1 -1
- package/esm/yPositionPlugin.d.ts +12 -4
- package/esm/yPositionPlugin.d.ts.map +1 -1
- package/esm/yPositionPlugin.js +114 -61
- package/esm/yPositionPlugin.js.map +1 -1
- package/esm/ySyncPlugin.d.ts +16 -78
- package/esm/ySyncPlugin.d.ts.map +1 -1
- package/esm/ySyncPlugin.js +81 -848
- package/esm/ySyncPlugin.js.map +1 -1
- package/esm/yUndoPlugin.d.ts +1 -1
- package/esm/yUndoPlugin.d.ts.map +1 -1
- package/esm/yUndoPlugin.js +1 -1
- package/esm/yUndoPlugin.js.map +1 -1
- package/package.json +9 -3
- package/src/ExtensionYjs.ts +65 -9
- package/src/MarkYChange.ts +23 -0
- package/src/ProsemirrorBinding.ts +607 -0
- package/src/createNodeFromYElement.ts +175 -0
- package/src/debug.ts +218 -0
- package/src/keys.ts +9 -9
- package/src/lib.ts +11 -3
- package/src/updateYFragment.ts +439 -0
- package/src/utils.ts +6 -0
- package/src/yPositionPlugin.ts +167 -92
- package/src/ySyncPlugin.ts +135 -1193
- package/src/yUndoPlugin.ts +1 -1
- package/esm/convertUtils.d.ts +0 -59
- package/esm/convertUtils.d.ts.map +0 -1
- package/esm/convertUtils.js +0 -89
- package/esm/convertUtils.js.map +0 -1
- package/src/convertUtils.ts +0 -143
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
// deno-lint-ignore-file no-window
|
|
2
|
+
import * as dom from 'lib0/dom';
|
|
3
|
+
import * as environment from 'lib0/environment';
|
|
4
|
+
import * as error from 'lib0/error';
|
|
5
|
+
import * as eventloop from 'lib0/eventloop';
|
|
6
|
+
import * as math from 'lib0/math';
|
|
7
|
+
import { createMutex, mutex } from 'lib0/mutex';
|
|
8
|
+
import * as random from 'lib0/random';
|
|
9
|
+
import * as set from 'lib0/set';
|
|
10
|
+
import * as PModel from 'prosemirror-model';
|
|
11
|
+
import { Node } from 'prosemirror-model';
|
|
12
|
+
import {
|
|
13
|
+
AllSelection,
|
|
14
|
+
EditorState,
|
|
15
|
+
NodeSelection,
|
|
16
|
+
Selection,
|
|
17
|
+
TextSelection,
|
|
18
|
+
Transaction,
|
|
19
|
+
} from 'prosemirror-state';
|
|
20
|
+
import * as Y from 'yjs';
|
|
21
|
+
|
|
22
|
+
import { remoteSelectionPluginKey } from '@kerebron/extension-basic-editor/ExtensionRemoteSelection';
|
|
23
|
+
|
|
24
|
+
import { ySyncPluginKey } from './keys.js';
|
|
25
|
+
import {
|
|
26
|
+
absolutePositionToRelativePosition,
|
|
27
|
+
ProsemirrorMapping,
|
|
28
|
+
relativePositionToAbsolutePosition,
|
|
29
|
+
} from './lib.js';
|
|
30
|
+
import { updateYFragment } from './updateYFragment.js';
|
|
31
|
+
import {
|
|
32
|
+
createNodeFromYElement,
|
|
33
|
+
createNodeIfNotExists,
|
|
34
|
+
} from './createNodeFromYElement.js';
|
|
35
|
+
import { type ColorDef, type YSyncPluginState } from './ySyncPlugin.js';
|
|
36
|
+
import { isVisible } from './utils.js';
|
|
37
|
+
|
|
38
|
+
export const defaultColors: Array<ColorDef> = [{
|
|
39
|
+
light: '#ecd44433',
|
|
40
|
+
dark: '#ecd444',
|
|
41
|
+
}];
|
|
42
|
+
|
|
43
|
+
const getUserColor = (
|
|
44
|
+
colorMapping: Map<string, ColorDef>,
|
|
45
|
+
colors: Array<ColorDef>,
|
|
46
|
+
user: string,
|
|
47
|
+
): ColorDef => {
|
|
48
|
+
// @todo do not hit the same color twice if possible
|
|
49
|
+
if (!colorMapping.has(user)) {
|
|
50
|
+
if (colorMapping.size < colors.length) {
|
|
51
|
+
const usedColors = set.create();
|
|
52
|
+
colorMapping.forEach((color) => usedColors.add(color));
|
|
53
|
+
colors = colors.filter((color) => !usedColors.has(color));
|
|
54
|
+
}
|
|
55
|
+
colorMapping.set(user, random.oneOf(colors));
|
|
56
|
+
}
|
|
57
|
+
return colorMapping.get(user) || defaultColors[0];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export interface BindingMetadata {
|
|
61
|
+
mapping: ProsemirrorMapping;
|
|
62
|
+
isOMark: Map<string, boolean>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface TransactionSelection {
|
|
66
|
+
type: string;
|
|
67
|
+
anchor: Y.RelativePosition;
|
|
68
|
+
head: Y.RelativePosition;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getSelectionType(selection: Selection) {
|
|
72
|
+
if (selection instanceof TextSelection) {
|
|
73
|
+
return 'text';
|
|
74
|
+
}
|
|
75
|
+
if (selection instanceof AllSelection) {
|
|
76
|
+
return 'all';
|
|
77
|
+
}
|
|
78
|
+
if (selection instanceof NodeSelection) {
|
|
79
|
+
return 'node';
|
|
80
|
+
}
|
|
81
|
+
return 'other_selection';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const restoreRelativeSelection = (
|
|
85
|
+
tr: Transaction,
|
|
86
|
+
relSel: ReturnType<typeof getRelativeSelection>,
|
|
87
|
+
binding: ProsemirrorBinding,
|
|
88
|
+
) => {
|
|
89
|
+
if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
|
|
90
|
+
if (relSel.type === 'all') {
|
|
91
|
+
tr.setSelection(new AllSelection(tr.doc));
|
|
92
|
+
} else if (relSel.type === 'node') {
|
|
93
|
+
const anchor = relativePositionToAbsolutePosition(
|
|
94
|
+
binding.ydoc,
|
|
95
|
+
binding.type,
|
|
96
|
+
relSel.anchor,
|
|
97
|
+
binding.mapping,
|
|
98
|
+
);
|
|
99
|
+
if (anchor !== null) {
|
|
100
|
+
tr.setSelection(NodeSelection.create(tr.doc, anchor));
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
const anchor = relativePositionToAbsolutePosition(
|
|
104
|
+
binding.ydoc,
|
|
105
|
+
binding.type,
|
|
106
|
+
relSel.anchor,
|
|
107
|
+
binding.mapping,
|
|
108
|
+
);
|
|
109
|
+
const head = relativePositionToAbsolutePosition(
|
|
110
|
+
binding.ydoc,
|
|
111
|
+
binding.type,
|
|
112
|
+
relSel.head,
|
|
113
|
+
binding.mapping,
|
|
114
|
+
);
|
|
115
|
+
if (anchor !== null && head !== null) {
|
|
116
|
+
const sel = TextSelection.between(
|
|
117
|
+
tr.doc.resolve(anchor),
|
|
118
|
+
tr.doc.resolve(head),
|
|
119
|
+
);
|
|
120
|
+
tr.setSelection(sel);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const getRelativeSelection = (
|
|
127
|
+
pmbinding: ProsemirrorBinding,
|
|
128
|
+
state: EditorState,
|
|
129
|
+
): TransactionSelection => ({
|
|
130
|
+
type: getSelectionType(state.selection),
|
|
131
|
+
anchor: absolutePositionToRelativePosition(
|
|
132
|
+
state.selection.anchor,
|
|
133
|
+
pmbinding.type,
|
|
134
|
+
pmbinding.mapping,
|
|
135
|
+
),
|
|
136
|
+
head: absolutePositionToRelativePosition(
|
|
137
|
+
state.selection.head,
|
|
138
|
+
pmbinding.type,
|
|
139
|
+
pmbinding.mapping,
|
|
140
|
+
),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
interface IEditorView {
|
|
144
|
+
state: EditorState;
|
|
145
|
+
dispatch(tr: Transaction): void;
|
|
146
|
+
fake?: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Binding for prosemirror.
|
|
151
|
+
*
|
|
152
|
+
* @protected
|
|
153
|
+
*/
|
|
154
|
+
export class ProsemirrorBinding implements BindingMetadata {
|
|
155
|
+
public ydoc: Y.Doc;
|
|
156
|
+
public isOMark: Map<string, boolean>;
|
|
157
|
+
public type: Y.XmlFragment;
|
|
158
|
+
public readonly mux: mutex;
|
|
159
|
+
public prosemirrorView?: IEditorView;
|
|
160
|
+
|
|
161
|
+
private _beforeTransactionSelection: TransactionSelection | null = null;
|
|
162
|
+
private readonly beforeAllTransactions: () => void;
|
|
163
|
+
private readonly afterAllTransactions: () => void;
|
|
164
|
+
|
|
165
|
+
private _observeFunction: (event: any, transaction: any) => void;
|
|
166
|
+
private _domSelectionInView: boolean = false;
|
|
167
|
+
get beforeTransactionSelection(): TransactionSelection | null {
|
|
168
|
+
return this._beforeTransactionSelection;
|
|
169
|
+
}
|
|
170
|
+
set beforeTransactionSelection(value: TransactionSelection) {
|
|
171
|
+
this._beforeTransactionSelection = value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
constructor(
|
|
175
|
+
yXmlFragment: Y.XmlFragment,
|
|
176
|
+
public readonly mapping: ProsemirrorMapping = new Map(),
|
|
177
|
+
) {
|
|
178
|
+
this.type = yXmlFragment;
|
|
179
|
+
this.prosemirrorView = undefined;
|
|
180
|
+
// {
|
|
181
|
+
// state: new EditorState(),
|
|
182
|
+
// dispatch: () => false,
|
|
183
|
+
// fake: true
|
|
184
|
+
// };
|
|
185
|
+
this.mux = createMutex();
|
|
186
|
+
/**
|
|
187
|
+
* Is overlapping mark - i.e. mark does not exclude itself.
|
|
188
|
+
*/
|
|
189
|
+
this.isOMark = new Map();
|
|
190
|
+
this._observeFunction = (event, transaction) => {
|
|
191
|
+
this.yXmlChanged(event, transaction);
|
|
192
|
+
};
|
|
193
|
+
this.ydoc = yXmlFragment.doc!;
|
|
194
|
+
/**
|
|
195
|
+
* current selection as relative positions in the Yjs model
|
|
196
|
+
*/
|
|
197
|
+
this.beforeAllTransactions = () => {
|
|
198
|
+
if (
|
|
199
|
+
!this._beforeTransactionSelection &&
|
|
200
|
+
this.prosemirrorView
|
|
201
|
+
) {
|
|
202
|
+
this._beforeTransactionSelection = getRelativeSelection(
|
|
203
|
+
this,
|
|
204
|
+
this.prosemirrorView.state,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
this.afterAllTransactions = () => {
|
|
209
|
+
this._beforeTransactionSelection = null;
|
|
210
|
+
};
|
|
211
|
+
this._domSelectionInView = false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
changeRoom(yXmlFragment: Y.XmlFragment) {
|
|
215
|
+
const view = this.prosemirrorView;
|
|
216
|
+
this.destroy();
|
|
217
|
+
|
|
218
|
+
this.type = yXmlFragment;
|
|
219
|
+
this.ydoc = yXmlFragment.doc!;
|
|
220
|
+
this.initView(view);
|
|
221
|
+
// this._forceRerender();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
debug(msg = 'ydoc.prosemirror') {
|
|
225
|
+
console.log(msg, this.type.toString());
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_isLocalCursorInView(): boolean {
|
|
229
|
+
if (!this.prosemirrorView?.hasFocus()) return false;
|
|
230
|
+
if (environment.isBrowser && this._domSelectionInView === false) {
|
|
231
|
+
// Calculate the domSelectionInView and clear by next tick after all events are finished
|
|
232
|
+
eventloop.timeout(0, () => {
|
|
233
|
+
this._domSelectionInView = false;
|
|
234
|
+
});
|
|
235
|
+
this._domSelectionInView = this._isDomSelectionInView();
|
|
236
|
+
}
|
|
237
|
+
return this._domSelectionInView;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_isDomSelectionInView(): boolean {
|
|
241
|
+
const selection = this.prosemirrorView?.root?.getSelection(); // https://stackoverflow.com/questions/62054839/shadowroot-getselection
|
|
242
|
+
|
|
243
|
+
if (!selection || selection.anchorNode == null) return false;
|
|
244
|
+
|
|
245
|
+
const range = dom.doc.createRange(); // https://github.com/yjs/y-prosemirror/pull/193
|
|
246
|
+
range.setStart(selection.anchorNode, selection.anchorOffset);
|
|
247
|
+
range.setEnd(selection.focusNode, selection.focusOffset);
|
|
248
|
+
|
|
249
|
+
// This is a workaround for an edgecase where getBoundingClientRect will
|
|
250
|
+
// return zero values if the selection is collapsed at the start of a newline
|
|
251
|
+
// see reference here: https://stackoverflow.com/a/59780954
|
|
252
|
+
const rects = range.getClientRects();
|
|
253
|
+
if (rects.length === 0) {
|
|
254
|
+
// probably buggy newline behavior, explicitly select the node contents
|
|
255
|
+
if (range.startContainer && range.collapsed) {
|
|
256
|
+
range.selectNodeContents(range.startContainer);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const bounding = range.getBoundingClientRect();
|
|
261
|
+
const documentElement = dom.doc.documentElement;
|
|
262
|
+
|
|
263
|
+
return bounding.bottom >= 0 && bounding.right >= 0 &&
|
|
264
|
+
bounding.left <=
|
|
265
|
+
(globalThis.innerWidth || documentElement.clientWidth || 0) &&
|
|
266
|
+
bounding.top <= (globalThis.innerHeight || documentElement.clientHeight || 0);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
renderSnapshot(
|
|
270
|
+
snapshot: Y.Snapshot | undefined,
|
|
271
|
+
prevSnapshot: Y.Snapshot | undefined,
|
|
272
|
+
) {
|
|
273
|
+
if (!prevSnapshot) {
|
|
274
|
+
prevSnapshot = Y.createSnapshot(Y.createDeleteSet(), new Map());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!this.prosemirrorView) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const state = this.prosemirrorView.state;
|
|
281
|
+
|
|
282
|
+
const tr = state.tr.setMeta('addToHistory', false)
|
|
283
|
+
.setMeta(ySyncPluginKey, { snapshot, prevSnapshot });
|
|
284
|
+
this.prosemirrorView.dispatch(tr);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
unrenderSnapshot() {
|
|
288
|
+
this.mapping.clear();
|
|
289
|
+
this.mux(() => {
|
|
290
|
+
if (!this.prosemirrorView) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const state = this.prosemirrorView.state;
|
|
294
|
+
const children = this.type.toArray();
|
|
295
|
+
const fragmentContent = children.map((t) =>
|
|
296
|
+
createNodeFromYElement(
|
|
297
|
+
t as Y.XmlElement,
|
|
298
|
+
state.schema,
|
|
299
|
+
this,
|
|
300
|
+
)
|
|
301
|
+
).filter((n) => n !== null);
|
|
302
|
+
const _tr = state.tr.setMeta('addToHistory', false);
|
|
303
|
+
const tr = _tr.replace(
|
|
304
|
+
0,
|
|
305
|
+
state.doc.content.size,
|
|
306
|
+
new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0),
|
|
307
|
+
);
|
|
308
|
+
tr.setMeta(ySyncPluginKey, {
|
|
309
|
+
snapshot: undefined,
|
|
310
|
+
prevSnapshot: undefined,
|
|
311
|
+
});
|
|
312
|
+
if (this.prosemirrorView) {
|
|
313
|
+
this.prosemirrorView.dispatch(tr);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
_forceRerender() {
|
|
319
|
+
this.mapping.clear();
|
|
320
|
+
this.mux(() => {
|
|
321
|
+
if (!this.prosemirrorView) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const state = this.prosemirrorView.state;
|
|
325
|
+
// If this is a forced rerender, this might neither happen as a pm change nor within a Yjs
|
|
326
|
+
// transaction. Then the "before selection" doesn't exist. In this case, we need to create a
|
|
327
|
+
// relative position before replacing content. Fixes #126
|
|
328
|
+
const sel = this._beforeTransactionSelection !== null
|
|
329
|
+
? null
|
|
330
|
+
: state.selection;
|
|
331
|
+
const fragmentContent = this.type.toArray().map((t) =>
|
|
332
|
+
createNodeFromYElement(
|
|
333
|
+
t as Y.XmlElement,
|
|
334
|
+
state.schema,
|
|
335
|
+
this,
|
|
336
|
+
)
|
|
337
|
+
).filter((n) => n !== null);
|
|
338
|
+
const _tr = state.tr.setMeta('addToHistory', false);
|
|
339
|
+
const tr = _tr.replace(
|
|
340
|
+
0,
|
|
341
|
+
state.doc.content.size,
|
|
342
|
+
new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0),
|
|
343
|
+
);
|
|
344
|
+
if (sel) {
|
|
345
|
+
/**
|
|
346
|
+
* If the Prosemirror document we just created from this.type is
|
|
347
|
+
* smaller than the previous document, the selection might be
|
|
348
|
+
* out of bound, which would make Prosemirror throw an error.
|
|
349
|
+
*/
|
|
350
|
+
const clampedAnchor = math.min(
|
|
351
|
+
math.max(sel.anchor, 0),
|
|
352
|
+
tr.doc.content.size,
|
|
353
|
+
);
|
|
354
|
+
const clampedHead = math.min(
|
|
355
|
+
math.max(sel.head, 0),
|
|
356
|
+
tr.doc.content.size,
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
tr.setSelection(
|
|
360
|
+
TextSelection.create(tr.doc, clampedAnchor, clampedHead),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (this.prosemirrorView) {
|
|
364
|
+
this.prosemirrorView.dispatch(
|
|
365
|
+
tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, binding: this })
|
|
366
|
+
.setMeta(remoteSelectionPluginKey, { isChangeOrigin: true }),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_renderSnapshot(
|
|
373
|
+
snapshot: Y.Snapshot | Uint8Array | undefined,
|
|
374
|
+
prevSnapshot: Y.Snapshot | Uint8Array,
|
|
375
|
+
pluginState: YSyncPluginState,
|
|
376
|
+
) {
|
|
377
|
+
if (!snapshot) {
|
|
378
|
+
snapshot = Y.snapshot(this.ydoc);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The document that contains the full history of this document.
|
|
383
|
+
*/
|
|
384
|
+
let historyDoc = this.ydoc;
|
|
385
|
+
let historyType: Y.AbstractType<any> = this.type;
|
|
386
|
+
|
|
387
|
+
if (snapshot instanceof Uint8Array || prevSnapshot instanceof Uint8Array) {
|
|
388
|
+
if (
|
|
389
|
+
!(snapshot instanceof Uint8Array) ||
|
|
390
|
+
!(prevSnapshot instanceof Uint8Array)
|
|
391
|
+
) {
|
|
392
|
+
// expected both snapshots to be v2 updates
|
|
393
|
+
error.unexpectedCase();
|
|
394
|
+
}
|
|
395
|
+
historyDoc = new Y.Doc({ gc: false });
|
|
396
|
+
Y.applyUpdateV2(historyDoc, prevSnapshot);
|
|
397
|
+
prevSnapshot = Y.snapshot(historyDoc);
|
|
398
|
+
Y.applyUpdateV2(historyDoc, snapshot);
|
|
399
|
+
snapshot = Y.snapshot(historyDoc);
|
|
400
|
+
if (historyType._item === null) {
|
|
401
|
+
/**
|
|
402
|
+
* If is a root type, we need to find the root key in the initial document
|
|
403
|
+
* and use it to get the history type.
|
|
404
|
+
*/
|
|
405
|
+
const share: Map<string, Y.AbstractType<Y.YEvent<any>>> =
|
|
406
|
+
this.ydoc.share;
|
|
407
|
+
const rootKey = Array.from(share.keys()).find(
|
|
408
|
+
(key) => share.get(key) === this.type as Y.AbstractType<any>,
|
|
409
|
+
);
|
|
410
|
+
historyType = historyDoc.getXmlFragment(rootKey);
|
|
411
|
+
} else {
|
|
412
|
+
/**
|
|
413
|
+
* If it is a sub type, we use the item id to find the history type.
|
|
414
|
+
*/
|
|
415
|
+
const historyStructs =
|
|
416
|
+
historyDoc.store.clients.get(historyType._item.id.client) ?? [];
|
|
417
|
+
const itemIndex = Y.findIndexSS(
|
|
418
|
+
historyStructs,
|
|
419
|
+
historyType._item.id.clock,
|
|
420
|
+
);
|
|
421
|
+
if (historyStructs[itemIndex] instanceof Y.GC) {
|
|
422
|
+
throw new Error('Incorrect type Y.GC');
|
|
423
|
+
}
|
|
424
|
+
const item: Y.Item = historyStructs[itemIndex];
|
|
425
|
+
const content: Y.ContentType = item.content;
|
|
426
|
+
historyType = content.type;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// clear mapping because we are going to rerender
|
|
430
|
+
this.mapping.clear();
|
|
431
|
+
|
|
432
|
+
this.mux(() => {
|
|
433
|
+
historyDoc.transact((transaction) => {
|
|
434
|
+
if (!this.prosemirrorView) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const state = this.prosemirrorView.state;
|
|
438
|
+
|
|
439
|
+
// before rendering, we are going to sanitize ops and split deleted ops
|
|
440
|
+
// if they were deleted by seperate users.
|
|
441
|
+
const pud: Y.PermanentUserData | undefined =
|
|
442
|
+
pluginState.permanentUserData;
|
|
443
|
+
if (pud) {
|
|
444
|
+
pud.dss.forEach((ds) => {
|
|
445
|
+
Y.iterateDeletedStructs(transaction, ds, (_item) => {});
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
const computeYChange = (type: 'removed' | 'added', id: Y.ID) => {
|
|
449
|
+
const user = pud
|
|
450
|
+
? (type === 'added'
|
|
451
|
+
? pud.getUserByClientId(id.client)
|
|
452
|
+
: pud.getUserByDeletedId(id))
|
|
453
|
+
: undefined;
|
|
454
|
+
return {
|
|
455
|
+
user,
|
|
456
|
+
type,
|
|
457
|
+
color: getUserColor(
|
|
458
|
+
pluginState.colorMapping,
|
|
459
|
+
pluginState.colors,
|
|
460
|
+
user,
|
|
461
|
+
),
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// Create document fragment and render
|
|
466
|
+
const fragmentContent = Y.typeListToArraySnapshot(
|
|
467
|
+
historyType,
|
|
468
|
+
new Y.Snapshot(prevSnapshot.ds, snapshot.sv),
|
|
469
|
+
).map((t) => {
|
|
470
|
+
if (
|
|
471
|
+
!t._item.deleted || isVisible(t._item, snapshot) ||
|
|
472
|
+
isVisible(t._item, prevSnapshot)
|
|
473
|
+
) {
|
|
474
|
+
return createNodeFromYElement(
|
|
475
|
+
t,
|
|
476
|
+
state.schema,
|
|
477
|
+
{ mapping: new Map(), isOMark: new Map() },
|
|
478
|
+
snapshot,
|
|
479
|
+
prevSnapshot,
|
|
480
|
+
computeYChange,
|
|
481
|
+
);
|
|
482
|
+
} else {
|
|
483
|
+
// No need to render elements that are not visible by either snapshot.
|
|
484
|
+
// If a client adds and deletes content in the same snapshot the element is not visible by either snapshot.
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}).filter((n) => n !== null);
|
|
488
|
+
|
|
489
|
+
const tr = state.tr.setMeta('addToHistory', false);
|
|
490
|
+
tr.replace(
|
|
491
|
+
0,
|
|
492
|
+
state.doc.content.size,
|
|
493
|
+
new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0),
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
tr.setMeta(ySyncPluginKey, { isChangeOrigin: true }),
|
|
497
|
+
this.prosemirrorView.dispatch(tr);
|
|
498
|
+
}, ySyncPluginKey);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
yXmlChanged(events: Array<Y.YEvent<any>>, transaction: Y.Transaction) {
|
|
503
|
+
if (!this.prosemirrorView) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const syncState = ySyncPluginKey.getState(this.prosemirrorView.state)!;
|
|
508
|
+
if (
|
|
509
|
+
events.length === 0 || syncState.snapshot ||
|
|
510
|
+
syncState.prevSnapshot
|
|
511
|
+
) {
|
|
512
|
+
// drop out if snapshot is active
|
|
513
|
+
this.renderSnapshot(syncState.snapshot, syncState.prevSnapshot);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
this.mux(() => {
|
|
518
|
+
if (!this.prosemirrorView) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const state = this.prosemirrorView.state;
|
|
522
|
+
|
|
523
|
+
const delType = (_: any, type: Y.AbstractType<any>) =>
|
|
524
|
+
this.mapping.delete(type);
|
|
525
|
+
Y.iterateDeletedStructs(
|
|
526
|
+
transaction,
|
|
527
|
+
transaction.deleteSet,
|
|
528
|
+
(struct) => {
|
|
529
|
+
if (struct.constructor === Y.Item) {
|
|
530
|
+
const type = (struct as Y.Item).content.type;
|
|
531
|
+
type && this.mapping.delete(type);
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
);
|
|
535
|
+
transaction.changed.forEach(delType);
|
|
536
|
+
transaction.changedParentTypes.forEach(delType);
|
|
537
|
+
|
|
538
|
+
const fragmentContent = this.type.toArray().map((t) =>
|
|
539
|
+
createNodeIfNotExists(
|
|
540
|
+
t as Y.XmlElement,
|
|
541
|
+
state.schema,
|
|
542
|
+
this,
|
|
543
|
+
)
|
|
544
|
+
).filter((n) => n !== null);
|
|
545
|
+
|
|
546
|
+
const tr = state.tr.replace(
|
|
547
|
+
0,
|
|
548
|
+
this.prosemirrorView.state.doc.content.size,
|
|
549
|
+
new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0),
|
|
550
|
+
);
|
|
551
|
+
if (this._beforeTransactionSelection) {
|
|
552
|
+
restoreRelativeSelection(tr, this._beforeTransactionSelection, this);
|
|
553
|
+
}
|
|
554
|
+
tr.setMeta(ySyncPluginKey, {
|
|
555
|
+
isChangeOrigin: true,
|
|
556
|
+
isUndoRedoOperation: transaction.origin instanceof Y.UndoManager,
|
|
557
|
+
});
|
|
558
|
+
if (
|
|
559
|
+
this.beforeTransactionSelection !== null && this._isLocalCursorInView()
|
|
560
|
+
) {
|
|
561
|
+
tr.scrollIntoView();
|
|
562
|
+
}
|
|
563
|
+
this.prosemirrorView.dispatch(tr);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
prosemirrorChanged(doc: Node) {
|
|
568
|
+
this.ydoc.transact(() => {
|
|
569
|
+
if (!this.prosemirrorView) {
|
|
570
|
+
throw new Error('Prosemirror changed without view?!');
|
|
571
|
+
}
|
|
572
|
+
updateYFragment(this.ydoc, this.type, doc, this);
|
|
573
|
+
this._beforeTransactionSelection = getRelativeSelection(
|
|
574
|
+
this,
|
|
575
|
+
this.prosemirrorView.state,
|
|
576
|
+
);
|
|
577
|
+
}, ySyncPluginKey);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* View is ready to listen to changes. Register observers.
|
|
582
|
+
*/
|
|
583
|
+
initView(prosemirrorView?: IEditorView) {
|
|
584
|
+
if (this.prosemirrorView) {
|
|
585
|
+
this.destroy();
|
|
586
|
+
}
|
|
587
|
+
this.prosemirrorView = prosemirrorView;
|
|
588
|
+
this.ydoc.on('beforeAllTransactions', this.beforeAllTransactions);
|
|
589
|
+
this.ydoc.on('afterAllTransactions', this.afterAllTransactions);
|
|
590
|
+
this.type.observeDeep(this._observeFunction);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
destroy() {
|
|
594
|
+
this.type.unobserveDeep(this._observeFunction);
|
|
595
|
+
this.ydoc.off('beforeAllTransactions', this.beforeAllTransactions);
|
|
596
|
+
this.ydoc.off('afterAllTransactions', this.afterAllTransactions);
|
|
597
|
+
if (!this.prosemirrorView) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
this.prosemirrorView = undefined;
|
|
601
|
+
// {
|
|
602
|
+
// state: new EditorState(),
|
|
603
|
+
// dispatch: () => false,
|
|
604
|
+
// fake: true
|
|
605
|
+
// };
|
|
606
|
+
}
|
|
607
|
+
}
|