@kerebron/extension-yjs 0.4.28 → 0.4.30
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.js +1 -0
- package/esm/ExtensionYjs.js.map +1 -0
- package/esm/convertUtils.js +1 -0
- package/esm/convertUtils.js.map +1 -0
- package/esm/keys.js +1 -0
- package/esm/keys.js.map +1 -0
- package/esm/lib.js +1 -0
- package/esm/lib.js.map +1 -0
- package/esm/userColors.js +1 -0
- package/esm/userColors.js.map +1 -0
- package/esm/utils.js +1 -0
- package/esm/utils.js.map +1 -0
- package/esm/yPositionPlugin.js +1 -0
- package/esm/yPositionPlugin.js.map +1 -0
- package/esm/ySyncPlugin.js +1 -0
- package/esm/ySyncPlugin.js.map +1 -0
- package/esm/yUndoPlugin.js +1 -0
- package/esm/yUndoPlugin.js.map +1 -0
- package/package.json +7 -3
- package/src/ExtensionYjs.ts +55 -0
- package/src/convertUtils.ts +143 -0
- package/src/keys.ts +14 -0
- package/src/lib.ts +230 -0
- package/src/userColors.ts +10 -0
- package/src/utils.ts +13 -0
- package/src/yPositionPlugin.ts +228 -0
- package/src/ySyncPlugin.ts +1313 -0
- package/src/yUndoPlugin.ts +124 -0
package/src/lib.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import { type EditorView } from 'prosemirror-view';
|
|
3
|
+
import { type Node } from 'prosemirror-model';
|
|
4
|
+
|
|
5
|
+
import { ySyncPluginKey } from './keys.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Either a node if type is YXmlElement or an Array of text nodes if YXmlText
|
|
9
|
+
*/
|
|
10
|
+
type ProsemirrorMapping = Map<Y.AbstractType<any>, Node>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Is null if no timeout is in progress.
|
|
14
|
+
* Is defined if a timeout is in progress.
|
|
15
|
+
* Maps from view
|
|
16
|
+
*/
|
|
17
|
+
let viewsToUpdate: Map<EditorView, Map<any, any>> | null = null;
|
|
18
|
+
|
|
19
|
+
const updateMetas = () => {
|
|
20
|
+
const ups: Map<EditorView, Map<any, any>> | null = viewsToUpdate;
|
|
21
|
+
viewsToUpdate = null;
|
|
22
|
+
if (!ups) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
ups.forEach((metas, view) => {
|
|
26
|
+
const tr = view.state.tr;
|
|
27
|
+
const syncState = ySyncPluginKey.getState(view.state);
|
|
28
|
+
if (syncState && syncState.binding && !syncState.binding.isDestroyed) {
|
|
29
|
+
metas.forEach((val, key) => {
|
|
30
|
+
tr.setMeta(key, val);
|
|
31
|
+
});
|
|
32
|
+
view.dispatch(tr);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const setMeta = (view: EditorView, key, value) => {
|
|
38
|
+
if (!viewsToUpdate) {
|
|
39
|
+
viewsToUpdate = new Map();
|
|
40
|
+
setTimeout(updateMetas, 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let subMap = viewsToUpdate.get(view);
|
|
44
|
+
if (subMap === undefined) {
|
|
45
|
+
subMap = new Map();
|
|
46
|
+
viewsToUpdate.set(view, subMap);
|
|
47
|
+
}
|
|
48
|
+
subMap.set(key, value);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Transforms a Prosemirror based absolute position to a Yjs Cursor (relative position in the Yjs model).
|
|
53
|
+
*/
|
|
54
|
+
export const absolutePositionToRelativePosition = (
|
|
55
|
+
pos: number,
|
|
56
|
+
type: Y.XmlFragment,
|
|
57
|
+
mapping: ProsemirrorMapping,
|
|
58
|
+
): any => {
|
|
59
|
+
if (pos === 0) {
|
|
60
|
+
// if the type is later populated, we want to retain the 0 position (hence assoc=-1)
|
|
61
|
+
return Y.createRelativePositionFromTypeIndex(
|
|
62
|
+
type,
|
|
63
|
+
0,
|
|
64
|
+
type.length === 0 ? -1 : 0,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let n: Y.AbstractType<any> | null = type._first === null
|
|
69
|
+
? null
|
|
70
|
+
: /** @type {Y.ContentType} */ (type._first.content).type;
|
|
71
|
+
while (n !== null && type !== n) {
|
|
72
|
+
if (n instanceof Y.XmlText) {
|
|
73
|
+
if (n._length >= pos) {
|
|
74
|
+
return Y.createRelativePositionFromTypeIndex(
|
|
75
|
+
n,
|
|
76
|
+
pos,
|
|
77
|
+
type.length === 0 ? -1 : 0,
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
pos -= n._length;
|
|
81
|
+
}
|
|
82
|
+
if (n._item !== null && n._item.next !== null) {
|
|
83
|
+
n = /** @type {Y.ContentType} */ (n._item.next.content).type;
|
|
84
|
+
} else {
|
|
85
|
+
do {
|
|
86
|
+
n = n._item === null ? null : n._item.parent;
|
|
87
|
+
pos--;
|
|
88
|
+
} while (
|
|
89
|
+
n !== type && n !== null && n._item !== null && n._item.next === null
|
|
90
|
+
);
|
|
91
|
+
if (n !== null && n !== type) {
|
|
92
|
+
// @ts-gnore we know that n.next !== null because of above loop conditition
|
|
93
|
+
n = n._item === null
|
|
94
|
+
? null
|
|
95
|
+
: /** @type {Y.ContentType} */ (/** @type Y.Item */ (n._item.next)
|
|
96
|
+
.content).type;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
const pNodeSize =
|
|
101
|
+
/** @type {any} */ (mapping.get(n) || { nodeSize: 0 }).nodeSize;
|
|
102
|
+
if (n._first !== null && pos < pNodeSize) {
|
|
103
|
+
n = /** @type {Y.ContentType} */ (n._first.content).type;
|
|
104
|
+
pos--;
|
|
105
|
+
} else {
|
|
106
|
+
if (pos === 1 && n._length === 0 && pNodeSize > 1) {
|
|
107
|
+
// edge case, should end in this paragraph
|
|
108
|
+
return new Y.RelativePosition(
|
|
109
|
+
n._item === null ? null : n._item.id,
|
|
110
|
+
n._item === null ? Y.findRootTypeKey(n) : null,
|
|
111
|
+
null,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
pos -= pNodeSize;
|
|
115
|
+
if (n._item !== null && n._item.next !== null) {
|
|
116
|
+
n = /** @type {Y.ContentType} */ (n._item.next.content).type;
|
|
117
|
+
} else {
|
|
118
|
+
if (pos === 0) {
|
|
119
|
+
// set to end of n.parent
|
|
120
|
+
n = n._item === null ? n : n._item.parent;
|
|
121
|
+
return new Y.RelativePosition(
|
|
122
|
+
n._item === null ? null : n._item.id,
|
|
123
|
+
n._item === null ? Y.findRootTypeKey(n) : null,
|
|
124
|
+
null,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
do {
|
|
128
|
+
n = n._item.parent;
|
|
129
|
+
pos--;
|
|
130
|
+
} while (n !== type && /** @type {Y.Item} */ (n._item).next === null);
|
|
131
|
+
// if n is null at this point, we have an unexpected case
|
|
132
|
+
if (n !== type) {
|
|
133
|
+
// We know that n._item.next is defined because of above loop condition
|
|
134
|
+
n =
|
|
135
|
+
/** @type {Y.ContentType} */ (/** @type {Y.Item} */ (/** @type {Y.Item} */ (n
|
|
136
|
+
._item).next).content).type;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (n === null) {
|
|
142
|
+
throw new Error('Unexpected case');
|
|
143
|
+
}
|
|
144
|
+
if (pos === 0 && n.constructor !== Y.XmlText && n !== type) { // TODO: set to <= 0
|
|
145
|
+
return createRelativePosition(n._item.parent, n._item);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return Y.createRelativePositionFromTypeIndex(
|
|
149
|
+
type,
|
|
150
|
+
type._length,
|
|
151
|
+
type.length === 0 ? -1 : 0,
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const createRelativePosition = (type: Y.AbstractType<any>, item: Y.Item) => {
|
|
156
|
+
let typeid = null;
|
|
157
|
+
let tname = null;
|
|
158
|
+
if (type._item === null) {
|
|
159
|
+
tname = Y.findRootTypeKey(type);
|
|
160
|
+
} else {
|
|
161
|
+
typeid = Y.createID(type._item.id.client, type._item.id.clock);
|
|
162
|
+
}
|
|
163
|
+
return new Y.RelativePosition(typeid, tname, item.id);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const relativePositionToAbsolutePosition = (
|
|
167
|
+
yDoc: Y.Doc,
|
|
168
|
+
documentType: Y.XmlFragment,
|
|
169
|
+
relPos: any,
|
|
170
|
+
mapping: ProsemirrorMapping,
|
|
171
|
+
): null | number => {
|
|
172
|
+
const decodedPos = Y.createAbsolutePositionFromRelativePosition(relPos, yDoc);
|
|
173
|
+
if (
|
|
174
|
+
decodedPos === null ||
|
|
175
|
+
(decodedPos.type !== documentType &&
|
|
176
|
+
!Y.isParentOf(documentType, decodedPos.type._item))
|
|
177
|
+
) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
let type = decodedPos.type;
|
|
181
|
+
let pos = 0;
|
|
182
|
+
if (type instanceof Y.XmlText) {
|
|
183
|
+
pos = decodedPos.index;
|
|
184
|
+
} else if (type._item === null || !type._item.deleted) {
|
|
185
|
+
let n: Y.Item | null = type._first;
|
|
186
|
+
let i = 0;
|
|
187
|
+
while (i < type._length && i < decodedPos.index && n !== null) {
|
|
188
|
+
if (!n.deleted) {
|
|
189
|
+
const t: Y.AbstractType<any> = n.content.type;
|
|
190
|
+
i++;
|
|
191
|
+
if (t instanceof Y.XmlText) {
|
|
192
|
+
pos += t._length;
|
|
193
|
+
} else {
|
|
194
|
+
const node = mapping.get(t);
|
|
195
|
+
pos += node?.nodeSize || 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
n = n.right;
|
|
199
|
+
}
|
|
200
|
+
pos += 1; // increase because we go out of n
|
|
201
|
+
}
|
|
202
|
+
while (type !== documentType && type._item !== null) {
|
|
203
|
+
const parent = type._item.parent;
|
|
204
|
+
if (parent instanceof Y.ID || parent === null) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (parent._item === null || !parent._item.deleted) {
|
|
208
|
+
pos += 1; // the start tag
|
|
209
|
+
let n = /** @type {Y.AbstractType} */ (parent)._first;
|
|
210
|
+
// now iterate until we found type
|
|
211
|
+
while (n !== null) {
|
|
212
|
+
const contentType: Y.AbstractType<any> = n.content.type;
|
|
213
|
+
if (contentType === type) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
if (!n.deleted) {
|
|
217
|
+
if (contentType instanceof Y.XmlText) {
|
|
218
|
+
pos += contentType._length;
|
|
219
|
+
} else {
|
|
220
|
+
const node = mapping.get(contentType);
|
|
221
|
+
pos += node?.nodeSize || 0;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
n = n.right;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
type = parent;
|
|
228
|
+
}
|
|
229
|
+
return pos - 1; // we don't count the most outer tag, because it is a fragment
|
|
230
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const userColors = [
|
|
2
|
+
{ color: '#30bced', light: '#30bced33' },
|
|
3
|
+
{ color: '#6eeb83', light: '#6eeb8333' },
|
|
4
|
+
{ color: '#ffbc42', light: '#ffbc4233' },
|
|
5
|
+
{ color: '#ecd444', light: '#ecd44433' },
|
|
6
|
+
{ color: '#ee6352', light: '#ee635233' },
|
|
7
|
+
{ color: '#9ac2c9', light: '#9ac2c933' },
|
|
8
|
+
{ color: '#8acb88', light: '#8acb8833' },
|
|
9
|
+
{ color: '#1be7ff', light: '#1be7ff33' },
|
|
10
|
+
];
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as sha256 from 'lib0/hash/sha256';
|
|
2
|
+
import * as buf from 'lib0/buffer';
|
|
3
|
+
|
|
4
|
+
const _convolute = (digest: Uint8Array) => {
|
|
5
|
+
const N = 6;
|
|
6
|
+
for (let i = N; i < digest.length; i++) {
|
|
7
|
+
digest[i % N] = digest[i % N] ^ digest[i];
|
|
8
|
+
}
|
|
9
|
+
return digest.slice(0, N);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const hashOfJSON = (json: any) =>
|
|
13
|
+
buf.toBase64(_convolute(sha256.digest(buf.encodeAny(json))));
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import * as awarenessProtocol from 'y-protocols/awareness';
|
|
3
|
+
|
|
4
|
+
import { EditorState, Plugin, PluginKey } from 'prosemirror-state';
|
|
5
|
+
|
|
6
|
+
import type { CoreEditor } from '@kerebron/editor';
|
|
7
|
+
import type {
|
|
8
|
+
ExtensionRemoteSelection,
|
|
9
|
+
SelectionState,
|
|
10
|
+
} from '@kerebron/extension-basic-editor/ExtensionRemoteSelection';
|
|
11
|
+
import { remoteSelectionPluginKey } from '@kerebron/extension-basic-editor/ExtensionRemoteSelection';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
absolutePositionToRelativePosition,
|
|
15
|
+
relativePositionToAbsolutePosition,
|
|
16
|
+
setMeta,
|
|
17
|
+
} from './lib.js';
|
|
18
|
+
import { ySyncPluginKey } from './keys.js';
|
|
19
|
+
|
|
20
|
+
export const yPositionPluginKey = new PluginKey('yjs-position');
|
|
21
|
+
|
|
22
|
+
type AwarenessListener = (
|
|
23
|
+
{ added, updated, removed }: {
|
|
24
|
+
added: number[];
|
|
25
|
+
updated: number[];
|
|
26
|
+
removed: number[];
|
|
27
|
+
},
|
|
28
|
+
s: any,
|
|
29
|
+
t: any,
|
|
30
|
+
) => void;
|
|
31
|
+
|
|
32
|
+
interface PositionPluginConfig {
|
|
33
|
+
getSelection?: (arg0: any) => any;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default awareness state filter
|
|
38
|
+
*/
|
|
39
|
+
export const defaultAwarenessStateFilter = (
|
|
40
|
+
currentClientId: number,
|
|
41
|
+
userClientId: number,
|
|
42
|
+
_user: any,
|
|
43
|
+
): boolean => currentClientId !== userClientId;
|
|
44
|
+
|
|
45
|
+
export const yPositionPlugin = (
|
|
46
|
+
awareness: awarenessProtocol.Awareness,
|
|
47
|
+
editor: CoreEditor,
|
|
48
|
+
{
|
|
49
|
+
getSelection = (state: EditorState) => state.selection,
|
|
50
|
+
}: PositionPluginConfig = {},
|
|
51
|
+
cursorStateField: string = 'cursor',
|
|
52
|
+
) => {
|
|
53
|
+
return new Plugin({
|
|
54
|
+
key: yPositionPluginKey,
|
|
55
|
+
view: (view) => {
|
|
56
|
+
const extension: ExtensionRemoteSelection = editor.getExtension(
|
|
57
|
+
'remote-selection',
|
|
58
|
+
)!;
|
|
59
|
+
|
|
60
|
+
const awarenessListener: AwarenessListener = (
|
|
61
|
+
{ added, updated, removed },
|
|
62
|
+
) => {
|
|
63
|
+
const clients = added.concat(updated).concat(removed);
|
|
64
|
+
if (
|
|
65
|
+
clients.findIndex((id: number) => id !== awareness.doc.clientID) ===
|
|
66
|
+
-1
|
|
67
|
+
) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (view.docView) {
|
|
72
|
+
setMeta(view, remoteSelectionPluginKey, {
|
|
73
|
+
remotePositionUpdated: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const remoteStates: SelectionState[] = [];
|
|
78
|
+
|
|
79
|
+
const ystate = ySyncPluginKey.getState(view.state);
|
|
80
|
+
const y = ystate.doc;
|
|
81
|
+
|
|
82
|
+
awareness.getStates().forEach((aw, clientId) => {
|
|
83
|
+
if (!defaultAwarenessStateFilter(y.clientID, clientId, aw)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!aw.cursor) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let anchor = relativePositionToAbsolutePosition(
|
|
92
|
+
y,
|
|
93
|
+
ystate.type,
|
|
94
|
+
Y.createRelativePositionFromJSON(aw.cursor.anchor),
|
|
95
|
+
ystate.binding.mapping,
|
|
96
|
+
);
|
|
97
|
+
let head = relativePositionToAbsolutePosition(
|
|
98
|
+
y,
|
|
99
|
+
ystate.type,
|
|
100
|
+
Y.createRelativePositionFromJSON(aw.cursor.head),
|
|
101
|
+
ystate.binding.mapping,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (anchor !== null && head !== null) {
|
|
105
|
+
remoteStates.push({
|
|
106
|
+
clientId,
|
|
107
|
+
user: {
|
|
108
|
+
name: aw.user?.name,
|
|
109
|
+
color: aw.user?.color,
|
|
110
|
+
colorLight: aw.user?.colorLight,
|
|
111
|
+
},
|
|
112
|
+
cursor: {
|
|
113
|
+
anchor,
|
|
114
|
+
head,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
extension.setRemoteStates(remoteStates);
|
|
121
|
+
// view.dispatch({ annotations: [yRemoteSelectionsAnnotation.of([])] });
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
{
|
|
125
|
+
// if (
|
|
126
|
+
// ystate.snapshot != null || ystate.prevSnapshot != null ||
|
|
127
|
+
// ystate.binding.mapping.size === 0
|
|
128
|
+
// ) {
|
|
129
|
+
// // do not render cursors while snapshot is active
|
|
130
|
+
// return DecorationSet.create(state.doc, []);
|
|
131
|
+
// }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const updateAwareness = (
|
|
135
|
+
selectionAnchor: number,
|
|
136
|
+
selectionHead: number,
|
|
137
|
+
) => {
|
|
138
|
+
const ystate = ySyncPluginKey.getState(view.state);
|
|
139
|
+
const current = awareness.getLocalState() || {};
|
|
140
|
+
|
|
141
|
+
const anchor: Y.RelativePosition = absolutePositionToRelativePosition(
|
|
142
|
+
selectionAnchor,
|
|
143
|
+
ystate.type,
|
|
144
|
+
ystate.binding.mapping,
|
|
145
|
+
);
|
|
146
|
+
const head: Y.RelativePosition = absolutePositionToRelativePosition(
|
|
147
|
+
selectionHead,
|
|
148
|
+
ystate.type,
|
|
149
|
+
ystate.binding.mapping,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
current.cursor == null ||
|
|
154
|
+
!Y.compareRelativePositions(
|
|
155
|
+
Y.createRelativePositionFromJSON(current.cursor.anchor),
|
|
156
|
+
anchor,
|
|
157
|
+
) ||
|
|
158
|
+
!Y.compareRelativePositions(
|
|
159
|
+
Y.createRelativePositionFromJSON(current.cursor.head),
|
|
160
|
+
head,
|
|
161
|
+
)
|
|
162
|
+
) {
|
|
163
|
+
awareness.setLocalStateField(cursorStateField, {
|
|
164
|
+
anchor,
|
|
165
|
+
head,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const clearAwareness = () => {
|
|
171
|
+
const ystate = ySyncPluginKey.getState(view.state);
|
|
172
|
+
const current = awareness.getLocalState() || {};
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
current.cursor != null &&
|
|
176
|
+
relativePositionToAbsolutePosition(
|
|
177
|
+
ystate.doc,
|
|
178
|
+
ystate.type,
|
|
179
|
+
Y.createRelativePositionFromJSON(current.cursor.anchor),
|
|
180
|
+
ystate.binding.mapping,
|
|
181
|
+
) !== null
|
|
182
|
+
) {
|
|
183
|
+
// delete cursor information if current cursor information is owned by this editor binding
|
|
184
|
+
awareness.setLocalStateField(cursorStateField, null);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const updateCursorInfo = () => {
|
|
189
|
+
if (view.hasFocus()) {
|
|
190
|
+
const selection = getSelection(view.state);
|
|
191
|
+
updateAwareness(selection.anchor, selection.head);
|
|
192
|
+
} else {
|
|
193
|
+
// clearAwareness();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const localPositionChangedListener = (event: CustomEvent) => {
|
|
198
|
+
const { detail } = event;
|
|
199
|
+
updateAwareness(detail.anchor, detail.head);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
editor.addEventListener(
|
|
203
|
+
'localPositionChanged',
|
|
204
|
+
localPositionChangedListener,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
awareness.on('change', awarenessListener);
|
|
208
|
+
view.dom.addEventListener('focusin', updateCursorInfo);
|
|
209
|
+
view.dom.addEventListener('focusout', updateCursorInfo);
|
|
210
|
+
return {
|
|
211
|
+
update: updateCursorInfo,
|
|
212
|
+
destroy: () => {
|
|
213
|
+
view.dom.removeEventListener('focusin', updateCursorInfo);
|
|
214
|
+
view.dom.removeEventListener('focusout', updateCursorInfo);
|
|
215
|
+
awareness.off('change', awarenessListener);
|
|
216
|
+
awareness.setLocalStateField(cursorStateField, null);
|
|
217
|
+
editor.removeEventListener(
|
|
218
|
+
'localPositionChanged',
|
|
219
|
+
localPositionChangedListener,
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
updateCursorInfo(state: EditorState) {
|
|
225
|
+
throw new Error('TODO: merge with updateCursorInfo above');
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
};
|