From 747e476fc7e6151398c5f407dc73b07d6ea41d1a Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Wed, 27 Sep 2023 01:39:33 +0200 Subject: [PATCH 01/13] maint: Use a different webpack dev server port than the default Patternslib one. --- webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webpack.config.js b/webpack.config.js index b41efa6..d13b877 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -31,6 +31,7 @@ module.exports = () => { if (process.env.NODE_ENV === "development") { config.devServer.static.directory = __dirname; + config.devServer.port = "3002"; } return config; From 97d4f92783a97468c99fef330cca03a65bfc19d9 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Thu, 3 Feb 2022 10:31:27 +0100 Subject: [PATCH 02/13] feat(Collaboration): Add collaboration support to allow multiple users to work on the same document. --- package.json | 7 ++++++- src/index.html | 2 ++ src/tiptap.js | 22 ++++++++++++++++++++-- src/toolbar.js | 9 +++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 88ea5bb..92c12f6 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,14 @@ "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-document": "^2.1.11", "@tiptap/extension-dropcursor": "^2.1.11", "@tiptap/extension-gapcursor": "^2.1.11", @@ -33,7 +35,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", diff --git a/src/index.html b/src/index.html index 1a66318..3d36202 100644 --- a/src/index.html +++ b/src/index.html @@ -151,6 +151,8 @@

TipTap basic example

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; " placeholder="Your poem goes here..." autocomplete="off" diff --git a/src/tiptap.js b/src/tiptap.js index 69cd0a7..2c0a210 100644 --- a/src/tiptap.js +++ b/src/tiptap.js @@ -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); @@ -25,6 +23,9 @@ 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); + // TODO: Remove with next major version. // BBB - Compatibility aliases parser.addAlias("context-menu-link", "link-menu"); @@ -129,6 +130,23 @@ class Pattern extends BasePattern { ); } + if (this.options.collaboration.server && this.options.collaboration.document) { + // Set up the Hocuspocus WebSocket provider + const HocuspocusProvider = (await import("@hocuspocus/provider")).HocuspocusProvider; // prettier-ignore + const provider = new HocuspocusProvider({ + url: this.options.collaboration.server, + name: this.options.collaboration.document, + }); + + // Collaboration extension + const Collaboration = ( + await import("@tiptap/extension-collaboration") + ).default.configure({ + document: provider.document, + }); + extra_extensions.push(Collaboration); + } + this.toolbar_el = this.options.toolbarExternal ? document.querySelector(this.options.toolbarExternal) : null; diff --git a/src/toolbar.js b/src/toolbar.js index 6992ab2..d35515e 100644 --- a/src/toolbar.js +++ b/src/toolbar.js @@ -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({ From 57b4af494ef0ad1efe639f91d1988582dd3c527a Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Thu, 3 Feb 2022 10:42:50 +0100 Subject: [PATCH 03/13] feat(Collaboration): Add collaboration cursor support to visualize who is working on what section in the same document. --- package.json | 1 + src/styles/collaboration-cursor.css | 26 ++++++++++++++++++++++++++ src/tiptap.js | 18 ++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 src/styles/collaboration-cursor.css diff --git a/package.json b/package.json index 92c12f6..c65b358 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@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", diff --git a/src/styles/collaboration-cursor.css b/src/styles/collaboration-cursor.css new file mode 100644 index 0000000..759cff9 --- /dev/null +++ b/src/styles/collaboration-cursor.css @@ -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; +} diff --git a/src/tiptap.js b/src/tiptap.js index 2c0a210..9c12ea9 100644 --- a/src/tiptap.js +++ b/src/tiptap.js @@ -25,6 +25,8 @@ 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); // TODO: Remove with next major version. // BBB - Compatibility aliases @@ -145,6 +147,22 @@ class Pattern extends BasePattern { document: provider.document, }); extra_extensions.push(Collaboration); + + // Collaboration cursor + if (window.__patternslib_import_styles) { + import("./styles/collaboration-cursor.css"); + } + const random_color = "#" + ((Math.random() * 0xffffff) << 0).toString(16); // See: https://css-tricks.com/snippets/javascript/random-hex-color/ + const CollaborationCursor = ( + await import("@tiptap/extension-collaboration-cursor") + ).default.configure({ + provider: provider, + user: { + name: this.options.collaboration.user || random_color, + color: this.options.collaboration.color || random_color, + }, + }); + extra_extensions.push(CollaborationCursor); } this.toolbar_el = this.options.toolbarExternal From 4825d5072b5ab2e4d4d25bfd9cec5e6e3c61ca82 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Thu, 3 Feb 2022 22:00:41 +0100 Subject: [PATCH 04/13] feat(Collaboration): Main connection gets text from textfield In our current implementation of the collaboration mode, there is a main connection. The main connection is the one first connecting to the collaboration server. This one reads the text from the textarea input field (or another strucutre) and passes it to the tiptap instance. If only the main connection updates the textdocument other clients connecting later will not overwrite the text. All clients are syncing changes back to the textarea input. This also means any client should be able to sumit the content back to the server. A future addition would be to get/set the text only through the collaboration server which always has the latest state. If this more sophisticated mehtod is really needed we will implement it later. --- src/tiptap.js | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/tiptap.js b/src/tiptap.js index 9c12ea9..829b18b 100644 --- a/src/tiptap.js +++ b/src/tiptap.js @@ -132,13 +132,43 @@ 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 provider = new HocuspocusProvider({ url: this.options.collaboration.server, name: this.options.collaboration.document, }); + provider.setAwarenessField("user", { + name: user_name, + color: user_color, + }); + + // Wait for user being authenticated + const authenticated = () => + new Promise((resolve) => + provider.on("authenticated", resolve, { once: true }) + ); + await authenticated(); + + const connected_users = [...provider.awareness.states.values()].map( + (it) => it.user + ); + if (connected_users.length === 1) { + // it's only me. + config["content"] = getText(); + log.info(` + This is the main instance and gets text from textfield. + Other connected user will get their text from the collaboration server. + `); + } // Collaboration extension const Collaboration = ( @@ -152,17 +182,19 @@ class Pattern extends BasePattern { if (window.__patternslib_import_styles) { import("./styles/collaboration-cursor.css"); } - const random_color = "#" + ((Math.random() * 0xffffff) << 0).toString(16); // See: https://css-tricks.com/snippets/javascript/random-hex-color/ const CollaborationCursor = ( await import("@tiptap/extension-collaboration-cursor") ).default.configure({ provider: provider, user: { - name: this.options.collaboration.user || random_color, - color: this.options.collaboration.color || random_color, + 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 @@ -189,7 +221,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()); @@ -213,6 +244,7 @@ class Pattern extends BasePattern { this.toolbar_el?.classList.remove("tiptap-focus"); }, autofocus: set_focus, + ...config, }); toolbar_ext.init_post({ app: this }); From c7dc06e2292544f1d839abc069e1d68de2b0da20 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Thu, 3 Feb 2022 22:01:41 +0100 Subject: [PATCH 05/13] feat(Collaboration): Authentication: pass authentication token to the collaboration server. --- src/index.html | 6 ++++++ src/tiptap.js | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/index.html b/src/index.html index 3d36202..4d1ecca 100644 --- a/src/index.html +++ b/src/index.html @@ -138,6 +138,11 @@

TipTap basic example

+