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

Added collaboration support in the editor #1213

Merged
merged 12 commits into from
Sep 5, 2024
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@
"@tiptap/extension-table-row": "2.5.9",
"@tiptap/extension-text-style": "2.5.9",
"@tiptap/extension-underline": "2.5.9",
"@tiptap/extension-collaboration": "^2.6.5",
"@tiptap/extension-collaboration-cursor": "^2.6.5",
"@tiptap/pm": "2.5.9",
"@tiptap/react": "2.5.9",
"@tiptap/starter-kit": "2.5.9",
Expand Down Expand Up @@ -186,7 +188,11 @@
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.9.3",
"yup": "1.3.2",
"zustand": "4.3.2"
"zustand": "4.3.2",
"@hocuspocus/provider": "^2.13.5",
"y-prosemirror": "^1.2.12",
"y-protocols": "^1.0.6",
"yjs": "^13.6.18"
},
"peerDependencies": {
"@babel/runtime": "7.23.2",
Expand Down Expand Up @@ -228,6 +234,7 @@
"tippy.js": "6.3.7",
"util": "0.12.5",
"yup": "^0.32.11",
"yjs": "^13.6.18",
"zustand": "4.3.2"
},
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Document from "@tiptap/extension-document";
import { Plugin, PluginKey } from "prosemirror-state";
import { Plugin, PluginKey } from "@tiptap/pm/state";

export default Document.extend({
content: "root",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Node, mergeAttributes, PasteRule } from "@tiptap/core";
import { TextSelection } from "@tiptap/pm/state";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { TextSelection } from "prosemirror-state";

import { COMBINED_REGEX } from "common/constants";

import EmbedComponent from "./EmbedComponent";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Node } from "@tiptap/core";
import { PluginKey } from "@tiptap/pm/state";
import { ReactRenderer } from "@tiptap/react";
import Suggestion from "@tiptap/suggestion";
import { PluginKey } from "prosemirror-state";
import tippy from "tippy.js";

import EmojiPickerMenu from "./EmojiPickerMenu";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { PluginKey } from "@tiptap/pm/state";
import { ReactRenderer } from "@tiptap/react";
import Suggestion from "@tiptap/suggestion";
import { PluginKey } from "prosemirror-state";
import { isNil } from "ramda";
import tippy from "tippy.js";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { Plugin } from "@tiptap/pm/state";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { t } from "i18next";
import { globalProps } from "neetocommons/initializers";
import { Toastr } from "neetoui";
import { Plugin } from "prosemirror-state";
import { isEmpty } from "ramda";

import { DIRECT_UPLOAD_ENDPOINT } from "src/common/constants";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Extension } from "@tiptap/core";
import { Plugin } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { t } from "i18next";
import { Plugin } from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";

const Placeholder = Extension.create({
name: "placeholder",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Document from "@tiptap/extension-document";
import { Plugin, PluginKey } from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";

export default Document.extend({
name: "selectionDecorator",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Extension } from "@tiptap/core";
import { PluginKey } from "@tiptap/pm/state";
import Suggestion from "@tiptap/suggestion";
import { PluginKey } from "prosemirror-state";

export const CommandsPluginKey = new PluginKey("commands");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Extension } from "@tiptap/core";
import { PluginKey } from "@tiptap/pm/state";
import { ReactRenderer } from "@tiptap/react";
import Suggestion from "@tiptap/suggestion";
import { PluginKey } from "prosemirror-state";
import { isEmpty } from "ramda";
import tippy from "tippy.js";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mergeAttributes } from "@tiptap/core";
import Mention from "@tiptap/extension-mention";
import { PluginKey } from "@tiptap/pm/state";
import { ReactRenderer } from "@tiptap/react";
import { PluginKey } from "prosemirror-state";
import tippy from "tippy.js";

import { MentionList } from "../Mention/MentionList";
Expand Down
2 changes: 1 addition & 1 deletion src/components/Editor/CustomExtensions/Table/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CellSelection } from "@tiptap/pm/tables";
import { t } from "i18next";
import { isNotPresent } from "neetocist";
import {
Expand All @@ -9,7 +10,6 @@ import {
MergeSplit,
ToggleHeaderRow,
} from "neetoicons";
import { CellSelection } from "prosemirror-tables";

const shouldShowMergeCellToggler = selection => {
if (isNotPresent(selection)) return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { PluginKey } from "prosemirror-state";
import { PluginKey } from "@tiptap/pm/state";

const VariablePluginKey = new PluginKey("variables");
const Variable = Node.create({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import CharacterCount from "@tiptap/extension-character-count";
import Code from "@tiptap/extension-code";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import Color from "@tiptap/extension-color";
import Document from "@tiptap/extension-document";
import Focus from "@tiptap/extension-focus";
Expand Down Expand Up @@ -48,6 +50,7 @@ const useCustomExtensions = ({
openImageInNewTab,
openLinkInNewTab,
enableReactNodeViewOptimization,
collaborationProvider,
}) => {
let customExtensions = [
CharacterCount,
Expand Down Expand Up @@ -78,6 +81,7 @@ const useCustomExtensions = ({
blockquote: options.includes(EDITOR_OPTIONS.BLOCKQUOTE),
orderedList: options.includes(EDITOR_OPTIONS.LIST_ORDERED),
bulletList: options.includes(EDITOR_OPTIONS.LIST_BULLETS),
history: !collaborationProvider,
}),
TextStyle,
Underline,
Expand Down Expand Up @@ -127,6 +131,20 @@ const useCustomExtensions = ({
customExtensions.push(Variable);
}

if (collaborationProvider) {
customExtensions.push(
Collaboration.configure({
document: collaborationProvider.document,
})
);

customExtensions.push(
CollaborationCursor.configure({
provider: collaborationProvider,
})
);
}

customExtensions = customExtensions.concat(extensions);

return customExtensions;
Expand Down
4 changes: 3 additions & 1 deletion src/components/Editor/index.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { forwardRef, useImperativeHandle, useState, useRef, memo } from "react";

import { EditorView } from "@tiptap/pm/view";
import { useEditor, EditorContent, useEditorState } from "@tiptap/react";
import classnames from "classnames";
import { EDITOR_OPTIONS } from "common/constants";
import { noop, slugify } from "neetocist";
import { useFuncDebounce } from "neetocommons/react-utils";
import { Label } from "neetoui";
import { EditorView } from "prosemirror-view";

import ErrorWrapper from "components/Common/ErrorWrapper";
import useEditorWarnings from "hooks/useEditorWarnings";
Expand Down Expand Up @@ -66,6 +66,7 @@ const Editor = (
children,
openImageInNewTab = true,
openLinkInNewTab = true,
collaborationProvider = null,
enableReactNodeViewOptimization = false,
...otherProps
},
Expand Down Expand Up @@ -119,6 +120,7 @@ const Editor = (
openImageInNewTab,
openLinkInNewTab,
enableReactNodeViewOptimization,
collaborationProvider,
});
useEditorWarnings({ initialValue });

Expand Down
11 changes: 4 additions & 7 deletions src/components/Editor/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Slice, Fragment, Node } from "@tiptap/pm/model";
import { Selection } from "@tiptap/pm/state";
import { isNotEmpty } from "neetocist";
import { Slice, Fragment, Node } from "prosemirror-model";
import { Selection } from "prosemirror-state";

import { URL_REGEXP } from "src/common/constants";

Expand Down Expand Up @@ -47,14 +47,11 @@ export const setInitialPosition = editor => {
view.dispatch(transaction);
};

export const isEditorOverlaysActive = () => {
const active = document.querySelector(
export const isEditorOverlaysActive = () =>
document.querySelector(
".ne-media-uploader,.ne-embed-modal,.tippy-content,.ne-link-popover"
);

return active;
};

export const validateAndFormatUrl = url => {
if (!URL_REGEXP.test(url)) {
return null;
Expand Down
1 change: 1 addition & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@import "./styles/editor/emoji";
@import "./styles/editor/table";
@import "./styles/editor/link-popover";
@import "./styles/editor/collaboration-cursor";
@import "./styles/components/attachments";

.ProseMirror {
Expand Down
28 changes: 28 additions & 0 deletions src/styles/editor/_collaboration-cursor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.tiptap {
/* 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;
}
}
1 change: 1 addition & 0 deletions src/styles/editor/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
@import "./emoji";
@import "./table";
@import "./link-popover";
@import "./collaboration-cursor";

.ProseMirror {
overflow-y: auto;
Expand Down
5 changes: 5 additions & 0 deletions stories/API-Reference/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ export const EDITOR_PROPS = [
"Improvises the editor performance by removing the images and code block components from the UI if they are out of the view port.",
"true",
],
[
"collaborationProvider",
"Provider to enable collaborative editing in the editor.",
"null",
],
];

export const UTILITIES_TABLE_COLUMNS = ["Name", "Arguments", "Description"];
Expand Down
96 changes: 96 additions & 0 deletions stories/Examples/Collaborative-editor.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Meta, Canvas } from "@storybook/addon-docs";
import { Editor } from "../../src";
import CollaborativeNeetoEditor from "./CollaborativeNeetoEditor";

<Meta
title="Examples/Collaborative editor"
parameters={{
layout: "padded",
previewTabs: {
canvas: {
hidden: true,
},
},
}}
component={Editor}
/>

# Collaborative editor

Open current page in two different browser.

Check [documentation for provider](https://tiptap.dev/docs/hocuspocus/provider/introduction)

<Canvas>
<CollaborativeNeetoEditor />
</Canvas>

```jsx
import React, { useCallback, useState, useEffect } from "react";

import { TiptapCollabProvider } from "@hocuspocus/provider";
import { findBy } from "neetocist";
import * as Y from "yjs";

import { Editor } from "src";

const CollaborativeNeetoEditor = () => {
const doc = new Y.Doc();
const [editor, setEditor] = useState(null);
const editorRef = useCallback(node => {
if (node) setEditor(node.editor);
}, []);

const provider = new TiptapCollabProvider({
name: "neeto-editor_document.name_new",
appId: "7j9y6m10",
token: "notoken",
document: doc,
});
const sampleNames = ["Sam", "Alex", "Liam", "Olivia", "Emma", "Oliver"];

const sampleColors = [
"#F56565",
"#ED64A6",
"#68D391",
"#63B3ED",
"#F6E05E",
"#FC8181",
];

const getRandom = list => {
const randomIndex = Math.floor(Math.random() * list.length);

return list[randomIndex];
};

useEffect(() => {
if (!editor) return;
const collaborationCursorExtension = findBy(
{ name: "collaborationCursor" },
editor.extensionManager.extensions
);

if (collaborationCursorExtension) {
const newUser = {
name: getRandom(sampleNames),
color: getRandom(sampleColors),
};
editor.commands.updateUser(newUser);
}
}, [editor]);

return (
<div className="space-y-4">
<Editor
autoFocus
collaborationProvider={provider}
contentClassName="border"
ref={editorRef}
/>
</div>
);
};

export default CollaborativeNeetoEditor;
```
Loading
Loading