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:
Philipp Kühn 2021-01-19 22:33:22 +01:00
commit b1d3b4ce8d
46 changed files with 1207 additions and 66 deletions

View File

@ -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"

View File

@ -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>

View File

@ -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, thisll 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>

View File

@ -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>

View File

@ -0,0 +1,10 @@
export class AnnotationItem {
public id!: number
public text!: string
constructor(id: number, text: string) {
this.id = id
this.text = text
}
}

View File

@ -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
},
},
})

View File

@ -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)
}
}

View File

@ -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,
}
}

View File

@ -0,0 +1,5 @@
import { Annotation } from './annotation'
export * from './annotation'
export default Annotation

View File

@ -0,0 +1,7 @@
context('/api/extensions/annotations', () => {
before(() => {
cy.visit('/api/extensions/annotations')
})
// TODO: Write tests
})

View 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 youre 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>

View File

@ -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,
}
}

View File

@ -0,0 +1,4 @@
import { CharacterLimit } from './CharacterLimit'
export * from './CharacterLimit'
export default CharacterLimit

View 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. Thats it. Its 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. Youll 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>

View File

@ -0,0 +1,7 @@
context('/examples/annotations', () => {
before(() => {
cy.visit('/examples/annotations')
})
// TODO: Write tests
})

View 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 youre 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>

View 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,
}
}

View 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
}
}

View 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'

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View 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>

View 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" />

View File

@ -1,7 +1,7 @@
# Suggestion
:::pro Fund the development 💖
We need your support to maintain, update, support and develop tiptap 2. If youre 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 youre waiting for this extension, [become a sponsor and fund open source](/sponsor).
:::
TODO

View File

@ -0,0 +1,7 @@
# Emoji
:::pro Fund the development 💖
We need your support to maintain, update, support and develop tiptap 2. If youre waiting for this extension, [become a sponsor and fund open source](/sponsor).
:::
TODO

View File

@ -0,0 +1,7 @@
# Hashtag
:::pro Fund the development 💖
We need your support to maintain, update, support and develop tiptap 2. If youre waiting for this extension, [become a sponsor and fund open source](/sponsor).
:::
TODO

View File

@ -1,5 +1,9 @@
# Mention
:::pro Fund the development 💖
We need your support to maintain, update, support and develop tiptap 2. If youre waiting for this extension, [become a sponsor and fund open source](/sponsor).
:::
## Installation
```bash
# with npm

View File

@ -1,7 +1,7 @@
# TableCell
:::pro Fund the development 💖
We need your support to maintain, update, support and develop tiptap 2. If youre 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 youre waiting for this extension, [become a sponsor and fund open source](/sponsor).
:::
TODO

View File

@ -1,7 +1,7 @@
# TableRow
:::pro Fund the development 💖
We need your support to maintain, update, support and develop tiptap 2. If youre 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 youre waiting for this extension, [become a sponsor and fund open source](/sponsor).
:::
TODO

View File

@ -1,7 +1,7 @@
# Table
:::pro Fund the development 💖
We need your support to maintain, update, support and develop tiptap 2. If youre 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 youre waiting for this extension, [become a sponsor and fund open source](/sponsor).
:::
TODO

View File

@ -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" />

View File

@ -0,0 +1,7 @@
# Experiments
Congratulations! Youve 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, dont open an issue for a bug youve found here or send pull requests. :-)
* [Linter](/experiments/linter)
* [Annotation](/experiments/annotation)
* [Comments](/experiments/comments)
* [CharacterLimit](/experiments/character-limit)

View File

@ -0,0 +1,5 @@
# Annotation
⚠️ Experiment
<demo name="Experiments/Annotation" />

View File

@ -0,0 +1,5 @@
# CharacterLimit
⚠️ Experiment
<demo name="Experiments/CharacterLimit" />

View File

@ -0,0 +1,5 @@
# Comments
⚠️ Experiment
<demo name="Experiments/Comments" />

View File

@ -0,0 +1,5 @@
# Linter
⚠️ Experiment
<demo name="Experiments/Linter" highlight="" />

View File

@ -0,0 +1,30 @@
# Accessibility
:::pro Fund the development 💖
We need your support to maintain, update, support and develop tiptap 2. If youre 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, theres not much work done now. From our current understanding, thats what needs to be done:
### Interface
An interface needs to have semantic markup, must be keyboard accessible and well documented. Currently, we dont even provide an interface, so for now thats totally up to you. But no worries, well 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 thats 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) |

View File

@ -0,0 +1,51 @@
# Working with TypeScript
## toc
## Introduction
The whole tiptap is code base is written in TypeScript. If you havent heard of it or never used it, no worries. You dont have to.
TypeScript extends JavaScript by adding types (hence the name). It adds new syntax, which doesnt exist in plain JavaScript. Its 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, thats very valuable. It means well get notified of lot of bugs, before shipping code to you.
Anyway, if you dont use TypeScript in your project, thats fine. Youll 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 youre 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, youll 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 }) => {
// …
},
}
},
})
```
Thats basically it. Were doing all the rest automatically.

View File

@ -1,7 +0,0 @@
# Feedback
Were 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)

View File

@ -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 youre 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 youre 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 cant use GitHub.
If youre a company, dont want to use GitHub, dont 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 youre thankful for tiptap, you should say thank you to all 12 lovely people of [überdosis](https://twitter.com/_ueberdosis). The amazing company were all building together and the amazing company that funded the initial development costs of tiptap 2.
## I want consulting.
We dont 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 its 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). Youve 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 dont 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 cant use GitHub. How can I support you?
If youre a company, dont want to use GitHub, dont 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. Whats your rate?
We dont 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 dont expect technical email support.

View File

@ -65,10 +65,6 @@
<div class="app__inner">
<a :href="editLink" target="_blank">Edit this page on GitHub</a>
&middot;
<a href="/impressum">Impressum</a>
&middot;
<a href="/privacy-policy">Privacy Policy</a>
&middot;
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

View File

@ -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);

View File

@ -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

View File

@ -67,6 +67,10 @@
}
}
> p > img {
max-width: 100%;
}
:first-child {
margin-top: 0;
}

BIN
docs/static/philipp-and-hans.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB