Merge branch 'main' into develop

# Conflicts:
#	CHANGELOG.md
#	demos/CHANGELOG.md
#	demos/package-lock.json
#	demos/package.json
#	demos/src/Examples/CollaborativeEditing/React/index.jsx
#	lerna.json
#	package-lock.json
#	packages/core/CHANGELOG.md
#	packages/core/package.json
#	packages/extension-blockquote/CHANGELOG.md
#	packages/extension-blockquote/package.json
#	packages/extension-bold/CHANGELOG.md
#	packages/extension-bold/package.json
#	packages/extension-bubble-menu/CHANGELOG.md
#	packages/extension-bubble-menu/package.json
#	packages/extension-bullet-list/CHANGELOG.md
#	packages/extension-bullet-list/package.json
#	packages/extension-character-count/CHANGELOG.md
#	packages/extension-character-count/package.json
#	packages/extension-code-block-lowlight/CHANGELOG.md
#	packages/extension-code-block-lowlight/package.json
#	packages/extension-code-block/CHANGELOG.md
#	packages/extension-code-block/package.json
#	packages/extension-code/CHANGELOG.md
#	packages/extension-code/package.json
#	packages/extension-collaboration-cursor/CHANGELOG.md
#	packages/extension-collaboration-cursor/package.json
#	packages/extension-collaboration/CHANGELOG.md
#	packages/extension-collaboration/package.json
#	packages/extension-color/CHANGELOG.md
#	packages/extension-color/package.json
#	packages/extension-document/CHANGELOG.md
#	packages/extension-document/package.json
#	packages/extension-dropcursor/CHANGELOG.md
#	packages/extension-dropcursor/package.json
#	packages/extension-floating-menu/CHANGELOG.md
#	packages/extension-floating-menu/package.json
#	packages/extension-focus/CHANGELOG.md
#	packages/extension-focus/package.json
#	packages/extension-font-family/CHANGELOG.md
#	packages/extension-font-family/package.json
#	packages/extension-gapcursor/CHANGELOG.md
#	packages/extension-gapcursor/package.json
#	packages/extension-hard-break/CHANGELOG.md
#	packages/extension-hard-break/package.json
#	packages/extension-heading/CHANGELOG.md
#	packages/extension-heading/package.json
#	packages/extension-highlight/CHANGELOG.md
#	packages/extension-highlight/package.json
#	packages/extension-history/CHANGELOG.md
#	packages/extension-history/package.json
#	packages/extension-horizontal-rule/CHANGELOG.md
#	packages/extension-horizontal-rule/package.json
#	packages/extension-image/CHANGELOG.md
#	packages/extension-image/package.json
#	packages/extension-italic/CHANGELOG.md
#	packages/extension-italic/package.json
#	packages/extension-link/CHANGELOG.md
#	packages/extension-link/package.json
#	packages/extension-list-item/CHANGELOG.md
#	packages/extension-list-item/package.json
#	packages/extension-mention/CHANGELOG.md
#	packages/extension-mention/package.json
#	packages/extension-ordered-list/CHANGELOG.md
#	packages/extension-ordered-list/package.json
#	packages/extension-paragraph/CHANGELOG.md
#	packages/extension-paragraph/package.json
#	packages/extension-placeholder/CHANGELOG.md
#	packages/extension-placeholder/package.json
#	packages/extension-strike/CHANGELOG.md
#	packages/extension-strike/package.json
#	packages/extension-subscript/CHANGELOG.md
#	packages/extension-subscript/package.json
#	packages/extension-superscript/CHANGELOG.md
#	packages/extension-superscript/package.json
#	packages/extension-table-cell/CHANGELOG.md
#	packages/extension-table-cell/package.json
#	packages/extension-table-header/CHANGELOG.md
#	packages/extension-table-header/package.json
#	packages/extension-table-row/CHANGELOG.md
#	packages/extension-table-row/package.json
#	packages/extension-table/CHANGELOG.md
#	packages/extension-table/package.json
#	packages/extension-task-item/CHANGELOG.md
#	packages/extension-task-item/package.json
#	packages/extension-task-list/CHANGELOG.md
#	packages/extension-task-list/package.json
#	packages/extension-text-align/CHANGELOG.md
#	packages/extension-text-align/package.json
#	packages/extension-text-style/CHANGELOG.md
#	packages/extension-text-style/package.json
#	packages/extension-text/CHANGELOG.md
#	packages/extension-text/package.json
#	packages/extension-typography/CHANGELOG.md
#	packages/extension-typography/package.json
#	packages/extension-underline/CHANGELOG.md
#	packages/extension-underline/package.json
#	packages/extension-youtube/CHANGELOG.md
#	packages/extension-youtube/package.json
#	packages/html/CHANGELOG.md
#	packages/html/package.json
#	packages/pm/CHANGELOG.md
#	packages/pm/package.json
#	packages/react/CHANGELOG.md
#	packages/react/package.json
#	packages/starter-kit/CHANGELOG.md
#	packages/starter-kit/package.json
#	packages/suggestion/CHANGELOG.md
#	packages/suggestion/package.json
#	packages/vue-2/CHANGELOG.md
#	packages/vue-2/package.json
#	packages/vue-3/CHANGELOG.md
#	packages/vue-3/package.json
This commit is contained in:
svenadlung 2023-07-17 13:03:44 +02:00
commit bdc51d12b5
76 changed files with 1067 additions and 53 deletions

View File

@ -1,5 +1,7 @@
module.exports = { module.exports = {
plugins: { plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },

View File

@ -17,7 +17,7 @@ const sendData = (eventName: string, data: any) => {
export function splitName(name: string) { export function splitName(name: string) {
const parts = name.split('/') const parts = name.split('/')
if (parts.length !== 2) { if (parts.length !== 3) {
throw Error('Demos must always be within exactly one category. Nested categories are not supported.') throw Error('Demos must always be within exactly one category. Nested categories are not supported.')
} }

View File

@ -11,15 +11,28 @@ export default function init(name: string, source: any) {
window.source = source window.source = source
document.title = name document.title = name
const [demoCategory, demoName] = splitName(name) const [demoCategory, demoName, frameworkName] = splitName(name)
import(`../src/${demoCategory}/${demoName}/React/index.jsx`) import(`../src/${demoCategory}/${demoName}/${frameworkName}/index.tsx`)
.then(module => { .then(module => {
const root = document.getElementById('app') const root = document.getElementById('app')
if (root) { if (root) {
createRoot(root).render(React.createElement(module.default)) createRoot(root)
.render(React.createElement(module.default))
} }
debug() debug()
}) })
.catch(() => {
import(`../src/${demoCategory}/${demoName}/${frameworkName}/index.jsx`)
.then(module => {
const root = document.getElementById('app')
if (root) {
createRoot(root)
.render(React.createElement(module.default))
}
debug()
})
})
} }

View File

@ -8,9 +8,9 @@ export default function init(name: string, source: any) {
window.source = source window.source = source
document.title = name document.title = name
const [demoCategory, demoName] = splitName(name) const [demoCategory, demoName, frameworkName] = splitName(name)
import(`../src/${demoCategory}/${demoName}/Svelte/index.svelte`) import(`../src/${demoCategory}/${demoName}/${frameworkName}/index.svelte`)
.then(Module => { .then(Module => {
const Component = Module.default const Component = Module.default

View File

@ -10,9 +10,9 @@ export default function init(name: string, source: any) {
window.source = source window.source = source
document.title = name document.title = name
const [demoCategory, demoName] = splitName(name) const [demoCategory, demoName, frameworkName] = splitName(name)
import(`../src/${demoCategory}/${demoName}/Vue/index.vue`) import(`../src/${demoCategory}/${demoName}/${frameworkName}/index.vue`)
.then(module => { .then(module => {
createApp(module.default).mount('#app') createApp(module.default).mount('#app')
debug() debug()

View File

@ -1,4 +1,3 @@
/* eslint-disable */
import './styles.scss' import './styles.scss'
import { TiptapCollabProvider } from '@hocuspocus/provider' import { TiptapCollabProvider } from '@hocuspocus/provider'
@ -16,13 +15,8 @@ import React, {
} from 'react' } from 'react'
import * as Y from 'yjs' import * as Y from 'yjs'
import { variables } from '../../../variables.js' import { variables } from '../../../variables'
import MenuBar from './MenuBar.jsx' import MenuBar from './MenuBar'
const content = `
<h1>Hello <s>World</s> Tiptap!</h1>
<p>Build the next <mark>gold standard in content editing</mark> experiences that your competitors can´t match.</p>
`
const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D'] const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D']
const names = [ const names = [
@ -84,10 +78,9 @@ export default () => {
const [currentUser, setCurrentUser] = useState(getInitialUser) const [currentUser, setCurrentUser] = useState(getInitialUser)
const editor = useEditor({ const editor = useEditor({
content,
extensions: [ extensions: [
StarterKit.configure({ StarterKit.configure({
history: true, history: false,
}), }),
Highlight, Highlight,
TaskList, TaskList,
@ -95,12 +88,12 @@ export default () => {
CharacterCount.configure({ CharacterCount.configure({
limit: 10000, limit: 10000,
}), }),
/* Collaboration.configure({ Collaboration.configure({
document: ydoc, document: ydoc,
}), }),
CollaborationCursor.configure({ CollaborationCursor.configure({
provider: websocketProvider, provider: websocketProvider,
}), */ }),
], ],
}) })
@ -115,7 +108,7 @@ export default () => {
useEffect(() => { useEffect(() => {
if (editor && currentUser) { if (editor && currentUser) {
localStorage.setItem('currentUser', JSON.stringify(currentUser)) localStorage.setItem('currentUser', JSON.stringify(currentUser))
// editor.chain().focus().updateUser(currentUser).run() editor.chain().focus().updateUser(currentUser).run()
} }
}, [editor, currentUser]) }, [editor, currentUser])
@ -131,7 +124,7 @@ export default () => {
<div className="editor"> <div className="editor">
{editor && <MenuBar editor={editor} />} {editor && <MenuBar editor={editor} />}
<EditorContent className="editor__content" editor={editor} /> <EditorContent className="editor__content" editor={editor} />
{/* <div className="editor__footer"> <div className="editor__footer">
<div className={`editor__status editor__status--${status}`}> <div className={`editor__status editor__status--${status}`}>
{status === 'connected' {status === 'connected'
? `${editor.storage.collaborationCursor.users.length} user${editor.storage.collaborationCursor.users.length === 1 ? '' : 's'} online in ${room}` ? `${editor.storage.collaborationCursor.users.length} user${editor.storage.collaborationCursor.users.length === 1 ? '' : 's'} online in ${room}`
@ -140,7 +133,7 @@ export default () => {
<div className="editor__name"> <div className="editor__name">
<button onClick={setName}>{currentUser.name}</button> <button onClick={setName}>{currentUser.name}</button>
</div> </div>
</div> */} </div>
</div> </div>
) )
} }

View File

@ -0,0 +1,15 @@
import './styles.css'
import React, { useState } from 'react'
import { TNote } from './types'
export default ({ note }: { note: TNote }) => {
const [modelValue, setModelValue] = useState(note.content)
return (
<textarea onChange={e => setModelValue(e.target.value)} value={modelValue}
className="p-2 border border-black rounded-lg"
></textarea>
)
}

View File

@ -0,0 +1,26 @@
import './styles.css'
import React from 'react'
import Note from './Note'
import { TNote } from './types'
const notes: TNote[] = [
{
id: 'note-1',
content: 'some random note text',
},
{
id: 'note-2',
content: 'some really random note text',
},
]
export default () => {
return (
<div>
{notes.map(note => <Note note={note} key={note.id}/>)}
</div>
)
}

View File

@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View File

@ -0,0 +1,4 @@
export type TNote = {
id: string;
content: string;
};

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { TNote } from './types'
const props = defineProps<{ note: TNote }>()
const modelValueProxy = ref('')
watch(props, () => modelValueProxy.value = props.note?.content, {
immediate: true,
})
</script>
<template>
<textarea v-model="modelValueProxy" class="p-2 border border-black rounded-lg"></textarea>
</template>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import './styles.css'
import Note from './Note.vue'
import { TNote } from './types'
const notes: TNote[] = [
{ id: 'note-1', content: 'some random note text' },
{ id: 'note-2', content: 'some really random note text' },
]
</script>
<template>
<div class="p-3">
<div v-for="note in notes" :key="note.id">
<Note :note="note"/>
</div>
</div>
</template>

View File

@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View File

@ -0,0 +1,4 @@
export type TNote = {
id: string;
content: string;
};

View File

@ -0,0 +1,31 @@
import './styles.css'
import { EditorContent, useEditor } from '@tiptap/react'
import { StarterKit } from '@tiptap/starter-kit'
import React, { useState } from 'react'
import { TNote } from './types'
export default ({ note }: { note: TNote }) => {
const [modelValue, setModelValue] = useState(note.content)
const editor = useEditor({
content: modelValue,
editorProps: {
attributes: {
class: 'm-2 p-2 border border-black rounded-lg',
},
},
onUpdate() {
setModelValue(editor?.getText() ?? '')
},
extensions: [
StarterKit,
],
})
return (
// @ts-ignore
<EditorContent editor={editor}/>
)
}

View File

@ -0,0 +1,26 @@
import './styles.css'
import React from 'react'
import Note from './Note'
import { TNote } from './types'
const notes: TNote[] = [
{
id: 'note-1',
content: 'some random note text',
},
{
id: 'note-2',
content: 'some really random note text',
},
]
export default () => {
return (
<div>
{notes.map(note => <Note note={note} key={note.id}/>)}
</div>
)
}

View File

@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View File

@ -0,0 +1,4 @@
export type TNote = {
id: string;
content: string;
};

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import StarterKit from '@tiptap/starter-kit'
import { EditorContent, useEditor } from '@tiptap/vue-3'
import { ref, watch } from 'vue'
import type { TNote } from './types'
const props = defineProps<{note: TNote}>()
const modelValueProxy = ref('')
watch(props, () => modelValueProxy.value = props.note.content)
const editor = useEditor({
content: props.note.content,
editorProps: {
attributes: {
class: 'm-2 p-2 border border-black rounded-lg',
},
},
extensions: [
StarterKit,
],
})
</script>
<template>
<editor-content :editor="editor"></editor-content>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import './styles.css'
import Note from './Note.vue'
import { TNote } from './types'
const notes: TNote[] = [
{ id: 'note-1', content: 'some random note text' },
{ id: 'note-2', content: 'some really random note text' },
]
</script>
<template>
<div v-for="note in notes" :key="note.id">
<Note :note="note"/>
</div>
</template>

View File

@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View File

@ -0,0 +1,4 @@
export type TNote = {
id: string;
content: string;
};

View File

@ -0,0 +1,36 @@
import './styles.css'
import { InitialConfigType, LexicalComposer } from '@lexical/react/LexicalComposer'
import { ContentEditable } from '@lexical/react/LexicalContentEditable'
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
import { EditorState } from 'lexical/LexicalEditorState'
import React, { useRef } from 'react'
import { TNote } from './types'
export default ({ note }: { note: TNote }) => {
const editorStateRef = useRef<EditorState>()
const initialConfig: InitialConfigType = {
onError(error: Error): void {
throw error
},
namespace: 'myeditor',
editable: true,
}
return (
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin
contentEditable={<ContentEditable/>}
placeholder={<p>{note.content}</p>}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin/>
<OnChangePlugin onChange={editorState => editorStateRef.current = editorState}/>
</LexicalComposer>
)
}

View File

@ -0,0 +1,26 @@
import './styles.css'
import React from 'react'
import Note from './Note'
import { TNote } from './types'
const notes: TNote[] = [
{
id: 'note-1',
content: 'some random note text',
},
{
id: 'note-2',
content: 'some really random note text',
},
]
export default () => {
return (
<div>
{notes.map(note => <Note note={note} key={note.id}/>)}
</div>
)
}

View File

@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View File

@ -0,0 +1,4 @@
export type TNote = {
id: string;
content: string;
};

View File

@ -0,0 +1,35 @@
import './styles.css'
import { Collaboration } from '@tiptap/extension-collaboration'
import { EditorContent, useEditor } from '@tiptap/react'
import { StarterKit } from '@tiptap/starter-kit'
import React from 'react'
import * as Y from 'yjs'
import { TNote } from './types'
export default ({ note }: { note: TNote }) => {
const doc = new Y.Doc()
const editor = useEditor({
content: note.defaultContent,
editorProps: {
attributes: {
class: 'm-2 p-2 border border-black rounded-lg',
},
},
extensions: [
StarterKit.configure({
history: false, // important because history will now be handled by Y.js
}),
Collaboration.configure({
document: doc,
}),
],
})
return (
// @ts-ignore
<EditorContent editor={editor}/>
)
}

View File

@ -0,0 +1,26 @@
import './styles.css'
import React from 'react'
import Note from './Note'
import { TNote } from './types'
const notes: TNote[] = [
{
id: 'note-1',
defaultContent: 'some random note text',
},
{
id: 'note-2',
defaultContent: 'some really random note text',
},
]
export default () => {
return (
<div>
{notes.map(note => <Note note={note} key={note.id}/>)}
</div>
)
}

View File

@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View File

@ -0,0 +1,4 @@
export type TNote = {
id: string;
defaultContent: string;
};

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import { Collaboration } from '@tiptap/extension-collaboration'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent, useEditor } from '@tiptap/vue-3'
import * as Y from 'yjs'
import type { TNote } from './types'
const props = defineProps<{note: TNote}>()
const doc = new Y.Doc()
const editor = useEditor({
content: props.note.defaultContent,
editorProps: {
attributes: {
class: 'm-2 p-2 border border-black rounded-lg',
},
},
extensions: [
StarterKit.configure({
history: false, // important because history will now be handled by Y.js
}),
Collaboration.configure({
document: doc,
}),
],
})
</script>
<template>
<editor-content :editor="editor"></editor-content>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import './styles.css'
import Note from './Note.vue'
import { TNote } from './types'
const notes: TNote[] = [
{ id: 'note-1', defaultContent: 'some random note text' },
{ id: 'note-2', defaultContent: 'some really random note text' },
]
</script>
<template>
<div v-for="note in notes" :key="note.id">
<Note :note="note"/>
</div>
</template>

View File

@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View File

@ -0,0 +1,4 @@
export type TNote = {
id: string;
defaultContent: string;
};

View File

@ -0,0 +1,52 @@
import './styles.css'
import { TiptapCollabProvider } from '@hocuspocus/provider'
import { CollaborationPlugin } from '@lexical/react/LexicalCollaborationPlugin'
import { InitialConfigType, LexicalComposer } from '@lexical/react/LexicalComposer'
import { ContentEditable } from '@lexical/react/LexicalContentEditable'
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
import React from 'react'
import * as Y from 'yjs'
import { TNote } from './types'
export default ({ note }: { note: TNote }) => {
const initialConfig: InitialConfigType = {
onError(error: Error): void {
throw error
},
namespace: 'myeditor',
editable: true,
}
return (
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin
contentEditable={<ContentEditable/>}
placeholder={<p>{note.defaultContent}</p>}
ErrorBoundary={LexicalErrorBoundary}
/>
<CollaborationPlugin
id={note.id}
key={note.id}
// @ts-ignore
providerFactory={(id, yjsDocMap) => {
const doc = new Y.Doc()
yjsDocMap.set(id, doc)
const provider = new TiptapCollabProvider({
name: note.id, // any identifier - all connections sharing the same identifier will be synced
appId: '7j9y6m10', // replace with YOUR_APP_ID
token: 'notoken', // replace with your JWT
document: doc,
})
return provider
}}
shouldBootstrap={true}
/>
</LexicalComposer>
)
}

View File

@ -0,0 +1,26 @@
import './styles.css'
import React from 'react'
import Note from './Note'
import { TNote } from './types'
const notes: TNote[] = [
{
id: 'note-1',
defaultContent: 'some random note text',
},
{
id: 'note-2',
defaultContent: 'some really random note text',
},
]
export default () => {
return (
<div>
{notes.map(note => <Note note={note} key={note.id}/>)}
</div>
)
}

View File

@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View File

@ -0,0 +1,4 @@
export type TNote = {
id: string;
defaultContent: string;
};

View File

@ -0,0 +1,52 @@
import './styles.css'
import { TiptapCollabProvider } from '@hocuspocus/provider'
import { Collaboration } from '@tiptap/extension-collaboration'
import { EditorContent, useEditor } from '@tiptap/react'
import { StarterKit } from '@tiptap/starter-kit'
import React, { useEffect } from 'react'
import * as Y from 'yjs'
import { TNote } from './types'
export default ({ note }: { note: TNote }) => {
const doc = new Y.Doc()
useEffect(() => {
const provider = new TiptapCollabProvider({
name: note.id, // any identifier - all connections sharing the same identifier will be synced
appId: '7j9y6m10', // replace with YOUR_APP_ID
token: 'notoken', // replace with your JWT
document: doc,
})
return () => {
provider.destroy()
}
}, [note.id])
const editor = useEditor({
// make sure that you don't use `content` property anymore!
// If you want to add default content, feel free to just write text to the tiptap editor (i.e. editor.setContent (https://tiptap.dev/api/commands/set-content), but make sure that
// you do this only once per document, otherwise the content will
// be added again, and again, and again ..
editorProps: {
attributes: {
class: 'm-2 p-2 border border-black rounded-lg',
},
},
extensions: [
StarterKit.configure({
history: false, // important because history will now be handled by Y.js
}),
Collaboration.configure({
document: doc,
}),
],
})
return (
// @ts-ignore
<EditorContent editor={editor}/>
)
}

View File

@ -0,0 +1,26 @@
import './styles.css'
import React from 'react'
import Note from './Note'
import { TNote } from './types'
const notes: TNote[] = [
{
id: 'note-1',
defaultContent: 'some random note text',
},
{
id: 'note-2',
defaultContent: 'some really random note text',
},
]
export default () => {
return (
<div>
{notes.map(note => <Note note={note} key={note.id}/>)}
</div>
)
}

View File

@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View File

@ -0,0 +1,4 @@
export type TNote = {
id: string;
defaultContent: string;
};

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { TiptapCollabProvider } from '@hocuspocus/provider'
import { Collaboration } from '@tiptap/extension-collaboration'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent, useEditor } from '@tiptap/vue-3'
import { onMounted, onUnmounted } from 'vue'
import * as Y from 'yjs'
import type { TNote } from './types'
const props = defineProps<{ note: TNote }>()
let provider: TiptapCollabProvider | undefined
const doc = new Y.Doc()
onMounted(() => {
provider = new TiptapCollabProvider({
name: props.note.id, // any identifier - all connections sharing the same identifier will be synced
appId: '7j9y6m10', // replace with YOUR_APP_ID
token: 'notoken', // replace with your JWT
document: doc,
})
})
onUnmounted(() => provider?.destroy())
const editor = useEditor({
// make sure that you don't use `content` property anymore!
// If you want to add default content, feel free to just write text to the tiptap editor (i.e. editor.setContent (https://tiptap.dev/api/commands/set-content), but make sure that
// you do this only once per document, otherwise the content will
// be added again, and again, and again ..
editorProps: {
attributes: {
class: 'm-2 p-2 border border-black rounded-lg',
},
},
extensions: [
StarterKit.configure({
history: false, // important because history will now be handled by Y.js
}),
Collaboration.configure({
document: doc,
}),
],
})
</script>
<template>
<editor-content :editor="editor"></editor-content>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import './styles.css'
import Note from './Note.vue'
import { TNote } from './types'
const notes: TNote[] = [
{ id: 'note-1' },
{ id: 'note-2' },
]
</script>
<template>
<div v-for="note in notes" :key="note.id">
<Note :note="note"/>
</div>
</template>

View File

@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View File

@ -0,0 +1,3 @@
export type TNote = {
id: string;
};

View File

@ -1,9 +1,9 @@
const defaultTheme = require('tailwindcss/defaultTheme') const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = { module.exports = {
mode: 'jit', content: [
purge: [
'./preview/**/*.{vue,js,ts,jsx,tsx}', './preview/**/*.{vue,js,ts,jsx,tsx}',
'./src/**/*.{vue,js,ts,jsx,tsx}',
], ],
theme: { theme: {

View File

@ -17,6 +17,11 @@ import { defineConfig } from 'vite'
const getPackageDependencies = () => { const getPackageDependencies = () => {
const paths: Array<{ find: string, replacement: any }> = [] const paths: Array<{ find: string, replacement: any }> = []
paths.push({
find: 'yjs',
replacement: resolve('../node_modules/yjs/src/index.js'),
})
fg.sync('../packages/*', { onlyDirectories: true }) fg.sync('../packages/*', { onlyDirectories: true })
.map(name => name.replace('../packages/', '')) .map(name => name.replace('../packages/', ''))
.forEach(name => { .forEach(name => {
@ -70,10 +75,12 @@ export default defineConfig({
transform(html: string, context) { transform(html: string, context) {
const dir = dirname(context.path) const dir = dirname(context.path)
const data = dir.split('/') const data = dir.split('/')
const demoCategory = data[2] const demoCategory = data[2]
const demoName = data[3] const demoName = data[3]
const frameworkName = data[4]
if (dir.endsWith('/JS')) { if (dir.endsWith('/JS') || dir.endsWith('-JS')) {
return { return {
html: ` html: `
<!DOCTYPE html> <!DOCTYPE html>
@ -87,7 +94,7 @@ export default defineConfig({
<script type="module"> <script type="module">
import setup from '../../../../setup/js.ts' import setup from '../../../../setup/js.ts'
import source from '@source' import source from '@source'
setup('${demoCategory}/${demoName}', source) setup('${demoCategory}/${demoName}/${frameworkName}', source)
</script> </script>
</body> </body>
</html> </html>
@ -96,7 +103,7 @@ export default defineConfig({
} }
} }
if (dir.endsWith('/Vue')) { if (dir.endsWith('/Vue') || dir.endsWith('-Vue')) {
return { return {
html: ` html: `
<!DOCTYPE html> <!DOCTYPE html>
@ -110,7 +117,7 @@ export default defineConfig({
<script type="module"> <script type="module">
import setup from '../../../../setup/vue.ts' import setup from '../../../../setup/vue.ts'
import source from '@source' import source from '@source'
setup('${demoCategory}/${demoName}', source) setup('${demoCategory}/${demoName}/${frameworkName}', source)
</script> </script>
</body> </body>
</html> </html>
@ -119,7 +126,7 @@ export default defineConfig({
} }
} }
if (dir.endsWith('/Svelte')) { if (dir.endsWith('/Svelte') || dir.endsWith('-Svelte')) {
return { return {
html: ` html: `
<!DOCTYPE html> <!DOCTYPE html>
@ -133,7 +140,7 @@ export default defineConfig({
<script type="module"> <script type="module">
import setup from '../../../../setup/svelte.ts' import setup from '../../../../setup/svelte.ts'
import source from '@source' import source from '@source'
setup('${demoCategory}/${demoName}', source) setup('${demoCategory}/${demoName}/${frameworkName}', source)
</script> </script>
</body> </body>
</html> </html>
@ -142,7 +149,7 @@ export default defineConfig({
} }
} }
if (dir.endsWith('/React')) { if (dir.endsWith('/React') || dir.endsWith('-React')) {
return { return {
html: ` html: `
<!DOCTYPE html> <!DOCTYPE html>
@ -156,7 +163,7 @@ export default defineConfig({
<script type="module"> <script type="module">
import setup from '../../../../setup/react.ts' import setup from '../../../../setup/react.ts'
import source from '@source' import source from '@source'
setup('${demoCategory}/${demoName}', source) setup('${demoCategory}/${demoName}/${frameworkName}', source)
</script> </script>
</body> </body>
</html> </html>

View File

@ -39,3 +39,12 @@ If you have an issue, a question, want to talk something through or anything els
### Can we have a call? ### Can we have a call?
Nope, we are big fans of asynchronous communication. If you really need to reach out in private, send us an email to [humans@tiptap.dev](mailto:humans@tiptap.dev), but dont expect technical email support. That all happens on [GitHub](https://github.com/ueberdosis/tiptap/issues) Nope, we are big fans of asynchronous communication. If you really need to reach out in private, send us an email to [humans@tiptap.dev](mailto:humans@tiptap.dev), but dont expect technical email support. That all happens on [GitHub](https://github.com/ueberdosis/tiptap/issues)
## P.S. Did you know, were also into realtime text editing?
With [Collaboration](https://tiptap.dev/collab) we built a realtime backend for conflict-free text editing. And the best part: Integration into Tiptap is just a few clicks away.
Make sure to check it out!
!!tiptap-collab-cta

View File

@ -8,7 +8,7 @@ See also: [setTextSelection](/api/commands/set-text-selection), [blur](/api/comm
## Parameters ## Parameters
`position: 'start' | 'end' | 'all' | number | boolean | null (false)` `position: 'start' | 'end' | 'all' | number | boolean | null (false)`
By default, its restoring the cursor position (and text selection). Pass a position to move the cursor too. By default, its restoring the cursor position (and text selection). Pass a position to move the cursor to.
`options: { scrollIntoView: boolean }` `options: { scrollIntoView: boolean }`

View File

@ -1,5 +1,5 @@
# toggleNode # toggleNode
`toggleNode` will a node with another node. `toggleNode` will toggle a node with another node.
## Parameters ## Parameters
`typeOrName: string | NodeType` `typeOrName: string | NodeType`

View File

@ -55,3 +55,5 @@ The content of this editor is shared with other users.
::: :::
https://embed.tiptap.dev/preview/Extensions/CollaborationCursor?hideSource https://embed.tiptap.dev/preview/Extensions/CollaborationCursor?hideSource
https://embed.tiptap.dev/preview/Extensions/CollaborationCursor https://embed.tiptap.dev/preview/Extensions/CollaborationCursor
!!tiptap-collab-cta

View File

@ -83,3 +83,5 @@ The content of this editor is shared with other users.
::: :::
https://embed.tiptap.dev/preview/Extensions/Collaboration?hideSource https://embed.tiptap.dev/preview/Extensions/Collaboration?hideSource
https://embed.tiptap.dev/preview/Extensions/Collaboration https://embed.tiptap.dev/preview/Extensions/Collaboration
!!tiptap-collab-cta

View File

@ -261,7 +261,7 @@ Mark.create({
``` ```
#### Excludes #### Excludes
By default all nodes can be applied at the same time. With the excludes attribute you can define which marks must not coexist with the mark. For example, the inline code mark excludes any other mark (bold, italic, and all others). By default all marks can be applied at the same time. With the excludes attribute you can define which marks must not coexist with the mark. For example, the inline code mark excludes any other mark (bold, italic, and all others).
```js ```js
Mark.create({ Mark.create({

160
docs/cloud.md Normal file
View File

@ -0,0 +1,160 @@
---
tableOfContents: true
---
"Embed real-time collaboration into your app in under one minute, and everything is in sync." ([Live-Demo](/collab))- If that sounds interesting to you, we might have something :)
# Tiptap Collab
Tiptap Collab is our hosted solution of Hocuspocus (The plugnplay collaborative editing backend) making it a blast to add real-time collaboration to any app.
:::warning Pro Feature
To get started, you need a Tiptap Pro account ([sign up / login here](https://tiptap.dev/pro)).
:::
[![Cloud Dashboard](https://tiptap.dev/images/docs/server/cloud/dashboard.png)](https://tiptap.dev/images/docs/server/cloud/dashboard.png)
Note that you need `@hocuspocus/provider` [~v2.0.0](https://github.com/ueberdosis/hocuspocus/releases/tag/v2.0.0)
## Getting started
Tiptap Collab makes your app collaborative by syncing your Y.Doc across users using websockets. If you are already using yjs in your app, getting started is as simple as shown below.
If you are not, you might want to start in our [Tutorials](/tutorials) section.
```typescript
import { TiptapCollabProvider } from '@hocuspocus/provider'
import * as Y from 'yjs'
const provider = new TiptapCollabProvider({
appId: 'your_app_id', // get this at collab.tiptap.dev
name: 'your_document_name', // e.g. a uuid uuidv4();
token: 'your_JWT', // see "Authentication" below
doc: new Y.Doc() // pass your existing doc, or leave this out and use provider.document
});
// That's it! Your Y.Doc will now be synced to any other user currently connected
```
### Upgrade from self-hosted deployments
If you are upgrading from a self-hosted deployment, on the frontend you just need to replace `HocuspocusProvider` with the new `TiptapCollabProvider`. The API is the same, it's just a wrapper that handles hostnames / auth.
## Examples
##### replit / Sandbox: Fully functional prototype
[![Cloud Documents](https://tiptap.dev/images/docs/server/cloud/tiptapcollab-demo.png)](https://tiptap.dev/images/docs/server/cloud/tiptapcollab-demo.png)
We have created a simple client / server setup using replit, which you can review and fork here:
[Github](https://github.com/janthurau/TiptapCollab) or [Replit (Live-Demo)](https://replit.com/@ueberdosis/TiptapCollab?v=1)
The example load multiple documents over the same websocket (multiplexing), and shows how to realize per-document authentication using JWT.
##### Authentication
Authentication is done using JWT. You can see your secret in the admin interface and use it to generate tokens for your clients. If you want to generate a JWT and add some attributes for testing, you can use http://jwtbuilder.jamiekurtz.com/ . You can leave all fields default, just replace the "key" with the secret from your settings.
In Node.js, you can generate a JWT like this:
```typescript
import jsonwebtoken from 'jsonwebtoken'
const data = {
// use this list to limit the number of documents that can be accessed by this client.
// empty array means no access at all
// not sending this property means access to all documents
// we are supporting a wildcard at the end of the string (only there)
allowedDocumentNames: ['document-1', 'document-2', 'my-user-uuid/*', 'my-organization-uuid/*']
}
const jwt = jsonwebtoken.sign(data, 'your_secret')
// this JWT should be sent in the `token` field of the provider. Never expose 'your_secret' to a frontend!
```
#### Getting the JSON document
If you want to access the JSON representation (we're currently exporting the `default` fragment of the YDoc), you can add a webhook in the admin interface. We are calling it when storing to our database, so it's debounced by 2 seconds (max 10 seconds).
All requests contain a header `X-Hocuspocus-Signature-256` which signs the entire message using 'your_secret' (find it in the settings). The payload looks like this:
```json
{
"appName": '', // name of your app
"name": '', // name of the document
"time": // current time as ISOString (new Date()).toISOString())
"tiptapData": {}, // JSON output from Tiptap (see https://tiptap.dev/guide/output#option-1-json): TiptapTransformer.fromYdoc()
"ydocState"?: {}, // optionally contains the entire yDoc as base64. Contact us to enable this property!
"clientsCount": 100 // number of currently connected clients
}
```
## API
Each Tiptap Collab instance comes with an API for the most common operations. Is is provided directly by your Tiptap Collab instance, so it's available under your custom URL:
`https://YOUR_APP_ID.collab.tiptap.cloud/`
Authentication is done using an admin secret which you can find in your collab [settings area](https://collab.tiptap.dev/). The secret has to be sent as an `Authorization` header.
If your document identifier contains a slash (`/`), just make sure that you encode it as `%2F` (e.g. using javascripts `encodeURIComponent`).
### POST /api/documents/:identifier
This call takes a binary yjs update message (an existing Ydoc on your side must be encoded using `Y.encodeStateAsUpdate`) and creates a document. This can be used to seed documents before a user connects to the Tiptap Collab server.
```bash
curl --location 'https://YOUR_APP_ID.collab.tiptap.cloud/api/documents/DOCUMENT_NAME' \
--header 'Authorization: YOUR_SECRET_FROM_SETTINGS_AREA' \
--data '@yjsUpdate.binary.txt'
// returns either http status 204 if the document was created successfully
// or http status 409 if the document already exists (if you wish to overwrite it, just delete it first)
```
### GET /api/documents/:identifier?format=:format&fragment=:fragment
This call exports the given document (all fragments) in json format. We are exporting either the current in-memory version (in case the document is currently open on your server, or we fetch the most recent version from the database).
`format` supports either `yjs` or `json`, default=`json`
If you choose the `yjs` format, you'll get the binary Y.js update message (created using `Y.encodeStateAsUpdate`)
`fragment` can be an array of (`fragment=a&fragment=b`) or a single fragment that you want exported. By default we're exporting all fragments. Note that this is only considered when using `json` format, otherwise you'll always get the entire Y.doc.
```bash
curl --location 'https://YOUR_APP_ID.collab.tiptap.cloud/api/documents/DOCUMENT_NAME' \
--header 'Authorization: YOUR_SECRET_FROM_SETTINGS_AREA'
// returns either http status 200 and the requested document
// or http status 404 if the document was not found
```
### DELETE /api/documents/:identifier
This simply deletes a document from the server after terminating any open connection to the document.
```bash
curl --location --request DELETE 'https://YOUR_APP_ID.collab.tiptap.cloud/api/documents/DOCUMENT_NAME' \
--header 'Authorization: YOUR_SECRET_FROM_SETTINGS_AREA'
// returns either http status 204 if the document was deleted successfully
// or http status 404 if the document was not found
```
### ANY /api/
Need something else? Hit us up and we'll see what we can do (links below)
## Screenshots
[![Cloud Documents](https://tiptap.dev/images/docs/server/cloud/documents.png)](https://tiptap.dev/images/docs/server/cloud/documents.png)
[![Cloud Settings](https://tiptap.dev/images/docs/server/cloud/settings.png)](https://tiptap.dev/images/docs/server/cloud/settings.png)
## Need anything else?
Contact us on [Discord](https://tiptap.dev/discord) or send an email to [humans@tiptap.dev](mailto:humans@tiptap.dev).

View File

@ -7,11 +7,6 @@ tableOfContents: true
## Introduction ## Introduction
Real-time collaboration, syncing between different devices and working offline used to be hard. We provide everything you need to keep everything in sync with the power of [Y.js](https://github.com/yjs/yjs). The following guide helps you get started with collaborative editing in Tiptap. Dont worry, a production-grade setup doesnt require much code. Real-time collaboration, syncing between different devices and working offline used to be hard. We provide everything you need to keep everything in sync with the power of [Y.js](https://github.com/yjs/yjs). The following guide helps you get started with collaborative editing in Tiptap. Dont worry, a production-grade setup doesnt require much code.
## The video course
We are working on a video course which teaches everything you need to know about collaborative text editing with Tiptap. The first video is available for sponsors here:
https://tiptap.dev/screencasts/collaborative-editing/make-tiptap-collaborative
## Configure the editor ## Configure the editor
The underyling schema Tiptap uses is an excellent foundation to sync documents. With the [`Collaboration`](/api/extensions/collaboration) extension you can tell Tiptap to track changes to the document with [Y.js](https://github.com/yjs/yjs). The underyling schema Tiptap uses is an excellent foundation to sync documents. With the [`Collaboration`](/api/extensions/collaboration) extension you can tell Tiptap to track changes to the document with [Y.js](https://github.com/yjs/yjs).
@ -128,6 +123,9 @@ Try opening http://127.0.0.1:1234 in your browser. You should see a plain text `
Go back to your Tiptap editor and hit reload, it should now connect to the Hocuspocus WebSocket server and changes should sync with all other clients. Amazing, isnt it? Go back to your Tiptap editor and hit reload, it should now connect to the Hocuspocus WebSocket server and changes should sync with all other clients. Amazing, isnt it?
!!tiptap-collab-cta
### Multiple network providers ### Multiple network providers
You can even combine multiple providers. Thats not needed, but could keep clients connected, even if one connection - for example the WebSocket server - goes down for a while. Here is an example: You can even combine multiple providers. Thats not needed, but could keep clients connected, even if one connection - for example the WebSocket server - goes down for a while. Here is an example:
@ -282,6 +280,14 @@ const server = Server.configure({
server.listen() server.listen()
``` ```
## Tiptap Collab our hosted solution
If you dont want the struggle of self-hosting and scaling Hocuspocus, make sure to check out our managed solution Tiptap Collab.
Its just a few clicks away, really.
!!tiptap-collab-cta
## Pitfalls ## Pitfalls
### Schema updates ### Schema updates

View File

@ -19,7 +19,6 @@ This is how your node extension could look like:
```js ```js
import { Node } from '@tiptap/core' import { Node } from '@tiptap/core'
import Component from './Component.vue'
export default Node.create({ export default Node.create({
// configuration … // configuration …

View File

@ -86,7 +86,7 @@ this.updateAttributes({
}) })
``` ```
And yes, all of that is reactive, too. A pretty seemless communication, isnt it? And yes, all of that is reactive, too. A pretty seamless communication, isnt it?
## Adding a content editable ## Adding a content editable
There is another component called `NodeViewContent` which helps you adding editable content to your node view. Here is an example: There is another component called `NodeViewContent` which helps you adding editable content to your node view. Here is an example:

View File

@ -17,11 +17,6 @@ If you just want to get up and running with Tiptap you can use the [Tiptap Creat
npx create-react-app my-tiptap-project --template tiptap npx create-react-app my-tiptap-project --template tiptap
``` ```
### Step by step
All steps are listed below, but if you prefer to watch a video weve got something for you, too:
https://tiptap.dev/screencasts/installation/install-tiptap-with-create-react-app
#### 1. Create a project (optional) #### 1. Create a project (optional)
Lets start with a fresh React project called `my-tiptap-project`. [Create React App](https://reactjs.org/docs/getting-started.html) will set up everything we need. Lets start with a fresh React project called `my-tiptap-project`. [Create React App](https://reactjs.org/docs/getting-started.html) will set up everything we need.

View File

@ -19,3 +19,12 @@ There is no provided user interface, you are absolutely free to build whatever i
## Why should I use Tiptap? ## Why should I use Tiptap?
[ProseMirror](https://ProseMirror.net) is a well-written, reliable and very powerful editor toolkit. Its not so much the ready-to-use editor most people look for, but with Tiptap you can start in minutes, choose from tons of amazing extensions and still have that powerful ProseMirror API accessible when you really need it. [ProseMirror](https://ProseMirror.net) is a well-written, reliable and very powerful editor toolkit. Its not so much the ready-to-use editor most people look for, but with Tiptap you can start in minutes, choose from tons of amazing extensions and still have that powerful ProseMirror API accessible when you really need it.
## Do you enjoy real-time editing?
Great! We do so, too. [Hocupocus](https://hocuspocus.dev) is our yjs-based real-time solution for conflict-free text editing with Tiptap.
Even better: Our managed solution [Tiptap Collab](https://tiptap.dev/collab) is just a few clicks away.<br />
Enhance your Tiptap experience with multiplayer support in minutes instead of hours.
!!tiptap-collab-cta

View File

@ -110,11 +110,25 @@
type: sponsor type: sponsor
- title: Contributing - title: Contributing
link: /overview/contributing link: /overview/contributing
- title: Support # - title: Support
link: /support # link: /support
- title: Jobs - title: Jobs
link: /jobs link: /jobs
- title: Tiptap Collab
items:
- title: Cloud
link: /cloud
type: new
- title: Tutorials
link: /tutorials
type: new
items:
- title: Getting Started
link: /tutorials/get-started-with-tiptap-collab
- title: JWT Authentication
link: /tutorials/jwt-authentication
- title: Guide - title: Guide
items: items:
- title: Configuration - title: Configuration

View File

@ -7,6 +7,8 @@ tableOfContents: true
## Introduction ## Introduction
First of all, Tiptap v1 isnt supported anymore and wont receive any further updates. First of all, Tiptap v1 isnt supported anymore and wont receive any further updates.
If youre still using Tiptap v1, you can find the documentation [here](https://v1.tiptap.dev/), but we strongly recommend that you upgrade to version 2.
Yes, its tedious work to upgrade your favorite text editor to a new API, but we made sure youve got enough reasons to upgrade to the newest version. Yes, its tedious work to upgrade your favorite text editor to a new API, but we made sure youve got enough reasons to upgrade to the newest version.
* Autocompletion in your IDE (thanks to TypeScript) * Autocompletion in your IDE (thanks to TypeScript)

19
docs/tutorials.md Normal file
View File

@ -0,0 +1,19 @@
---
tableOfContents: true
---
# Tutorials
## Introduction
We have prepared a few tutorials on to get started with a fully collaborative Tiptap editor using Tiptap Collab. More instructions on more complex usages are already on their way :)
## Tiptap Collab Tutorials
- [Get started with Tiptap Collab](/tutorials/get-started-with-tiptap-collab)
- [JWT Authentication](/tutorials/jwt-authentication)
### Need something else?
- [Discord](https://discord.gg/WtJ49jGshW)
- [Github](https://github.com/ueberdosis/tiptap/discussions)

View File

@ -0,0 +1,75 @@
# Getting started with Tiptap Collab
## Introduction
**Welcome** to the first of a series of tutorials about collaboration in Tiptap (or Lexical, Quill, Slate, and others that have a [Yjs editor binding](https://docs.yjs.dev/ecosystem/editor-bindings)) using Tiptap Collab. This series will start covering the basics, and expand to more specific use cases in the next posts. For today, well start moving from a simple textarea box to a fully collaborative editor instance.
Imagine that you are building a simple sticky note app, where a user can create notes.
So let's say you have a few textareas. Depending on your framework (Vue, React, ..), the code probably looks similar to this:
<tiptap-demo name="Tutorials/1-1-textarea"></tiptap-demo>
## Setting Up Tiptap
In order to incorporate the Tiptap editor instance for better collaboration and formatting options, you start by modifying your code to include Tiptap in the Note component.
You begin by importing the necessary Tiptap components and creating a new editor instance within the Note component.
```bash
npm install @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit
# for React: npm install @tiptap/react @tiptap/pm @tiptap/starter-kit
```
<tiptap-demo name="Tutorials/1-2-tiptap"></tiptap-demo>
Now your Note component has a fully functional Tiptap editor instance! The user can now format their text (see https://tiptap.dev/guide/menus on how to add a menu bar, in our example, you can make text bold using cmd+b). But what about collaboration?
## Adding Yjs
To enable collaboration, you need to add the Collaboration extension to your editor instance. This extension allows multiple users to edit the same document simultaneously, with changes being synced in realtime.
To add the Collaboration extension to your editor instance, you first need to install the `@tiptap/extension-collaboration` package:
```bash
npm install @tiptap/extension-collaboration yjs
```
Then, you can import the `Collaboration` extension and add it to your editor extensions:
<tiptap-demo name="Tutorials/1-3-yjs"></tiptap-demo>
ok, so what have we done?
We just added the collaboration extension as well as the technology behind it, Yjs. Instead of raw text we are passing the Y.Doc which basically takes care of merging changes. But so far, there is no collaboration...
## Real-Time Collaboration with Tiptap Collab
To enable real-time collaboration, we need to connect the Y.Doc with the TiptapCollabProvider. The TiptapCollabProvider is a package that provides a simple way to synchronize Y.Doc's across different clients.
To start using TiptapCollabProvider, we need to create a new instance of the TiptapCollabProvider class and pass our Y.Doc. We also need to provide a document name.
To get started, let's sign up for a Tiptap Pro account, which comes with a free licence of Tiptap Collab:
!!tiptap-collab-cta
After you signed up, click on "Join the Beta". Just follow the instructions and you'll be set up within a few minutes.
Your app ID is shown in the collab admin interface: https://collab.tiptap.dev/ - just copy that and also already get the JWT from the settings area. It's valid for two hours, so more than enough for our quick test. We'll cover generating JWTs using your secret later.
Now, back to our application:
```bash
npm install @hocuspocus/provider
```
Let's now create the TiptapCollabProvider to finally get syncing:
<tiptap-demo name="Tutorials/1-4-collab"></tiptap-demo>
And that's it! With these changes, our Tiptap note-taking application is now fully collaborative. Notes will get synced to other users in realtime.
We've only scratched the surface of what Tiptap Collab and Hocuspocus can do. Keep an eye out for future articles where we'll delve into more complex scenarios like permissions, presence indicators, and beyond. Don't miss out!

View File

@ -0,0 +1,46 @@
# JWT authentication with Tiptap Collab
In our first tutorial, we've gone from a simple textarea to a fully collaborative Tiptap editor instance.
However, the JWT that is given by Tiptap Collab is valid for just a few hours, which is enough for testing,
but certainly not enough for a real live application.
## What is a JWT
In a short explanation, a JWT (JSON Web Token) is a json object that is cryptographically signed, which means a generated JWT cannot be altered.
## How to generate a JWT
The JWT **must** be generated on the server side, as your `secret` **must not** leave your server (i.e. don't even try to generate the JWT on the frontend).
You can use the following snippet on a NodeJS server and build an API around it.
```typescript
import jsonwebtoken from 'jsonwebtoken'
const jwt = jsonwebtoken.sign({ /* object to be encoded in the JWT */ }, 'your_secret')
// this JWT should be sent in the `token` field of the provider. Never expose 'your_secret' to a frontend!
```
A full server / API example is available [here](https://github.com/ueberdosis/tiptap-collab-replit/blob/main/src/server-collab.ts).
Make sure to put the `secret` inside the server environment variable (or just make it a constant in the server file, don't transfer it from the client).
You probably want to create an API call like `GET /getCollabToken` which will generate the JWT based on the server secret and the list of documents that the user is allowed to access.
## How to limit access to specific documents
Documents can only be accessed by knowing the exact document name, as there is no way to get a list of documents from TiptapCollab.
Thus, it's a good practice to name them like `userUuid/documentUuid` (i.e. `1500c624-8f9f-496a-b196-5e5dd8ec3c25/7865975c-38d0-4bb5-846b-df909cdc66d3`), which
already makes it impossible to open random documents by guessing the name.
If you want to further limit which documents can be accessed using which JWT, you can encode the `allowedDocumentNames` property in the JWT, as in the following
example. The created JWT will only allow access to the document(s) specified.
```typescript
import jsonwebtoken from 'jsonwebtoken'
const jwt = jsonwebtoken.sign({
allowedDocumentNames: [
'1500c624-8f9f-496a-b196-5e5dd8ec3c25/7865975c-38d0-4bb5-846b-df909cdc66d3', // userUuid/documentUuid
'1500c624-8f9f-496a-b196-5e5dd8ec3c25/*' // userUuid/*
]
}, 'your_secret')
// this JWT should be sent in the `token` field of the provider. Never expose 'your_secret' to a frontend!
```

View File

@ -1,5 +1,5 @@
export function createStyleTag(style: string, nonce?: string): HTMLStyleElement { export function createStyleTag(style: string, nonce?: string, suffix?: string): HTMLStyleElement {
const tiptapStyleTag = (<HTMLStyleElement>document.querySelector('style[data-tiptap-style]')) const tiptapStyleTag = (<HTMLStyleElement>document.querySelector(`style[data-tiptap-style${suffix ? `-${suffix}` : ''}]`))
if (tiptapStyleTag !== null) { if (tiptapStyleTag !== null) {
return tiptapStyleTag return tiptapStyleTag
@ -11,7 +11,7 @@ export function createStyleTag(style: string, nonce?: string): HTMLStyleElement
styleNode.setAttribute('nonce', nonce) styleNode.setAttribute('nonce', nonce)
} }
styleNode.setAttribute('data-tiptap-style', '') styleNode.setAttribute(`data-tiptap-style${suffix ? `-${suffix}` : ''}`, '')
styleNode.innerHTML = style styleNode.innerHTML = style
document.getElementsByTagName('head')[0].appendChild(styleNode) document.getElementsByTagName('head')[0].appendChild(styleNode)