Skip to content

Commit c2046c1

Browse files
feat: Multiplayer API and homepage collaboration (#200)
* upgrade tiptap and node/npm * upgrade node in ci * fix collab api and add partykit to homepage (wip) * wip * make homepage collaborative * fix typing at start of line * add docs * Fixed text alignment styling with flexbox * Fixed text alignment styling with flexbox * Fixed text alignment styling with flexbox * Fixed text alignment styling with flexbox * Copy/paste tests are now skipped for WebKit * Copy/paste tests are now skipped for WebKit * Updated firefox colors snapshot --------- Co-authored-by: Matthew Lipski <[email protected]>
1 parent 5159fa9 commit c2046c1

File tree

15 files changed

+2698
-15231
lines changed

15 files changed

+2698
-15231
lines changed

package-lock.json

Lines changed: 2309 additions & 15176 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@
8080
"unified": "^10.1.2",
8181
"uuid": "^8.3.2",
8282
"y-prosemirror": "1.0.20",
83-
"y-protocols": "1.0.5",
84-
"yjs": "13.5.44"
83+
"y-protocols": "^1.0.5",
84+
"yjs": "^13.6.1"
8585
},
8686
"devDependencies": {
8787
"@types/hast": "^2.3.4",

packages/core/src/BlockNoteEditor.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Editor, EditorOptions } from "@tiptap/core";
22
import { Node } from "prosemirror-model";
33
// import "./blocknote.css";
44
import { Editor as TiptapEditor } from "@tiptap/core/dist/packages/core/src/Editor";
5+
import * as Y from "yjs";
56
import {
67
insertBlocks,
78
removeBlocks,
@@ -23,12 +24,12 @@ import {
2324
BlockIdentifier,
2425
PartialBlock,
2526
} from "./extensions/Blocks/api/blockTypes";
27+
import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes";
2628
import {
2729
ColorStyle,
2830
Styles,
2931
ToggledStyle,
3032
} from "./extensions/Blocks/api/inlineContentTypes";
31-
import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes";
3233
import { Selection } from "./extensions/Blocks/api/selectionTypes";
3334
import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFromPos";
3435
import {
@@ -37,9 +38,9 @@ import {
3738
} from "./extensions/SlashMenu";
3839

3940
export type BlockNoteEditorOptions = {
40-
// TODO: Figure out if enableBlockNoteExtensions/disableHistoryExtension are needed and document them.
41+
// TODO: Figure out if enableBlockNoteExtensions is needed and document them.
4142
enableBlockNoteExtensions: boolean;
42-
disableHistoryExtension: boolean;
43+
4344
/**
4445
* UI element factories for creating a custom UI, including custom positioning
4546
* & rendering.
@@ -97,6 +98,31 @@ export type BlockNoteEditorOptions = {
9798
*/
9899
theme: "light" | "dark";
99100

101+
/**
102+
* When enabled, allows for collaboration between multiple users.
103+
*/
104+
collaboration: {
105+
/**
106+
* The Yjs XML fragment that's used for collaboration.
107+
*/
108+
fragment: Y.XmlFragment;
109+
/**
110+
* The user info for the current user that's shown to other collaborators.
111+
*/
112+
user: {
113+
name: string;
114+
color: string;
115+
};
116+
/**
117+
* A Yjs provider (used for awareness / cursor information)
118+
*/
119+
provider: any;
120+
/**
121+
* Optional function to customize how cursors of users are rendered
122+
*/
123+
renderCursor?: (user: any) => HTMLElement;
124+
};
125+
100126
// tiptap options, undocumented
101127
_tiptapOptions: any;
102128
};
@@ -119,23 +145,20 @@ export class BlockNoteEditor {
119145
this._tiptapEditor.view.focus();
120146
}
121147

122-
constructor(options: Partial<BlockNoteEditorOptions> = {}) {
148+
constructor(private readonly options: Partial<BlockNoteEditorOptions> = {}) {
123149
// apply defaults
124150
options = {
125151
defaultStyles: true,
126152
...options,
127153
};
128154

129-
const blockNoteExtensions = getBlockNoteExtensions({
155+
const extensions = getBlockNoteExtensions({
130156
editor: this,
131157
uiFactories: options.uiFactories || {},
132158
slashCommands: options.slashCommands || defaultSlashMenuItems,
159+
collaboration: options.collaboration,
133160
});
134161

135-
let extensions = options.disableHistoryExtension
136-
? blockNoteExtensions.filter((e) => e.name !== "history")
137-
: blockNoteExtensions;
138-
139162
const tiptapOptions: EditorOptions = {
140163
// TODO: This approach to setting initial content is "cleaner" but requires the PM editor schema, which is only
141164
// created after initializing the TipTap editor. Not sure it's feasible.
@@ -644,4 +667,16 @@ export class BlockNoteEditor {
644667
public async markdownToBlocks(markdown: string): Promise<Block[]> {
645668
return markdownToBlocks(markdown, this._tiptapEditor.schema);
646669
}
670+
671+
/**
672+
* Updates the user info for the current user that's shown to other collaborators.
673+
*/
674+
public updateCollaborationUserInfo(user: { name: string; color: string }) {
675+
if (!this.options.collaboration) {
676+
throw new Error(
677+
"Cannot update collaboration user info when collaboration is disabled."
678+
);
679+
}
680+
this._tiptapEditor.commands.updateUser(user);
681+
}
647682
}

packages/core/src/BlockNoteExtensions.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { BlockNoteEditor } from "./BlockNoteEditor";
44

55
import { Bold } from "@tiptap/extension-bold";
66
import { Code } from "@tiptap/extension-code";
7+
import Collaboration from "@tiptap/extension-collaboration";
8+
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
79
import { Dropcursor } from "@tiptap/extension-dropcursor";
810
import { Gapcursor } from "@tiptap/extension-gapcursor";
911
import { HardBreak } from "@tiptap/extension-hard-break";
@@ -13,6 +15,8 @@ import { Link } from "@tiptap/extension-link";
1315
import { Strike } from "@tiptap/extension-strike";
1416
import { Text } from "@tiptap/extension-text";
1517
import { Underline } from "@tiptap/extension-underline";
18+
import * as Y from "yjs";
19+
import styles from "./editor.module.css";
1620
import { BackgroundColorExtension } from "./extensions/BackgroundColor/BackgroundColorExtension";
1721
import { BackgroundColorMark } from "./extensions/BackgroundColor/BackgroundColorMark";
1822
import { blocks } from "./extensions/Blocks";
@@ -46,6 +50,15 @@ export const getBlockNoteExtensions = (opts: {
4650
editor: BlockNoteEditor;
4751
uiFactories: UiFactories;
4852
slashCommands: BaseSlashMenuItem[];
53+
collaboration?: {
54+
fragment: Y.XmlFragment;
55+
user: {
56+
name: string;
57+
color: string;
58+
};
59+
provider: any;
60+
renderCursor?: (user: any) => HTMLElement;
61+
};
4962
}) => {
5063
const ret: Extensions = [
5164
extensions.ClipboardTextSerializer,
@@ -90,12 +103,48 @@ export const getBlockNoteExtensions = (opts: {
90103
...blocks,
91104

92105
Dropcursor.configure({ width: 5, color: "#ddeeff" }),
93-
History,
94106
// This needs to be at the bottom of this list, because Key events (such as enter, when selecting a /command),
95107
// should be handled before Enter handlers in other components like splitListItem
96108
TrailingNode,
97109
];
98110

111+
if (opts.collaboration) {
112+
ret.push(
113+
Collaboration.configure({
114+
fragment: opts.collaboration.fragment,
115+
})
116+
);
117+
const defaultRender = (user: { color: string; name: string }) => {
118+
const cursor = document.createElement("span");
119+
120+
cursor.classList.add(styles["collaboration-cursor__caret"]);
121+
cursor.setAttribute("style", `border-color: ${user.color}`);
122+
123+
const label = document.createElement("span");
124+
125+
label.classList.add(styles["collaboration-cursor__label"]);
126+
label.setAttribute("style", `background-color: ${user.color}`);
127+
label.insertBefore(document.createTextNode(user.name), null);
128+
129+
const nonbreakingSpace1 = document.createTextNode("\u2060");
130+
const nonbreakingSpace2 = document.createTextNode("\u2060");
131+
cursor.insertBefore(nonbreakingSpace1, null);
132+
cursor.insertBefore(label, null);
133+
cursor.insertBefore(nonbreakingSpace2, null);
134+
return cursor;
135+
};
136+
ret.push(
137+
CollaborationCursor.configure({
138+
user: opts.collaboration.user,
139+
render: opts.collaboration.renderCursor || defaultRender,
140+
provider: opts.collaboration.provider,
141+
})
142+
);
143+
} else {
144+
// disable history extension when collaboration is enabled as Yjs takes care of undo / redo
145+
ret.push(History);
146+
}
147+
99148
if (opts.uiFactories.blockSideMenuFactory) {
100149
ret.push(
101150
DraggableBlocksExtension.configure({

packages/core/src/editor.module.css

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ Tippy popups that are appended to document.body directly
3838
.defaultStyles h3,
3939
.defaultStyles li {
4040
all: unset !important;
41+
flex-grow: 1 !important;
4142
margin: 0;
4243
padding: 0;
4344
font-size: inherit;
45+
/* min width to make sure cursor is always visible */
46+
min-width: 2px !important;
4447
}
4548

4649
.defaultStyles {
@@ -54,16 +57,43 @@ Tippy popups that are appended to document.body directly
5457
}
5558

5659
[data-theme="light"] {
57-
background-color: #FFFFFF;
60+
background-color: #ffffff;
5861
color: #444444;
5962
}
6063

6164
[data-theme="dark"] {
6265
background-color: #444444;
63-
color: #DDDDDD;
66+
color: #dddddd;
6467
}
6568

6669
.dragPreview {
6770
position: absolute;
6871
top: -1000px;
6972
}
73+
74+
/* Give a remote user a caret */
75+
.collaboration-cursor__caret {
76+
border-left: 1px solid #0d0d0d;
77+
border-right: 1px solid #0d0d0d;
78+
margin-left: -1px;
79+
margin-right: -1px;
80+
pointer-events: none;
81+
position: relative;
82+
word-break: normal;
83+
}
84+
85+
/* Render the username above the caret */
86+
.collaboration-cursor__label {
87+
border-radius: 3px 3px 3px 0;
88+
color: #0d0d0d;
89+
font-size: 12px;
90+
font-style: normal;
91+
font-weight: 600;
92+
left: -1px;
93+
line-height: normal;
94+
padding: 0.1rem 0.3rem;
95+
position: absolute;
96+
top: -1.4em;
97+
user-select: none;
98+
white-space: nowrap;
99+
}

packages/core/src/extensions/Blocks/nodes/Block.module.css

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ BASIC STYLES
1010
.blockContent {
1111
padding: 3px 0;
1212
transition: font-size 0.2s;
13-
/* display: inline-block; */
13+
/*
14+
because the content elements are display: block
15+
we use flex to position them next to list markers
16+
*/
17+
display: flex;
1418
}
1519

1620
.blockContent::before {
@@ -40,13 +44,17 @@ NESTED BLOCKS
4044
transition: all 0.2s 0.1s;
4145
}
4246

43-
[data-theme="light"] .blockGroup .blockGroup
44-
> .blockOuter:not([data-prev-depth-changed])::before {
45-
border-left: 1px solid #CCCCCC;
47+
[data-theme="light"]
48+
.blockGroup
49+
.blockGroup
50+
> .blockOuter:not([data-prev-depth-changed])::before {
51+
border-left: 1px solid #cccccc;
4652
}
4753

48-
[data-theme="dark"] .blockGroup .blockGroup
49-
> .blockOuter:not([data-prev-depth-changed])::before {
54+
[data-theme="dark"]
55+
.blockGroup
56+
.blockGroup
57+
> .blockOuter:not([data-prev-depth-changed])::before {
5058
border-left: 1px solid #999999;
5159
}
5260

@@ -217,10 +225,6 @@ NESTED BLOCKS
217225

218226
/* PLACEHOLDERS*/
219227

220-
.blockContent > :first-child {
221-
display: inline;
222-
}
223-
224228
.blockContent.isEmpty > :first-child:before,
225229
.blockContent.isFilter > :first-child:before {
226230
/*float: left; */
@@ -234,7 +238,7 @@ NESTED BLOCKS
234238

235239
[data-theme="light"] .blockContent.isEmpty > :first-child:before,
236240
.blockContent.isFilter > :first-child:before {
237-
color: #CCCCCC;
241+
color: #cccccc;
238242
}
239243

240244
[data-theme="dark"] .blockContent.isEmpty > :first-child:before,

packages/website/docs/.vitepress/config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ const SIDEBAR_DEFAULT = [
6767
text: "Advanced",
6868
collapsed: true,
6969
items: [
70-
{
71-
text: "Without React (vanilla JS)",
72-
link: "/docs/vanilla-js",
73-
},
7470
{
7571
text: "Real-time collaboration",
7672
link: "/docs/real-time-collaboration",
7773
},
74+
{
75+
text: "Without React (vanilla JS)",
76+
link: "/docs/vanilla-js",
77+
},
7878
],
7979
},
8080

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
.editor {
2-
height: 600px;
2+
height: 500px;
33
}

0 commit comments

Comments
 (0)