@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/src/index.js CHANGED
@@ -1,573 +1,227 @@
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 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
+ */