mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-01-18 06:03:22 +08:00
Merge branch 'main' of https://github.com/ueberdosis/tiptap-next into feature/suggestions
# Conflicts: # docs/src/docPages/api/nodes/mention.md # docs/src/links.yaml
This commit is contained in:
commit
b1d3b4ce8d
@ -29,6 +29,7 @@
|
||||
"vue-github-button": "^1.1.2",
|
||||
"vue-live": "^1.16.0",
|
||||
"y-indexeddb": "^9.0.6",
|
||||
"y-prosemirror": "^1.0.5",
|
||||
"y-webrtc": "^10.1.7",
|
||||
"y-websocket": "^1.3.8",
|
||||
"yjs": "^13.4.7"
|
||||
|
@ -18,7 +18,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
provider: null,
|
||||
}
|
||||
},
|
||||
|
||||
@ -36,7 +35,6 @@ export default {
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
this.provider.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -18,6 +18,12 @@
|
||||
<div v-if="description" class="form__item form__item--description">
|
||||
<editor-content :editor="description" />
|
||||
</div>
|
||||
<div class="form__label">
|
||||
JSON
|
||||
</div>
|
||||
<div class="form__item form__item--json">
|
||||
<code>{{ json }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -31,7 +37,7 @@ import TaskList from '@tiptap/extension-task-list'
|
||||
import TaskItem from '@tiptap/extension-task-item'
|
||||
import Collaboration from '@tiptap/extension-collaboration'
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import { yDocToProsemirrorJSON } from 'y-prosemirror'
|
||||
|
||||
const ParagraphDocument = Document.extend({
|
||||
content: 'paragraph',
|
||||
@ -55,13 +61,12 @@ export default {
|
||||
title: null,
|
||||
tasks: null,
|
||||
description: null,
|
||||
ydoc: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const ydoc = new Y.Doc()
|
||||
|
||||
this.provider = new WebsocketProvider('wss://websocket.tiptap.dev', 'tiptap-multiple-editors-example', ydoc)
|
||||
this.ydoc = new Y.Doc()
|
||||
|
||||
this.title = new Editor({
|
||||
extensions: [
|
||||
@ -69,10 +74,11 @@ export default {
|
||||
Paragraph,
|
||||
Text,
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
document: this.ydoc,
|
||||
field: 'title',
|
||||
}),
|
||||
],
|
||||
content: '<p>No matter what you do, this’ll be a single paragraph.',
|
||||
})
|
||||
|
||||
this.tasks = new Editor({
|
||||
@ -83,10 +89,17 @@ export default {
|
||||
TaskList,
|
||||
CustomTaskItem,
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
document: this.ydoc,
|
||||
field: 'tasks',
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<ul data-type="taskList">
|
||||
<li data-type="taskItem" data-checked="true">And this</li>
|
||||
<li data-type="taskItem" data-checked="false">is a task list</li>
|
||||
<li data-type="taskItem" data-checked="false">and only a task list.</li>
|
||||
</ul>
|
||||
`,
|
||||
})
|
||||
|
||||
this.description = new Editor({
|
||||
@ -95,13 +108,28 @@ export default {
|
||||
Paragraph,
|
||||
Text,
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
document: this.ydoc,
|
||||
field: 'description',
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<p>
|
||||
This can be lengthy text.
|
||||
</p>
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
computed: {
|
||||
json() {
|
||||
return {
|
||||
title: yDocToProsemirrorJSON(this.ydoc, 'title'),
|
||||
tasks: yDocToProsemirrorJSON(this.ydoc, 'tasks'),
|
||||
description: yDocToProsemirrorJSON(this.ydoc, 'description'),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.title.destroy()
|
||||
this.tasks.destroy()
|
||||
@ -150,5 +178,23 @@ export default {
|
||||
&--title {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
&--json {
|
||||
background: #0D0D0D;
|
||||
color: #FFF;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
code {
|
||||
color: inherit;
|
||||
background: none;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -43,8 +43,13 @@ export default {
|
||||
],
|
||||
content: `
|
||||
<ul data-type="taskList">
|
||||
<li data-type="taskItem" data-checked="true">A list item</li>
|
||||
<li data-type="taskItem" data-checked="false">And another one</li>
|
||||
<li data-type="taskItem" data-checked="true">flour
|
||||
<li data-type="taskItem" data-checked="false">baking powder</li>
|
||||
<li data-type="taskItem" data-checked="false">salt</li>
|
||||
<li data-type="taskItem" data-checked="false">sugar</li>
|
||||
<li data-type="taskItem" data-checked="false">milk</li>
|
||||
<li data-type="taskItem" data-checked="false">eggs</li>
|
||||
<li data-type="taskItem" data-checked="false">butter</li>
|
||||
</ul>
|
||||
`,
|
||||
})
|
||||
@ -70,5 +75,9 @@ ul[data-type="taskList"] {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,10 @@
|
||||
export class AnnotationItem {
|
||||
public id!: number
|
||||
|
||||
public text!: string
|
||||
|
||||
constructor(id: number, text: string) {
|
||||
this.id = id
|
||||
this.text = text
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
import { AnnotationState } from './AnnotationState'
|
||||
|
||||
export const AnnotationPluginKey = new PluginKey('annotation')
|
||||
|
||||
export const AnnotationPlugin = (options: any) => new Plugin({
|
||||
key: AnnotationPluginKey,
|
||||
state: {
|
||||
init: AnnotationState.init,
|
||||
apply(transaction, oldState) {
|
||||
return oldState.apply(transaction)
|
||||
},
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
const { decorations } = this.getState(state)
|
||||
const { selection } = state
|
||||
|
||||
if (!selection.empty) {
|
||||
return decorations
|
||||
}
|
||||
|
||||
const annotations = this
|
||||
.getState(state)
|
||||
.annotationsAt(selection.from)
|
||||
|
||||
options.onUpdate(annotations)
|
||||
|
||||
return decorations
|
||||
},
|
||||
|
||||
},
|
||||
})
|
@ -0,0 +1,95 @@
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
import { ySyncPluginKey } from 'y-prosemirror'
|
||||
import { AnnotationPluginKey } from './AnnotationPlugin'
|
||||
|
||||
export class AnnotationState {
|
||||
private decorations: any
|
||||
|
||||
constructor(decorations: any) {
|
||||
this.decorations = decorations
|
||||
}
|
||||
|
||||
findAnnotation(id: number) {
|
||||
const current = this.decorations.find()
|
||||
|
||||
for (let i = 0; i < current.length; i += 1) {
|
||||
if (current[i].spec.data.id === id) {
|
||||
return current[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
annotationsAt(position: number) {
|
||||
return this.decorations.find(position, position)
|
||||
}
|
||||
|
||||
apply(transaction: any) {
|
||||
console.log('transaction', transaction.meta, transaction.docChanged, transaction)
|
||||
|
||||
const yjsTransaction = transaction.getMeta(ySyncPluginKey)
|
||||
if (yjsTransaction) {
|
||||
// TODO: Map positions
|
||||
// absolutePositionToRelativePosition(state.selection.anchor, pmbinding.type, pmbinding.mapping)
|
||||
console.log('map positions', transaction, yjsTransaction)
|
||||
|
||||
return this
|
||||
|
||||
// const { binding } = yjsTransaction
|
||||
// console.log({ binding }, { transaction }, transaction.docChanged)
|
||||
// console.log('yjsTransaction.isChangeOrigin', yjsTransaction.isChangeOrigin)
|
||||
|
||||
// console.log('yjs mapping', yjsTransaction.binding?.mapping)
|
||||
// console.log('all decorations', this.decorations.find())
|
||||
// console.log('original prosemirror mapping', this.decorations.map(transaction.mapping, transaction.doc))
|
||||
// console.log('difference between ProseMirror & Y.js', transaction.mapping, yjsTransaction.binding?.mapping)
|
||||
|
||||
// Code to sync the selection:
|
||||
// export const getRelativeSelection = (pmbinding, state) => ({
|
||||
// anchor: absolutePositionToRelativePosition(state.selection.anchor, pmbinding.type, pmbinding.mapping),
|
||||
// head: absolutePositionToRelativePosition(state.selection.head, pmbinding.type, pmbinding.mapping)
|
||||
// })
|
||||
|
||||
// console.log(yjsTransaction.binding.mapping, transaction.curSelection.anchor)
|
||||
}
|
||||
|
||||
if (transaction.docChanged) {
|
||||
// TODO: Fixes the initial load (complete replace of the document)
|
||||
// return this
|
||||
|
||||
// TODO: Fixes later changes (typing before the annotation)
|
||||
const decorations = this.decorations.map(transaction.mapping, transaction.doc)
|
||||
|
||||
return new AnnotationState(decorations)
|
||||
}
|
||||
|
||||
const action = transaction.getMeta(AnnotationPluginKey)
|
||||
const actionType = action && action.type
|
||||
|
||||
if (action) {
|
||||
let { decorations } = this
|
||||
|
||||
if (actionType === 'addAnnotation') {
|
||||
decorations = decorations.add(transaction.doc, [
|
||||
Decoration.inline(action.from, action.to, { class: 'annotation' }, { data: action.data }),
|
||||
])
|
||||
} else if (actionType === 'deleteAnnotation') {
|
||||
decorations = decorations.remove([
|
||||
this.findAnnotation(action.id),
|
||||
])
|
||||
}
|
||||
|
||||
return new AnnotationState(decorations)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
static init(config: any, state: any) {
|
||||
// TODO: Load initial decorations from Y.js?
|
||||
const decorations = DecorationSet.create(state.doc, [
|
||||
Decoration.inline(105, 190, { class: 'annotation' }, { data: { id: 123, content: 'foobar' } }),
|
||||
])
|
||||
|
||||
return new AnnotationState(decorations)
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import { Extension, Command } from '@tiptap/core'
|
||||
import { AnnotationItem } from './AnnotationItem'
|
||||
import { AnnotationPlugin, AnnotationPluginKey } from './AnnotationPlugin'
|
||||
|
||||
function randomId() {
|
||||
return Math.floor(Math.random() * 0xffffffff)
|
||||
}
|
||||
|
||||
export interface AnnotationOptions {
|
||||
HTMLAttributes: {
|
||||
[key: string]: any
|
||||
},
|
||||
onUpdate: (items: [any?]) => {},
|
||||
}
|
||||
|
||||
export const Annotation = Extension.create({
|
||||
name: 'annotation',
|
||||
|
||||
defaultOptions: <AnnotationOptions>{
|
||||
HTMLAttributes: {
|
||||
class: 'annotation',
|
||||
},
|
||||
onUpdate: decorations => decorations,
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
addAnnotation: (content: any): Command => ({ dispatch, state }) => {
|
||||
const { selection } = state
|
||||
|
||||
if (selection.empty) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dispatch && content) {
|
||||
dispatch(state.tr.setMeta(AnnotationPluginKey, {
|
||||
type: 'addAnnotation',
|
||||
from: selection.from,
|
||||
to: selection.to,
|
||||
data: new AnnotationItem(
|
||||
randomId(),
|
||||
content,
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
deleteAnnotation: (id: number): Command => ({ dispatch, state }) => {
|
||||
if (dispatch) {
|
||||
dispatch(state.tr.setMeta(AnnotationPluginKey, { type: 'deleteAnnotation', id }))
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
AnnotationPlugin(this.options),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface AllExtensions {
|
||||
Annotation: typeof Annotation,
|
||||
}
|
||||
}
|
5
docs/src/demos/Experiments/Annotation/extension/index.ts
Normal file
5
docs/src/demos/Experiments/Annotation/extension/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Annotation } from './annotation'
|
||||
|
||||
export * from './annotation'
|
||||
|
||||
export default Annotation
|
7
docs/src/demos/Experiments/Annotation/index.spec.js
Normal file
7
docs/src/demos/Experiments/Annotation/index.spec.js
Normal file
@ -0,0 +1,7 @@
|
||||
context('/api/extensions/annotations', () => {
|
||||
before(() => {
|
||||
cy.visit('/api/extensions/annotations')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
87
docs/src/demos/Experiments/Annotation/index.vue
Normal file
87
docs/src/demos/Experiments/Annotation/index.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="editor">
|
||||
<button @click="addAnnotation" :disabled="!editor.can().addAnnotation()">
|
||||
add annotation
|
||||
</button>
|
||||
<editor-content :editor="editor" />
|
||||
<div v-for="comment in comments" :key="comment.type.spec.data.id">
|
||||
{{ comment.type.spec.data }}
|
||||
|
||||
<button @click="deleteAnnotation(comment.type.spec.data.id)">
|
||||
remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor, EditorContent } from '@tiptap/vue-starter-kit'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import Annotation from './extension'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
comments: [],
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Annotation.configure({
|
||||
onUpdate: items => { this.comments = items },
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<p>
|
||||
Annotations can be used to add additional information to the content, for example comments. They live on a different level than the actual editor content.
|
||||
</p>
|
||||
<p>
|
||||
This example allows you to add plain text, but you’re free to add more complex data, for example JSON from another tiptap instance. :-)
|
||||
</p>
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
addAnnotation() {
|
||||
const content = prompt('Annotation', '')
|
||||
|
||||
this.editor.commands.addAnnotation(content)
|
||||
},
|
||||
deleteAnnotation(id) {
|
||||
this.editor.commands.deleteAnnotation(id)
|
||||
},
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.ProseMirror {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
.annotation {
|
||||
background: #9DEF8F;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,64 @@
|
||||
// @ts-nocheck
|
||||
import { Extension } from '@tiptap/core'
|
||||
import {
|
||||
Plugin, PluginKey,
|
||||
} from 'prosemirror-state'
|
||||
|
||||
export interface CharacterLimitOptions {
|
||||
limit: number,
|
||||
}
|
||||
|
||||
export const CharacterLimit = Extension.create({
|
||||
name: 'characterLimit',
|
||||
|
||||
defaultOptions: <CharacterLimitOptions>{
|
||||
limit: 100,
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const { options } = this
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
|
||||
key: new PluginKey('characterLimit'),
|
||||
|
||||
// state: {
|
||||
// init(_, config) {
|
||||
// // console.log(_, config)
|
||||
// // const length = config.doc.content.size
|
||||
|
||||
// // if (length > options.limit) {
|
||||
// // console.log('too long', options.limit, config)
|
||||
|
||||
// // const transaction = config.tr.insertText('', options.limit + 1, length)
|
||||
|
||||
// // return config.apply(transaction)
|
||||
// // }
|
||||
// },
|
||||
// apply() {
|
||||
// //
|
||||
// },
|
||||
// },
|
||||
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
const oldLength = oldState.doc.content.size
|
||||
const newLength = newState.doc.content.size
|
||||
|
||||
if (newLength > options.limit && newLength > oldLength) {
|
||||
const newTr = newState.tr
|
||||
newTr.insertText('', options.limit + 1, newLength)
|
||||
|
||||
return newTr
|
||||
}
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface AllExtensions {
|
||||
CharacterLimit: typeof CharacterLimit,
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import { CharacterLimit } from './CharacterLimit'
|
||||
|
||||
export * from './CharacterLimit'
|
||||
export default CharacterLimit
|
73
docs/src/demos/Experiments/CharacterLimit/index.vue
Normal file
73
docs/src/demos/Experiments/CharacterLimit/index.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div>
|
||||
<editor-content :editor="editor" />
|
||||
<div>
|
||||
{{ characters }}/{{ limit }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor, EditorContent } from '@tiptap/vue-starter-kit'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import CharacterLimit from './extension'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
limit: 10,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
CharacterLimit.configure({
|
||||
limit: this.limit,
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<p>
|
||||
This is a radically reduced version of tiptap. It has only support for a document, paragraphs and text. That’s it. It’s probably too much for real minimalists though.
|
||||
</p>
|
||||
<p>
|
||||
The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different. You’ll mostly likely want to add a paragraph though.
|
||||
</p>
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
computed: {
|
||||
characters() {
|
||||
if (this.editor) {
|
||||
return this.editor.state.doc.content.size - 2
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.ProseMirror {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
}
|
||||
</style>
|
7
docs/src/demos/Experiments/Comments/index.spec.js
Normal file
7
docs/src/demos/Experiments/Comments/index.spec.js
Normal file
@ -0,0 +1,7 @@
|
||||
context('/examples/annotations', () => {
|
||||
before(() => {
|
||||
cy.visit('/examples/annotations')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
145
docs/src/demos/Experiments/Comments/index.vue
Normal file
145
docs/src/demos/Experiments/Comments/index.vue
Normal file
@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="editor">
|
||||
<h2>
|
||||
Original
|
||||
</h2>
|
||||
<button @click="addComment" :disabled="!editor.can().addAnnotation()">
|
||||
comment
|
||||
</button>
|
||||
<editor-content :editor="editor" />
|
||||
<div v-for="comment in comments" :key="comment.type.spec.data.id">
|
||||
{{ comment.type.spec.data }}
|
||||
|
||||
<button @click="deleteComment(comment.type.spec.data.id)">
|
||||
remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- <br>
|
||||
<h2>
|
||||
ProseMirror JSON from Y.js document
|
||||
</h2>
|
||||
{{ rawDocument }} -->
|
||||
|
||||
<br>
|
||||
<h2>
|
||||
Y.js document
|
||||
</h2>
|
||||
{{ json }}
|
||||
|
||||
<br>
|
||||
<h2>
|
||||
Mirror
|
||||
</h2>
|
||||
<editor-content :editor="anotherEditor" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor, EditorContent } from '@tiptap/vue-starter-kit'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import Collaboration from '@tiptap/extension-collaboration'
|
||||
import Bold from '@tiptap/extension-bold'
|
||||
import Heading from '@tiptap/extension-heading'
|
||||
import * as Y from 'yjs'
|
||||
import { yDocToProsemirrorJSON } from 'y-prosemirror'
|
||||
import Annotation from '../Annotation/extension'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
anotherEditor: null,
|
||||
comments: [],
|
||||
ydoc: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.ydoc = new Y.Doc()
|
||||
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Bold,
|
||||
Heading,
|
||||
Annotation.configure({
|
||||
onUpdate: items => { this.comments = items },
|
||||
}),
|
||||
Collaboration.configure({
|
||||
document: this.ydoc,
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<p>
|
||||
Annotations can be used to add additional information to the content, for example comments. They live on a different level than the actual editor content.
|
||||
</p>
|
||||
<p>
|
||||
This example allows you to add plain text, but you’re free to add more complex data, for example JSON from another tiptap instance. :-)
|
||||
</p>
|
||||
`,
|
||||
})
|
||||
|
||||
this.anotherEditor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
// Annotation.configure({
|
||||
// onUpdate: items => { this.comments = items },
|
||||
// }),
|
||||
Collaboration.configure({
|
||||
document: this.ydoc,
|
||||
}),
|
||||
],
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
addComment() {
|
||||
const content = prompt('Comment', '')
|
||||
|
||||
this.editor.commands.addAnnotation(content)
|
||||
},
|
||||
deleteComment(id) {
|
||||
this.editor.commands.deleteAnnotation(id)
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
rawDocument() {
|
||||
return yDocToProsemirrorJSON(this.ydoc, 'default')
|
||||
},
|
||||
json() {
|
||||
return this.ydoc.toJSON()
|
||||
},
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.ProseMirror {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
.annotation {
|
||||
background: #9DEF8F;
|
||||
}
|
||||
</style>
|
98
docs/src/demos/Experiments/Linter/extension/Linter.ts
Normal file
98
docs/src/demos/Experiments/Linter/extension/Linter.ts
Normal file
@ -0,0 +1,98 @@
|
||||
// @ts-nocheck
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
|
||||
|
||||
function renderIcon(issue) {
|
||||
const icon = document.createElement('div')
|
||||
|
||||
icon.className = 'lint-icon'
|
||||
icon.title = issue.message
|
||||
icon.issue = issue
|
||||
|
||||
return icon
|
||||
}
|
||||
|
||||
function runAllLinterPlugins(doc, plugins) {
|
||||
const decorations: [any?] = []
|
||||
|
||||
const results = plugins.map(LinterPlugin => {
|
||||
return new LinterPlugin(doc).scan().getResults()
|
||||
}).flat()
|
||||
|
||||
results.forEach(issue => {
|
||||
decorations.push(Decoration.inline(issue.from, issue.to, {
|
||||
class: 'problem',
|
||||
}),
|
||||
Decoration.widget(issue.from, renderIcon(issue)))
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
}
|
||||
|
||||
export interface LinterOptions {
|
||||
plugins: [any],
|
||||
}
|
||||
|
||||
export const Linter = Extension.create({
|
||||
name: 'linter',
|
||||
|
||||
defaultOptions: <LinterOptions>{
|
||||
plugins: [],
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const { plugins } = this.options
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('linter'),
|
||||
state: {
|
||||
init(_, { doc }) {
|
||||
return runAllLinterPlugins(doc, plugins)
|
||||
},
|
||||
apply(transaction, oldState) {
|
||||
return transaction.docChanged
|
||||
? runAllLinterPlugins(transaction.doc, plugins)
|
||||
: oldState
|
||||
},
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state)
|
||||
},
|
||||
handleClick(view, _, event) {
|
||||
if (/lint-icon/.test(event.target.className)) {
|
||||
const { from, to } = event.target.issue
|
||||
|
||||
view.dispatch(
|
||||
view.state.tr
|
||||
.setSelection(TextSelection.create(view.state.doc, from, to))
|
||||
.scrollIntoView(),
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
handleDoubleClick(view, _, event) {
|
||||
if (/lint-icon/.test(event.target.className)) {
|
||||
const prob = event.target.issue
|
||||
|
||||
if (prob.fix) {
|
||||
prob.fix(view)
|
||||
view.focus()
|
||||
return true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface AllExtensions {
|
||||
Linter: typeof Linter,
|
||||
}
|
||||
}
|
23
docs/src/demos/Experiments/Linter/extension/LinterPlugin.ts
Normal file
23
docs/src/demos/Experiments/Linter/extension/LinterPlugin.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// @ts-nocheck
|
||||
export default class LinterPlugin {
|
||||
protected doc
|
||||
|
||||
private results = []
|
||||
|
||||
constructor(doc: any) {
|
||||
this.doc = doc
|
||||
}
|
||||
|
||||
record(message: string, from: number, to: number, fix?: null) {
|
||||
this.results.push({
|
||||
message,
|
||||
from,
|
||||
to,
|
||||
fix,
|
||||
})
|
||||
}
|
||||
|
||||
getResults() {
|
||||
return this.results
|
||||
}
|
||||
}
|
8
docs/src/demos/Experiments/Linter/extension/index.ts
Normal file
8
docs/src/demos/Experiments/Linter/extension/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Linter } from './Linter'
|
||||
|
||||
export * from './Linter'
|
||||
export default Linter
|
||||
|
||||
export { BadWords } from './plugins/BadWords'
|
||||
export { Punctuation } from './plugins/Punctuation'
|
||||
export { HeadingLevel } from './plugins/HeadingLevel'
|
@ -0,0 +1,26 @@
|
||||
// @ts-nocheck
|
||||
import LinterPlugin from '../LinterPlugin'
|
||||
|
||||
export class BadWords extends LinterPlugin {
|
||||
|
||||
public regex = /\b(obviously|clearly|evidently|simply)\b/ig
|
||||
|
||||
scan() {
|
||||
this.doc.descendants((node: any, position: any) => {
|
||||
if (!node.isText) {
|
||||
return
|
||||
}
|
||||
|
||||
const matches = this.regex.exec(node.text)
|
||||
|
||||
if (matches) {
|
||||
this.record(
|
||||
`Try not to say '${matches[0]}'`,
|
||||
position + matches.index, position + matches.index + matches[0].length,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
// @ts-nocheck
|
||||
import LinterPlugin from '../LinterPlugin'
|
||||
|
||||
export class HeadingLevel extends LinterPlugin {
|
||||
fixHeader(level) {
|
||||
return function ({ state, dispatch }) {
|
||||
dispatch(state.tr.setNodeMarkup(this.from - 1, null, { level }))
|
||||
}
|
||||
}
|
||||
|
||||
scan() {
|
||||
let lastHeadLevel = null
|
||||
|
||||
this.doc.descendants((node, position) => {
|
||||
if (node.type.name === 'heading') {
|
||||
// Check whether heading levels fit under the current level
|
||||
const { level } = node.attrs
|
||||
|
||||
if (lastHeadLevel != null && level > lastHeadLevel + 1) {
|
||||
this.record(`Heading too small (${level} under ${lastHeadLevel})`,
|
||||
position + 1, position + 1 + node.content.size,
|
||||
this.fixHeader(lastHeadLevel + 1))
|
||||
}
|
||||
lastHeadLevel = level
|
||||
}
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
// @ts-nocheck
|
||||
import LinterPlugin from '../LinterPlugin'
|
||||
|
||||
export class Punctuation extends LinterPlugin {
|
||||
public regex = / ([,.!?:]) ?/g
|
||||
|
||||
fix(replacement: any) {
|
||||
return function ({ state, dispatch }) {
|
||||
dispatch(
|
||||
state.tr.replaceWith(
|
||||
this.from, this.to,
|
||||
state.schema.text(replacement),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
scan() {
|
||||
this.doc.descendants((node, position) => {
|
||||
if (!node.isText) {
|
||||
return
|
||||
}
|
||||
|
||||
const matches = this.regex.exec(node.text)
|
||||
|
||||
if (matches) {
|
||||
this.record(
|
||||
'Suspicious spacing around punctuation',
|
||||
position + matches.index, position + matches.index + matches[0].length,
|
||||
this.fix(`${matches[1]} `),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
96
docs/src/demos/Experiments/Linter/index.vue
Normal file
96
docs/src/demos/Experiments/Linter/index.vue
Normal file
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div>
|
||||
<editor-content :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor, EditorContent } from '@tiptap/vue-starter-kit'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Heading from '@tiptap/extension-heading'
|
||||
import Linter, { BadWords, Punctuation, HeadingLevel } from './extension'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Heading,
|
||||
Text,
|
||||
Linter.configure({
|
||||
plugins: [
|
||||
BadWords,
|
||||
Punctuation,
|
||||
HeadingLevel,
|
||||
],
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<h1>
|
||||
Lint example
|
||||
</h1>
|
||||
<p>
|
||||
This is a sentence ,but the comma clearly isn't in the right place.
|
||||
</p>
|
||||
<h3>
|
||||
Too-minor header
|
||||
</h3>
|
||||
<p>
|
||||
You can hover over the icons on the right to see what the problem is, click them to select the relevant text, and, obviously, double-click them to automatically fix it (if supported).
|
||||
</ul>
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.problem {
|
||||
background: #fdd;
|
||||
border-bottom: 1px solid #f22;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.lint-icon {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 100px;
|
||||
background: #f22;
|
||||
color: white;
|
||||
font-family: times, georgia, serif;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
text-align: center;
|
||||
padding-left: .5px;
|
||||
line-height: 1.1em
|
||||
}
|
||||
|
||||
.lint-icon:before {
|
||||
content: "!";
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
padding-right: 20px;
|
||||
}
|
||||
</style>
|
8
docs/src/docPages/api/extensions/annotation.md
Normal file
8
docs/src/docPages/api/extensions/annotation.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Annotation
|
||||
TODO
|
||||
|
||||
## Source code
|
||||
[packages/extension-annotation/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-annotation/)
|
||||
|
||||
## Usage
|
||||
<demo name="Extensions/Annotation" highlight="24,44-46,60-67" />
|
@ -1,7 +1,7 @@
|
||||
# Suggestion
|
||||
|
||||
:::pro Fund the development 💖
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund the open-source](/sponsor).
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund open source](/sponsor).
|
||||
:::
|
||||
|
||||
TODO
|
||||
|
7
docs/src/docPages/api/nodes/emoji.md
Normal file
7
docs/src/docPages/api/nodes/emoji.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Emoji
|
||||
|
||||
:::pro Fund the development 💖
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund open source](/sponsor).
|
||||
:::
|
||||
|
||||
TODO
|
7
docs/src/docPages/api/nodes/hashtag.md
Normal file
7
docs/src/docPages/api/nodes/hashtag.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Hashtag
|
||||
|
||||
:::pro Fund the development 💖
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund open source](/sponsor).
|
||||
:::
|
||||
|
||||
TODO
|
@ -1,5 +1,9 @@
|
||||
# Mention
|
||||
|
||||
:::pro Fund the development 💖
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund open source](/sponsor).
|
||||
:::
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
# with npm
|
||||
|
@ -1,7 +1,7 @@
|
||||
# TableCell
|
||||
|
||||
:::pro Fund the development 💖
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund the open-source](/sponsor).
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund open source](/sponsor).
|
||||
:::
|
||||
|
||||
TODO
|
||||
|
@ -1,7 +1,7 @@
|
||||
# TableRow
|
||||
|
||||
:::pro Fund the development 💖
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund the open-source](/sponsor).
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund open source](/sponsor).
|
||||
:::
|
||||
|
||||
TODO
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Table
|
||||
|
||||
:::pro Fund the development 💖
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund the open-source](/sponsor).
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund open source](/sponsor).
|
||||
:::
|
||||
|
||||
TODO
|
||||
|
@ -1,9 +1,5 @@
|
||||
# Multiple editors
|
||||
|
||||
The following examples has three different instances of tiptap. The first is configured to have a single paragraph of text, the second to have a task list and the third to have text. All of them are stored in a single Y.js document, which is synced with other users.
|
||||
|
||||
:::warning Shared Document
|
||||
Be nice! The content of this editor is shared with other users from the Internet.
|
||||
:::
|
||||
The following example has three different instances of tiptap. The first is configured to have a single paragraph of text, the second to have a task list and the third to have text. All of them are stored in a single Y.js document, which can be synced with other users.
|
||||
|
||||
<demo name="Examples/MultipleEditors" />
|
||||
|
7
docs/src/docPages/experiments.md
Normal file
7
docs/src/docPages/experiments.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Experiments
|
||||
Congratulations! You’ve found our secret playground with a list of experiments. Be aware, that nothing here is ready to use. Feel free to play around, but please, don’t open an issue for a bug you’ve found here or send pull requests. :-)
|
||||
|
||||
* [Linter](/experiments/linter)
|
||||
* [Annotation](/experiments/annotation)
|
||||
* [Comments](/experiments/comments)
|
||||
* [CharacterLimit](/experiments/character-limit)
|
5
docs/src/docPages/experiments/annotation.md
Normal file
5
docs/src/docPages/experiments/annotation.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Annotation
|
||||
|
||||
⚠️ Experiment
|
||||
|
||||
<demo name="Experiments/Annotation" />
|
5
docs/src/docPages/experiments/character-limit.md
Normal file
5
docs/src/docPages/experiments/character-limit.md
Normal file
@ -0,0 +1,5 @@
|
||||
# CharacterLimit
|
||||
|
||||
⚠️ Experiment
|
||||
|
||||
<demo name="Experiments/CharacterLimit" />
|
5
docs/src/docPages/experiments/comments.md
Normal file
5
docs/src/docPages/experiments/comments.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Comments
|
||||
|
||||
⚠️ Experiment
|
||||
|
||||
<demo name="Experiments/Comments" />
|
5
docs/src/docPages/experiments/linter.md
Normal file
5
docs/src/docPages/experiments/linter.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Linter
|
||||
|
||||
⚠️ Experiment
|
||||
|
||||
<demo name="Experiments/Linter" highlight="" />
|
30
docs/src/docPages/guide/accessibility.md
Normal file
30
docs/src/docPages/guide/accessibility.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Accessibility
|
||||
|
||||
:::pro Fund the development 💖
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for progress here, [become a sponsor and fund open source](/sponsor).
|
||||
:::
|
||||
|
||||
## toc
|
||||
|
||||
## Introduction
|
||||
We strive to make tiptap accessible to everyone, but to be honest, there’s not much work done now. From our current understanding, that’s what needs to be done:
|
||||
|
||||
### Interface
|
||||
An interface needs to have semantic markup, must be keyboard accessible and well documented. Currently, we don’t even provide an interface, so for now that’s totally up to you. But no worries, we’ll provide an interface soon and take accessibility into account early on.
|
||||
|
||||
### Editor
|
||||
The editor needs to produce semantic markup, must be keyboard accessible and well documented. The tiptap content is well structured so that’s a good foundation already. That said, we can add support and encourage the usage of additional attributes, for example the Alt-attribute for images.
|
||||
|
||||
### Writing assistance (optional)
|
||||
An optional writing assitance could help people writing content semanticly correct, for example pointing out an incorrect usage of heading levels. With that kind of assistance provided by the core developers, we could help to improve the content of a lot of applications.
|
||||
|
||||
## Resources
|
||||
|
||||
| Document | Section | Heading |
|
||||
| -------- | ------- | -------------------------------------------------------------------------------------- |
|
||||
| WCAG 2.1 | 1.1 | [Text Alternatives](https://www.w3.org/WAI/WCAG21/Understanding/text-alternatives) |
|
||||
| WCAG 2.1 | 1.1.1 | [Non-text Content](https://www.w3.org/WAI/WCAG21/Understanding/non-text-content) |
|
||||
| WCAG 2.1 | 2.1 | [Keyboard Accessible](https://www.w3.org/WAI/WCAG21/Understanding/keyboard-accessible) |
|
||||
| WCAG 2.1 | 2.1.1 | [Keyboard](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
||||
| WCAG 2.1 | 4.1.1 | [Parsing](https://www.w3.org/WAI/WCAG21/Understanding/parsing) |
|
||||
| WCAG 2.1 | 4.1.2 | [Name, Role, Value](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
51
docs/src/docPages/guide/typescript.md
Normal file
51
docs/src/docPages/guide/typescript.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Working with TypeScript
|
||||
|
||||
## toc
|
||||
|
||||
## Introduction
|
||||
The whole tiptap is code base is written in TypeScript. If you haven’t heard of it or never used it, no worries. You don’t have to.
|
||||
|
||||
TypeScript extends JavaScript by adding types (hence the name). It adds new syntax, which doesn’t exist in plain JavaScript. It’s actually removed before running in the browser, but this step – the compilation – is important to find bugs early. It checks if you passe the right types of data to functions. For a big and complex project, that’s very valuable. It means we’ll get notified of lot of bugs, before shipping code to you.
|
||||
|
||||
Anyway, if you don’t use TypeScript in your project, that’s fine. You’ll still be able to use tiptap and even get a really nice autocomplete for the tiptap API (if your editor supports it, but most do).
|
||||
|
||||
If you’re using TypeScript in your project and want to extend tiptap, there are two things that are good to know.
|
||||
|
||||
## Options type
|
||||
To extend or create default options for an extension, you’ll need to define a custom type, here is an example:
|
||||
|
||||
```ts
|
||||
import { Extension } from '@tiptap/core'
|
||||
|
||||
export interface CustomExtensionOptions {
|
||||
awesomeness: number,
|
||||
}
|
||||
|
||||
const CustomExtension = Extension.create({
|
||||
defaultOptions: <CustomExtensionOptions>{
|
||||
awesomeness: 100,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Command type
|
||||
The core package also exports a `Command` type, which needs to be added to all commands that you specify in your code. Here is an example:
|
||||
|
||||
```ts
|
||||
import { Command, Extension } from '@tiptap/core'
|
||||
|
||||
const CustomExtension = Extension.create({
|
||||
addCommands() {
|
||||
return {
|
||||
/**
|
||||
* Comments will be added to the autocomplete.
|
||||
*/
|
||||
yourCommand: (): Command => ({ commands }) => {
|
||||
// …
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
That’s basically it. We’re doing all the rest automatically.
|
@ -1,7 +0,0 @@
|
||||
# Feedback
|
||||
|
||||
We’re looking for your feedback to improve tiptap 2 before the first public release! Share everything that helps to make it better for everyone!
|
||||
|
||||
* Create issues on GitHub! [Link](https://github.com/ueberdosis/tiptap-next/issues)
|
||||
* Send an email! [humans@tiptap.dev](mailto:humans@tiptap.dev)
|
||||
* Follow us on Twitter! [@hanspagel](https://twitter.com/hanspagel), [@_philippkuehn](https://twitter.com/_philippkuehn), or [@_ueberdosis](https://twitter.com/_ueberdosis)
|
@ -1,11 +1,12 @@
|
||||
# Become a sponsor
|
||||
To deliver a top-notch developer experience and user experience, we put hundreds of hours of unpaid work into tiptap. Your funding helps us to make this work more and more financially sustainable. This enables us, to provide helpful support, maintain all our packages, keep everything up to date, and develop new features and extensions for tiptap.
|
||||
To deliver a top-notch developer experience and user experience, we put ~~hundreds~~ thousands of hours of unpaid work into tiptap. Your funding helps us to make this work more and more financially sustainable. This enables us to provide helpful support, maintain all our packages, keep everything up to date, and develop new features and extensions for tiptap.
|
||||
|
||||
If you’re using tiptap in a commercial project or just want to give back to the open source community, you can [sponsor us on GitHub](https://github.com/sponsors/ueberdosis).
|
||||
Give back to the open source community and [sponsor us on GitHub](https://github.com/sponsors/ueberdosis)! 💖
|
||||
|
||||
## Your benefits as a sponsor
|
||||
* Give back to the open source community
|
||||
* Get early access to private repositories
|
||||
* Ensure the further maintenace and development of tiptap
|
||||
* Your issues and pull requests get a `sponsor 💖` label
|
||||
* Get a sponsor badge in all your comments on GitHub
|
||||
* Show support in your GitHub profile
|
||||
@ -13,11 +14,28 @@ If you’re using tiptap in a commercial project or just want to give back to th
|
||||
|
||||
Does that sound good? [Sponsor us on GitHub!](https://github.com/sponsors/ueberdosis)
|
||||
|
||||
## I can’t use GitHub.
|
||||
If you’re a company, don’t want to use GitHub, don’t have a credit card or want a proper invoice form us, just reach out to us at [humans@tiptap.dev](mailto:humans@tiptap.dev).
|
||||
## The maintainers of tiptap
|
||||
If you’re thankful for tiptap, you should say thank you to all 12 lovely people of [überdosis](https://twitter.com/_ueberdosis). The amazing company we’re all building together and the amazing company that funded the initial development costs of tiptap 2.
|
||||
|
||||
## I want consulting.
|
||||
We don’t do any calls, consulting or personal support. If you have an issue, a question, want to talk something through or anything else, [please use GitHub issues](https://github.com/ueberdosis/tiptap-next/issues), to keep everything accessible for the whole community.
|
||||
AND you should definitely hire us if you want us to design und build an amazing digital product for you. Bonus points if it’s somehow text editing related.
|
||||
|
||||
## Can we have a call?
|
||||
But here are the friendly faces of the two maintainer of tiptap, Philipp Kühn (left) and Hans Pagel (right). You’ve probably read our names in the thousands of commits, pull requests or Tweets already.
|
||||
|
||||
![Philipp and Hans, the maintainers of tiptap, looking happy](/philipp-and-hans.jpg)
|
||||
|
||||
## More peace of mind
|
||||
Companies betting on tiptap probably want some peace of mind and ensure that we keep maintaining tiptap, but don’t forget that our work is based on the work of other lovely people that you should definitel sponsor too:
|
||||
|
||||
* [Sponsor Marijn Haverbeke](https://marijnhaverbeke.nl/fund/) (ProseMirror)
|
||||
* [Sponsor Kevin Jahns](https://github.com/sponsors/dmonad) (Y.js)
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
### I can’t use GitHub. How can I support you?
|
||||
If you’re a company, don’t want to use GitHub, don’t have a credit card or want a proper invoice from us, just reach out to us at [humans@tiptap.dev](mailto:humans@tiptap.dev).
|
||||
|
||||
### I want consulting. What’s your rate?
|
||||
We don’t do any calls, consulting or personal support for tiptap. If you have an issue, a question, want to talk something through or anything else, [please use GitHub issues](https://github.com/ueberdosis/tiptap-next/issues) to keep everything accessible for the whole community.
|
||||
|
||||
### 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.
|
||||
|
@ -65,10 +65,6 @@
|
||||
<div class="app__inner">
|
||||
<a :href="editLink" target="_blank">Edit this page on GitHub</a>
|
||||
·
|
||||
<a href="/impressum">Impressum</a>
|
||||
·
|
||||
<a href="/privacy-policy">Privacy Policy</a>
|
||||
·
|
||||
Made with 🖤 by <a href="https://twitter.com/_ueberdosis">überdosis</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -87,9 +83,7 @@
|
||||
'app__link': true,
|
||||
'app__link--exact': $router.currentRoute.path === item.link,
|
||||
'app__link--active': $router.currentRoute.path.startsWith(item.link),
|
||||
'app__link--draft': item.draft === true,
|
||||
'app__link--pro': item.pro === true,
|
||||
'app__link--new': item.new === true,
|
||||
[`app__link--${item.type}`]: item.type !== null,
|
||||
'app__link--with-children': !!item.items
|
||||
}"
|
||||
:to="item.redirect || item.link"
|
||||
@ -104,9 +98,7 @@
|
||||
'app__link': true,
|
||||
'app__link--exact': $router.currentRoute.path === item.link,
|
||||
'app__link--active': $router.currentRoute.path.startsWith(item.link),
|
||||
'app__link--draft': item.draft === true,
|
||||
'app__link--pro': item.pro === true,
|
||||
'app__link--new': item.new === true,
|
||||
[`app__link--${item.type}`]: item.type !== null,
|
||||
}"
|
||||
:to="item.link"
|
||||
exact
|
||||
|
@ -255,6 +255,18 @@ $menuBreakPoint: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
&--sponsor {
|
||||
color: $colorWhite;
|
||||
|
||||
&::after {
|
||||
content: '💖';
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
text-transform: uppercase;
|
||||
padding: 0 0.5em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&--with-children::after {
|
||||
content: '↓';
|
||||
color: rgba($colorWhite, 0.2);
|
||||
|
@ -8,8 +8,9 @@
|
||||
link: /overview/upgrade-guide
|
||||
- title: Contributing
|
||||
link: /overview/contributing
|
||||
- title: Feedback
|
||||
link: /overview/feedback
|
||||
- title: Become a sponsor
|
||||
link: /sponsor
|
||||
type: sponsor
|
||||
|
||||
- title: Examples
|
||||
link: /examples
|
||||
@ -19,7 +20,7 @@
|
||||
link: /examples/basic
|
||||
- title: Collaborative editing
|
||||
link: /examples/collaborative-editing
|
||||
pro: true
|
||||
type: pro
|
||||
- title: Markdown shortcuts
|
||||
link: /examples/markdown-shortcuts
|
||||
- title: Formatting
|
||||
@ -34,6 +35,10 @@
|
||||
link: /examples/drawing
|
||||
- title: Multiple editors
|
||||
link: /examples/multiple-editors
|
||||
- title: Comments
|
||||
link: /examples/comments
|
||||
draft: true
|
||||
|
||||
|
||||
- title: Guide
|
||||
items:
|
||||
@ -48,11 +53,11 @@
|
||||
skip: true
|
||||
- title: Alpine.js
|
||||
link: /guide/getting-started/alpinejs
|
||||
draft: true
|
||||
type: draft
|
||||
skip: true
|
||||
- title: Livewire
|
||||
link: /guide/getting-started/livewire
|
||||
draft: true
|
||||
type: draft
|
||||
skip: true
|
||||
- title: Configure the editor
|
||||
link: /guide/configuration
|
||||
@ -66,12 +71,15 @@
|
||||
link: /guide/build-extensions
|
||||
- title: Complex node views
|
||||
link: /guide/node-views
|
||||
draft: true
|
||||
- title: Working with TypeScript
|
||||
link: /guide/working-with-typescript
|
||||
type: draft
|
||||
- title: Collaborative editing
|
||||
link: /guide/collaborative-editing
|
||||
pro: true
|
||||
type: pro
|
||||
- title: Accessibility
|
||||
link: /guide/accessibility
|
||||
type: draft
|
||||
- title: Working with TypeScript
|
||||
link: /guide/typescript
|
||||
|
||||
- title: API
|
||||
items:
|
||||
@ -90,8 +98,14 @@
|
||||
link: /api/nodes/code-block
|
||||
- title: Document
|
||||
link: /api/nodes/document
|
||||
- title: Emoji
|
||||
link: /api/nodes/emoji
|
||||
type: draft
|
||||
- title: HardBreak
|
||||
link: /api/nodes/hard-break
|
||||
- title: Hashtag
|
||||
link: /api/nodes/hashtag
|
||||
type: draft
|
||||
- title: Heading
|
||||
link: /api/nodes/heading
|
||||
- title: HorizontalRule
|
||||
@ -102,20 +116,20 @@
|
||||
link: /api/nodes/list-item
|
||||
- title: Mention
|
||||
link: /api/nodes/mention
|
||||
draft: true
|
||||
type: draft
|
||||
- title: OrderedList
|
||||
link: /api/nodes/ordered-list
|
||||
- title: Paragraph
|
||||
link: /api/nodes/paragraph
|
||||
- title: Table
|
||||
link: /api/nodes/table
|
||||
draft: true
|
||||
type: draft
|
||||
- title: TableRow
|
||||
link: /api/nodes/table-row
|
||||
draft: true
|
||||
type: draft
|
||||
- title: TableCell
|
||||
link: /api/nodes/table-cell
|
||||
draft: true
|
||||
type: draft
|
||||
- title: TaskList
|
||||
link: /api/nodes/task-list
|
||||
- title: TaskItem
|
||||
@ -144,12 +158,15 @@
|
||||
- title: Extensions
|
||||
link: /api/extensions
|
||||
items:
|
||||
- title: Annotation
|
||||
link: /api/extensions/annotation
|
||||
draft: true
|
||||
- title: Collaboration
|
||||
link: /api/extensions/collaboration
|
||||
pro: true
|
||||
type: pro
|
||||
- title: CollaborationCursor
|
||||
link: /api/extensions/collaboration-cursor
|
||||
pro: true
|
||||
type: pro
|
||||
- title: Dropcursor
|
||||
link: /api/extensions/dropcursor
|
||||
- title: Focus
|
||||
@ -162,7 +179,7 @@
|
||||
link: /api/extensions/history
|
||||
- title: Suggestion
|
||||
link: /api/extensions/suggestion
|
||||
draft: true
|
||||
type: draft
|
||||
- title: TextAlign
|
||||
link: /api/extensions/text-align
|
||||
- title: Typography
|
||||
@ -176,14 +193,17 @@
|
||||
- title: Keyboard Shortcuts
|
||||
link: /api/keyboard-shortcuts
|
||||
|
||||
- title: Sponsoring
|
||||
items:
|
||||
- title: Become a sponsor
|
||||
link: /sponsor
|
||||
- title: Monthly reports
|
||||
link: /reports
|
||||
|
||||
- title: Links
|
||||
items:
|
||||
- title: 'Follow on Twitter'
|
||||
link: https://twitter.com/tiptap_editor
|
||||
- title: Documentation for tiptap 1.x
|
||||
link: https://v1.tiptap.dev
|
||||
|
||||
- title: Legal
|
||||
items:
|
||||
- title: Impressum
|
||||
link: /impressum
|
||||
- title: Privacy Policy
|
||||
link: /privacy-policy
|
||||
|
@ -67,6 +67,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
> p > img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
BIN
docs/static/philipp-and-hans.jpg
vendored
Normal file
BIN
docs/static/philipp-and-hans.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
Loading…
Reference in New Issue
Block a user