|
1 | 1 | import { AnyExtension, Extension, extensions } from "@tiptap/core";
|
| 2 | +import { Awareness } from "y-protocols/awareness"; |
2 | 3 |
|
3 | 4 | import type { BlockNoteEditor, BlockNoteExtension } from "./BlockNoteEditor.js";
|
4 | 5 |
|
@@ -64,6 +65,7 @@ type ExtensionOptions<
|
64 | 65 | };
|
65 | 66 | provider: any;
|
66 | 67 | renderCursor?: (user: any) => HTMLElement;
|
| 68 | + showCursorLabels?: "always" | "activity"; |
67 | 69 | };
|
68 | 70 | disableExtensions: string[] | undefined;
|
69 | 71 | setIdAttribute?: boolean;
|
@@ -250,25 +252,114 @@ const getTipTapExtensions = <
|
250 | 252 | fragment: opts.collaboration.fragment,
|
251 | 253 | })
|
252 | 254 | );
|
253 |
| - if (opts.collaboration.provider?.awareness) { |
254 |
| - const defaultRender = (user: { color: string; name: string }) => { |
255 |
| - const cursor = document.createElement("span"); |
256 | 255 |
|
257 |
| - cursor.classList.add("collaboration-cursor__caret"); |
258 |
| - cursor.setAttribute("style", `border-color: ${user.color}`); |
| 256 | + const awareness = opts.collaboration?.provider.awareness as Awareness; |
| 257 | + |
| 258 | + if (awareness) { |
| 259 | + const cursors = new Map< |
| 260 | + number, |
| 261 | + { element: HTMLElement; hideTimeout: NodeJS.Timeout | undefined } |
| 262 | + >(); |
| 263 | + |
| 264 | + if (opts.collaboration.showCursorLabels !== "always") { |
| 265 | + awareness.on( |
| 266 | + "change", |
| 267 | + ({ |
| 268 | + updated, |
| 269 | + }: { |
| 270 | + added: Array<number>; |
| 271 | + updated: Array<number>; |
| 272 | + removed: Array<number>; |
| 273 | + }) => { |
| 274 | + for (const clientID of updated) { |
| 275 | + const cursor = cursors.get(clientID); |
| 276 | + |
| 277 | + if (cursor) { |
| 278 | + cursor.element.setAttribute("data-active", ""); |
| 279 | + |
| 280 | + if (cursor.hideTimeout) { |
| 281 | + clearTimeout(cursor.hideTimeout); |
| 282 | + } |
| 283 | + |
| 284 | + cursors.set(clientID, { |
| 285 | + element: cursor.element, |
| 286 | + hideTimeout: setTimeout(() => { |
| 287 | + cursor.element.removeAttribute("data-active"); |
| 288 | + }, 2000), |
| 289 | + }); |
| 290 | + } |
| 291 | + } |
| 292 | + } |
| 293 | + ); |
| 294 | + } |
| 295 | + |
| 296 | + const createCursor = (clientID: number, name: string, color: string) => { |
| 297 | + const cursorElement = document.createElement("span"); |
| 298 | + |
| 299 | + cursorElement.classList.add("collaboration-cursor__caret"); |
| 300 | + cursorElement.setAttribute("style", `border-color: ${color}`); |
| 301 | + if (opts.collaboration?.showCursorLabels !== "always") { |
| 302 | + cursorElement.setAttribute("data-active", ""); |
| 303 | + } |
| 304 | + |
| 305 | + const labelElement = document.createElement("span"); |
| 306 | + |
| 307 | + labelElement.classList.add("collaboration-cursor__label"); |
| 308 | + labelElement.setAttribute("style", `background-color: ${color}`); |
| 309 | + labelElement.insertBefore(document.createTextNode(name), null); |
| 310 | + |
| 311 | + cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space |
| 312 | + cursorElement.insertBefore(labelElement, null); |
| 313 | + cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space |
| 314 | + |
| 315 | + cursors.set(clientID, { |
| 316 | + element: cursorElement, |
| 317 | + hideTimeout: undefined, |
| 318 | + }); |
| 319 | + |
| 320 | + if (opts.collaboration?.showCursorLabels !== "always") { |
| 321 | + cursorElement.addEventListener("mouseenter", () => { |
| 322 | + const cursor = cursors.get(clientID)!; |
| 323 | + cursor.element.setAttribute("data-active", ""); |
| 324 | + |
| 325 | + if (cursor.hideTimeout) { |
| 326 | + clearTimeout(cursor.hideTimeout); |
| 327 | + cursors.set(clientID, { |
| 328 | + element: cursor.element, |
| 329 | + hideTimeout: undefined, |
| 330 | + }); |
| 331 | + } |
| 332 | + }); |
| 333 | + |
| 334 | + cursorElement.addEventListener("mouseleave", () => { |
| 335 | + const cursor = cursors.get(clientID)!; |
| 336 | + |
| 337 | + cursors.set(clientID, { |
| 338 | + element: cursor.element, |
| 339 | + hideTimeout: setTimeout(() => { |
| 340 | + cursor.element.removeAttribute("data-active"); |
| 341 | + }, 2000), |
| 342 | + }); |
| 343 | + }); |
| 344 | + } |
| 345 | + |
| 346 | + return cursors.get(clientID)!; |
| 347 | + }; |
| 348 | + |
| 349 | + const defaultRender = (user: { color: string; name: string }) => { |
| 350 | + const clientState = [...awareness.getStates().entries()].find( |
| 351 | + (state) => state[1].user === user |
| 352 | + ); |
259 | 353 |
|
260 |
| - const label = document.createElement("span"); |
| 354 | + if (!clientState) { |
| 355 | + throw new Error("Could not find client state for user"); |
| 356 | + } |
261 | 357 |
|
262 |
| - label.classList.add("collaboration-cursor__label"); |
263 |
| - label.setAttribute("style", `background-color: ${user.color}`); |
264 |
| - label.insertBefore(document.createTextNode(user.name), null); |
| 358 | + const clientID = clientState[0]; |
265 | 359 |
|
266 |
| - const nonbreakingSpace1 = document.createTextNode("\u2060"); |
267 |
| - const nonbreakingSpace2 = document.createTextNode("\u2060"); |
268 |
| - cursor.insertBefore(nonbreakingSpace1, null); |
269 |
| - cursor.insertBefore(label, null); |
270 |
| - cursor.insertBefore(nonbreakingSpace2, null); |
271 |
| - return cursor; |
| 360 | + return ( |
| 361 | + cursors.get(clientID) || createCursor(clientID, user.name, user.color) |
| 362 | + ).element; |
272 | 363 | };
|
273 | 364 | tiptapExtensions.push(
|
274 | 365 | CollaborationCursor.configure({
|
|
0 commit comments