Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add collaboration support #6

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
"license": "MIT",
"main": "./src/tiptap.js",
"dependencies": {
"@hocuspocus/provider": "^2.5.0",
"@tiptap/core": "2.1.11",
"@tiptap/extension-blockquote": "^2.1.11",
"@tiptap/extension-bold": "^2.1.11",
"@tiptap/extension-bullet-list": "^2.1.11",
"@tiptap/extension-code": "^2.1.11",
"@tiptap/extension-code-block": "^2.1.11",
"@tiptap/extension-collaboration": "^2.1.11",
"@tiptap/extension-collaboration-cursor": "^2.1.11",
"@tiptap/extension-document": "^2.1.11",
"@tiptap/extension-dropcursor": "^2.1.11",
"@tiptap/extension-gapcursor": "^2.1.11",
Expand All @@ -33,7 +36,10 @@
"@tiptap/extension-table-row": "^2.1.11",
"@tiptap/extension-text": "^2.1.11",
"@tiptap/pm": "^2.1.11",
"@tiptap/suggestion": "^2.1.11"
"@tiptap/suggestion": "^2.1.11",
"y-prosemirror": "^1.2.1",
"y-protocols": "^1.0.6",
"yjs": "^13.6.8"
},
"devDependencies": {
"@patternslib/dev": "^3.5.1",
Expand Down
61 changes: 61 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import ExtBlockquote from "@tiptap/extension-blockquote";
import ExtBold from "@tiptap/extension-bold";
import ExtBulletList from "@tiptap/extension-bullet-list";
import ExtCode from "@tiptap/extension-code";
import ExtCodeBlock from "@tiptap/extension-code-block";
import ExtDocument from "@tiptap/extension-document";
import ExtEmbed from "./extensions/embed";
import ExtFigcaption from "./extensions/figcaption";
import ExtFigure from "./extensions/figure";
import ExtHardBreak from "@tiptap/extension-hard-break";
import ExtHeading from "./extensions/heading";
import ExtHorizontalRule from "@tiptap/extension-horizontal-rule";
import ExtImageFigure from "./extensions/image-figure";
import ExtImageInline from "./extensions/image-inline";
import ExtItalic from "@tiptap/extension-italic";
import ExtLink from "./extensions/link";
import ExtListItem from "@tiptap/extension-list-item";
import ExtOrderedList from "@tiptap/extension-ordered-list";
import ExtParagraph from "@tiptap/extension-paragraph";
import ExtStrike from "@tiptap/extension-strike";
import ExtTable from "@tiptap/extension-table";
import ExtTableCell from "@tiptap/extension-table-cell";
import ExtTableHeader from "@tiptap/extension-table-header";
import ExtTableRow from "@tiptap/extension-table-row";
import ExtText from "@tiptap/extension-text";

// source extension?

export const modes = {
"text/html": [
ExtBlockquote,
ExtBold,
ExtBulletList,
ExtCode,
ExtCodeBlock,
ExtDocument,
ExtEmbed,
ExtFigcaption,
ExtFigure,
ExtHardBreak,
ExtHeading,
ExtHorizontalRule,
ExtImageFigure,
ExtImageInline,
ExtItalic,
ExtLink,
ExtListItem,
ExtOrderedList,
ExtParagraph,
ExtStrike,
ExtTable,
ExtTableCell,
ExtTableHeader,
ExtTableRow,
ExtText,
],
"text/plain": [ExtDocument],
"text/markdown": [ExtDocument],
};

export default { modes };
2 changes: 1 addition & 1 deletion src/extensions/embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export const factory = () => {
parseHTML() {
return [
{
tag: `iframe[src*="youtube.com"]`,
tag: `iframe[src*="youtube.com"]`, // needs vimeo too??
getAttrs: (node) =>
(is_youtube(node.getAttribute("src")) ||
is_vimeo(node.getAttribute("src")) > -1) &&
Expand Down
8 changes: 8 additions & 0 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ <h2>TipTap basic example</h2>

</div>

<!--
NOTE: The following `collaboration-authentication-token` is a JWT token
that is a `{"user": "admin"}` object encoded with the secret
`supersecret`.
-->
<textarea
class="pat-tiptap pat-autofocus"
data-pat-tiptap="
Expand All @@ -151,6 +156,9 @@ <h2>TipTap basic example</h2>
link-menu: #context-menu-link;
mentions-menu: ./index-mentions-results.html?u=;
tags-menu: ./index-tags-results.html?okay=1234&q=;
collaboration-server: ws://127.0.0.1:1234;
collaboration-document: example-document;
collaboration-authentication-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.TFEeScRRa2dBzHLf_6cCkeouJkB8vsrRKaqvBzGMnVA;
"
placeholder="Your poem goes here..."
autocomplete="off"
Expand Down
26 changes: 26 additions & 0 deletions src/styles/collaboration-cursor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* Give a remote user a caret */
.collaboration-cursor__caret {
border-left: 1px solid #0d0d0d;
border-right: 1px solid #0d0d0d;
margin-left: -1px;
margin-right: -1px;
pointer-events: none;
position: relative;
word-break: normal;
}

/* 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;
position: absolute;
top: -1.4em;
user-select: none;
white-space: nowrap;
}
125 changes: 120 additions & 5 deletions src/tiptap.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import utils from "@patternslib/patternslib/src/core/utils";
export const log = logging.getLogger("tiptap");

export const parser = new Parser("tiptap");
parser.addArgument("collaboration-server", null);
parser.addArgument("collaboration-document", null);

parser.addArgument("toolbar-external", null);

Expand All @@ -25,12 +23,20 @@ parser.addArgument("link-menu", null);
parser.addArgument("mentions-menu", null);
parser.addArgument("tags-menu", null);

parser.addArgument("collaboration-server", null);
parser.addArgument("collaboration-document", null);
parser.addArgument("collaboration-user", null);
parser.addArgument("collaboration-color", null);
parser.addArgument("collaboration-authentication-token", null);

// TODO: Remove with next major version.
// BBB - Compatibility aliases
parser.addAlias("context-menu-link", "link-menu");
parser.addAlias("context-menu-mentions", "mentions-menu");
parser.addAlias("context-menu-tags", "tags-menu");

let collaboration_states = [];

class Pattern extends BasePattern {
static name = "tiptap";
static trigger = ".pat-tiptap";
Expand Down Expand Up @@ -85,8 +91,6 @@ class Pattern extends BasePattern {
(await import("@tiptap/extension-hard-break")).default.configure(),
// Gapcursor for images, tables etc to be able to add content below/above.
(await import("@tiptap/extension-gapcursor")).Gapcursor.configure(),
// Allways include undo/redo support via keyboard shortcuts.
(await import("@tiptap/extension-history")).History.configure(),
];
const placeholder = this.el.getAttribute("placeholder");
if (placeholder) {
Expand Down Expand Up @@ -129,6 +133,117 @@ class Pattern extends BasePattern {
);
}

const config = {};
if (this.options.collaboration.server && this.options.collaboration.document) {
// Random color, see: https://css-tricks.com/snippets/javascript/random-hex-color/
const random_color = "#" + ((Math.random() * 0xffffff) << 0).toString(16);
// Information about the current user
const user_name = this.options.collaboration.user || random_color;
const user_color = this.options.collaboration.color || random_color;

// Set up the Hocuspocus WebSocket provider
const HocuspocusProvider = (await import("@hocuspocus/provider")).HocuspocusProvider; // prettier-ignore
const YDoc = (await import("yjs")).Doc;
const y_doc = new YDoc();
const provider = new HocuspocusProvider({
url: this.options.collaboration.server,
name: this.options.collaboration.document,
document: y_doc,
token: this.options.collaboration["authentication-token"],
});

provider.on("awarenessUpdate", (state) => {
console.log(`awarenessUpdate`, state);
});

provider.on("awarenessChange", (states) => {
console.log(`awarenessChange`, states);
collaboration_states = states;
});

provider.setAwarenessField("user", {
name: user_name,
color: user_color,
document_name: this.options.collaboration.document,
});

// Wait for user being authenticated
const authenticated = () =>
new Promise((resolve) =>
provider.on("authenticated", resolve, { once: true })
);
await authenticated();

// Wait for user being authenticated
const synced = () =>
new Promise((resolve) => provider.on("synced", resolve, { once: true }));
await synced();

// Attempt 1: Only if the y_doc is the same as the document on the provider object,
// which I believe should be returned from the hocuspocus server.
// The problem here is, that also for the second connection with another browser
// returns true for the following if clause.
if (y_doc === provider.document) {
// Initialize the tiptap editor later with some initial content.
console.log("aha, ahsou");
config["content"] = getText();
}

// Attempt 2: Only for the first connecting user.
// The problem here is that I apparently do not reliably get the number of connected users.
// With two browsers connecting, one time I get 0 users, one time 1 user, another time 2.
const connected_users = [...provider.awareness.states.values()].map(
(it) => it.user
);
if (connected_users.length === 1) {
// Initialize the tiptap editor later with some initial content.
config["content"] = getText();
console.log("jojo, jo wou");
log.info(`
This is the main instance and gets text from textfield.
Other connected user will get their text from the collaboration server.
`);
}

console.log("provider");
console.log(provider);

console.log("collaboration states");
console.log(collaboration_states);

console.log("awareness states");
console.log(provider.awareness.states);

console.log("connected users");
console.log(connected_users);

// Collaboration extension
const Collaboration = (
await import("@tiptap/extension-collaboration")
).default.configure({
document: provider.document,
});
extra_extensions.push(Collaboration);

// Collaboration cursor
if (window.__patternslib_import_styles) {
import("./styles/collaboration-cursor.css");
}
const CollaborationCursor = (
await import("@tiptap/extension-collaboration-cursor")
).default.configure({
provider: provider,
user: {
name: user_name,
color: user_color,
},
});
extra_extensions.push(CollaborationCursor);
} else {
// Non-collaborative editing is always getting the initial text from the textarea.
config["content"] = getText();
}

this.toolbar_el = this.options.toolbarExternal
? document.querySelector(this.options.toolbarExternal)
: null;
Expand All @@ -153,7 +268,6 @@ class Pattern extends BasePattern {
...(await toolbar_ext.init_extensions({ app: this })),
...extra_extensions,
],
content: getText(),
onUpdate() {
// Note: ``this`` is the editor instance.
setText(this.getHTML());
Expand All @@ -177,6 +291,7 @@ class Pattern extends BasePattern {
this.toolbar_el?.classList.remove("tiptap-focus");
},
autofocus: set_focus,
...config,
});
toolbar_ext.init_post({ app: this });

Expand Down
9 changes: 9 additions & 0 deletions src/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ export async function init_extensions({ app }) {
);
}

if (
!(app.options.collaboration.server && app.options.collaboration.document) &&
(tb.undo || tb.redo)
) {
// Do not initialize this with the collaboration feature turned on.
// The collaboration extension comeswith it's own history handling.
extensions.push((await import("@tiptap/extension-history")).History);
}

if (tb.link) {
extensions.push(
(await import("./extensions/link")).factory().configure({
Expand Down
1 change: 1 addition & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module.exports = () => {

if (process.env.NODE_ENV === "development") {
config.devServer.static.directory = __dirname;
config.devServer.port = "3002";
}

return config;
Expand Down
Loading