mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-11-23 19:19:03 +08:00
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:
commit
bdc51d12b5
@ -1,5 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
'tailwindcss/nesting': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
|
@ -17,7 +17,7 @@ const sendData = (eventName: string, data: any) => {
|
||||
export function splitName(name: string) {
|
||||
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.')
|
||||
}
|
||||
|
||||
|
@ -11,15 +11,28 @@ export default function init(name: string, source: any) {
|
||||
window.source = source
|
||||
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 => {
|
||||
const root = document.getElementById('app')
|
||||
|
||||
if (root) {
|
||||
createRoot(root).render(React.createElement(module.default))
|
||||
createRoot(root)
|
||||
.render(React.createElement(module.default))
|
||||
}
|
||||
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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -8,9 +8,9 @@ export default function init(name: string, source: any) {
|
||||
window.source = source
|
||||
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 => {
|
||||
const Component = Module.default
|
||||
|
||||
|
@ -10,9 +10,9 @@ export default function init(name: string, source: any) {
|
||||
window.source = source
|
||||
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 => {
|
||||
createApp(module.default).mount('#app')
|
||||
debug()
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable */
|
||||
import './styles.scss'
|
||||
|
||||
import { TiptapCollabProvider } from '@hocuspocus/provider'
|
||||
@ -16,13 +15,8 @@ import React, {
|
||||
} from 'react'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
import { variables } from '../../../variables.js'
|
||||
import MenuBar from './MenuBar.jsx'
|
||||
|
||||
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>
|
||||
`
|
||||
import { variables } from '../../../variables'
|
||||
import MenuBar from './MenuBar'
|
||||
|
||||
const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D']
|
||||
const names = [
|
||||
@ -84,10 +78,9 @@ export default () => {
|
||||
const [currentUser, setCurrentUser] = useState(getInitialUser)
|
||||
|
||||
const editor = useEditor({
|
||||
content,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
history: true,
|
||||
history: false,
|
||||
}),
|
||||
Highlight,
|
||||
TaskList,
|
||||
@ -95,12 +88,12 @@ export default () => {
|
||||
CharacterCount.configure({
|
||||
limit: 10000,
|
||||
}),
|
||||
/* Collaboration.configure({
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
}),
|
||||
CollaborationCursor.configure({
|
||||
provider: websocketProvider,
|
||||
}), */
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
@ -115,7 +108,7 @@ export default () => {
|
||||
useEffect(() => {
|
||||
if (editor && currentUser) {
|
||||
localStorage.setItem('currentUser', JSON.stringify(currentUser))
|
||||
// editor.chain().focus().updateUser(currentUser).run()
|
||||
editor.chain().focus().updateUser(currentUser).run()
|
||||
}
|
||||
}, [editor, currentUser])
|
||||
|
||||
@ -131,7 +124,7 @@ export default () => {
|
||||
<div className="editor">
|
||||
{editor && <MenuBar editor={editor} />}
|
||||
<EditorContent className="editor__content" editor={editor} />
|
||||
{/* <div className="editor__footer">
|
||||
<div className="editor__footer">
|
||||
<div className={`editor__status editor__status--${status}`}>
|
||||
{status === 'connected'
|
||||
? `${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">
|
||||
<button onClick={setName}>{currentUser.name}</button>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
15
demos/src/Tutorials/1-1-textarea/React/Note.tsx
Normal file
15
demos/src/Tutorials/1-1-textarea/React/Note.tsx
Normal 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>
|
||||
)
|
||||
}
|
0
demos/src/Tutorials/1-1-textarea/React/index.html
Normal file
0
demos/src/Tutorials/1-1-textarea/React/index.html
Normal file
26
demos/src/Tutorials/1-1-textarea/React/index.tsx
Normal file
26
demos/src/Tutorials/1-1-textarea/React/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
3
demos/src/Tutorials/1-1-textarea/React/styles.css
Normal file
3
demos/src/Tutorials/1-1-textarea/React/styles.css
Normal file
@ -0,0 +1,3 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
4
demos/src/Tutorials/1-1-textarea/React/types.ts
Normal file
4
demos/src/Tutorials/1-1-textarea/React/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type TNote = {
|
||||
id: string;
|
||||
content: string;
|
||||
};
|
19
demos/src/Tutorials/1-1-textarea/Vue/Note.vue
Normal file
19
demos/src/Tutorials/1-1-textarea/Vue/Note.vue
Normal 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>
|
0
demos/src/Tutorials/1-1-textarea/Vue/index.html
Normal file
0
demos/src/Tutorials/1-1-textarea/Vue/index.html
Normal file
20
demos/src/Tutorials/1-1-textarea/Vue/index.vue
Normal file
20
demos/src/Tutorials/1-1-textarea/Vue/index.vue
Normal 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>
|
3
demos/src/Tutorials/1-1-textarea/Vue/styles.css
Normal file
3
demos/src/Tutorials/1-1-textarea/Vue/styles.css
Normal file
@ -0,0 +1,3 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
4
demos/src/Tutorials/1-1-textarea/Vue/types.ts
Normal file
4
demos/src/Tutorials/1-1-textarea/Vue/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type TNote = {
|
||||
id: string;
|
||||
content: string;
|
||||
};
|
31
demos/src/Tutorials/1-2-tiptap/React/Note.tsx
Normal file
31
demos/src/Tutorials/1-2-tiptap/React/Note.tsx
Normal 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}/>
|
||||
)
|
||||
}
|
0
demos/src/Tutorials/1-2-tiptap/React/index.html
Normal file
0
demos/src/Tutorials/1-2-tiptap/React/index.html
Normal file
26
demos/src/Tutorials/1-2-tiptap/React/index.tsx
Normal file
26
demos/src/Tutorials/1-2-tiptap/React/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
3
demos/src/Tutorials/1-2-tiptap/React/styles.css
Normal file
3
demos/src/Tutorials/1-2-tiptap/React/styles.css
Normal file
@ -0,0 +1,3 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
4
demos/src/Tutorials/1-2-tiptap/React/types.ts
Normal file
4
demos/src/Tutorials/1-2-tiptap/React/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type TNote = {
|
||||
id: string;
|
||||
content: string;
|
||||
};
|
31
demos/src/Tutorials/1-2-tiptap/Vue/Note.vue
Normal file
31
demos/src/Tutorials/1-2-tiptap/Vue/Note.vue
Normal 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>
|
0
demos/src/Tutorials/1-2-tiptap/Vue/index.html
Normal file
0
demos/src/Tutorials/1-2-tiptap/Vue/index.html
Normal file
19
demos/src/Tutorials/1-2-tiptap/Vue/index.vue
Normal file
19
demos/src/Tutorials/1-2-tiptap/Vue/index.vue
Normal 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>
|
3
demos/src/Tutorials/1-2-tiptap/Vue/styles.css
Normal file
3
demos/src/Tutorials/1-2-tiptap/Vue/styles.css
Normal file
@ -0,0 +1,3 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
4
demos/src/Tutorials/1-2-tiptap/Vue/types.ts
Normal file
4
demos/src/Tutorials/1-2-tiptap/Vue/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type TNote = {
|
||||
id: string;
|
||||
content: string;
|
||||
};
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
@ -0,0 +1,4 @@
|
||||
export type TNote = {
|
||||
id: string;
|
||||
content: string;
|
||||
};
|
35
demos/src/Tutorials/1-3-yjs/React/Note.tsx
Normal file
35
demos/src/Tutorials/1-3-yjs/React/Note.tsx
Normal 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}/>
|
||||
)
|
||||
}
|
0
demos/src/Tutorials/1-3-yjs/React/index.html
Normal file
0
demos/src/Tutorials/1-3-yjs/React/index.html
Normal file
26
demos/src/Tutorials/1-3-yjs/React/index.tsx
Normal file
26
demos/src/Tutorials/1-3-yjs/React/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
3
demos/src/Tutorials/1-3-yjs/React/styles.css
Normal file
3
demos/src/Tutorials/1-3-yjs/React/styles.css
Normal file
@ -0,0 +1,3 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
4
demos/src/Tutorials/1-3-yjs/React/types.ts
Normal file
4
demos/src/Tutorials/1-3-yjs/React/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type TNote = {
|
||||
id: string;
|
||||
defaultContent: string;
|
||||
};
|
35
demos/src/Tutorials/1-3-yjs/Vue/Note.vue
Normal file
35
demos/src/Tutorials/1-3-yjs/Vue/Note.vue
Normal 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>
|
0
demos/src/Tutorials/1-3-yjs/Vue/index.html
Normal file
0
demos/src/Tutorials/1-3-yjs/Vue/index.html
Normal file
19
demos/src/Tutorials/1-3-yjs/Vue/index.vue
Normal file
19
demos/src/Tutorials/1-3-yjs/Vue/index.vue
Normal 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>
|
3
demos/src/Tutorials/1-3-yjs/Vue/styles.css
Normal file
3
demos/src/Tutorials/1-3-yjs/Vue/styles.css
Normal file
@ -0,0 +1,3 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
4
demos/src/Tutorials/1-3-yjs/Vue/types.ts
Normal file
4
demos/src/Tutorials/1-3-yjs/Vue/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type TNote = {
|
||||
id: string;
|
||||
defaultContent: string;
|
||||
};
|
52
demos/src/Tutorials/1-3-yjs_lexical/Lexical-React/Note.tsx
Normal file
52
demos/src/Tutorials/1-3-yjs_lexical/Lexical-React/Note.tsx
Normal 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>
|
||||
)
|
||||
}
|
26
demos/src/Tutorials/1-3-yjs_lexical/Lexical-React/index.tsx
Normal file
26
demos/src/Tutorials/1-3-yjs_lexical/Lexical-React/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
@ -0,0 +1,4 @@
|
||||
export type TNote = {
|
||||
id: string;
|
||||
defaultContent: string;
|
||||
};
|
52
demos/src/Tutorials/1-4-collab/React/Note.tsx
Normal file
52
demos/src/Tutorials/1-4-collab/React/Note.tsx
Normal 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}/>
|
||||
)
|
||||
}
|
0
demos/src/Tutorials/1-4-collab/React/index.html
Normal file
0
demos/src/Tutorials/1-4-collab/React/index.html
Normal file
26
demos/src/Tutorials/1-4-collab/React/index.tsx
Normal file
26
demos/src/Tutorials/1-4-collab/React/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
3
demos/src/Tutorials/1-4-collab/React/styles.css
Normal file
3
demos/src/Tutorials/1-4-collab/React/styles.css
Normal file
@ -0,0 +1,3 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
4
demos/src/Tutorials/1-4-collab/React/types.ts
Normal file
4
demos/src/Tutorials/1-4-collab/React/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type TNote = {
|
||||
id: string;
|
||||
defaultContent: string;
|
||||
};
|
53
demos/src/Tutorials/1-4-collab/Vue/Note.vue
Normal file
53
demos/src/Tutorials/1-4-collab/Vue/Note.vue
Normal 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>
|
0
demos/src/Tutorials/1-4-collab/Vue/index.html
Normal file
0
demos/src/Tutorials/1-4-collab/Vue/index.html
Normal file
19
demos/src/Tutorials/1-4-collab/Vue/index.vue
Normal file
19
demos/src/Tutorials/1-4-collab/Vue/index.vue
Normal 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>
|
3
demos/src/Tutorials/1-4-collab/Vue/styles.css
Normal file
3
demos/src/Tutorials/1-4-collab/Vue/styles.css
Normal file
@ -0,0 +1,3 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
3
demos/src/Tutorials/1-4-collab/Vue/types.ts
Normal file
3
demos/src/Tutorials/1-4-collab/Vue/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type TNote = {
|
||||
id: string;
|
||||
};
|
@ -1,9 +1,9 @@
|
||||
const defaultTheme = require('tailwindcss/defaultTheme')
|
||||
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
purge: [
|
||||
content: [
|
||||
'./preview/**/*.{vue,js,ts,jsx,tsx}',
|
||||
'./src/**/*.{vue,js,ts,jsx,tsx}',
|
||||
],
|
||||
|
||||
theme: {
|
||||
|
@ -17,6 +17,11 @@ import { defineConfig } from 'vite'
|
||||
const getPackageDependencies = () => {
|
||||
const paths: Array<{ find: string, replacement: any }> = []
|
||||
|
||||
paths.push({
|
||||
find: 'yjs',
|
||||
replacement: resolve('../node_modules/yjs/src/index.js'),
|
||||
})
|
||||
|
||||
fg.sync('../packages/*', { onlyDirectories: true })
|
||||
.map(name => name.replace('../packages/', ''))
|
||||
.forEach(name => {
|
||||
@ -70,10 +75,12 @@ export default defineConfig({
|
||||
transform(html: string, context) {
|
||||
const dir = dirname(context.path)
|
||||
const data = dir.split('/')
|
||||
|
||||
const demoCategory = data[2]
|
||||
const demoName = data[3]
|
||||
const frameworkName = data[4]
|
||||
|
||||
if (dir.endsWith('/JS')) {
|
||||
if (dir.endsWith('/JS') || dir.endsWith('-JS')) {
|
||||
return {
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
@ -87,7 +94,7 @@ export default defineConfig({
|
||||
<script type="module">
|
||||
import setup from '../../../../setup/js.ts'
|
||||
import source from '@source'
|
||||
setup('${demoCategory}/${demoName}', source)
|
||||
setup('${demoCategory}/${demoName}/${frameworkName}', source)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -96,7 +103,7 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
|
||||
if (dir.endsWith('/Vue')) {
|
||||
if (dir.endsWith('/Vue') || dir.endsWith('-Vue')) {
|
||||
return {
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
@ -110,7 +117,7 @@ export default defineConfig({
|
||||
<script type="module">
|
||||
import setup from '../../../../setup/vue.ts'
|
||||
import source from '@source'
|
||||
setup('${demoCategory}/${demoName}', source)
|
||||
setup('${demoCategory}/${demoName}/${frameworkName}', source)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -119,7 +126,7 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
|
||||
if (dir.endsWith('/Svelte')) {
|
||||
if (dir.endsWith('/Svelte') || dir.endsWith('-Svelte')) {
|
||||
return {
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
@ -133,7 +140,7 @@ export default defineConfig({
|
||||
<script type="module">
|
||||
import setup from '../../../../setup/svelte.ts'
|
||||
import source from '@source'
|
||||
setup('${demoCategory}/${demoName}', source)
|
||||
setup('${demoCategory}/${demoName}/${frameworkName}', source)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -142,7 +149,7 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
|
||||
if (dir.endsWith('/React')) {
|
||||
if (dir.endsWith('/React') || dir.endsWith('-React')) {
|
||||
return {
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
@ -156,7 +163,7 @@ export default defineConfig({
|
||||
<script type="module">
|
||||
import setup from '../../../../setup/react.ts'
|
||||
import source from '@source'
|
||||
setup('${demoCategory}/${demoName}', source)
|
||||
setup('${demoCategory}/${demoName}/${frameworkName}', source)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -39,3 +39,12 @@ If you have an issue, a question, want to talk something through or anything els
|
||||
|
||||
### 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 don’t expect technical email support. That all happens on [GitHub](https://github.com/ueberdosis/tiptap/issues)
|
||||
|
||||
|
||||
## P.S. Did you know, we’re 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
|
||||
|
@ -8,7 +8,7 @@ See also: [setTextSelection](/api/commands/set-text-selection), [blur](/api/comm
|
||||
## Parameters
|
||||
`position: 'start' | 'end' | 'all' | number | boolean | null (false)`
|
||||
|
||||
By default, it’s restoring the cursor position (and text selection). Pass a position to move the cursor too.
|
||||
By default, it’s restoring the cursor position (and text selection). Pass a position to move the cursor to.
|
||||
|
||||
`options: { scrollIntoView: boolean }`
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# toggleNode
|
||||
`toggleNode` will a node with another node.
|
||||
`toggleNode` will toggle a node with another node.
|
||||
|
||||
## Parameters
|
||||
`typeOrName: string | NodeType`
|
||||
|
@ -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
|
||||
|
||||
!!tiptap-collab-cta
|
||||
|
@ -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
|
||||
|
||||
!!tiptap-collab-cta
|
||||
|
@ -261,7 +261,7 @@ Mark.create({
|
||||
```
|
||||
|
||||
#### 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
|
||||
Mark.create({
|
||||
|
160
docs/cloud.md
Normal file
160
docs/cloud.md
Normal 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 plug’n’play 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).
|
@ -7,11 +7,6 @@ tableOfContents: true
|
||||
## 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. Don’t worry, a production-grade setup doesn’t 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
|
||||
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, isn’t it?
|
||||
|
||||
|
||||
!!tiptap-collab-cta
|
||||
|
||||
### Multiple network providers
|
||||
You can even combine multiple providers. That’s 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()
|
||||
```
|
||||
|
||||
## Tiptap Collab – our hosted solution
|
||||
|
||||
If you don’t want the struggle of self-hosting and scaling Hocuspocus, make sure to check out our managed solution Tiptap Collab.
|
||||
|
||||
It’s just a few clicks away, really.
|
||||
|
||||
!!tiptap-collab-cta
|
||||
|
||||
## Pitfalls
|
||||
|
||||
### Schema updates
|
||||
|
@ -19,7 +19,6 @@ This is how your node extension could look like:
|
||||
|
||||
```js
|
||||
import { Node } from '@tiptap/core'
|
||||
import Component from './Component.vue'
|
||||
|
||||
export default Node.create({
|
||||
// configuration …
|
||||
|
@ -86,7 +86,7 @@ this.updateAttributes({
|
||||
})
|
||||
```
|
||||
|
||||
And yes, all of that is reactive, too. A pretty seemless communication, isn’t it?
|
||||
And yes, all of that is reactive, too. A pretty seamless communication, isn’t it?
|
||||
|
||||
## Adding a content editable
|
||||
There is another component called `NodeViewContent` which helps you adding editable content to your node view. Here is an example:
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
### Step by step
|
||||
All steps are listed below, but if you prefer to watch a video we’ve got something for you, too:
|
||||
|
||||
https://tiptap.dev/screencasts/installation/install-tiptap-with-create-react-app
|
||||
|
||||
#### 1. Create a project (optional)
|
||||
Let’s 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.
|
||||
|
||||
|
@ -19,3 +19,12 @@ There is no provided user interface, you are absolutely free to build whatever i
|
||||
|
||||
## Why should I use Tiptap?
|
||||
[ProseMirror](https://ProseMirror.net) is a well-written, reliable and very powerful editor toolkit. It’s 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
|
||||
|
@ -110,11 +110,25 @@
|
||||
type: sponsor
|
||||
- title: Contributing
|
||||
link: /overview/contributing
|
||||
- title: Support
|
||||
link: /support
|
||||
# - title: Support
|
||||
# link: /support
|
||||
- title: 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
|
||||
items:
|
||||
- title: Configuration
|
||||
|
@ -7,6 +7,8 @@ tableOfContents: true
|
||||
## Introduction
|
||||
First of all, Tiptap v1 isn’t supported anymore and won’t receive any further updates.
|
||||
|
||||
If you’re 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, it’s tedious work to upgrade your favorite text editor to a new API, but we made sure you’ve got enough reasons to upgrade to the newest version.
|
||||
|
||||
* Autocompletion in your IDE (thanks to TypeScript)
|
||||
|
19
docs/tutorials.md
Normal file
19
docs/tutorials.md
Normal 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)
|
75
docs/tutorials/get-started-with-tiptap-collab.md
Normal file
75
docs/tutorials/get-started-with-tiptap-collab.md
Normal 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, we’ll 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!
|
46
docs/tutorials/jwt-authentication.md
Normal file
46
docs/tutorials/jwt-authentication.md
Normal 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!
|
||||
```
|
@ -1,5 +1,5 @@
|
||||
export function createStyleTag(style: string, nonce?: string): HTMLStyleElement {
|
||||
const tiptapStyleTag = (<HTMLStyleElement>document.querySelector('style[data-tiptap-style]'))
|
||||
export function createStyleTag(style: string, nonce?: string, suffix?: string): HTMLStyleElement {
|
||||
const tiptapStyleTag = (<HTMLStyleElement>document.querySelector(`style[data-tiptap-style${suffix ? `-${suffix}` : ''}]`))
|
||||
|
||||
if (tiptapStyleTag !== null) {
|
||||
return tiptapStyleTag
|
||||
@ -11,7 +11,7 @@ export function createStyleTag(style: string, nonce?: string): HTMLStyleElement
|
||||
styleNode.setAttribute('nonce', nonce)
|
||||
}
|
||||
|
||||
styleNode.setAttribute('data-tiptap-style', '')
|
||||
styleNode.setAttribute(`data-tiptap-style${suffix ? `-${suffix}` : ''}`, '')
|
||||
styleNode.innerHTML = style
|
||||
document.getElementsByTagName('head')[0].appendChild(styleNode)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user