@kerebron/extension-yjs 0.6.7 → 0.7.1
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/esm/ExtensionYjs.d.ts +3 -11
- package/esm/ExtensionYjs.d.ts.map +1 -1
- package/esm/ExtensionYjs.js +71 -45
- package/esm/ExtensionYjs.js.map +1 -1
- package/esm/WebsocketProvider.d.ts +70 -0
- package/esm/WebsocketProvider.d.ts.map +1 -0
- package/esm/WebsocketProvider.js +377 -0
- package/esm/WebsocketProvider.js.map +1 -0
- package/esm/YjsProvider.d.ts +48 -0
- package/esm/YjsProvider.d.ts.map +1 -0
- package/esm/YjsProvider.js +12 -0
- package/esm/YjsProvider.js.map +1 -0
- package/esm/_dnt.shims.d.ts +2 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +58 -0
- package/esm/_dnt.shims.js.map +1 -0
- package/esm/binding/BindingMetadata.d.ts +6 -0
- package/esm/binding/BindingMetadata.d.ts.map +1 -0
- package/esm/binding/BindingMetadata.js +2 -0
- package/esm/binding/BindingMetadata.js.map +1 -0
- package/esm/binding/DiffViewer.d.ts +17 -0
- package/esm/binding/DiffViewer.d.ts.map +1 -0
- package/esm/binding/DiffViewer.js +96 -0
- package/esm/binding/DiffViewer.js.map +1 -0
- package/esm/binding/PmYjsBinding.d.ts +45 -0
- package/esm/binding/PmYjsBinding.d.ts.map +1 -0
- package/esm/binding/PmYjsBinding.js +230 -0
- package/esm/binding/PmYjsBinding.js.map +1 -0
- package/esm/binding/convertUtils.d.ts +48 -0
- package/esm/binding/convertUtils.d.ts.map +1 -0
- package/esm/binding/convertUtils.js +80 -0
- package/esm/binding/convertUtils.js.map +1 -0
- package/esm/{createNodeFromYElement.d.ts → binding/createNodeFromYElement.d.ts} +2 -2
- package/esm/binding/createNodeFromYElement.d.ts.map +1 -0
- package/esm/{createNodeFromYElement.js → binding/createNodeFromYElement.js} +2 -2
- package/esm/binding/createNodeFromYElement.js.map +1 -0
- package/esm/{updateYFragment.d.ts → binding/updateYFragment.d.ts} +3 -3
- package/esm/binding/updateYFragment.d.ts.map +1 -0
- package/esm/{updateYFragment.js → binding/updateYFragment.js} +10 -7
- package/esm/binding/updateYFragment.js.map +1 -0
- package/esm/debug.d.ts.map +1 -1
- package/esm/debug.js +11 -0
- package/esm/debug.js.map +1 -1
- package/esm/lib.d.ts +1 -7
- package/esm/lib.d.ts.map +1 -1
- package/esm/lib.js +1 -200
- package/esm/lib.js.map +1 -1
- package/esm/position.d.ts +8 -0
- package/esm/position.d.ts.map +1 -0
- package/esm/position.js +165 -0
- package/esm/position.js.map +1 -0
- package/esm/ui/selection.d.ts +29 -0
- package/esm/ui/selection.d.ts.map +1 -0
- package/esm/ui/selection.js +129 -0
- package/esm/ui/selection.js.map +1 -0
- package/esm/utils.d.ts +1 -1
- package/esm/utils.d.ts.map +1 -1
- package/esm/utils.js.map +1 -1
- package/esm/yPositionPlugin.d.ts +6 -1
- package/esm/yPositionPlugin.d.ts.map +1 -1
- package/esm/yPositionPlugin.js +91 -50
- package/esm/yPositionPlugin.js.map +1 -1
- package/esm/ySyncPlugin.d.ts +5 -22
- package/esm/ySyncPlugin.d.ts.map +1 -1
- package/esm/ySyncPlugin.js +70 -101
- package/esm/ySyncPlugin.js.map +1 -1
- package/esm/yUndoPlugin.d.ts +11 -10
- package/esm/yUndoPlugin.d.ts.map +1 -1
- package/esm/yUndoPlugin.js +90 -52
- package/esm/yUndoPlugin.js.map +1 -1
- package/package.json +9 -6
- package/src/ExtensionYjs.ts +98 -67
- package/src/WebsocketProvider.ts +528 -0
- package/src/YjsProvider.ts +75 -0
- package/src/_dnt.shims.ts +60 -0
- package/src/binding/BindingMetadata.ts +6 -0
- package/src/binding/DiffViewer.ts +138 -0
- package/src/binding/PmYjsBinding.ts +360 -0
- package/src/binding/convertUtils.ts +124 -0
- package/src/{createNodeFromYElement.ts → binding/createNodeFromYElement.ts} +4 -4
- package/src/{updateYFragment.ts → binding/updateYFragment.ts} +15 -8
- package/src/debug.ts +21 -0
- package/src/lib.ts +4 -230
- package/src/position.ts +191 -0
- package/src/ui/selection.ts +218 -0
- package/src/utils.ts +1 -1
- package/src/yPositionPlugin.ts +122 -74
- package/src/ySyncPlugin.ts +111 -155
- package/src/yUndoPlugin.ts +113 -62
- package/esm/ProsemirrorBinding.d.ts +0 -60
- package/esm/ProsemirrorBinding.d.ts.map +0 -1
- package/esm/ProsemirrorBinding.js +0 -405
- package/esm/ProsemirrorBinding.js.map +0 -1
- package/esm/createNodeFromYElement.d.ts.map +0 -1
- package/esm/createNodeFromYElement.js.map +0 -1
- package/esm/updateYFragment.d.ts.map +0 -1
- package/esm/updateYFragment.js.map +0 -1
- package/esm/userColors.d.ts +0 -5
- package/esm/userColors.d.ts.map +0 -1
- package/esm/userColors.js +0 -11
- package/esm/userColors.js.map +0 -1
- package/src/ProsemirrorBinding.ts +0 -607
- package/src/userColors.ts +0 -10
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
|
|
3
|
+
import { Node } from 'prosemirror-model';
|
|
4
|
+
import { EditorState } from 'prosemirror-state';
|
|
5
|
+
|
|
6
|
+
import { isVisible } from '../utils.js';
|
|
7
|
+
import { YjsData } from './PmYjsBinding.js';
|
|
8
|
+
import { createNodeFromYElement } from './createNodeFromYElement.js';
|
|
9
|
+
|
|
10
|
+
export class DiffViewer {
|
|
11
|
+
prevSnapshot?: Y.Snapshot;
|
|
12
|
+
snapshot?: Y.Snapshot;
|
|
13
|
+
|
|
14
|
+
private historyYXmlFragment: Y.XmlFragment | undefined;
|
|
15
|
+
private historyDoc: Y.Doc | undefined;
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
isActive() {
|
|
21
|
+
return !!this.snapshot || !!this.prevSnapshot;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setSnapshot(yjs: YjsData, uSnapshot: Uint8Array, uPrevSnapshot?: Uint8Array) {
|
|
25
|
+
this.prevSnapshot = undefined;
|
|
26
|
+
if (uPrevSnapshot) {
|
|
27
|
+
this.prevSnapshot = Y.decodeSnapshotV2(uPrevSnapshot);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.snapshot = Y.decodeSnapshotV2(uSnapshot);
|
|
31
|
+
|
|
32
|
+
this.historyDoc = Y.createDocFromSnapshot(
|
|
33
|
+
yjs.ydoc,
|
|
34
|
+
this.snapshot,
|
|
35
|
+
new Y.Doc({ gc: false }),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (yjs.xmlFragment._item === null) {
|
|
39
|
+
/**
|
|
40
|
+
* If is a root type, we need to find the root key in the initial document
|
|
41
|
+
* and use it to get the history type.
|
|
42
|
+
*/
|
|
43
|
+
const share: Map<string, Y.AbstractType<Y.YEvent<any>>> = yjs.ydoc.share;
|
|
44
|
+
const rootKey = Array.from(share.keys()).find(
|
|
45
|
+
(key) => share.get(key) === yjs.xmlFragment as Y.AbstractType<any>,
|
|
46
|
+
);
|
|
47
|
+
this.historyYXmlFragment = this.historyDoc.getXmlFragment(rootKey);
|
|
48
|
+
} else {
|
|
49
|
+
/**
|
|
50
|
+
* If it is a sub type, we use the item id to find the history type.
|
|
51
|
+
*/
|
|
52
|
+
const historyStructs =
|
|
53
|
+
this.historyDoc.store.clients.get(yjs.xmlFragment._item.id.client) ??
|
|
54
|
+
[];
|
|
55
|
+
const itemIndex = Y.findIndexSS(
|
|
56
|
+
historyStructs,
|
|
57
|
+
yjs.xmlFragment._item.id.clock,
|
|
58
|
+
);
|
|
59
|
+
if (historyStructs[itemIndex] instanceof Y.GC) {
|
|
60
|
+
throw new Error('Incorrect type Y.GC');
|
|
61
|
+
}
|
|
62
|
+
const item: Y.Item = historyStructs[itemIndex];
|
|
63
|
+
const content: Y.ContentType = item.content;
|
|
64
|
+
this.historyYXmlFragment = content.type as Y.XmlFragment;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
reset() {
|
|
69
|
+
this.snapshot = undefined;
|
|
70
|
+
this.prevSnapshot = undefined;
|
|
71
|
+
this.historyDoc = undefined;
|
|
72
|
+
this.historyYXmlFragment = undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getFragmentContent(
|
|
76
|
+
state: EditorState,
|
|
77
|
+
ytr: Y.Transaction,
|
|
78
|
+
): Node[] | undefined {
|
|
79
|
+
if (!this.historyYXmlFragment || !this.snapshot) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// before rendering, we are going to sanitize ops and split deleted ops
|
|
84
|
+
// if they were deleted by seperate users.
|
|
85
|
+
const pud: Y.PermanentUserData | undefined = undefined;
|
|
86
|
+
// pluginState.permanentUserData;
|
|
87
|
+
if (pud) {
|
|
88
|
+
pud.dss.forEach((ds) => {
|
|
89
|
+
Y.iterateDeletedStructs(ytr, ds, (_item) => {});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const computeYChange = (type: 'removed' | 'added', id: Y.ID) => {
|
|
93
|
+
const user = pud
|
|
94
|
+
? (type === 'added'
|
|
95
|
+
? pud.getUserByClientId(id.client)
|
|
96
|
+
: pud.getUserByDeletedId(id))
|
|
97
|
+
: undefined;
|
|
98
|
+
return {
|
|
99
|
+
user,
|
|
100
|
+
type,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const deleteSet = this.prevSnapshot
|
|
105
|
+
? this.prevSnapshot.ds
|
|
106
|
+
: Y.emptySnapshot.ds;
|
|
107
|
+
|
|
108
|
+
// Create document fragment and render
|
|
109
|
+
const fragmentContent = Y.typeListToArraySnapshot(
|
|
110
|
+
this.historyYXmlFragment,
|
|
111
|
+
new Y.Snapshot(deleteSet, this.snapshot.sv),
|
|
112
|
+
).map((t) => {
|
|
113
|
+
if (
|
|
114
|
+
!t._item.deleted || isVisible(t._item, this.snapshot) ||
|
|
115
|
+
isVisible(t._item, this.prevSnapshot)
|
|
116
|
+
) {
|
|
117
|
+
return createNodeFromYElement(
|
|
118
|
+
t,
|
|
119
|
+
state.schema,
|
|
120
|
+
{ mapping: new Map(), isOverlappingMark: new Map() },
|
|
121
|
+
this.snapshot,
|
|
122
|
+
this.prevSnapshot,
|
|
123
|
+
computeYChange,
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
// No need to render elements that are not visible by either snapshot.
|
|
127
|
+
// If a client adds and deletes content in the same snapshot the element is not visible by either snapshot.
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}).filter((n) => n !== null);
|
|
131
|
+
|
|
132
|
+
return fragmentContent;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getHistoryDoc(): Y.Doc | undefined {
|
|
136
|
+
return this.historyDoc;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
|
|
3
|
+
import { Fragment, Slice } from 'prosemirror-model';
|
|
4
|
+
import { Transaction } from 'prosemirror-state';
|
|
5
|
+
|
|
6
|
+
import { CoreEditor } from '@kerebron/editor';
|
|
7
|
+
import { User } from '@kerebron/editor/user';
|
|
8
|
+
|
|
9
|
+
import { CreateYjsProvider, YjsProvider } from '../YjsProvider.js';
|
|
10
|
+
import { ProsemirrorMapping } from '../lib.js';
|
|
11
|
+
import { SelectionStash } from '../ui/selection.js';
|
|
12
|
+
import { ySyncPluginKey } from '../keys.js';
|
|
13
|
+
|
|
14
|
+
import { yXmlFragmentToProseMirrorRootNode } from './convertUtils.js';
|
|
15
|
+
import { updateYFragment } from './updateYFragment.js';
|
|
16
|
+
import { BindingMetadata } from './BindingMetadata.js';
|
|
17
|
+
import { createNodeIfNotExists } from './createNodeFromYElement.js';
|
|
18
|
+
import { DiffViewer } from './DiffViewer.js';
|
|
19
|
+
|
|
20
|
+
type Mutex = (f: () => void, g?: () => void) => void;
|
|
21
|
+
|
|
22
|
+
export const createMutex = (): Mutex => {
|
|
23
|
+
let token = true;
|
|
24
|
+
return (f: () => void, g?: () => void) => {
|
|
25
|
+
if (token) {
|
|
26
|
+
token = false;
|
|
27
|
+
try {
|
|
28
|
+
f();
|
|
29
|
+
} finally {
|
|
30
|
+
token = true;
|
|
31
|
+
}
|
|
32
|
+
} else if (g !== undefined) {
|
|
33
|
+
g();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type ConnectionState = 'idle' | 'joining' | 'synced' | 'leaving' | 'error';
|
|
39
|
+
|
|
40
|
+
export type YjsData = { ydoc: Y.Doc; xmlFragment: Y.XmlFragment };
|
|
41
|
+
|
|
42
|
+
export class PmYjsBinding extends EventTarget {
|
|
43
|
+
private provider: YjsProvider | undefined;
|
|
44
|
+
private connectionState: ConnectionState = 'idle';
|
|
45
|
+
|
|
46
|
+
private yjs: YjsData | undefined;
|
|
47
|
+
private selectionStash: SelectionStash | undefined;
|
|
48
|
+
public readonly diffViewer: DiffViewer;
|
|
49
|
+
|
|
50
|
+
public addToYjsHistory = true;
|
|
51
|
+
private hasImported = false;
|
|
52
|
+
private mux: Mutex;
|
|
53
|
+
private bindingMetadata: BindingMetadata;
|
|
54
|
+
|
|
55
|
+
private syncedHandler: (
|
|
56
|
+
event: CustomEvent<{
|
|
57
|
+
state: boolean;
|
|
58
|
+
}>,
|
|
59
|
+
) => void;
|
|
60
|
+
private _observeFunction: (
|
|
61
|
+
events: Array<Y.YEvent<any>>,
|
|
62
|
+
transaction: Y.Transaction,
|
|
63
|
+
) => void;
|
|
64
|
+
|
|
65
|
+
constructor(private readonly editor: CoreEditor) {
|
|
66
|
+
super();
|
|
67
|
+
|
|
68
|
+
this.diffViewer = new DiffViewer();
|
|
69
|
+
this.bindingMetadata = { mapping: new Map(), isOverlappingMark: new Map() };
|
|
70
|
+
|
|
71
|
+
this.mux = createMutex();
|
|
72
|
+
|
|
73
|
+
this.syncedHandler = (
|
|
74
|
+
event: CustomEvent<{
|
|
75
|
+
state: boolean;
|
|
76
|
+
}>,
|
|
77
|
+
) => {
|
|
78
|
+
const synced = event.detail.state;
|
|
79
|
+
if (synced && !this.hasImported) {
|
|
80
|
+
this.importRemoteYdoc();
|
|
81
|
+
} else {
|
|
82
|
+
// this.ydocChanged();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
this._observeFunction = (events, transaction) => {
|
|
87
|
+
this.yXmlChanged(events, transaction);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
destroy() {
|
|
92
|
+
this.connectionState = 'idle';
|
|
93
|
+
const tr = this.editor.state.tr;
|
|
94
|
+
this.leaveRoom(tr);
|
|
95
|
+
this.editor.dispatchTransaction(tr);
|
|
96
|
+
this.diffViewer.reset();
|
|
97
|
+
this.selectionStash?.destroy();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
changeUser(user: User) {
|
|
101
|
+
if (!this.provider) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.provider.awareness.setLocalStateField('kerebron:user', user);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
changeRoom(
|
|
108
|
+
roomId: string,
|
|
109
|
+
createYjsProvider: CreateYjsProvider,
|
|
110
|
+
tr: Transaction,
|
|
111
|
+
) {
|
|
112
|
+
this.connectionState = 'joining';
|
|
113
|
+
this.hasImported = false;
|
|
114
|
+
|
|
115
|
+
if (this.provider) {
|
|
116
|
+
this.provider.removeEventListener('synced', this.syncedHandler);
|
|
117
|
+
this.provider.destroy();
|
|
118
|
+
this.provider = undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const [provider, ydoc] = createYjsProvider(
|
|
122
|
+
roomId,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
this.addToYjsHistory = true;
|
|
126
|
+
this.provider = provider;
|
|
127
|
+
|
|
128
|
+
this.bindingMetadata.mapping.clear();
|
|
129
|
+
this.bindingMetadata.isOverlappingMark.clear();
|
|
130
|
+
this.diffViewer.reset();
|
|
131
|
+
|
|
132
|
+
const fieldName = 'kerebron:' + this.editor.schema.topNodeType.name;
|
|
133
|
+
this.yjs = { ydoc, xmlFragment: ydoc.getXmlFragment(fieldName) };
|
|
134
|
+
this.selectionStash = new SelectionStash(
|
|
135
|
+
this.yjs,
|
|
136
|
+
this.getMapping(),
|
|
137
|
+
this.editor,
|
|
138
|
+
);
|
|
139
|
+
this.yjs.xmlFragment.observeDeep(this._observeFunction);
|
|
140
|
+
|
|
141
|
+
this.provider.addEventListener('synced', this.syncedHandler);
|
|
142
|
+
|
|
143
|
+
tr.setMeta('yjs:setAwareness', provider.awareness);
|
|
144
|
+
tr.setMeta('setYjs', this.yjs);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
leaveRoom(tr: Transaction) {
|
|
148
|
+
this.connectionState = 'leaving';
|
|
149
|
+
if (this.provider) {
|
|
150
|
+
this.provider.removeEventListener('synced', this.syncedHandler);
|
|
151
|
+
this.provider.destroy();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.addToYjsHistory = true;
|
|
155
|
+
this.provider = undefined;
|
|
156
|
+
|
|
157
|
+
this.bindingMetadata.mapping.clear();
|
|
158
|
+
this.bindingMetadata.isOverlappingMark.clear();
|
|
159
|
+
this.diffViewer.reset();
|
|
160
|
+
|
|
161
|
+
this.selectionStash?.destroy();
|
|
162
|
+
this.selectionStash = undefined;
|
|
163
|
+
this.yjs?.xmlFragment.unobserveDeep(this._observeFunction);
|
|
164
|
+
this.yjs = undefined;
|
|
165
|
+
|
|
166
|
+
this.hasImported = false;
|
|
167
|
+
|
|
168
|
+
tr.setMeta('addToYjsHistory', false);
|
|
169
|
+
tr.setMeta('yjs:removeAwareness', true);
|
|
170
|
+
tr.setMeta('clearYjs', true);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
importRemoteYdoc() {
|
|
174
|
+
if (!this.yjs) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.addToYjsHistory = this.editor.state.doc.content.size > 2; // Non empty para
|
|
179
|
+
|
|
180
|
+
if (this.yjs.xmlFragment.length === 0) {
|
|
181
|
+
this.createFromProseMirror(this.yjs);
|
|
182
|
+
} else {
|
|
183
|
+
this.overwriteProseMirror(this.yjs.xmlFragment);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.connectionState = 'synced';
|
|
187
|
+
this.hasImported = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
yXmlChanged(events: Array<Y.YEvent<any>>, ytr: Y.Transaction) {
|
|
191
|
+
this.mux(() => {
|
|
192
|
+
if (!this.yjs) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this.diffViewer.isActive()) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const state = this.editor.state;
|
|
201
|
+
|
|
202
|
+
const { xmlFragment } = this.yjs;
|
|
203
|
+
|
|
204
|
+
const mapping = this.bindingMetadata.mapping;
|
|
205
|
+
const delType = (_: any, type: Y.AbstractType<any>) =>
|
|
206
|
+
mapping.delete(type);
|
|
207
|
+
Y.iterateDeletedStructs(
|
|
208
|
+
ytr,
|
|
209
|
+
ytr.deleteSet,
|
|
210
|
+
(struct) => {
|
|
211
|
+
if (struct.constructor === Y.Item) {
|
|
212
|
+
const type = (struct as Y.Item).content.type;
|
|
213
|
+
type && mapping.delete(type);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
ytr.changed.forEach(delType);
|
|
218
|
+
ytr.changedParentTypes.forEach(delType);
|
|
219
|
+
|
|
220
|
+
const fragmentContent = xmlFragment.toArray().map((t) =>
|
|
221
|
+
createNodeIfNotExists(
|
|
222
|
+
t as Y.XmlElement,
|
|
223
|
+
state.schema,
|
|
224
|
+
this.bindingMetadata,
|
|
225
|
+
)
|
|
226
|
+
).filter((n) => n !== null);
|
|
227
|
+
|
|
228
|
+
const tr = state.tr;
|
|
229
|
+
tr.setMeta('addToYjsHistory', false);
|
|
230
|
+
tr.replace(
|
|
231
|
+
0,
|
|
232
|
+
state.doc.content.size,
|
|
233
|
+
new Slice(Fragment.from(fragmentContent), 0, 0),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
this.selectionStash?.restore(tr);
|
|
237
|
+
this.editor.dispatchTransaction(tr);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
pmChanged() {
|
|
242
|
+
if (!this.hasImported) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!this.yjs) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (this.diffViewer.isActive()) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const doc = this.editor.state.doc;
|
|
255
|
+
const { ydoc, xmlFragment } = this.yjs;
|
|
256
|
+
|
|
257
|
+
this.mux(() => {
|
|
258
|
+
const origin = ySyncPluginKey;
|
|
259
|
+
|
|
260
|
+
ydoc.transact((ytr) => {
|
|
261
|
+
ytr.meta.set('addToYjsHistory', this.addToYjsHistory);
|
|
262
|
+
updateYFragment(
|
|
263
|
+
ydoc,
|
|
264
|
+
xmlFragment,
|
|
265
|
+
doc,
|
|
266
|
+
this.bindingMetadata,
|
|
267
|
+
this.addToYjsHistory,
|
|
268
|
+
);
|
|
269
|
+
this.selectionStash?.store();
|
|
270
|
+
}, origin);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
createFromProseMirror(yjs: YjsData) {
|
|
275
|
+
const doc = this.editor.state.doc;
|
|
276
|
+
const { ydoc, xmlFragment } = yjs;
|
|
277
|
+
updateYFragment(
|
|
278
|
+
ydoc,
|
|
279
|
+
xmlFragment,
|
|
280
|
+
doc,
|
|
281
|
+
this.bindingMetadata,
|
|
282
|
+
this.addToYjsHistory,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
overwriteProseMirror(yXmlFragment: Y.XmlFragment) {
|
|
287
|
+
const state = this.editor.state;
|
|
288
|
+
const newRoot = yXmlFragmentToProseMirrorRootNode(
|
|
289
|
+
yXmlFragment,
|
|
290
|
+
state.schema,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const tr = state.tr;
|
|
294
|
+
tr.setMeta('addToYjsHistory', false);
|
|
295
|
+
tr.replaceWith(
|
|
296
|
+
0,
|
|
297
|
+
state.doc.content.size,
|
|
298
|
+
newRoot,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
this.editor.dispatchTransaction(tr);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getYjs(): YjsData | undefined {
|
|
305
|
+
return this.yjs;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getMapping(): ProsemirrorMapping {
|
|
309
|
+
return this.bindingMetadata.mapping;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
getSelectionStash(): SelectionStash | undefined {
|
|
313
|
+
return this.selectionStash;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
isEditable() {
|
|
317
|
+
return !this.diffViewer.isActive();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
setSnapshot(
|
|
321
|
+
snapshot: Uint8Array<ArrayBufferLike>,
|
|
322
|
+
prevSnapshot: Uint8Array<ArrayBufferLike> | undefined,
|
|
323
|
+
) {
|
|
324
|
+
if (!this.yjs) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this.diffViewer.setSnapshot(this.yjs, snapshot, prevSnapshot);
|
|
328
|
+
|
|
329
|
+
// clear mapping because we are going to rerender
|
|
330
|
+
this.bindingMetadata.mapping.clear();
|
|
331
|
+
this.bindingMetadata.isOverlappingMark.clear();
|
|
332
|
+
|
|
333
|
+
this.mux(() => {
|
|
334
|
+
const historyDoc = this.diffViewer.getHistoryDoc();
|
|
335
|
+
if (!historyDoc) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
historyDoc.transact((ytr) => {
|
|
340
|
+
const state = this.editor.state;
|
|
341
|
+
const fragmentContent = this.diffViewer.getFragmentContent(state, ytr);
|
|
342
|
+
|
|
343
|
+
if (!fragmentContent) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const tr = state.tr;
|
|
348
|
+
tr.replace(
|
|
349
|
+
0,
|
|
350
|
+
state.doc.content.size,
|
|
351
|
+
new Slice(Fragment.from(fragmentContent), 0, 0),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
tr.setMeta('addToHistory', false);
|
|
355
|
+
tr.setMeta(ySyncPluginKey, { isChangeOrigin: true }),
|
|
356
|
+
this.editor.dispatchTransaction(tr);
|
|
357
|
+
}, ySyncPluginKey);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
|
|
3
|
+
import { Fragment, Node, type Schema } from 'prosemirror-model';
|
|
4
|
+
|
|
5
|
+
import { TransactFunc } from '../lib.js';
|
|
6
|
+
|
|
7
|
+
import { updateYFragment } from './updateYFragment.js';
|
|
8
|
+
import { createNodeFromYElement } from './createNodeFromYElement.js';
|
|
9
|
+
import { BindingMetadata } from './BindingMetadata.js';
|
|
10
|
+
|
|
11
|
+
export const createEmptyMeta = (): BindingMetadata => ({
|
|
12
|
+
mapping: new Map(),
|
|
13
|
+
isOverlappingMark: new Map(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Utility function for converting an Y.Fragment to a ProseMirror fragment.
|
|
18
|
+
*/
|
|
19
|
+
export const yXmlFragmentToProseMirrorFragment = (
|
|
20
|
+
yXmlFragment: Y.XmlFragment,
|
|
21
|
+
schema: Schema,
|
|
22
|
+
) => {
|
|
23
|
+
const elements: Y.XmlElement[] = yXmlFragment.toArray().filter((i) =>
|
|
24
|
+
i instanceof Y.XmlElement
|
|
25
|
+
);
|
|
26
|
+
const fragmentContent = elements.map((t) =>
|
|
27
|
+
createNodeFromYElement(
|
|
28
|
+
t,
|
|
29
|
+
schema,
|
|
30
|
+
createEmptyMeta(),
|
|
31
|
+
)
|
|
32
|
+
).filter((n) => n !== null);
|
|
33
|
+
return Fragment.fromArray(fragmentContent);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Utility function for converting an Y.Fragment to a ProseMirror node.
|
|
38
|
+
*/
|
|
39
|
+
export const yXmlFragmentToProseMirrorRootNode = (
|
|
40
|
+
yXmlFragment: Y.XmlFragment,
|
|
41
|
+
schema: Schema,
|
|
42
|
+
) =>
|
|
43
|
+
schema.topNodeType.create(
|
|
44
|
+
null,
|
|
45
|
+
yXmlFragmentToProseMirrorFragment(yXmlFragment, schema),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Utility method to convert a Prosemirror Doc Node into a Y.Doc.
|
|
50
|
+
*
|
|
51
|
+
* This can be used when importing existing content to Y.Doc for the first time,
|
|
52
|
+
* note that this should not be used to rehydrate a Y.Doc from a database once
|
|
53
|
+
* collaboration has begun as all history will be lost
|
|
54
|
+
*/
|
|
55
|
+
export function prosemirrorToYDoc(
|
|
56
|
+
doc: Node,
|
|
57
|
+
xmlFragment: string = 'prosemirror',
|
|
58
|
+
): Y.Doc {
|
|
59
|
+
const ydoc = new Y.Doc();
|
|
60
|
+
const type: Y.XmlFragment = ydoc.get(xmlFragment, Y.XmlFragment);
|
|
61
|
+
if (!type.doc) {
|
|
62
|
+
return ydoc;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
prosemirrorToYXmlFragment(doc, type);
|
|
66
|
+
return type.doc;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Utility method to update an empty Y.XmlFragment with content from a Prosemirror Doc Node.
|
|
71
|
+
*
|
|
72
|
+
* This can be used when importing existing content to Y.Doc for the first time,
|
|
73
|
+
* note that this should not be used to rehydrate a Y.Doc from a database once
|
|
74
|
+
* collaboration has begun as all history will be lost
|
|
75
|
+
*
|
|
76
|
+
* Note: The Y.XmlFragment does not need to be part of a Y.Doc document at the time that this
|
|
77
|
+
* method is called, but it must be added before any other operations are performed on it.
|
|
78
|
+
*/
|
|
79
|
+
export function prosemirrorToYXmlFragment(
|
|
80
|
+
doc: Node,
|
|
81
|
+
xmlFragment: Y.XmlFragment,
|
|
82
|
+
): Y.XmlFragment {
|
|
83
|
+
const type = xmlFragment || new Y.XmlFragment();
|
|
84
|
+
const ydoc: { transact: TransactFunc<void> } = type.doc
|
|
85
|
+
? type.doc
|
|
86
|
+
: { transact: (transaction) => transaction(undefined) };
|
|
87
|
+
updateYFragment(ydoc, type, doc, {
|
|
88
|
+
mapping: new Map(),
|
|
89
|
+
isOverlappingMark: new Map(),
|
|
90
|
+
});
|
|
91
|
+
return type;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Utility method to convert Prosemirror compatible JSON into a Y.Doc.
|
|
96
|
+
*
|
|
97
|
+
* This can be used when importing existing content to Y.Doc for the first time,
|
|
98
|
+
* note that this should not be used to rehydrate a Y.Doc from a database once
|
|
99
|
+
* collaboration has begun as all history will be lost
|
|
100
|
+
*/
|
|
101
|
+
export function prosemirrorJSONToYDoc(
|
|
102
|
+
schema: Schema,
|
|
103
|
+
state: any,
|
|
104
|
+
xmlFragment: string = 'prosemirror',
|
|
105
|
+
): Y.Doc {
|
|
106
|
+
const doc = Node.fromJSON(schema, state);
|
|
107
|
+
return prosemirrorToYDoc(doc, xmlFragment);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Utility method to convert Prosemirror compatible JSON to a Y.XmlFragment
|
|
112
|
+
*
|
|
113
|
+
* This can be used when importing existing content to Y.Doc for the first time,
|
|
114
|
+
* note that this should not be used to rehydrate a Y.Doc from a database once
|
|
115
|
+
* collaboration has begun as all history will be lost
|
|
116
|
+
*/
|
|
117
|
+
export function prosemirrorJSONToYXmlFragment(
|
|
118
|
+
schema: Schema,
|
|
119
|
+
state: any,
|
|
120
|
+
xmlFragment: Y.XmlFragment,
|
|
121
|
+
): Y.XmlFragment {
|
|
122
|
+
const doc = Node.fromJSON(schema, state);
|
|
123
|
+
return prosemirrorToYXmlFragment(doc, xmlFragment);
|
|
124
|
+
}
|
|
@@ -2,9 +2,9 @@ import * as PModel from 'prosemirror-model';
|
|
|
2
2
|
import { Mark, Schema } from 'prosemirror-model';
|
|
3
3
|
import * as Y from 'yjs';
|
|
4
4
|
|
|
5
|
-
import type { BindingMetadata } from './
|
|
6
|
-
import { ySyncPluginKey } from '
|
|
7
|
-
import { isVisible } from '
|
|
5
|
+
import type { BindingMetadata } from './BindingMetadata.js';
|
|
6
|
+
import { ySyncPluginKey } from '../keys.js';
|
|
7
|
+
import { isVisible } from '../utils.js';
|
|
8
8
|
import { yattr2markname } from './updateYFragment.js';
|
|
9
9
|
|
|
10
10
|
export const attributesToMarks = (
|
|
@@ -51,7 +51,7 @@ export const createNodeFromYElement = (
|
|
|
51
51
|
meta: BindingMetadata,
|
|
52
52
|
snapshot?: Y.Snapshot,
|
|
53
53
|
prevSnapshot?: Y.Snapshot,
|
|
54
|
-
computeYChange?: (
|
|
54
|
+
computeYChange?: (changeType: 'removed' | 'added', yid: Y.ID) => any,
|
|
55
55
|
): PModel.Node | null => {
|
|
56
56
|
const children: PModel.Node[] = [];
|
|
57
57
|
const createChildren = (type: Y.XmlElement | Y.XmlText) => {
|
|
@@ -4,10 +4,10 @@ import { Mark, Node, Schema } from 'prosemirror-model';
|
|
|
4
4
|
|
|
5
5
|
import { simpleDiff } from 'lib0/diff';
|
|
6
6
|
|
|
7
|
-
import { ySyncPluginKey } from '
|
|
8
|
-
import * as utils from '
|
|
9
|
-
import type { BindingMetadata } from './
|
|
10
|
-
import { TransactFunc } from '
|
|
7
|
+
import { ySyncPluginKey } from '../keys.js';
|
|
8
|
+
import * as utils from '../utils.js';
|
|
9
|
+
import type { BindingMetadata } from './BindingMetadata.js';
|
|
10
|
+
import { TransactFunc } from '../lib.js';
|
|
11
11
|
|
|
12
12
|
const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/;
|
|
13
13
|
export const yattr2markname = (attrName: string) =>
|
|
@@ -21,8 +21,11 @@ const marksToAttributes = (
|
|
|
21
21
|
marks.forEach((mark) => {
|
|
22
22
|
if (mark.type.name !== 'ychange') {
|
|
23
23
|
let isOverlapping = true;
|
|
24
|
-
if (!meta.
|
|
25
|
-
meta.
|
|
24
|
+
if (!meta.isOverlappingMark.has(mark.type.name)) {
|
|
25
|
+
meta.isOverlappingMark.set(
|
|
26
|
+
mark.type.name,
|
|
27
|
+
!mark.type.excludes(mark.type),
|
|
28
|
+
);
|
|
26
29
|
isOverlapping = false;
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -278,7 +281,9 @@ export const updateYFragment = (
|
|
|
278
281
|
yDomFragment: Y.XmlFragment,
|
|
279
282
|
pNode: Node,
|
|
280
283
|
meta: BindingMetadata,
|
|
284
|
+
addToYjsHistory?: boolean,
|
|
281
285
|
) => {
|
|
286
|
+
const origin = ySyncPluginKey;
|
|
282
287
|
if (
|
|
283
288
|
yDomFragment instanceof Y.XmlElement &&
|
|
284
289
|
yDomFragment.nodeName !== pNode.type.name
|
|
@@ -342,7 +347,9 @@ export const updateYFragment = (
|
|
|
342
347
|
}
|
|
343
348
|
}
|
|
344
349
|
}
|
|
345
|
-
ydoc.transact(() => {
|
|
350
|
+
ydoc.transact((ytr) => {
|
|
351
|
+
ytr.meta.set('updateYFragment', 1);
|
|
352
|
+
ytr.meta.set('addToYjsHistory', addToYjsHistory);
|
|
346
353
|
// try to compare and update
|
|
347
354
|
while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
|
|
348
355
|
const leftY: Y.XmlElement | Y.XmlText = yChildren[left];
|
|
@@ -435,5 +442,5 @@ export const updateYFragment = (
|
|
|
435
442
|
}
|
|
436
443
|
yDomFragment.insert(left, ins);
|
|
437
444
|
}
|
|
438
|
-
},
|
|
445
|
+
}, origin);
|
|
439
446
|
};
|