@cocreate/cursors 1.30.0 → 2.0.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/src/index.js CHANGED
@@ -1,573 +1,233 @@
1
- /********************************************************************************
2
- * Copyright (C) 2023 CoCreate and Contributors.
3
- *
4
- * This program is free software: you can redistribute it and/or modify
5
- * it under the terms of the GNU Affero General Public License as published
6
- * by the Free Software Foundation, either version 3 of the License, or
7
- * (at your option) any later version.
8
- *
9
- * This program is distributed in the hope that it will be useful,
10
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
- * GNU Affero General Public License for more details.
13
- *
14
- * You should have received a copy of the GNU Affero General Public License
15
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
- ********************************************************************************/
17
-
18
- // Commercial Licensing Information:
19
- // For commercial use of this software without the copyleft provisions of the AGPLv3,
20
- // you must obtain a commercial license from CoCreate LLC.
21
- // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
22
-
23
- /*globals ResizeObserver*/
24
- import observer from "@cocreate/observer";
25
- import { getAttributeNames, uuid } from "@cocreate/utils";
26
- import socket from "@cocreate/socket-client";
27
- import localStorage from "@cocreate/local-storage";
28
- import { getElementPosition } from "@cocreate/selection";
29
- import { randomColor } from "@cocreate/random-color";
30
- import "./index.css";
31
-
32
- const clientId = socket.clientId || uuid(12);
33
- const cursorBackground = randomColor();
34
-
35
- let environment_prod = true;
36
-
37
- let selector =
38
- "[array][object][key][crdt='true']:not([contentEditable='false'])";
39
-
40
- function init() {
41
- let elements = document.querySelectorAll(selector);
42
- initElements(elements);
43
- }
44
-
45
- function initElements(elements) {
46
- for (let element of elements) initElement(element);
47
- }
48
-
49
- function initElement(element) {
50
- let realtime = element.getAttribute("realtime");
51
- let cursors = element.getAttribute("cursors");
52
- if (!realtime || realtime === "false" || cursors === "false") return;
53
- if (element.hasAttribute("actions")) return;
54
- if (element.cursor) return;
55
- element.cursor = true;
56
- }
57
-
58
- function initDocument(doc) {
59
- let documents;
60
- try {
61
- documents = window.top.cursorDocuments;
62
- } catch (e) {
63
- console.log("cross-origin failed");
64
- }
65
-
66
- if (!documents) {
67
- documents = new Map();
68
- try {
69
- window.top.cursorDocuments = documents;
70
- } catch (e) {
71
- console.log("cross-origin failed");
72
- }
73
- }
74
- if (!documents.has(doc)) {
75
- initResizeObserver(doc.documentElement);
76
- documents.set(doc);
77
- }
78
- }
79
-
80
- function drawCursors(selection) {
81
- const socket_id = selection["socketId"];
82
- const array = selection["array"];
83
- const object = selection["object"];
84
- const key = selection["key"];
85
- let elements = selection.element;
86
- if (!elements) {
87
- let selector =
88
- '[array="' +
89
- array +
90
- '"][object="' +
91
- object +
92
- '"][key="' +
93
- key +
94
- '"]';
95
- selector += ":not([actions])";
96
- elements = document.querySelectorAll(selector);
97
- }
98
- for (let element of elements) {
99
- let start = selection["start"];
100
- let end = selection["end"];
101
- if (
102
- window.activeElement == element &&
103
- socket.frameId === selection.frameId
104
- ) {
105
- continue;
106
- }
107
- let realtime = element.getAttribute("realtime");
108
- let cursors = element.getAttribute("cursors");
109
- let contenteditable = element.getAttribute("contenteditable");
110
- if (!realtime || realtime == "false" || cursors == "false") continue;
111
- if (element.tagName != "INPUT" && element.tagName != "TEXTAREA")
112
- if (contenteditable == "false" || contenteditable === null) {
113
- if (cursors != "true") continue;
114
- }
115
-
116
- let domTextEditor = element;
117
-
118
- if (element.hasAttribute("contenteditable")) {
119
- contenteditable = true;
120
- // let domTextEditor = element;
121
- if (element.tagName == "IFRAME") {
122
- // let frameClientId = element.contentDocument.defaultView.CoCreateSockets.id;
123
- // if (frameClientId == selection.socketId) continue;
124
- domTextEditor = element =
125
- element.contentDocument.documentElement;
126
-
127
- let pos = getElementPosition(
128
- domTextEditor.htmlString,
129
- start,
130
- end
131
- );
132
-
133
- if (pos.path) {
134
- element = element.querySelector(pos.path);
135
- if (pos.start) {
136
- let endPos = end - start;
137
- if (endPos > 0) end = pos.start + endPos;
138
- else end = pos.start;
139
- start = pos.start;
140
- }
141
- }
142
- }
143
-
144
- if (!domTextEditor.htmlString) continue;
145
-
146
- // let pos = getElementPosition(domTextEditor.htmlString, start, end);
147
-
148
- // if (pos.path) {
149
- // element = domTextEditor.querySelector(pos.path);
150
- // if (pos.start) {
151
- // let endPos = end - start;
152
- // if (endPos > 0)
153
- // end = pos.start + endPos;
154
- // else
155
- // end = pos.start;
156
- // start = pos.start;
157
- // }
158
- // }
159
- }
160
-
161
- if (!element) continue;
162
- let document = element.ownerDocument;
163
- let mirrorDiv;
164
- let id_mirror = element.getAttribute("mirror_id");
165
- if (!id_mirror) {
166
- id_mirror = uuid(8);
167
- element.setAttribute("mirror_id", id_mirror);
168
-
169
- mirrorDiv = document.createElement("cursor-container");
170
- mirrorDiv.id = id_mirror;
171
- mirrorDiv.className = environment_prod
172
- ? "mirror mirror_scroll mirror_color"
173
- : "mirror mirror_scroll";
174
- mirrorDiv.contentEditable = false;
175
- mirrorDiv.element = element;
176
- if (element.tagName === "HTML")
177
- element.insertAdjacentElement("beforeend", mirrorDiv);
178
- else element.insertAdjacentElement("afterend", mirrorDiv);
179
- // let parent = element.parentElement;
180
- // let parentComputed = getComputedStyle(parent);
181
- // let parentStylePosition = parentComputed['position'];
182
- // if (parentStylePosition == 'static')
183
- // parent.style.position = 'relative';
184
- // let parentStyleDisplay = parentComputed['display'];
185
- // if (parentStyleDisplay == 'inline')
186
- // parent.style.display = 'inline';
187
-
188
- _initEvents(element);
189
- initDocument(document);
190
- } else mirrorDiv = document.getElementById(id_mirror);
191
-
192
- let computed = getComputedStyle(element);
193
- let rect = element.getBoundingClientRect();
194
-
195
- let borderWidth = parseInt(computed["border-width"]) * 2;
196
- let scrollBarWidth = 0;
197
- if (element.clientWidth != 0)
198
- scrollBarWidth =
199
- element.offsetWidth - element.clientWidth - borderWidth;
200
-
201
- let style = mirrorDiv.style;
202
- style.position = "absolute";
203
- style.width = rect.width + "px";
204
- style.height = rect.height + "px";
205
- style.margin = "0px";
206
- style.borderColor = "transparent";
207
- style.outlineColor = "transparent";
208
-
209
- style.overflowX = computed["overflowX"];
210
- style.overflowY = computed["overflowY"];
211
- style.padding = computed["padding"];
212
- style.border = computed["border"];
213
- style.outline = computed["outline"];
214
-
215
- if (
216
- element.parentElement &&
217
- element.parentElement.style["display"] == "inline"
218
- ) {
219
- style.top = element.clientTop + "px";
220
- style.left = element.clientLeft + "px";
221
- } else {
222
- style.top = element.offsetTop + "px";
223
- style.left = element.offsetLeft + "px";
224
- }
225
-
226
- if (contenteditable) {
227
- const actualStyles = getActualStyles(element);
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 member
184
+ * @attribute
185
+ * @environment client
186
+ */
187
+
188
+ /**
189
+ *
190
+ * @name class
191
+ * @kind member
192
+ * @attribute
193
+ * @environment client
194
+ */
195
+
196
+ /**
197
+ *
198
+ * @name crdt
199
+ * @kind member
200
+ * @attribute
201
+ * @environment client
202
+ */
203
+
204
+ /**
205
+ *
206
+ * @name cursors
207
+ * @kind member
208
+ * @attribute
209
+ * @environment client
210
+ */
211
+
212
+ /**
213
+ *
214
+ * @name data-editor-cursors
215
+ * @kind member
216
+ * @attribute
217
+ * @environment client
218
+ */
219
+
220
+ /**
221
+ *
222
+ * @name style
223
+ * @kind member
224
+ * @attribute
225
+ * @environment client
226
+ */
227
+
228
+ /**
229
+ * Collaborative cursor overlays for CoCreate Text and Editor surfaces.
230
+ *
231
+ * @module cursors
232
+ * @environment client
233
+ */