@jupyter/collaboration 5.0.3 → 5.0.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/lib/cursors.d.ts +7 -0
- package/lib/cursors.js +235 -18
- package/package.json +1 -1
package/lib/cursors.d.ts
CHANGED
|
@@ -54,6 +54,13 @@ export interface IAwarenessState extends Record<string, any> {
|
|
|
54
54
|
* Facet storing the Yjs document objects
|
|
55
55
|
*/
|
|
56
56
|
export declare const editorAwarenessFacet: Facet<EditorAwareness, EditorAwareness>;
|
|
57
|
+
/**
|
|
58
|
+
* Build the avatar and name badge shown for a collaborator.
|
|
59
|
+
*
|
|
60
|
+
* @param user The collaborator identity, if known
|
|
61
|
+
* @returns The badge element
|
|
62
|
+
*/
|
|
63
|
+
export declare function collaboratorPill(user: User.IIdentity | undefined): HTMLDivElement;
|
|
57
64
|
/**
|
|
58
65
|
* CodeMirror extension to display remote users cursors
|
|
59
66
|
*
|
package/lib/cursors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Jupyter Development Team.
|
|
2
2
|
// Distributed under the terms of the Modified BSD License.
|
|
3
|
-
import { Annotation, EditorSelection, Facet } from '@codemirror/state';
|
|
4
|
-
import { EditorView, hoverTooltip, layer, RectangleMarker, tooltips, ViewPlugin } from '@codemirror/view';
|
|
3
|
+
import { Annotation, EditorSelection, Facet, StateField } from '@codemirror/state';
|
|
4
|
+
import { EditorView, hoverTooltip, layer, RectangleMarker, showTooltip, tooltips, ViewPlugin } from '@codemirror/view';
|
|
5
5
|
import { JSONExt } from '@lumino/coreutils';
|
|
6
6
|
import { createAbsolutePositionFromRelativePosition, createRelativePositionFromJSON, createRelativePositionFromTypeIndex } from 'yjs';
|
|
7
7
|
/**
|
|
@@ -18,7 +18,8 @@ export const editorAwarenessFacet = Facet.define({
|
|
|
18
18
|
const remoteSelectionTheme = EditorView.baseTheme({
|
|
19
19
|
'.jp-remote-cursor': {
|
|
20
20
|
borderLeft: '1px solid black',
|
|
21
|
-
marginLeft: '-1px'
|
|
21
|
+
marginLeft: '-1px',
|
|
22
|
+
pointerEvents: 'none'
|
|
22
23
|
},
|
|
23
24
|
'.jp-remote-cursor.jp-mod-primary': {
|
|
24
25
|
borderLeftWidth: '2px'
|
|
@@ -29,13 +30,205 @@ const remoteSelectionTheme = EditorView.baseTheme({
|
|
|
29
30
|
'.cm-tooltip': {
|
|
30
31
|
border: 'none'
|
|
31
32
|
},
|
|
32
|
-
'.
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
'.jp-remote-userFlag': {
|
|
34
|
+
display: 'flex',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
gap: '4px',
|
|
37
|
+
maxWidth: '140px',
|
|
38
|
+
padding: '2px 6px 2px 2px',
|
|
39
|
+
background: 'var(--jp-layout-color1)',
|
|
40
|
+
color: 'var(--jp-ui-font-color1)',
|
|
41
|
+
border: '1px solid',
|
|
42
|
+
borderRadius: '10px',
|
|
43
|
+
fontSize: '11px',
|
|
44
|
+
lineHeight: '1.2',
|
|
45
|
+
whiteSpace: 'nowrap',
|
|
46
|
+
boxShadow: 'var(--jp-elevation-z2)',
|
|
47
|
+
pointerEvents: 'none',
|
|
48
|
+
userSelect: 'none',
|
|
49
|
+
opacity: '1',
|
|
50
|
+
transition: 'opacity 300ms ease'
|
|
51
|
+
},
|
|
52
|
+
'.jp-remote-userFlag.jp-mod-idle': {
|
|
53
|
+
opacity: '0'
|
|
54
|
+
},
|
|
55
|
+
'.cm-tooltip.jp-remote-userFlag-host': {
|
|
56
|
+
background: 'none'
|
|
57
|
+
},
|
|
58
|
+
'.jp-remote-userFlag-avatar': {
|
|
59
|
+
width: '16px',
|
|
60
|
+
height: '16px',
|
|
61
|
+
flex: '0 0 auto',
|
|
62
|
+
display: 'flex',
|
|
63
|
+
alignItems: 'center',
|
|
64
|
+
justifyContent: 'center',
|
|
65
|
+
overflow: 'hidden',
|
|
66
|
+
borderRadius: '100%',
|
|
67
|
+
color: 'var(--jp-ui-inverse-font-color1)',
|
|
68
|
+
fontSize: '8px'
|
|
69
|
+
},
|
|
70
|
+
'.jp-remote-userFlag-avatar img': {
|
|
71
|
+
width: '100%',
|
|
72
|
+
height: '100%',
|
|
73
|
+
objectFit: 'cover'
|
|
74
|
+
},
|
|
75
|
+
'.jp-remote-userFlag-name': {
|
|
76
|
+
overflow: 'hidden',
|
|
77
|
+
textOverflow: 'ellipsis',
|
|
78
|
+
whiteSpace: 'nowrap'
|
|
35
79
|
}
|
|
36
80
|
});
|
|
37
81
|
// TODO fix which user needs update
|
|
38
82
|
const remoteSelectionsAnnotation = Annotation.define();
|
|
83
|
+
/**
|
|
84
|
+
* How long a collaborator flag stays visible after their last edit.
|
|
85
|
+
*/
|
|
86
|
+
const FLAG_IDLE_MS = 2000;
|
|
87
|
+
const collaborators = new WeakMap();
|
|
88
|
+
function collaborator(awareness, clientID) {
|
|
89
|
+
let byClient = collaborators.get(awareness);
|
|
90
|
+
if (!byClient) {
|
|
91
|
+
byClient = new Map();
|
|
92
|
+
collaborators.set(awareness, byClient);
|
|
93
|
+
}
|
|
94
|
+
let entry = byClient.get(clientID);
|
|
95
|
+
if (!entry) {
|
|
96
|
+
entry = { at: 0 };
|
|
97
|
+
byClient.set(clientID, entry);
|
|
98
|
+
}
|
|
99
|
+
return entry;
|
|
100
|
+
}
|
|
101
|
+
function markActive(awareness, clientIDs) {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
for (const clientID of clientIDs) {
|
|
104
|
+
collaborator(awareness, clientID).at = now;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function isFlagVisible(awareness, clientID) {
|
|
108
|
+
return Date.now() - collaborator(awareness, clientID).at < FLAG_IDLE_MS;
|
|
109
|
+
}
|
|
110
|
+
const editTrackedDocs = new WeakSet();
|
|
111
|
+
/**
|
|
112
|
+
* Mark edit authors from the Yjs clocks; awareness misses end-of-line typing.
|
|
113
|
+
*/
|
|
114
|
+
function trackRemoteEdits(awareness, ydoc) {
|
|
115
|
+
if (editTrackedDocs.has(ydoc)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
editTrackedDocs.add(ydoc);
|
|
119
|
+
// Before the observers, so the flag is up when the edit reaches the editor.
|
|
120
|
+
ydoc.on('beforeObserverCalls', (tr) => {
|
|
121
|
+
const authors = [];
|
|
122
|
+
tr.afterState.forEach((clock, clientID) => {
|
|
123
|
+
if (clientID !== ydoc.clientID &&
|
|
124
|
+
tr.beforeState.get(clientID) !== clock) {
|
|
125
|
+
authors.push(clientID);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
if (authors.length > 0) {
|
|
129
|
+
markActive(awareness, authors);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build the avatar and name badge shown for a collaborator.
|
|
135
|
+
*
|
|
136
|
+
* @param user The collaborator identity, if known
|
|
137
|
+
* @returns The badge element
|
|
138
|
+
*/
|
|
139
|
+
export function collaboratorPill(user) {
|
|
140
|
+
const dom = document.createElement('div');
|
|
141
|
+
dom.className = 'jp-remote-userFlag';
|
|
142
|
+
renderPill(dom, user);
|
|
143
|
+
return dom;
|
|
144
|
+
}
|
|
145
|
+
function renderPill(dom, user) {
|
|
146
|
+
var _a, _b, _c, _d;
|
|
147
|
+
dom.replaceChildren();
|
|
148
|
+
dom.style.borderColor = (_a = user === null || user === void 0 ? void 0 : user.color) !== null && _a !== void 0 ? _a : 'darkgrey';
|
|
149
|
+
const avatar = document.createElement('div');
|
|
150
|
+
avatar.className = 'jp-remote-userFlag-avatar';
|
|
151
|
+
if (user === null || user === void 0 ? void 0 : user.avatar_url) {
|
|
152
|
+
const img = document.createElement('img');
|
|
153
|
+
img.src = user.avatar_url;
|
|
154
|
+
img.alt = '';
|
|
155
|
+
img.onerror = () => {
|
|
156
|
+
var _a, _b;
|
|
157
|
+
avatar.style.backgroundColor = (_a = user.color) !== null && _a !== void 0 ? _a : 'darkgrey';
|
|
158
|
+
avatar.textContent = (_b = user.initials) !== null && _b !== void 0 ? _b : '';
|
|
159
|
+
};
|
|
160
|
+
avatar.appendChild(img);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
avatar.style.backgroundColor = (_b = user === null || user === void 0 ? void 0 : user.color) !== null && _b !== void 0 ? _b : 'darkgrey';
|
|
164
|
+
avatar.textContent = (_c = user === null || user === void 0 ? void 0 : user.initials) !== null && _c !== void 0 ? _c : '';
|
|
165
|
+
}
|
|
166
|
+
const name = document.createElement('span');
|
|
167
|
+
name.className = 'jp-remote-userFlag-name';
|
|
168
|
+
name.textContent = (_d = user === null || user === void 0 ? void 0 : user.display_name) !== null && _d !== void 0 ? _d : 'Anonymous';
|
|
169
|
+
dom.append(avatar, name);
|
|
170
|
+
}
|
|
171
|
+
function drawFlag(awareness, clientID) {
|
|
172
|
+
const entry = collaborator(awareness, clientID);
|
|
173
|
+
const dom = collaboratorPill(entry.user);
|
|
174
|
+
let shown = entry.user;
|
|
175
|
+
const sync = () => {
|
|
176
|
+
if (!JSONExt.deepEqual({ ...shown }, { ...entry.user })) {
|
|
177
|
+
shown = entry.user;
|
|
178
|
+
renderPill(dom, shown);
|
|
179
|
+
}
|
|
180
|
+
dom.classList.toggle('jp-mod-idle', !isFlagVisible(awareness, clientID));
|
|
181
|
+
};
|
|
182
|
+
sync();
|
|
183
|
+
return { dom, update: sync };
|
|
184
|
+
}
|
|
185
|
+
function flagCreator(awareness, clientID) {
|
|
186
|
+
var _a;
|
|
187
|
+
const entry = collaborator(awareness, clientID);
|
|
188
|
+
return ((_a = entry.create) !== null && _a !== void 0 ? _a : (entry.create = () => drawFlag(awareness, clientID)));
|
|
189
|
+
}
|
|
190
|
+
function collaboratorFlags(state) {
|
|
191
|
+
const { awareness, ytext } = state.facet(editorAwarenessFacet);
|
|
192
|
+
const ydoc = ytext.doc;
|
|
193
|
+
if (!ydoc) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
const flags = [];
|
|
197
|
+
awareness.getStates().forEach((remote, clientID) => {
|
|
198
|
+
var _a;
|
|
199
|
+
if (clientID === awareness.doc.clientID) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const cursor = (_a = remote.cursors) === null || _a === void 0 ? void 0 : _a.find(c => { var _a; return (_a = c.primary) !== null && _a !== void 0 ? _a : true; });
|
|
203
|
+
if (!(cursor === null || cursor === void 0 ? void 0 : cursor.head)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const head = createAbsolutePositionFromRelativePosition(cursor.head, ydoc);
|
|
207
|
+
if ((head === null || head === void 0 ? void 0 : head.type) !== ytext) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
collaborator(awareness, clientID).user = remote.user;
|
|
211
|
+
flags.push({
|
|
212
|
+
pos: Math.min(head.index, state.doc.length),
|
|
213
|
+
above: true,
|
|
214
|
+
create: flagCreator(awareness, clientID)
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
return flags;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Extension showing a fading name flag above each remote cursor
|
|
221
|
+
*/
|
|
222
|
+
const remoteCursorFlags = StateField.define({
|
|
223
|
+
create: collaboratorFlags,
|
|
224
|
+
update(flags, tr) {
|
|
225
|
+
return tr.docChanged ||
|
|
226
|
+
tr.annotation(remoteSelectionsAnnotation) !== undefined
|
|
227
|
+
? collaboratorFlags(tr.state)
|
|
228
|
+
: flags;
|
|
229
|
+
},
|
|
230
|
+
provide: field => showTooltip.computeN([field], state => state.field(field))
|
|
231
|
+
});
|
|
39
232
|
/**
|
|
40
233
|
* Wrapper around RectangleMarker to be able to set the user color for the remote cursor and selection ranges.
|
|
41
234
|
*/
|
|
@@ -127,6 +320,9 @@ const userHover = hoverTooltip((view, pos) => {
|
|
|
127
320
|
if (clientID === awareness.doc.clientID) {
|
|
128
321
|
continue;
|
|
129
322
|
}
|
|
323
|
+
if (isFlagVisible(awareness, clientID)) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
130
326
|
for (const cursor of (_a = state.cursors) !== null && _a !== void 0 ? _a : []) {
|
|
131
327
|
if (!(cursor === null || cursor === void 0 ? void 0 : cursor.head)) {
|
|
132
328
|
continue;
|
|
@@ -136,18 +332,18 @@ const userHover = hoverTooltip((view, pos) => {
|
|
|
136
332
|
continue;
|
|
137
333
|
}
|
|
138
334
|
// Use some margin around the cursor to display the user.
|
|
139
|
-
|
|
335
|
+
const index = Math.min(head.index, view.state.doc.length);
|
|
336
|
+
if (index - 3 <= pos && pos <= index + 3) {
|
|
140
337
|
return {
|
|
141
|
-
pos:
|
|
338
|
+
pos: index,
|
|
142
339
|
above: true,
|
|
143
340
|
create: () => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return { dom };
|
|
341
|
+
const dom = collaboratorPill(state.user);
|
|
342
|
+
return {
|
|
343
|
+
dom,
|
|
344
|
+
overlap: true,
|
|
345
|
+
mount: () => { var _a; return (_a = dom.parentElement) === null || _a === void 0 ? void 0 : _a.classList.add('jp-remote-userFlag-host'); }
|
|
346
|
+
};
|
|
151
347
|
}
|
|
152
348
|
};
|
|
153
349
|
}
|
|
@@ -156,7 +352,7 @@ const userHover = hoverTooltip((view, pos) => {
|
|
|
156
352
|
return null;
|
|
157
353
|
}, {
|
|
158
354
|
hideOn: (tr, tooltip) => !!tr.annotation(remoteSelectionsAnnotation),
|
|
159
|
-
hoverTime:
|
|
355
|
+
hoverTime: 1
|
|
160
356
|
});
|
|
161
357
|
/**
|
|
162
358
|
* Extension defining a new editor layer storing the remote selections
|
|
@@ -206,15 +402,32 @@ const showCollaborators = ViewPlugin.fromClass(class {
|
|
|
206
402
|
constructor(view) {
|
|
207
403
|
this.editorAwareness = view.state.facet(editorAwarenessFacet);
|
|
208
404
|
this._listener = ({ added, updated, removed }) => {
|
|
209
|
-
const
|
|
210
|
-
|
|
405
|
+
const { awareness } = this.editorAwareness;
|
|
406
|
+
const clients = added
|
|
407
|
+
.concat(updated)
|
|
408
|
+
.concat(removed)
|
|
409
|
+
.filter(id => id !== awareness.doc.clientID);
|
|
410
|
+
if (clients.length > 0) {
|
|
411
|
+
markActive(awareness, clients);
|
|
211
412
|
// Trick to get the remoteCursorLayers to be updated
|
|
212
413
|
view.dispatch({ annotations: [remoteSelectionsAnnotation.of([])] });
|
|
414
|
+
this.scheduleFade(view);
|
|
213
415
|
}
|
|
214
416
|
};
|
|
417
|
+
const ydoc = this.editorAwareness.ytext.doc;
|
|
418
|
+
if (ydoc) {
|
|
419
|
+
trackRemoteEdits(this.editorAwareness.awareness, ydoc);
|
|
420
|
+
}
|
|
215
421
|
this.editorAwareness.awareness.on('change', this._listener);
|
|
216
422
|
}
|
|
423
|
+
scheduleFade(view) {
|
|
424
|
+
clearTimeout(this._fadeTimer);
|
|
425
|
+
this._fadeTimer = setTimeout(() => {
|
|
426
|
+
view.dispatch({ annotations: [remoteSelectionsAnnotation.of([])] });
|
|
427
|
+
}, FLAG_IDLE_MS + 50);
|
|
428
|
+
}
|
|
217
429
|
destroy() {
|
|
430
|
+
clearTimeout(this._fadeTimer);
|
|
218
431
|
this.editorAwareness.awareness.off('change', this._listener);
|
|
219
432
|
}
|
|
220
433
|
/**
|
|
@@ -222,6 +435,9 @@ const showCollaborators = ViewPlugin.fromClass(class {
|
|
|
222
435
|
*/
|
|
223
436
|
update(update) {
|
|
224
437
|
var _a;
|
|
438
|
+
if (update.docChanged) {
|
|
439
|
+
this.scheduleFade(update.view);
|
|
440
|
+
}
|
|
225
441
|
if (!update.docChanged && !update.selectionSet) {
|
|
226
442
|
return;
|
|
227
443
|
}
|
|
@@ -270,6 +486,7 @@ const showCollaborators = ViewPlugin.fromClass(class {
|
|
|
270
486
|
remoteSelectionTheme,
|
|
271
487
|
remoteCursorsLayer,
|
|
272
488
|
remoteSelectionLayer,
|
|
489
|
+
remoteCursorFlags,
|
|
273
490
|
userHover,
|
|
274
491
|
// As we use relative positioning of widget, the tooltip must be positioned absolutely
|
|
275
492
|
// And we attach the tooltip to the body to avoid overflow rules
|