mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-26 06:31:07 +10:00
vite
* replace next with vite * disable strictmode (it interferes with collaboration in dev mode)
This commit is contained in:
161
client/src/features/editor/editor.tsx
Normal file
161
client/src/features/editor/editor.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import '@/features/editor/styles/editor.css';
|
||||
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import * as Y from 'yjs';
|
||||
import { EditorContent, useEditor } from '@tiptap/react';
|
||||
import { StarterKit } from '@tiptap/starter-kit';
|
||||
import { Placeholder } from '@tiptap/extension-placeholder';
|
||||
import { Collaboration } from '@tiptap/extension-collaboration';
|
||||
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor';
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
|
||||
import { authTokensAtom } from '@/features/auth/atoms/auth-tokens-atom';
|
||||
import useCollaborationUrl from '@/features/editor/hooks/use-collaboration-url';
|
||||
import { IndexeddbPersistence } from 'y-indexeddb';
|
||||
import { RichTextEditor } from '@mantine/tiptap';
|
||||
import { TextAlign } from '@tiptap/extension-text-align';
|
||||
import { Highlight } from '@tiptap/extension-highlight';
|
||||
import { Superscript } from '@tiptap/extension-superscript';
|
||||
import SubScript from '@tiptap/extension-subscript';
|
||||
import { Link } from '@tiptap/extension-link';
|
||||
import { Underline } from '@tiptap/extension-underline';
|
||||
|
||||
interface EditorProps {
|
||||
pageId: string,
|
||||
token?: string,
|
||||
}
|
||||
|
||||
const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D'];
|
||||
const getRandomElement = list => list[Math.floor(Math.random() * list.length)];
|
||||
const getRandomColor = () => getRandomElement(colors);
|
||||
|
||||
export default function Editor({ pageId }: EditorProps) {
|
||||
const [token] = useAtom(authTokensAtom);
|
||||
const collaborationURL = useCollaborationUrl();
|
||||
const [provider, setProvider] = useState<any>();
|
||||
const [yDoc] = useState(() => new Y.Doc());
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
const indexeddbProvider = new IndexeddbPersistence(pageId, yDoc)
|
||||
const provider = new HocuspocusProvider({
|
||||
url: collaborationURL,
|
||||
name: pageId,
|
||||
document: yDoc,
|
||||
token: token.accessToken,
|
||||
});
|
||||
|
||||
setProvider(provider);
|
||||
|
||||
return () => {
|
||||
provider.destroy();
|
||||
setProvider(null);
|
||||
indexeddbProvider.destroy();
|
||||
};
|
||||
}
|
||||
}, [pageId, token]);
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TiptapEditor ydoc={yDoc} provider={provider} />
|
||||
);
|
||||
}
|
||||
|
||||
interface TiptapEditorProps {
|
||||
ydoc: Y.Doc,
|
||||
provider: HocuspocusProvider
|
||||
}
|
||||
|
||||
function TiptapEditor({ ydoc, provider }: TiptapEditorProps) {
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
|
||||
const extensions = [
|
||||
StarterKit.configure({
|
||||
history: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: 'Write here',
|
||||
}),
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
}),
|
||||
CollaborationCursor.configure({
|
||||
provider,
|
||||
}),
|
||||
Underline,
|
||||
Link,
|
||||
Superscript,
|
||||
SubScript,
|
||||
Highlight,
|
||||
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
||||
];
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: extensions,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && currentUser.user) {
|
||||
editor.chain().focus().updateUser({ ...currentUser.user, color: getRandomColor() }).run();
|
||||
}
|
||||
}, [editor, currentUser.user]);
|
||||
|
||||
useEffect(() => {
|
||||
provider.on('status', event => {
|
||||
console.log(event);
|
||||
});
|
||||
|
||||
}, [provider]);
|
||||
|
||||
|
||||
return (
|
||||
<RichTextEditor editor={editor}>
|
||||
<RichTextEditor.Toolbar sticky stickyOffset={60}>
|
||||
<RichTextEditor.ControlsGroup>
|
||||
<RichTextEditor.Bold />
|
||||
<RichTextEditor.Italic />
|
||||
<RichTextEditor.Underline />
|
||||
<RichTextEditor.Strikethrough />
|
||||
<RichTextEditor.ClearFormatting />
|
||||
<RichTextEditor.Highlight />
|
||||
<RichTextEditor.Code />
|
||||
</RichTextEditor.ControlsGroup>
|
||||
|
||||
<RichTextEditor.ControlsGroup>
|
||||
<RichTextEditor.H1 />
|
||||
<RichTextEditor.H2 />
|
||||
<RichTextEditor.H3 />
|
||||
<RichTextEditor.H4 />
|
||||
</RichTextEditor.ControlsGroup>
|
||||
|
||||
<RichTextEditor.ControlsGroup>
|
||||
<RichTextEditor.Blockquote />
|
||||
<RichTextEditor.Hr />
|
||||
<RichTextEditor.BulletList />
|
||||
<RichTextEditor.OrderedList />
|
||||
<RichTextEditor.Subscript />
|
||||
<RichTextEditor.Superscript />
|
||||
</RichTextEditor.ControlsGroup>
|
||||
|
||||
<RichTextEditor.ControlsGroup>
|
||||
<RichTextEditor.Link />
|
||||
<RichTextEditor.Unlink />
|
||||
</RichTextEditor.ControlsGroup>
|
||||
|
||||
<RichTextEditor.ControlsGroup>
|
||||
<RichTextEditor.AlignLeft />
|
||||
<RichTextEditor.AlignCenter />
|
||||
<RichTextEditor.AlignJustify />
|
||||
<RichTextEditor.AlignRight />
|
||||
</RichTextEditor.ControlsGroup>
|
||||
</RichTextEditor.Toolbar>
|
||||
|
||||
<RichTextEditor.Content />
|
||||
</RichTextEditor>
|
||||
);
|
||||
}
|
||||
17
client/src/features/editor/hooks/use-collaboration-url.ts
Normal file
17
client/src/features/editor/hooks/use-collaboration-url.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
const useCollaborationURL = (): string => {
|
||||
const PATH = "/collaboration";
|
||||
|
||||
if (import.meta.env.VITE_COLLABORATION_URL) {
|
||||
return import.meta.env.VITE_COLLABORATION_URL + PATH;
|
||||
}
|
||||
|
||||
const API_URL = import.meta.env.VITE_BACKEND_API_URL;
|
||||
if (!API_URL) {
|
||||
throw new Error("Backend API URL is not defined");
|
||||
}
|
||||
|
||||
const wsProtocol = API_URL.startsWith('https') ? 'wss' : 'ws';
|
||||
return `${wsProtocol}://${API_URL.split('://')[1]}${PATH}`;
|
||||
};
|
||||
|
||||
export default useCollaborationURL;
|
||||
199
client/src/features/editor/styles/editor.css
Normal file
199
client/src/features/editor/styles/editor.css
Normal file
@@ -0,0 +1,199 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: rgba(#616161, 0.1);
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #0d0d0d;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
font-family: "JetBrainsMono", monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: #faf594;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid rgba(#0d0d0d, 0.1);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid rgba(#0d0d0d, 0.1);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor {
|
||||
background-color: #fff;
|
||||
border: 2px solid #0d0d0d;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 5px 5px #000;
|
||||
color: #0d0d0d;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 26rem;
|
||||
|
||||
&__header {
|
||||
align-items: center;
|
||||
border-bottom: 2px solid #0d0d0d;
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-top-right-radius: 0.25rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
&__users {
|
||||
color: rgba(#000, 0.8);
|
||||
display: flex;
|
||||
font-size: 0.85rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1 1 auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem 1rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Some information about the status */
|
||||
&__status {
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
|
||||
&::before {
|
||||
background: rgba(#0d0d0d, 0.5);
|
||||
border-radius: 50%;
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
height: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
&--connecting::before {
|
||||
background: #616161;
|
||||
}
|
||||
|
||||
&--connected::before {
|
||||
background: #b9f18d;
|
||||
}
|
||||
}
|
||||
|
||||
&__name button {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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: 0.75rem;
|
||||
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;
|
||||
}
|
||||
|
||||
.dots {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
background: #000;
|
||||
border-radius: 100%;
|
||||
height: 0.625rem;
|
||||
width: 0.625rem;
|
||||
}
|
||||
Reference in New Issue
Block a user