Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ export type BlockNoteEditorOptions<
* Optional function to customize how cursors of users are rendered
*/
renderCursor?: (user: any) => HTMLElement;
/**
* Optional flag to set when the user label should be shown with the default
* collaboration cursor. Setting to "always" will always show the label,
* while "activity" will only show the label when the user moves the cursor
* or types. Defaults to "activity".
*/
showCursorLabels?: "always" | "activity";
};

/**
Expand Down
121 changes: 106 additions & 15 deletions packages/core/src/editor/BlockNoteExtensions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AnyExtension, Extension, extensions } from "@tiptap/core";
import { Awareness } from "y-protocols/awareness";

import type { BlockNoteEditor, BlockNoteExtension } from "./BlockNoteEditor.js";

Expand Down Expand Up @@ -64,6 +65,7 @@ type ExtensionOptions<
};
provider: any;
renderCursor?: (user: any) => HTMLElement;
showCursorLabels?: "always" | "activity";
};
disableExtensions: string[] | undefined;
setIdAttribute?: boolean;
Expand Down Expand Up @@ -250,25 +252,114 @@ const getTipTapExtensions = <
fragment: opts.collaboration.fragment,
})
);
if (opts.collaboration.provider?.awareness) {
const defaultRender = (user: { color: string; name: string }) => {
const cursor = document.createElement("span");

cursor.classList.add("collaboration-cursor__caret");
cursor.setAttribute("style", `border-color: ${user.color}`);
const awareness = opts.collaboration?.provider.awareness as Awareness;

if (awareness) {
const cursors = new Map<
number,
{ element: HTMLElement; hideTimeout: NodeJS.Timeout | undefined }
>();

if (opts.collaboration.showCursorLabels !== "always") {
awareness.on(
"change",
({
updated,
}: {
added: Array<number>;
updated: Array<number>;
removed: Array<number>;
}) => {
for (const clientID of updated) {
const cursor = cursors.get(clientID);

if (cursor) {
cursor.element.setAttribute("data-active", "");

if (cursor.hideTimeout) {
clearTimeout(cursor.hideTimeout);
}

cursors.set(clientID, {
element: cursor.element,
hideTimeout: setTimeout(() => {
cursor.element.removeAttribute("data-active");
}, 2000),
});
}
}
}
);
}

const createCursor = (clientID: number, name: string, color: string) => {
const cursorElement = document.createElement("span");

cursorElement.classList.add("collaboration-cursor__caret");
cursorElement.setAttribute("style", `border-color: ${color}`);
if (opts.collaboration?.showCursorLabels !== "always") {
cursorElement.setAttribute("data-active", "");
}

const labelElement = document.createElement("span");

labelElement.classList.add("collaboration-cursor__label");
labelElement.setAttribute("style", `background-color: ${color}`);
labelElement.insertBefore(document.createTextNode(name), null);

cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space
cursorElement.insertBefore(labelElement, null);
cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space

cursors.set(clientID, {
element: cursorElement,
hideTimeout: undefined,
});

if (opts.collaboration?.showCursorLabels !== "always") {
cursorElement.addEventListener("mouseenter", () => {
const cursor = cursors.get(clientID)!;
cursor.element.setAttribute("data-active", "");

if (cursor.hideTimeout) {
clearTimeout(cursor.hideTimeout);
cursors.set(clientID, {
element: cursor.element,
hideTimeout: undefined,
});
}
});

cursorElement.addEventListener("mouseleave", () => {
const cursor = cursors.get(clientID)!;

cursors.set(clientID, {
element: cursor.element,
hideTimeout: setTimeout(() => {
cursor.element.removeAttribute("data-active");
}, 2000),
});
});
}

return cursors.get(clientID)!;
};

const defaultRender = (user: { color: string; name: string }) => {
const clientState = [...awareness.getStates().entries()].find(
(state) => state[1].user === user
);

const label = document.createElement("span");
if (!clientState) {
throw new Error("Could not find client state for user");
}

label.classList.add("collaboration-cursor__label");
label.setAttribute("style", `background-color: ${user.color}`);
label.insertBefore(document.createTextNode(user.name), null);
const clientID = clientState[0];

const nonbreakingSpace1 = document.createTextNode("\u2060");
const nonbreakingSpace2 = document.createTextNode("\u2060");
cursor.insertBefore(nonbreakingSpace1, null);
cursor.insertBefore(label, null);
cursor.insertBefore(nonbreakingSpace2, null);
return cursor;
return (
cursors.get(clientID) || createCursor(clientID, user.name, user.color)
).element;
};
tiptapExtensions.push(
CollaborationCursor.configure({
Expand Down
27 changes: 21 additions & 6 deletions packages/core/src/editor/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ Tippy popups that are appended to document.body directly
border-right: 1px solid #0d0d0d;
margin-left: -1px;
margin-right: -1px;
pointer-events: none;
position: relative;
word-break: normal;
white-space: nowrap !important;
Expand All @@ -92,17 +91,33 @@ Tippy popups that are appended to document.body directly
/* Render the username above the caret */
.collaboration-cursor__label {
border-radius: 3px 3px 3px 0;
color: #0d0d0d;
font-size: 12px;
font-style: normal;
font-weight: 600;
left: -1px;
line-height: normal;
padding: 0.1rem 0.3rem;
left: -1px;
overflow: hidden;
position: absolute;
top: -1.4em;
user-select: none;
white-space: nowrap;

color: transparent;
max-height: 4px;
max-width: 4px;
padding: 0;
transform: translateY(3px);

transition: all 0.2s;

}

.collaboration-cursor__caret[data-active] > .collaboration-cursor__label {
color: #0d0d0d;
max-height: 1.1rem;
max-width: 20rem;
padding: 0.1rem 0.3rem;
transform: translateY(-14px);

transition: all 0.2s;
}

/* .tableWrapper {
Expand Down
Loading