@cocreate/cursors 1.30.0 → 2.0.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/.github/FUNDING.yml +1 -1
- package/.github/workflows/automated.yml +18 -7
- package/CHANGELOG.md +602 -569
- package/CONTRIBUTING.md +6 -6
- package/README.md +17 -21
- package/demo/index.html +1 -4
- package/docs/index.html +5 -5
- package/package.json +13 -30
- package/src/index.js +227 -573
- package/webpack.config.js +2 -13
- package/src/index.css +0 -54
package/src/index.js
CHANGED
|
@@ -1,573 +1,227 @@
|
|
|
1
|
-
|
|
2
|
-
* Copyright (C) 2023 CoCreate and Contributors.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
for (const [prop, value] of Object.entries(actualStyles)) {
|
|
229
|
-
style[prop] = value;
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
let properties = [
|
|
233
|
-
"boxSizing",
|
|
234
|
-
"fontStyle",
|
|
235
|
-
"fontVariant",
|
|
236
|
-
"fontWeight",
|
|
237
|
-
"fontStretch",
|
|
238
|
-
"fontSize",
|
|
239
|
-
"fontFamily",
|
|
240
|
-
"letterSpacing",
|
|
241
|
-
"lineHeight",
|
|
242
|
-
"textAlign",
|
|
243
|
-
"textTransform",
|
|
244
|
-
"textIndent",
|
|
245
|
-
"textDecoration",
|
|
246
|
-
"textRendering",
|
|
247
|
-
"textTransform",
|
|
248
|
-
"textIndent",
|
|
249
|
-
"overflowWrap",
|
|
250
|
-
"tabSize",
|
|
251
|
-
"webkitWritingMode",
|
|
252
|
-
"whiteSpace",
|
|
253
|
-
"wordSpacing"
|
|
254
|
-
];
|
|
255
|
-
properties.forEach(function (prop) {
|
|
256
|
-
style[prop] = computed[prop];
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
let paddingBottom = parseFloat(
|
|
260
|
-
computed["padding-bottom"].replace("px", "")
|
|
261
|
-
);
|
|
262
|
-
style.height = rect.height - paddingBottom + "px";
|
|
263
|
-
|
|
264
|
-
if (element.nodeName === "INPUT") style.whiteSpace = "pre";
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
let cursor, cursorFlag;
|
|
268
|
-
let details = { array, object, key };
|
|
269
|
-
if (socket_id) {
|
|
270
|
-
let selection_user = mirrorDiv.querySelector(
|
|
271
|
-
`selection[socket_id="${socket_id}"]`
|
|
272
|
-
);
|
|
273
|
-
if (selection_user && selection_user.details != details)
|
|
274
|
-
selection_user.remove();
|
|
275
|
-
|
|
276
|
-
selection_user = document.createElement("selection");
|
|
277
|
-
selection_user.setAttribute("socket_id", socket_id);
|
|
278
|
-
selection_user.style["position"] = "absolute";
|
|
279
|
-
let style_mirror = getComputedStyle(mirrorDiv);
|
|
280
|
-
selection_user.style["top"] = style_mirror.paddingTop;
|
|
281
|
-
selection_user.style["left"] = style_mirror.paddingLeft;
|
|
282
|
-
selection_user.style["width"] =
|
|
283
|
-
mirrorDiv.clientWidth -
|
|
284
|
-
parseInt(computed["padding-left"]) -
|
|
285
|
-
parseInt(computed["padding-right"]) -
|
|
286
|
-
scrollBarWidth +
|
|
287
|
-
"px";
|
|
288
|
-
selection_user.details = { array, object, key };
|
|
289
|
-
|
|
290
|
-
let value_element =
|
|
291
|
-
["TEXTAREA", "INPUT"].indexOf(element.nodeName) == -1
|
|
292
|
-
? element.innerHTML
|
|
293
|
-
: element.value;
|
|
294
|
-
if (contenteditable) {
|
|
295
|
-
selection_user.innerHTML = value_element.substring(0, start);
|
|
296
|
-
} else
|
|
297
|
-
selection_user.textContent = value_element.substring(0, start);
|
|
298
|
-
|
|
299
|
-
let selectionElement;
|
|
300
|
-
if (contenteditable) {
|
|
301
|
-
selectionElement = getElementPosition(
|
|
302
|
-
domTextEditor.htmlString,
|
|
303
|
-
selection.start,
|
|
304
|
-
selection.end
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
let lastChildTagName, lastChildElement;
|
|
309
|
-
if (
|
|
310
|
-
selectionElement &&
|
|
311
|
-
selectionElement.element &&
|
|
312
|
-
selectionElement.position !== "afterend"
|
|
313
|
-
) {
|
|
314
|
-
lastChildElement = selection_user.lastElementChild;
|
|
315
|
-
if (lastChildElement)
|
|
316
|
-
lastChildTagName = lastChildElement.tagName.toLowerCase();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
let selectedText = document.createElement("selected-text");
|
|
320
|
-
selectedText.style["backgroundColor"] = selection.background;
|
|
321
|
-
|
|
322
|
-
let setCursorInLastChild = false;
|
|
323
|
-
if (contenteditable) {
|
|
324
|
-
if (lastChildTagName && start !== end) {
|
|
325
|
-
let selectedTextFrag =
|
|
326
|
-
document.createElement("selected-text");
|
|
327
|
-
selectedTextFrag.style["backgroundColor"] =
|
|
328
|
-
selection.background;
|
|
329
|
-
|
|
330
|
-
const closingTagRegex = new RegExp(
|
|
331
|
-
"</\\s*" + lastChildTagName + "\\s*>",
|
|
332
|
-
"i"
|
|
333
|
-
);
|
|
334
|
-
let secondHalfString =
|
|
335
|
-
value_element.substring(start, end) || "";
|
|
336
|
-
const closingTagMatch =
|
|
337
|
-
secondHalfString.match(closingTagRegex);
|
|
338
|
-
// console.log('secondHalfString', secondHalfString)
|
|
339
|
-
|
|
340
|
-
let closingTagEnd;
|
|
341
|
-
if (closingTagMatch) {
|
|
342
|
-
closingTagEnd =
|
|
343
|
-
closingTagMatch.index + closingTagMatch[0].length;
|
|
344
|
-
// console.log('closingTagEnd', closingTagEnd)
|
|
345
|
-
|
|
346
|
-
selectedTextFrag.innerHTML =
|
|
347
|
-
secondHalfString.substring(0, closingTagEnd) || "";
|
|
348
|
-
lastChildElement.appendChild(selectedTextFrag);
|
|
349
|
-
// console.log('selectedTextFrag', selectedTextFrag)
|
|
350
|
-
|
|
351
|
-
selectedText.innerHTML =
|
|
352
|
-
value_element.substring(
|
|
353
|
-
start + closingTagEnd,
|
|
354
|
-
end
|
|
355
|
-
) || "";
|
|
356
|
-
// console.log('selectedText', selectedText)
|
|
357
|
-
} else {
|
|
358
|
-
selectedText.innerHTML =
|
|
359
|
-
value_element.substring(start, end) || "";
|
|
360
|
-
setCursorInLastChild = true;
|
|
361
|
-
}
|
|
362
|
-
} else {
|
|
363
|
-
if (lastChildTagName) setCursorInLastChild = true;
|
|
364
|
-
|
|
365
|
-
selectedText.innerHTML =
|
|
366
|
-
value_element.substring(start, end) || "";
|
|
367
|
-
}
|
|
368
|
-
} else {
|
|
369
|
-
selectedText.textContent =
|
|
370
|
-
value_element.substring(start, end) || "";
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
cursor = document.createElement("cursor");
|
|
374
|
-
cursor.setAttribute("socket_id", socket_id);
|
|
375
|
-
cursor.dataset.start = start;
|
|
376
|
-
cursor.dataset.end = end;
|
|
377
|
-
cursor.setAttribute("user-name", selection.userName);
|
|
378
|
-
cursor.setAttribute("user-background", selection.background);
|
|
379
|
-
cursor.setAttribute("user-color", selection.color);
|
|
380
|
-
cursor.style["background"] = selection.background;
|
|
381
|
-
cursor.details = { array, object, key };
|
|
382
|
-
cursor.textContent = " ";
|
|
383
|
-
|
|
384
|
-
cursorFlag = document.createElement("cursor-flag");
|
|
385
|
-
cursorFlag.setAttribute("array", "users");
|
|
386
|
-
cursorFlag.setAttribute("object", selection.user_id);
|
|
387
|
-
cursorFlag.setAttribute("key", "name");
|
|
388
|
-
cursorFlag.style["background"] = selection.background;
|
|
389
|
-
cursorFlag.innerHTML = selection.userName;
|
|
390
|
-
cursor.appendChild(cursorFlag);
|
|
391
|
-
|
|
392
|
-
if (setCursorInLastChild) {
|
|
393
|
-
lastChildElement.appendChild(selectedText);
|
|
394
|
-
lastChildElement.appendChild(cursor);
|
|
395
|
-
} else {
|
|
396
|
-
selection_user.appendChild(selectedText);
|
|
397
|
-
selection_user.appendChild(cursor);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
let span_end = document.createElement("span-end");
|
|
401
|
-
let value_end = value_element.substring(end) || "";
|
|
402
|
-
if (contenteditable) span_end.innerHTML = value_end;
|
|
403
|
-
else span_end.textContent = value_end;
|
|
404
|
-
|
|
405
|
-
selection_user.appendChild(span_end);
|
|
406
|
-
|
|
407
|
-
mirrorDiv.appendChild(selection_user);
|
|
408
|
-
}
|
|
409
|
-
scrollMirror(element);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function getActualStyles(element) {
|
|
414
|
-
const actualStyles = {};
|
|
415
|
-
|
|
416
|
-
// Copy styles that are directly applied to the element (inline styles)
|
|
417
|
-
for (const prop in element.style) {
|
|
418
|
-
if (element.style[prop]) {
|
|
419
|
-
actualStyles[prop] = element.style[prop];
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Get styles from classes, IDs, attributes, and tag names
|
|
424
|
-
for (const sheet of document.styleSheets) {
|
|
425
|
-
try {
|
|
426
|
-
for (const rule of sheet.cssRules) {
|
|
427
|
-
if (element.matches(rule.selectorText)) {
|
|
428
|
-
for (const prop of rule.style) {
|
|
429
|
-
actualStyles[prop] = rule.style.getPropertyValue(prop);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
} catch (e) {
|
|
434
|
-
console.warn("Unable to access stylesheet:", e);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return actualStyles;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function updateCursors(element) {
|
|
442
|
-
let mirrorDivs = [];
|
|
443
|
-
let id_mirror = element.getAttribute("mirror_id");
|
|
444
|
-
if (id_mirror) {
|
|
445
|
-
let mirrorDiv = element.ownerDocument.getElementById(id_mirror);
|
|
446
|
-
if (mirrorDiv) mirrorDivs = [mirrorDiv];
|
|
447
|
-
} else {
|
|
448
|
-
mirrorDivs = element.querySelectorAll(".mirror");
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
for (let mirrorDiv of mirrorDivs) {
|
|
452
|
-
element = mirrorDiv.element;
|
|
453
|
-
let cursors = mirrorDiv.querySelectorAll("cursor");
|
|
454
|
-
|
|
455
|
-
cursors.forEach(function (child_cursor, index, array) {
|
|
456
|
-
let socketId = child_cursor.getAttribute("socket_id");
|
|
457
|
-
let userName = child_cursor.getAttribute("user-name");
|
|
458
|
-
let background = child_cursor.getAttribute("user-background");
|
|
459
|
-
let color = child_cursor.getAttribute("user-color");
|
|
460
|
-
let start = child_cursor.getAttribute("data-start");
|
|
461
|
-
let end = child_cursor.getAttribute("data-end");
|
|
462
|
-
let selection = {
|
|
463
|
-
element: [element],
|
|
464
|
-
start,
|
|
465
|
-
end,
|
|
466
|
-
socketId,
|
|
467
|
-
userName,
|
|
468
|
-
background,
|
|
469
|
-
color
|
|
470
|
-
};
|
|
471
|
-
drawCursors(selection);
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function removeCursor(socketId) {
|
|
477
|
-
let elements = document.querySelectorAll(`cursor[socket_id="${socketId}"]`);
|
|
478
|
-
elements.forEach(function (element, index, array) {
|
|
479
|
-
element.parentNode.removeChild(element);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
let sel_elements = document.querySelectorAll(
|
|
483
|
-
`selection[socket_id="${socketId}"]`
|
|
484
|
-
);
|
|
485
|
-
sel_elements.forEach(function (sel_element, index, array) {
|
|
486
|
-
sel_element.parentNode.removeChild(sel_element);
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function _initEvents(element) {
|
|
491
|
-
element.addEventListener("keydown", function (event) {
|
|
492
|
-
scrollMirror(element);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
element.addEventListener("scroll", function () {
|
|
496
|
-
scrollMirror(element);
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
initResizeObserver(element);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function scrollMirror(element) {
|
|
503
|
-
let id_mirror = element.getAttribute("mirror_id");
|
|
504
|
-
let elementMirror = document.getElementById(id_mirror);
|
|
505
|
-
if (elementMirror)
|
|
506
|
-
elementMirror.scrollTo(element.scrollLeft, element.scrollTop);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function initResizeObserver(element) {
|
|
510
|
-
const watch = new ResizeObserver(() => updateCursors(element));
|
|
511
|
-
watch.observe(element);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function sendPosition(info) {
|
|
515
|
-
try {
|
|
516
|
-
socket.send({
|
|
517
|
-
method: "cursor",
|
|
518
|
-
data: {
|
|
519
|
-
array: info.array,
|
|
520
|
-
object: info.object,
|
|
521
|
-
key: info.key,
|
|
522
|
-
start: info.start,
|
|
523
|
-
end: info.end,
|
|
524
|
-
color: info.color || localStorage.getItem("cursorColor"),
|
|
525
|
-
background:
|
|
526
|
-
info.background ||
|
|
527
|
-
localStorage.getItem("cursorBackground") ||
|
|
528
|
-
cursorBackground,
|
|
529
|
-
userName:
|
|
530
|
-
info.userName ||
|
|
531
|
-
localStorage.getItem("userName") ||
|
|
532
|
-
clientId,
|
|
533
|
-
user_id:
|
|
534
|
-
info.user_id || localStorage.getItem("user_id") || clientId
|
|
535
|
-
},
|
|
536
|
-
broadcastClient: true
|
|
537
|
-
});
|
|
538
|
-
} catch (e) {
|
|
539
|
-
console.error(e);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
socket.listen("cursor", function (response) {
|
|
544
|
-
// if (socket.has(selection.socketId)) return;
|
|
545
|
-
let selection = response.data;
|
|
546
|
-
selection.socketId = response.socketId;
|
|
547
|
-
selection.frameId = response.frameId;
|
|
548
|
-
if (selection.start != null && selection.end != null)
|
|
549
|
-
drawCursors(selection);
|
|
550
|
-
else removeCursor(selection.socketId);
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
observer.init({
|
|
554
|
-
name: "CoCreateCursor",
|
|
555
|
-
types: ["addedNodes"],
|
|
556
|
-
selector,
|
|
557
|
-
callback: function (mutation) {
|
|
558
|
-
initElement(mutation.target);
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
observer.init({
|
|
563
|
-
name: "CoCreateCursorAtt",
|
|
564
|
-
types: ["attributes"],
|
|
565
|
-
attributeFilter: getAttributeNames(["array", "object", "key", "crdt"]),
|
|
566
|
-
callback: function (mutation) {
|
|
567
|
-
initElement(mutation.target);
|
|
568
|
-
}
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
init();
|
|
572
|
-
|
|
573
|
-
export default { drawCursors, updateCursors, removeCursor, sendPosition };
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2023 CoCreate and Contributors.
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
* Commercial licensing information: https://CoCreateJS.com/licenses/
|
|
5
|
+
*/
|
|
6
|
+
import { getSelectionRects } from "@cocreate/utils/selection";
|
|
7
|
+
|
|
8
|
+
const editors = new Set();
|
|
9
|
+
const sameField = (a, b) => ["array", "object", "key"].every(name => a[name] === b[name]);
|
|
10
|
+
const sender = data => JSON.stringify([data.clientId, data.frameId]);
|
|
11
|
+
const box = (a, b) => {
|
|
12
|
+
const left = Math.max(a.left, b.left), top = Math.max(a.top, b.top);
|
|
13
|
+
const right = Math.min(a.left + a.width, b.right);
|
|
14
|
+
const bottom = Math.min(a.top + a.height, b.bottom);
|
|
15
|
+
return right >= left && bottom > top
|
|
16
|
+
? { left, top, width: right - left, height: bottom - top } : null;
|
|
17
|
+
};
|
|
18
|
+
function mark(doc, root, rect, color, caret = false, name = "") {
|
|
19
|
+
if (!rect) return;
|
|
20
|
+
const node = doc.createElement("div");
|
|
21
|
+
Object.assign(node.style, {
|
|
22
|
+
position: "fixed", left: `${rect.left}px`, top: `${rect.top}px`,
|
|
23
|
+
width: `${caret ? 2 : rect.width}px`, height: `${rect.height}px`,
|
|
24
|
+
backgroundColor: color, opacity: caret ? "1" : "0.22"
|
|
25
|
+
});
|
|
26
|
+
if (caret && name) {
|
|
27
|
+
const label = doc.createElement("span");
|
|
28
|
+
label.textContent = name;
|
|
29
|
+
Object.assign(label.style, {
|
|
30
|
+
position: "absolute", bottom: "100%", left: "0", padding: "2px 4px",
|
|
31
|
+
font: "11px sans-serif", whiteSpace: "nowrap", color: "white",
|
|
32
|
+
backgroundColor: color, borderRadius: "2px"
|
|
33
|
+
});
|
|
34
|
+
node.append(label);
|
|
35
|
+
}
|
|
36
|
+
root.append(node);
|
|
37
|
+
}
|
|
38
|
+
function draw(record) {
|
|
39
|
+
const { element, root, positions } = record;
|
|
40
|
+
root.replaceChildren();
|
|
41
|
+
if (!element.isConnected || element.getAttribute("crdt") !== "true" ||
|
|
42
|
+
element.getAttribute("cursors") === "false") return;
|
|
43
|
+
const snapshot = record.getSelections?.();
|
|
44
|
+
if (snapshot) {
|
|
45
|
+
positions.clear();
|
|
46
|
+
for (const entry of snapshot)
|
|
47
|
+
if (entry.socketId && entry.resolved) positions.set(sender(entry), entry);
|
|
48
|
+
}
|
|
49
|
+
const doc = element.ownerDocument, field = element.getBoundingClientRect();
|
|
50
|
+
const viewport = { left: 0, top: 0, right: doc.documentElement.clientWidth,
|
|
51
|
+
bottom: doc.documentElement.clientHeight };
|
|
52
|
+
let clip = box({ left: field.left, top: field.top, width: field.width, height: field.height }, viewport);
|
|
53
|
+
if (!clip) return;
|
|
54
|
+
clip = { ...clip, right: clip.left + clip.width, bottom: clip.top + clip.height };
|
|
55
|
+
for (let parent = element.parentElement; parent; parent = parent.parentElement) {
|
|
56
|
+
const style = doc.defaultView.getComputedStyle(parent), bounds = parent.getBoundingClientRect();
|
|
57
|
+
if (/hidden|clip|auto|scroll/.test(style.overflowX)) {
|
|
58
|
+
clip.left = Math.max(clip.left, bounds.left); clip.right = Math.min(clip.right, bounds.right);
|
|
59
|
+
}
|
|
60
|
+
if (/hidden|clip|auto|scroll/.test(style.overflowY)) {
|
|
61
|
+
clip.top = Math.max(clip.top, bounds.top); clip.bottom = Math.min(clip.bottom, bounds.bottom);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const data of positions.values()) {
|
|
65
|
+
try {
|
|
66
|
+
const resolved = record.resolve(data.resolved);
|
|
67
|
+
const geometry = getSelectionRects(
|
|
68
|
+
element, resolved.anchor, resolved.head, record.getRange
|
|
69
|
+
);
|
|
70
|
+
const frame = element.tagName === "IFRAME";
|
|
71
|
+
const scaleX = frame ? field.width / (element.offsetWidth || field.width) : 1;
|
|
72
|
+
const scaleY = frame ? field.height / (element.offsetHeight || field.height) : 1;
|
|
73
|
+
const translate = rect => frame ? {
|
|
74
|
+
left: field.left + (element.clientLeft + rect.left) * scaleX,
|
|
75
|
+
top: field.top + (element.clientTop + rect.top) * scaleY,
|
|
76
|
+
width: rect.width * scaleX, height: rect.height * scaleY
|
|
77
|
+
} : rect;
|
|
78
|
+
const color = data.presence?.background || data.presence?.color || "#6366f1";
|
|
79
|
+
if (resolved.anchor !== resolved.head)
|
|
80
|
+
for (const rect of geometry.selection) mark(doc, root, box(translate(rect), clip), color);
|
|
81
|
+
mark(doc, root, box(translate(geometry.caret), clip), color, true,
|
|
82
|
+
String(data.presence?.userName || data.clientId || ""));
|
|
83
|
+
} catch {
|
|
84
|
+
// Invalid positions and navigating iframes are retried on refresh.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Attach the lightweight remote cursor overlay to a live CRDT editor. * @public
|
|
90
|
+
* @environment client
|
|
91
|
+
*/
|
|
92
|
+
export function attachEditorCursors(element, data, getRange, resolve = value => value, getSelections) {
|
|
93
|
+
const doc = element.ownerDocument, view = doc.defaultView;
|
|
94
|
+
const overlay = doc.createElement("div");
|
|
95
|
+
overlay.setAttribute("data-editor-cursors", "");
|
|
96
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
97
|
+
overlay.style.cssText = "all:initial;position:fixed;inset:0;pointer-events:none;z-index:2147483647;";
|
|
98
|
+
const root = overlay.attachShadow({ mode: "closed" });
|
|
99
|
+
doc.documentElement.append(overlay);
|
|
100
|
+
const record = { element, data, root, getRange, resolve, getSelections, positions: new Map(), pending: null };
|
|
101
|
+
const refresh = () => {
|
|
102
|
+
if (record.pending !== null) return;
|
|
103
|
+
record.pending = view.requestAnimationFrame(() => {
|
|
104
|
+
record.pending = null;
|
|
105
|
+
draw(record);
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
record.refresh = refresh;
|
|
109
|
+
editors.add(record);
|
|
110
|
+
let inner;
|
|
111
|
+
const watchDocument = () => {
|
|
112
|
+
inner?.removeEventListener("scroll", refresh, true);
|
|
113
|
+
inner?.defaultView?.removeEventListener("resize", refresh);
|
|
114
|
+
inner = element.tagName === "IFRAME" ? element.contentDocument : null;
|
|
115
|
+
inner?.addEventListener("scroll", refresh, true);
|
|
116
|
+
inner?.defaultView?.addEventListener("resize", refresh);
|
|
117
|
+
refresh();
|
|
118
|
+
};
|
|
119
|
+
const resize = new view.ResizeObserver(refresh);
|
|
120
|
+
resize.observe(element);
|
|
121
|
+
const attributes = new view.MutationObserver(refresh);
|
|
122
|
+
attributes.observe(element, { attributes: true, attributeFilter: ["cursors", "class", "style"] });
|
|
123
|
+
doc.addEventListener("scroll", refresh, true);
|
|
124
|
+
view.addEventListener("resize", refresh);
|
|
125
|
+
element.addEventListener("input", refresh);
|
|
126
|
+
element.addEventListener("editor-source-change", refresh);
|
|
127
|
+
element.addEventListener("editor-render", refresh);
|
|
128
|
+
element.addEventListener("editor-document-change", watchDocument);
|
|
129
|
+
watchDocument();
|
|
130
|
+
const destroy = () => {
|
|
131
|
+
editors.delete(record);
|
|
132
|
+
if (record.pending !== null) view.cancelAnimationFrame(record.pending);
|
|
133
|
+
resize.disconnect(); attributes.disconnect();
|
|
134
|
+
doc.removeEventListener("scroll", refresh, true);
|
|
135
|
+
view.removeEventListener("resize", refresh);
|
|
136
|
+
inner?.removeEventListener("scroll", refresh, true);
|
|
137
|
+
inner?.defaultView?.removeEventListener("resize", refresh);
|
|
138
|
+
element.removeEventListener("input", refresh);
|
|
139
|
+
element.removeEventListener("editor-source-change", refresh);
|
|
140
|
+
element.removeEventListener("editor-render", refresh);
|
|
141
|
+
element.removeEventListener("editor-document-change", watchDocument);
|
|
142
|
+
overlay.remove();
|
|
143
|
+
};
|
|
144
|
+
return { refresh, destroy };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const editorCursors = new WeakMap();
|
|
148
|
+
window.addEventListener("cocreate-editor-cursor-init", ({ detail }) => {
|
|
149
|
+
const { element, data, getRange, resolve, getSelections } = detail || {};
|
|
150
|
+
if (!element || !data || typeof getRange !== "function" ||
|
|
151
|
+
element.getAttribute("cursors") === "false") return;
|
|
152
|
+
editorCursors.get(element)?.destroy();
|
|
153
|
+
editorCursors.set(element, attachEditorCursors(
|
|
154
|
+
element, data, getRange, resolve, getSelections
|
|
155
|
+
));
|
|
156
|
+
});
|
|
157
|
+
window.addEventListener("cocreate-editor-cursor-remove", ({ detail }) => {
|
|
158
|
+
const element = detail?.element;
|
|
159
|
+
editorCursors.get(element)?.destroy();
|
|
160
|
+
editorCursors.delete(element);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
window.addEventListener("cocreate-crdt-selection", ({ detail }) => {
|
|
164
|
+
if (!detail?.socketId) return;
|
|
165
|
+
const localFrame = window.CoCreate?.socket?.frameId;
|
|
166
|
+
if (localFrame && detail.frameId === localFrame) return;
|
|
167
|
+
for (const record of editors) {
|
|
168
|
+
if (!sameField(record.data, detail)) continue;
|
|
169
|
+
const id = sender(detail);
|
|
170
|
+
if (detail.selection === null || !detail.resolved) record.positions.delete(id);
|
|
171
|
+
else record.positions.set(id, detail);
|
|
172
|
+
record.refresh();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
window.dispatchEvent(new Event("cocreate-editor-cursor-request"));
|
|
177
|
+
|
|
178
|
+
export default { attachEditorCursors };
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
*
|
|
182
|
+
* @name aria-hidden
|
|
183
|
+
* @kind attribute
|
|
184
|
+
* @environment client
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
*
|
|
189
|
+
* @name class
|
|
190
|
+
* @kind attribute
|
|
191
|
+
* @environment client
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
*
|
|
196
|
+
* @name crdt
|
|
197
|
+
* @kind attribute
|
|
198
|
+
* @environment client
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
*
|
|
203
|
+
* @name cursors
|
|
204
|
+
* @kind attribute
|
|
205
|
+
* @environment client
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
*
|
|
210
|
+
* @name data-editor-cursors
|
|
211
|
+
* @kind attribute
|
|
212
|
+
* @environment client
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
*
|
|
217
|
+
* @name style
|
|
218
|
+
* @kind attribute
|
|
219
|
+
* @environment client
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Collaborative cursor overlays for CoCreate Text and Editor surfaces.
|
|
224
|
+
*
|
|
225
|
+
* @module cursors
|
|
226
|
+
* @environment client
|
|
227
|
+
*/
|