@kerebron/extension-yjs 0.6.7 → 0.7.0
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 +38 -45
- package/esm/ExtensionYjs.js.map +1 -1
- package/esm/WebsocketProvider.d.ts +69 -0
- package/esm/WebsocketProvider.d.ts.map +1 -0
- package/esm/WebsocketProvider.js +354 -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/PmYjsBinding.d.ts +41 -0
- package/esm/binding/PmYjsBinding.d.ts.map +1 -0
- package/esm/binding/PmYjsBinding.js +190 -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} +1 -1
- 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/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/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 +54 -116
- 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 +55 -67
- package/src/WebsocketProvider.ts +516 -0
- package/src/YjsProvider.ts +75 -0
- package/src/_dnt.shims.ts +60 -0
- package/src/binding/BindingMetadata.ts +6 -0
- package/src/binding/PmYjsBinding.ts +300 -0
- package/src/binding/convertUtils.ts +124 -0
- package/src/{createNodeFromYElement.ts → binding/createNodeFromYElement.ts} +3 -3
- package/src/{updateYFragment.ts → binding/updateYFragment.ts} +15 -8
- package/src/lib.ts +4 -230
- package/src/position.ts +191 -0
- package/src/ui/selection.ts +216 -0
- package/src/yPositionPlugin.ts +122 -74
- package/src/ySyncPlugin.ts +87 -170
- 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,75 @@
|
|
|
1
|
+
// y-websocket is forked because we need better state connection handling
|
|
2
|
+
|
|
3
|
+
import * as Y from 'yjs';
|
|
4
|
+
import * as awarenessProtocol from 'y-protocols/awareness';
|
|
5
|
+
import * as encoding from 'lib0/encoding';
|
|
6
|
+
import * as decoding from 'lib0/decoding';
|
|
7
|
+
|
|
8
|
+
export const messageSync = 0;
|
|
9
|
+
export const messageQueryAwareness = 3;
|
|
10
|
+
export const messageAwareness = 1;
|
|
11
|
+
export const messageAuth = 2;
|
|
12
|
+
|
|
13
|
+
export type CreateYjsProvider = (roomId: string) => [YjsProvider, Y.Doc];
|
|
14
|
+
|
|
15
|
+
export const MessageType = {
|
|
16
|
+
Sync: 0,
|
|
17
|
+
Awareness: 1,
|
|
18
|
+
Auth: 2,
|
|
19
|
+
QueryAwareness: 3,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export type MessageType = typeof MessageType[keyof typeof MessageType];
|
|
23
|
+
|
|
24
|
+
export type MessageHandler<T extends YjsProvider> = (
|
|
25
|
+
encoder: encoding.Encoder,
|
|
26
|
+
decoder: decoding.Decoder,
|
|
27
|
+
provider: T,
|
|
28
|
+
emitSynced: boolean,
|
|
29
|
+
messageType: MessageType,
|
|
30
|
+
) => void;
|
|
31
|
+
|
|
32
|
+
export interface YjsProviderEventMap {
|
|
33
|
+
status: CustomEvent<{ status: 'connected' | 'disconnected' | 'connecting' }>;
|
|
34
|
+
sync: CustomEvent<{ state: boolean }>;
|
|
35
|
+
synced: CustomEvent<{ state: boolean }>;
|
|
36
|
+
'connection-error': CustomEvent<{ event: Event; provider: YjsProvider }>;
|
|
37
|
+
'connection-close': CustomEvent<
|
|
38
|
+
{ event: CloseEvent | null; provider: YjsProvider }
|
|
39
|
+
>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface YjsProvider extends EventTarget {
|
|
43
|
+
awareness: awarenessProtocol.Awareness;
|
|
44
|
+
doc: Y.Doc;
|
|
45
|
+
synced: boolean;
|
|
46
|
+
|
|
47
|
+
destroy();
|
|
48
|
+
|
|
49
|
+
addEventListener<K extends keyof YjsProviderEventMap>(
|
|
50
|
+
type: K,
|
|
51
|
+
listener: (event: YjsProviderEventMap[K]) => void,
|
|
52
|
+
options?: boolean | AddEventListenerOptions,
|
|
53
|
+
): void;
|
|
54
|
+
|
|
55
|
+
removeEventListener<K extends keyof YjsProviderEventMap>(
|
|
56
|
+
type: K,
|
|
57
|
+
listener: (event: YjsProviderEventMap[K]) => void,
|
|
58
|
+
options?: boolean | EventListenerOptions,
|
|
59
|
+
): void;
|
|
60
|
+
|
|
61
|
+
dispatchEvent(event: Event): boolean;
|
|
62
|
+
|
|
63
|
+
// fallback DOM signature (required)
|
|
64
|
+
addEventListener(
|
|
65
|
+
type: string,
|
|
66
|
+
listener: EventListenerOrEventListenerObject | null,
|
|
67
|
+
options?: boolean | AddEventListenerOptions,
|
|
68
|
+
): void;
|
|
69
|
+
|
|
70
|
+
removeEventListener(
|
|
71
|
+
type: string,
|
|
72
|
+
listener: EventListenerOrEventListenerObject | null,
|
|
73
|
+
options?: boolean | EventListenerOptions,
|
|
74
|
+
): void;
|
|
75
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const dntGlobals = {
|
|
2
|
+
};
|
|
3
|
+
export const dntGlobalThis = createMergeProxy(globalThis, dntGlobals);
|
|
4
|
+
|
|
5
|
+
function createMergeProxy<T extends object, U extends object>(
|
|
6
|
+
baseObj: T,
|
|
7
|
+
extObj: U,
|
|
8
|
+
): Omit<T, keyof U> & U {
|
|
9
|
+
return new Proxy(baseObj, {
|
|
10
|
+
get(_target, prop, _receiver) {
|
|
11
|
+
if (prop in extObj) {
|
|
12
|
+
return (extObj as any)[prop];
|
|
13
|
+
} else {
|
|
14
|
+
return (baseObj as any)[prop];
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
set(_target, prop, value) {
|
|
18
|
+
if (prop in extObj) {
|
|
19
|
+
delete (extObj as any)[prop];
|
|
20
|
+
}
|
|
21
|
+
(baseObj as any)[prop] = value;
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
24
|
+
deleteProperty(_target, prop) {
|
|
25
|
+
let success = false;
|
|
26
|
+
if (prop in extObj) {
|
|
27
|
+
delete (extObj as any)[prop];
|
|
28
|
+
success = true;
|
|
29
|
+
}
|
|
30
|
+
if (prop in baseObj) {
|
|
31
|
+
delete (baseObj as any)[prop];
|
|
32
|
+
success = true;
|
|
33
|
+
}
|
|
34
|
+
return success;
|
|
35
|
+
},
|
|
36
|
+
ownKeys(_target) {
|
|
37
|
+
const baseKeys = Reflect.ownKeys(baseObj);
|
|
38
|
+
const extKeys = Reflect.ownKeys(extObj);
|
|
39
|
+
const extKeysSet = new Set(extKeys);
|
|
40
|
+
return [...baseKeys.filter((k) => !extKeysSet.has(k)), ...extKeys];
|
|
41
|
+
},
|
|
42
|
+
defineProperty(_target, prop, desc) {
|
|
43
|
+
if (prop in extObj) {
|
|
44
|
+
delete (extObj as any)[prop];
|
|
45
|
+
}
|
|
46
|
+
Reflect.defineProperty(baseObj, prop, desc);
|
|
47
|
+
return true;
|
|
48
|
+
},
|
|
49
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
50
|
+
if (prop in extObj) {
|
|
51
|
+
return Reflect.getOwnPropertyDescriptor(extObj, prop);
|
|
52
|
+
} else {
|
|
53
|
+
return Reflect.getOwnPropertyDescriptor(baseObj, prop);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
has(_target, prop) {
|
|
57
|
+
return prop in extObj || prop in baseObj;
|
|
58
|
+
},
|
|
59
|
+
}) as any;
|
|
60
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
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
|
+
|
|
19
|
+
type Mutex = (f: () => void, g?: () => void) => void;
|
|
20
|
+
|
|
21
|
+
export const createMutex = (): Mutex => {
|
|
22
|
+
let token = true;
|
|
23
|
+
return (f: () => void, g?: () => void) => {
|
|
24
|
+
if (token) {
|
|
25
|
+
token = false;
|
|
26
|
+
try {
|
|
27
|
+
f();
|
|
28
|
+
} finally {
|
|
29
|
+
token = true;
|
|
30
|
+
}
|
|
31
|
+
} else if (g !== undefined) {
|
|
32
|
+
g();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ConnectionState = 'idle' | 'joining' | 'synced' | 'leaving' | 'error';
|
|
38
|
+
|
|
39
|
+
export type YjsData = { ydoc: Y.Doc; xmlFragment: Y.XmlFragment };
|
|
40
|
+
|
|
41
|
+
export class PmYjsBinding extends EventTarget {
|
|
42
|
+
private provider: YjsProvider | undefined;
|
|
43
|
+
private connectionState: ConnectionState = 'idle';
|
|
44
|
+
|
|
45
|
+
private yjs: YjsData | undefined;
|
|
46
|
+
private selectionStash: SelectionStash | undefined;
|
|
47
|
+
|
|
48
|
+
public addToYjsHistory = true;
|
|
49
|
+
private hasImported = false;
|
|
50
|
+
private mux: Mutex;
|
|
51
|
+
private bindingMetadata: BindingMetadata;
|
|
52
|
+
|
|
53
|
+
private syncedHandler: (
|
|
54
|
+
event: CustomEvent<{
|
|
55
|
+
state: boolean;
|
|
56
|
+
}>,
|
|
57
|
+
) => void;
|
|
58
|
+
private _observeFunction: (
|
|
59
|
+
events: Array<Y.YEvent<any>>,
|
|
60
|
+
transaction: Y.Transaction,
|
|
61
|
+
) => void;
|
|
62
|
+
|
|
63
|
+
constructor(private readonly editor: CoreEditor) {
|
|
64
|
+
super();
|
|
65
|
+
|
|
66
|
+
this.bindingMetadata = { mapping: new Map(), isOverlappingMark: new Map() };
|
|
67
|
+
|
|
68
|
+
this.mux = createMutex();
|
|
69
|
+
|
|
70
|
+
this.syncedHandler = (
|
|
71
|
+
event: CustomEvent<{
|
|
72
|
+
state: boolean;
|
|
73
|
+
}>,
|
|
74
|
+
) => {
|
|
75
|
+
const synced = event.detail.state;
|
|
76
|
+
if (synced && !this.hasImported) {
|
|
77
|
+
this.importRemoteYdoc();
|
|
78
|
+
} else {
|
|
79
|
+
// this.ydocChanged();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this._observeFunction = (events, transaction) => {
|
|
84
|
+
this.yXmlChanged(events, transaction);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
destroy() {
|
|
89
|
+
this.connectionState = 'idle';
|
|
90
|
+
const tr = this.editor.state.tr;
|
|
91
|
+
this.leaveRoom(tr);
|
|
92
|
+
this.editor.dispatchTransaction(tr);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
changeUser(user: User) {
|
|
96
|
+
if (!this.provider) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.provider.awareness.setLocalStateField('kerebron:user', user);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
changeRoom(
|
|
103
|
+
roomId: string,
|
|
104
|
+
createYjsProvider: CreateYjsProvider,
|
|
105
|
+
tr: Transaction,
|
|
106
|
+
) {
|
|
107
|
+
if (this.provider) {
|
|
108
|
+
this.provider.removeEventListener('synced', this.syncedHandler);
|
|
109
|
+
this.provider.destroy();
|
|
110
|
+
this.hasImported = false;
|
|
111
|
+
this.provider = undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.addToYjsHistory = true;
|
|
115
|
+
|
|
116
|
+
this.connectionState = 'joining';
|
|
117
|
+
const [provider, ydoc] = createYjsProvider(
|
|
118
|
+
roomId,
|
|
119
|
+
);
|
|
120
|
+
this.bindingMetadata.mapping.clear();
|
|
121
|
+
this.bindingMetadata.isOverlappingMark.clear();
|
|
122
|
+
|
|
123
|
+
this.provider = provider;
|
|
124
|
+
const fieldName = 'kerebron:' + this.editor.schema.topNodeType.name;
|
|
125
|
+
this.yjs = { ydoc, xmlFragment: ydoc.getXmlFragment(fieldName) };
|
|
126
|
+
this.selectionStash = new SelectionStash(
|
|
127
|
+
this.yjs,
|
|
128
|
+
this.getMapping(),
|
|
129
|
+
this.editor,
|
|
130
|
+
);
|
|
131
|
+
this.yjs.xmlFragment.observeDeep(this._observeFunction);
|
|
132
|
+
|
|
133
|
+
this.provider.addEventListener('synced', this.syncedHandler);
|
|
134
|
+
|
|
135
|
+
tr.setMeta('yjs:setAwareness', provider.awareness);
|
|
136
|
+
tr.setMeta('setYjs', this.yjs);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
leaveRoom(tr: Transaction) {
|
|
140
|
+
this.connectionState = 'leaving';
|
|
141
|
+
if (this.provider) {
|
|
142
|
+
this.provider.removeEventListener('synced', this.syncedHandler);
|
|
143
|
+
this.provider.destroy();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.addToYjsHistory = true;
|
|
147
|
+
this.provider = undefined;
|
|
148
|
+
|
|
149
|
+
this.selectionStash?.destroy();
|
|
150
|
+
this.selectionStash = undefined;
|
|
151
|
+
this.yjs?.xmlFragment.unobserveDeep(this._observeFunction);
|
|
152
|
+
this.yjs = undefined;
|
|
153
|
+
|
|
154
|
+
this.hasImported = false;
|
|
155
|
+
|
|
156
|
+
tr.setMeta('addToYjsHistory', false);
|
|
157
|
+
tr.setMeta('yjs:removeAwareness', true);
|
|
158
|
+
tr.setMeta('clearYjs', true);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
importRemoteYdoc() {
|
|
162
|
+
if (!this.yjs) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.addToYjsHistory = this.editor.state.doc.content.size > 2; // Non empty para
|
|
167
|
+
|
|
168
|
+
if (this.yjs.xmlFragment.length === 0) {
|
|
169
|
+
this.createFromProseMirror(this.yjs);
|
|
170
|
+
} else {
|
|
171
|
+
this.overwriteProseMirror(this.yjs.xmlFragment);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.connectionState = 'synced';
|
|
175
|
+
this.hasImported = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
yXmlChanged(events: Array<Y.YEvent<any>>, ytr: Y.Transaction) {
|
|
179
|
+
this.mux(() => {
|
|
180
|
+
if (!this.yjs) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const state = this.editor.state;
|
|
185
|
+
|
|
186
|
+
const { xmlFragment } = this.yjs;
|
|
187
|
+
|
|
188
|
+
// TODO handleSnapShot
|
|
189
|
+
|
|
190
|
+
const mapping = this.bindingMetadata.mapping;
|
|
191
|
+
const delType = (_: any, type: Y.AbstractType<any>) =>
|
|
192
|
+
mapping.delete(type);
|
|
193
|
+
Y.iterateDeletedStructs(
|
|
194
|
+
ytr,
|
|
195
|
+
ytr.deleteSet,
|
|
196
|
+
(struct) => {
|
|
197
|
+
if (struct.constructor === Y.Item) {
|
|
198
|
+
const type = (struct as Y.Item).content.type;
|
|
199
|
+
type && mapping.delete(type);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
ytr.changed.forEach(delType);
|
|
204
|
+
ytr.changedParentTypes.forEach(delType);
|
|
205
|
+
|
|
206
|
+
const fragmentContent = xmlFragment.toArray().map((t) =>
|
|
207
|
+
createNodeIfNotExists(
|
|
208
|
+
t as Y.XmlElement,
|
|
209
|
+
state.schema,
|
|
210
|
+
this.bindingMetadata,
|
|
211
|
+
)
|
|
212
|
+
).filter((n) => n !== null);
|
|
213
|
+
|
|
214
|
+
const tr = state.tr;
|
|
215
|
+
if (ytr.origin instanceof Y.UndoManager) {
|
|
216
|
+
} else {
|
|
217
|
+
}
|
|
218
|
+
tr.setMeta('addToYjsHistory', false);
|
|
219
|
+
tr.replace(
|
|
220
|
+
0,
|
|
221
|
+
state.doc.content.size,
|
|
222
|
+
new Slice(Fragment.from(fragmentContent), 0, 0),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
this.selectionStash?.restore(tr);
|
|
226
|
+
this.editor.dispatchTransaction(tr);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
pmChanged() {
|
|
231
|
+
if (!this.hasImported) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!this.yjs) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const doc = this.editor.state.doc;
|
|
240
|
+
const { ydoc, xmlFragment } = this.yjs;
|
|
241
|
+
|
|
242
|
+
this.mux(() => {
|
|
243
|
+
const origin = ySyncPluginKey;
|
|
244
|
+
|
|
245
|
+
ydoc.transact((ytr) => {
|
|
246
|
+
ytr.meta.set('addToYjsHistory', this.addToYjsHistory);
|
|
247
|
+
updateYFragment(
|
|
248
|
+
ydoc,
|
|
249
|
+
xmlFragment,
|
|
250
|
+
doc,
|
|
251
|
+
this.bindingMetadata,
|
|
252
|
+
this.addToYjsHistory,
|
|
253
|
+
);
|
|
254
|
+
this.selectionStash?.store();
|
|
255
|
+
}, origin);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
createFromProseMirror(yjs: YjsData) {
|
|
260
|
+
const doc = this.editor.state.doc;
|
|
261
|
+
const { ydoc, xmlFragment } = yjs;
|
|
262
|
+
updateYFragment(
|
|
263
|
+
ydoc,
|
|
264
|
+
xmlFragment,
|
|
265
|
+
doc,
|
|
266
|
+
this.bindingMetadata,
|
|
267
|
+
this.addToYjsHistory,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
overwriteProseMirror(yXmlFragment: Y.XmlFragment) {
|
|
272
|
+
const state = this.editor.state;
|
|
273
|
+
const newRoot = yXmlFragmentToProseMirrorRootNode(
|
|
274
|
+
yXmlFragment,
|
|
275
|
+
state.schema,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const tr = state.tr;
|
|
279
|
+
tr.setMeta('addToYjsHistory', false);
|
|
280
|
+
tr.replaceWith(
|
|
281
|
+
0,
|
|
282
|
+
state.doc.content.size,
|
|
283
|
+
newRoot,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
this.editor.dispatchTransaction(tr);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
getYjs(): YjsData | undefined {
|
|
290
|
+
return this.yjs;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
getMapping(): ProsemirrorMapping {
|
|
294
|
+
return this.bindingMetadata.mapping;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
getSelectionStash(): SelectionStash | undefined {
|
|
298
|
+
return this.selectionStash;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -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 = (
|
|
@@ -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
|
};
|