Merge branch 'main' of https://github.com/ueberdosis/tiptap-next into feature/remove-inferred-commands

# Conflicts:
#	docs/src/demos/Experiments/Annotation/extension/annotation.ts
#	docs/src/demos/Experiments/Color/extension/Color.ts
#	docs/src/demos/Experiments/Details/details.ts
This commit is contained in:
Philipp Kühn 2021-02-16 18:08:12 +01:00
commit e4cb53eab7
57 changed files with 813 additions and 694 deletions

View File

@ -1,4 +1,5 @@
<template> <template>
<client-only>
<demo-frame v-if="inline && mainFile" v-bind="props" /> <demo-frame v-if="inline && mainFile" v-bind="props" />
<div class="demo" v-else> <div class="demo" v-else>
<template v-if="mainFile"> <template v-if="mainFile">
@ -29,13 +30,11 @@
</a> </a>
</div> </div>
</template> </template>
<template v-else> <div v-else class="demo__error">
<div v-if="mainFile === false" class="demo__error">
Could not find a demo called {{ name }}. Could not find a demo called {{ name }}.
</div> </div>
<div v-else class="demo__skeleton" />
</template>
</div> </div>
</client-only>
</template> </template>
<script> <script>

View File

@ -89,16 +89,12 @@
} }
&__error { &__error {
padding: 1rem 1.5rem; padding: 1rem 1.25rem;
color: $colorRed; border-radius: 0.75rem;
background-color: rgba($colorRed, 0.1); border: 3px solid $colorBlack;
} background-color: $colorRed;
font-size: 1.1rem;
&__skeleton { font-weight: 700;
border-top-left-radius: inherit; margin-bottom: 0.25rem;
border-top-right-radius: inherit;
background-color: $colorWhite;
min-height: 20rem;
opacity: 0.1;
} }
} }

View File

@ -23,11 +23,6 @@ export default {
required: true, required: true,
}, },
mode: {
type: String,
default: 'vue',
},
inline: { inline: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -52,7 +47,7 @@ export default {
computed: { computed: {
query() { query() {
return `mode=${this.mode}&inline=${this.inline}&highlight=${this.highlight}&showSource=${this.showSource}` return `inline=${this.inline}&highlight=${this.highlight}&showSource=${this.showSource}`
}, },
}, },

View File

@ -7,11 +7,6 @@ export default {
required: true, required: true,
}, },
mode: {
type: String,
default: 'vue',
},
inline: { inline: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -41,7 +36,6 @@ export default {
props() { props() {
return { return {
name: this.name, name: this.name,
mode: this.mode,
inline: this.inline, inline: this.inline,
highlight: this.highlight, highlight: this.highlight,
showSource: this.showSource, showSource: this.showSource,
@ -49,14 +43,27 @@ export default {
}, },
mainFile() { mainFile() {
const file = this.files if (!this.mainFilePath) {
.find(item => item.path.endsWith('index.vue') || item.path.endsWith('index.jsx'))
if (!file) {
return false return false
} }
return require(`~/demos/${file.path}`).default return require(`~/demos/${this.mainFilePath}`).default
},
mainFilePath() {
const file = this.files.find(item => item.path.endsWith('index.vue') || item.path.endsWith('index.jsx'))
if (file) {
return file.path
}
},
mode() {
if (this.mainFilePath?.endsWith('.jsx')) {
return 'react'
}
return 'vue'
}, },
}, },

View File

@ -67,6 +67,8 @@ export default {
this.status = event.status this.status = event.status
}) })
window.ydoc = ydoc
this.indexdb = new IndexeddbPersistence('tiptap-collaboration-example', ydoc) this.indexdb = new IndexeddbPersistence('tiptap-collaboration-example', ydoc)
this.editor = new Editor({ this.editor = new Editor({
@ -141,7 +143,7 @@ export default {
.editor { .editor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 90vh; max-height: 400px;
color: #0D0D0D; color: #0D0D0D;
background-color: $colorWhite; background-color: $colorWhite;
border: 3px solid #0D0D0D; border: 3px solid #0D0D0D;

View File

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

View File

@ -1,95 +0,0 @@
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

@ -1,73 +0,0 @@
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?]) => {},
}
declare module '@tiptap/core' {
interface AllCommands {
annotation: {
addAnnotation: (content: any) => Command,
deleteAnnotation: (id: number) => Command,
}
}
}
export const Annotation = Extension.create({
name: 'annotation',
defaultOptions: <AnnotationOptions>{
HTMLAttributes: {
class: 'annotation',
},
onUpdate: decorations => decorations,
},
addCommands() {
return {
addAnnotation: (content: any) => ({ 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) => ({ dispatch, state }) => {
if (dispatch) {
dispatch(state.tr.setMeta(AnnotationPluginKey, { type: 'deleteAnnotation', id }))
}
return true
},
}
},
addProseMirrorPlugins() {
return [
AnnotationPlugin(this.options),
]
},
})

View File

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

View File

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

View File

@ -1,87 +0,0 @@
<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,37 @@
export class AnnotationItem {
private decoration!: any
constructor(decoration: any) {
this.decoration = decoration
}
get id() {
return this.decoration.type.spec.id
}
get from() {
return this.decoration.from
}
get to() {
return this.decoration.to
}
get data() {
return this.decoration.type.spec.data
}
get HTMLAttributes() {
return this.decoration.type.attrs
}
toString() {
return JSON.stringify({
id: this.id,
data: this.data,
from: this.from,
to: this.to,
HTMLAttributes: this.HTMLAttributes,
})
}
}

View File

@ -1,16 +1,34 @@
import * as Y from 'yjs'
import { Plugin, PluginKey } from 'prosemirror-state' import { Plugin, PluginKey } from 'prosemirror-state'
import { AnnotationState } from './AnnotationState' import { AnnotationState } from './AnnotationState'
export const AnnotationPluginKey = new PluginKey('annotation') export const AnnotationPluginKey = new PluginKey('annotation')
export const AnnotationPlugin = (options: any) => new Plugin({ export interface AnnotationPluginOptions {
HTMLAttributes: {
[key: string]: any
},
onUpdate: (items: [any?]) => {},
map: Y.Map<any>,
instance: string,
}
export const AnnotationPlugin = (options: AnnotationPluginOptions) => new Plugin({
key: AnnotationPluginKey, key: AnnotationPluginKey,
state: { state: {
init: AnnotationState.init, init() {
apply(transaction, oldState) { return new AnnotationState({
return oldState.apply(transaction) HTMLAttributes: options.HTMLAttributes,
map: options.map,
instance: options.instance,
})
},
apply(transaction, pluginState, oldState, newState) {
return pluginState.apply(transaction, newState)
}, },
}, },
props: { props: {
decorations(state) { decorations(state) {
const { decorations } = this.getState(state) const { decorations } = this.getState(state)
@ -28,6 +46,5 @@ export const AnnotationPlugin = (options: any) => new Plugin({
return decorations return decorations
}, },
}, },
}) })

View File

@ -0,0 +1,151 @@
import * as Y from 'yjs'
import { EditorState, Transaction } from 'prosemirror-state'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { ySyncPluginKey, relativePositionToAbsolutePosition, absolutePositionToRelativePosition } from 'y-prosemirror'
import { AddAnnotationAction, DeleteAnnotationAction, UpdateAnnotationAction } from './collaboration-annotation'
import { AnnotationPluginKey } from './AnnotationPlugin'
import { AnnotationItem } from './AnnotationItem'
export interface AnnotationStateOptions {
HTMLAttributes: {
[key: string]: any
},
map: Y.Map<any>,
instance: string,
}
export class AnnotationState {
options: AnnotationStateOptions
decorations = DecorationSet.empty
constructor(options: AnnotationStateOptions) {
this.options = options
}
randomId() {
// TODO: That seems … to simple.
return Math.floor(Math.random() * 0xffffffff).toString()
}
findAnnotation(id: string) {
const current = this.decorations.find()
for (let i = 0; i < current.length; i += 1) {
if (current[i].spec.id === id) {
return current[i]
}
}
}
addAnnotation(action: AddAnnotationAction, state: EditorState) {
const ystate = ySyncPluginKey.getState(state)
const { type, binding } = ystate
const { map } = this.options
const { from, to, data } = action
const absoluteFrom = absolutePositionToRelativePosition(from, type, binding.mapping)
const absoluteTo = absolutePositionToRelativePosition(to, type, binding.mapping)
map.set(this.randomId(), {
from: absoluteFrom,
to: absoluteTo,
data,
})
}
updateAnnotation(action: UpdateAnnotationAction) {
const { map } = this.options
const annotation = map.get(action.id)
map.set(action.id, {
from: annotation.from,
to: annotation.to,
data: action.data,
})
}
deleteAnnotation(id: string) {
const { map } = this.options
map.delete(id)
}
annotationsAt(position: number) {
return this.decorations.find(position, position).map(decoration => {
return new AnnotationItem(decoration)
})
}
createDecorations(state: EditorState) {
const { map, HTMLAttributes } = this.options
const ystate = ySyncPluginKey.getState(state)
const { doc, type, binding } = ystate
const decorations: Decoration[] = []
map.forEach((annotation, id) => {
const from = relativePositionToAbsolutePosition(doc, type, annotation.from, binding.mapping)
const to = relativePositionToAbsolutePosition(doc, type, annotation.to, binding.mapping)
if (!from || !to) {
return
}
console.log(`[${this.options.instance}] Decoration.inline()`, from, to, HTMLAttributes, { id, data: annotation.data })
if (from === to) {
console.warn(`[${this.options.instance}] corrupt decoration `, annotation.from, from, annotation.to, to)
}
decorations.push(
Decoration.inline(from, to, HTMLAttributes, { id, data: annotation.data, inclusiveEnd: true }),
)
})
this.decorations = DecorationSet.create(state.doc, decorations)
}
apply(transaction: Transaction, state: EditorState) {
// Add/Remove annotations
const action = transaction.getMeta(AnnotationPluginKey) as AddAnnotationAction | UpdateAnnotationAction | DeleteAnnotationAction
if (action && action.type) {
console.log(`[${this.options.instance}] action: ${action.type}`)
if (action.type === 'addAnnotation') {
this.addAnnotation(action, state)
}
if (action.type === 'updateAnnotation') {
this.updateAnnotation(action)
}
if (action.type === 'deleteAnnotation') {
this.deleteAnnotation(action.id)
}
// @ts-ignore
if (action.type === 'createDecorations') {
this.createDecorations(state)
}
return this
}
// Use Y.js to update positions
const ystate = ySyncPluginKey.getState(state)
if (ystate.isChangeOrigin) {
console.log(`[${this.options.instance}] isChangeOrigin: true → createDecorations`)
this.createDecorations(state)
return this
}
// Use ProseMirror to update positions
console.log(`[${this.options.instance}] isChangeOrigin: false → ProseMirror mapping`)
this.decorations = this.decorations.map(transaction.mapping, transaction.doc)
return this
}
}

View File

@ -0,0 +1,144 @@
import * as Y from 'yjs'
import { Extension, Command } from '@tiptap/core'
import { AnnotationPlugin, AnnotationPluginKey } from './AnnotationPlugin'
export interface AddAnnotationAction {
type: 'addAnnotation',
data: any,
from: number,
to: number,
}
export interface UpdateAnnotationAction {
type: 'updateAnnotation',
id: string,
data: any,
}
export interface DeleteAnnotationAction {
type: 'deleteAnnotation',
id: string,
}
export interface AnnotationOptions {
HTMLAttributes: {
[key: string]: any
},
/**
* An event listener which receives annotations for the current selection.
*/
onUpdate: (items: [any?]) => {},
/**
* An initialized Y.js document.
*/
document: Y.Doc | null,
/**
* Name of a Y.js map, can be changed to sync multiple fields with one Y.js document.
*/
field: string,
/**
* A raw Y.js map, can be used instead of `document` and `field`.
*/
map: Y.Map<any> | null,
instance: string,
}
function getMapFromOptions(options: AnnotationOptions): Y.Map<any> {
return options.map
? options.map
: options.document?.getMap(options.field) as Y.Map<any>
}
declare module '@tiptap/core' {
interface AllCommands {
annotation: {
addAnnotation: (data: any) => Command,
updateAnnotation: (id: string, data: any) => Command,
deleteAnnotation: (id: string) => Command,
}
}
}
export const CollaborationAnnotation = Extension.create({
name: 'annotation',
defaultOptions: <AnnotationOptions>{
HTMLAttributes: {
class: 'annotation',
},
onUpdate: decorations => decorations,
document: null,
field: 'annotations',
map: null,
instance: '',
},
onCreate() {
const map = getMapFromOptions(this.options)
map.observe(() => {
console.log(`[${this.options.instance}] map updated → createDecorations`)
const transaction = this.editor.state.tr.setMeta(AnnotationPluginKey, {
type: 'createDecorations',
})
this.editor.view.dispatch(transaction)
})
},
addCommands() {
return {
addAnnotation: (data: any) => ({ dispatch, state }) => {
const { selection } = state
if (selection.empty) {
return false
}
if (dispatch && data) {
state.tr.setMeta(AnnotationPluginKey, <AddAnnotationAction>{
type: 'addAnnotation',
from: selection.from,
to: selection.to,
data,
})
}
return true
},
updateAnnotation: (id: string, data: any) => ({ dispatch, state }) => {
if (dispatch) {
state.tr.setMeta(AnnotationPluginKey, <UpdateAnnotationAction>{
type: 'updateAnnotation',
id,
data,
})
}
return true
},
deleteAnnotation: id => ({ dispatch, state }) => {
if (dispatch) {
state.tr.setMeta(AnnotationPluginKey, <DeleteAnnotationAction>{
type: 'deleteAnnotation',
id,
})
}
return true
},
}
},
addProseMirrorPlugins() {
return [
AnnotationPlugin({
HTMLAttributes: this.options.HTMLAttributes,
onUpdate: this.options.onUpdate,
map: getMapFromOptions(this.options),
instance: this.options.instance,
}),
]
},
})

View File

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

View File

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

View File

@ -2,36 +2,30 @@
<div> <div>
<div v-if="editor"> <div v-if="editor">
<h2> <h2>
Original Original Editor
</h2> </h2>
<button @click="addComment" :disabled="!editor.can().addAnnotation()"> <button @click="addComment" :disabled="!editor.can().addAnnotation()">
comment comment
</button> </button>
<editor-content :editor="editor" /> <editor-content :editor="editor" />
<div v-for="comment in comments" :key="comment.type.spec.data.id"> <div v-for="comment in comments" :key="comment.id">
{{ comment.type.spec.data }} {{ comment }}
<button @click="deleteComment(comment.type.spec.data.id)"> <button @click="updateComment(comment.id)">
update
</button>
<button @click="deleteComment(comment.id)">
remove remove
</button> </button>
</div> </div>
<!-- <br>
<h2> <h2>
ProseMirror JSON from Y.js document Another Editor
</h2>
{{ rawDocument }} -->
<br>
<h2>
Y.js document
</h2>
{{ json }}
<br>
<h2>
Mirror
</h2> </h2>
<button @click="addAnotherComment" :disabled="!anotherEditor.can().addAnnotation()">
comment
</button>
<editor-content :editor="anotherEditor" /> <editor-content :editor="anotherEditor" />
</div> </div>
</div> </div>
@ -46,8 +40,7 @@ import Collaboration from '@tiptap/extension-collaboration'
import Bold from '@tiptap/extension-bold' import Bold from '@tiptap/extension-bold'
import Heading from '@tiptap/extension-heading' import Heading from '@tiptap/extension-heading'
import * as Y from 'yjs' import * as Y from 'yjs'
import { yDocToProsemirrorJSON } from 'y-prosemirror' import CollaborationAnnotation from './extension'
import Annotation from '../Annotation/extension'
export default { export default {
components: { components: {
@ -73,8 +66,10 @@ export default {
Text, Text,
Bold, Bold,
Heading, Heading,
Annotation.configure({ CollaborationAnnotation.configure({
document: this.ydoc,
onUpdate: items => { this.comments = items }, onUpdate: items => { this.comments = items },
instance: 'editor1',
}), }),
Collaboration.configure({ Collaboration.configure({
document: this.ydoc, document: this.ydoc,
@ -95,9 +90,12 @@ export default {
Document, Document,
Paragraph, Paragraph,
Text, Text,
// Annotation.configure({ Bold,
// onUpdate: items => { this.comments = items }, Heading,
// }), CollaborationAnnotation.configure({
document: this.ydoc,
instance: 'editor2',
}),
Collaboration.configure({ Collaboration.configure({
document: this.ydoc, document: this.ydoc,
}), }),
@ -107,26 +105,32 @@ export default {
methods: { methods: {
addComment() { addComment() {
const content = prompt('Comment', '') const data = prompt('Comment', '')
this.editor.commands.addAnnotation(content) this.editor.commands.addAnnotation(data)
},
updateComment(id) {
const comment = this.comments.find(item => {
return id === item.id
})
const data = prompt('Comment', comment.data)
this.editor.commands.updateAnnotation(id, data)
}, },
deleteComment(id) { deleteComment(id) {
this.editor.commands.deleteAnnotation(id) this.editor.commands.deleteAnnotation(id)
}, },
}, addAnotherComment() {
const data = prompt('Comment', '')
computed: { this.anotherEditor.commands.addAnnotation(data)
rawDocument() {
return yDocToProsemirrorJSON(this.ydoc, 'default')
},
json() {
return this.ydoc.toJSON()
}, },
}, },
beforeDestroy() { beforeDestroy() {
this.editor.destroy() this.editor.destroy()
this.anotherEditor.destroy()
}, },
} }
</script> </script>

View File

@ -1,62 +0,0 @@
// @ts-nocheck
import { Extension } from '@tiptap/core'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { Plugin } from 'prosemirror-state'
function detectColors(doc) {
const hexColor = /(#[0-9a-f]{3,6})\b/ig
const results = []
const decorations: [any?] = []
doc.descendants((node: any, position: any) => {
if (!node.isText) {
return
}
let matches
// eslint-disable-next-line
while (matches = hexColor.exec(node.text)) {
results.push({
color: matches[0],
from: position + matches.index,
to: position + matches.index + matches[0].length,
})
}
})
results.forEach(issue => {
decorations.push(Decoration.inline(issue.from, issue.to, {
class: 'color',
style: `--color: ${issue.color}`,
}))
})
return DecorationSet.create(doc, decorations)
}
export const Color = Extension.create({
name: 'color',
addProseMirrorPlugins() {
return [
new Plugin({
state: {
init(_, { doc }) {
return detectColors(doc)
},
apply(transaction, oldState) {
return transaction.docChanged
? detectColors(transaction.doc)
: oldState
},
},
props: {
decorations(state) {
return this.getState(state)
},
},
}),
]
},
})

View File

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

View File

@ -1,76 +0,0 @@
<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 Color from './extension'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Heading,
Text,
Color,
],
content: `
<p>
For triplets with repeated values, you can eliminate the repetition by writing in shorthand, for instance, #00FFFF becomes #0FF. This system is easy for computers to understand, and it pretty short to write, which makes it useful for quick copy paste and designation in programming. If youre going to work with colors in a more involved way, though, HSL is a little bit more human-readable.
</p>
<p>
A few more examples: #FFF, #0D0D0D, #616161, #A975FF, #FB5151, #FD9170, #FFCB6B, #68CEF8, #80cbc4, #9DEF8F
</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
}
.color {
white-space: nowrap;
&::before {
content: ' ';
display: inline-block;
width: 1em;
height: 1em;
border: 1px solid rgba(128, 128, 128, 0.3);
vertical-align: middle;
margin-right: 0.1em;
margin-bottom: 0.15em;
border-radius: 2px;
background-color: var(--color);
}
}
</style>

View File

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

View File

@ -9,9 +9,13 @@ export interface DetailsSummaryOptions {
export default Node.create<DetailsSummaryOptions>({ export default Node.create<DetailsSummaryOptions>({
name: 'detailsSummary', name: 'detailsSummary',
content: 'inline*', content: 'text*',
// group: 'block', marks: '',
group: 'block',
isolating: true,
defaultOptions: { defaultOptions: {
HTMLAttributes: {}, HTMLAttributes: {},

View File

@ -1,4 +1,4 @@
import { Node, mergeAttributes } from '@tiptap/core' import { Node, mergeAttributes, Command } from '@tiptap/core'
export interface DetailsOptions { export interface DetailsOptions {
HTMLAttributes: { HTMLAttributes: {
@ -6,6 +6,25 @@ export interface DetailsOptions {
}, },
} }
declare module '@tiptap/core' {
interface AllCommands {
details: {
/**
* Set a details node
*/
setDetails: () => Command,
/**
* Toggle a details node
*/
toggleDetails: () => Command,
/**
* Unset a details node
*/
unsetDetails: () => Command,
}
}
}
export default Node.create<DetailsOptions>({ export default Node.create<DetailsOptions>({
name: 'details', name: 'details',
@ -13,36 +32,21 @@ export default Node.create<DetailsOptions>({
group: 'block', group: 'block',
// defining: true,
defaultOptions: { defaultOptions: {
HTMLAttributes: {}, HTMLAttributes: {},
}, },
addAttributes() {
return {
open: {
default: true,
parseHTML: element => {
return {
open: element.hasAttribute('open'),
}
},
renderHTML: attributes => {
if (!attributes.open) {
return null
}
return {
open: 'open',
}
},
},
}
},
parseHTML() { parseHTML() {
return [{ return [
{
tag: 'details', tag: 'details',
}] },
{
tag: 'div[data-type="details"]',
},
]
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
@ -50,32 +54,25 @@ export default Node.create<DetailsOptions>({
}, },
addNodeView() { addNodeView() {
return ({ return ({ HTMLAttributes }) => {
node, const item = document.createElement('div')
HTMLAttributes, item.setAttribute('data-type', 'details')
getPos,
editor,
}) => {
const { view } = editor
const item = document.createElement('details')
item.addEventListener('click', event => { const toggle = document.createElement('div')
// @ts-ignore toggle.setAttribute('data-type', 'detailsToggle')
const { open } = event.target.parentElement as HTMLElement item.append(toggle)
// @ts-ignore
const { localName } = event.target
if (typeof getPos === 'function' && localName === 'summary') { const content = document.createElement('div')
view.dispatch(view.state.tr.setNodeMarkup(getPos(), undefined, { content.setAttribute('data-type', 'detailsContent')
open: !open, item.append(content)
}))
editor.commands.focus()
}
})
if (node.attrs.open) { toggle.addEventListener('click', () => {
if (item.hasAttribute('open')) {
item.removeAttribute('open')
} else {
item.setAttribute('open', 'open') item.setAttribute('open', 'open')
} }
})
Object.entries(HTMLAttributes).forEach(([key, value]) => { Object.entries(HTMLAttributes).forEach(([key, value]) => {
item.setAttribute(key, value) item.setAttribute(key, value)
@ -83,21 +80,28 @@ export default Node.create<DetailsOptions>({
return { return {
dom: item, dom: item,
contentDOM: item, contentDOM: content,
update: updatedNode => { ignoreMutation: (mutation: MutationRecord) => {
if (updatedNode.type !== this.type) { return !item.contains(mutation.target) || item === mutation.target
return false
}
if (updatedNode.attrs.open) {
item.setAttribute('open', 'open')
} else {
item.removeAttribute('open')
}
return true
}, },
} }
} }
}, },
addCommands() {
return {
setDetails: () => ({ commands }) => {
// TODO: Doesnt work
return commands.wrapIn('details')
},
toggleDetails: () => ({ commands }) => {
// TODO: Doesnt work
return commands.toggleWrap('details')
},
unsetDetails: () => ({ commands }) => {
// TODO: Doesnt work
return commands.lift('details')
},
}
},
}) })

View File

@ -1,6 +1,20 @@
<template> <template>
<div v-if="editor"> <div v-if="editor">
<button @click="editor.chain().focus().toggleDetails().run()" :class="{ 'is-active': editor.isActive('details') }">
details
</button>
<editor-content :editor="editor" /> <editor-content :editor="editor" />
<h2>HTML</h2>
{{ editor.getHTML() }}
<h2>Issues</h2>
<ul>
<li>Commands dont work</li>
<li>Fails to open nested details</li>
<li>Node cant be deleted (if its the last node)</li>
</ul>
</div> </div>
</template> </template>
@ -31,7 +45,7 @@ export default {
], ],
content: ` content: `
<p>Here is a details list:</p> <p>Here is a details list:</p>
<details open> <details>
<summary>An open details tag</summary> <summary>An open details tag</summary>
<p>More info about the details.</p> <p>More info about the details.</p>
</details> </details>
@ -39,6 +53,7 @@ export default {
<summary>A closed details tag</summary> <summary>A closed details tag</summary>
<p>More info about the details.</p> <p>More info about the details.</p>
</details> </details>
<p>Thats it.</p>
`, `,
}) })
}, },
@ -54,5 +69,31 @@ export default {
> * + * { > * + * {
margin-top: 0.75em; margin-top: 0.75em;
} }
details,
[data-type="details"] {
display: flex;
[data-type="detailsContent"] > *:not(summary) {
display: none;
}
[data-type="detailsToggle"]::before {
cursor: pointer;
content: '▸';
display: inline-block;
width: 1em;
}
&[open] {
[data-type="detailsContent"] > *:not(summary) {
display: inherit;
}
[data-type="detailsToggle"]::before {
content: '▾';
}
}
}
} }
</style> </style>

View File

@ -186,6 +186,29 @@ Have a look at all of the core commands listed below. They should give you a goo
| .selectNodeForward() | Select a node forward. | | .selectNodeForward() | Select a node forward. |
| .selectParentNode() | Select the parent node. | | .selectParentNode() | Select the parent node. |
## Example use cases
### Quote a text
TODO
Add a blockquote, with a specified text, add a paragraph below, set the cursor there.
```js
// Untested, work in progress, likely to change
this.editor
.chain()
.focus()
.createParagraphNear()
.insertText(text)
.setBlockquote()
.insertHTML('<p></p>')
.createParagraphNear()
.unsetBlockquote()
.createParagraphNear()
.insertHTML('<p></p>')
.run()
```
## Add your own commands ## Add your own commands
All extensions can add additional commands (and most do), check out the specific [documentation for the provided nodes](/api/nodes), [marks](/api/marks), and [extensions](/api/extensions) to learn more about those. All extensions can add additional commands (and most do), check out the specific [documentation for the provided nodes](/api/nodes), [marks](/api/marks), and [extensions](/api/extensions) to learn more about those.

View File

@ -3,7 +3,9 @@
## toc ## toc
## Introduction ## Introduction
Extensions add new capabilities to tiptap. [Nodes](/api/nodes) and [marks](/api/marks) are rendered in HTML. Extensions cant add to the schema, but can add functionality or change the behaviour of the editor. Extensions add new capabilities to tiptap and youll read the word extension here very often. Actually, there are literal Extensions. Those cant add to the schema, but can add functionality or change the behaviour of the editor.
There are also some extensions with more capabilities. We call them [nodes](/api/nodes) and [marks](/api/marks) which can render content in the editor.
## List of provided extensions ## List of provided extensions
| Title | Default Extension | Source Code | | Title | Default Extension | Source Code |
@ -19,7 +21,7 @@ Extensions add new capabilities to tiptap. [Nodes](/api/nodes) and [marks](/api/
| [TextAlign](/api/extensions/text-align) | | [GitHub](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-text-align/) | | [TextAlign](/api/extensions/text-align) | | [GitHub](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-text-align/) |
| [Typography](/api/extensions/typography) | | [GitHub](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-typography/) | | [Typography](/api/extensions/typography) | | [GitHub](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-typography/) |
You dont have to use it, but we prepared a `@tiptap/starter-kit` which includes the most common extensions. See an example on [how to use `defaultExtensions()`](/examples/default). You dont have to use it, but we prepared a `@tiptap/starter-kit` which includes the most common extensions. Read more about [`defaultExtensions()`](/guide/configuration#default-extensions).
## How extensions work ## How extensions work
Although tiptap tries to hide most of the complexity of ProseMirror, its built on top of its APIs and we recommend you to read through the [ProseMirror Guide](https://ProseMirror.net/docs/guide/) for advanced usage. Youll have a better understanding of how everything works under the hood and get more familiar with many terms and jargon used by tiptap. Although tiptap tries to hide most of the complexity of ProseMirror, its built on top of its APIs and we recommend you to read through the [ProseMirror Guide](https://ProseMirror.net/docs/guide/) for advanced usage. Youll have a better understanding of how everything works under the hood and get more familiar with many terms and jargon used by tiptap.
@ -50,7 +52,7 @@ const editor = new Editor({
], ],
``` ```
Learn [more about custom extensions in our guide](/guide/build-extensions). Learn [more about custom extensions in our guide](/guide/extend-extensions).
### ProseMirror plugins ### ProseMirror plugins
ProseMirror has a fantastic eco system with many amazing plugins. If you want to use one of them, you can register them with tiptap like that: ProseMirror has a fantastic eco system with many amazing plugins. If you want to use one of them, you can register them with tiptap like that:

View File

@ -10,10 +10,6 @@ Open this page in multiple browser windows to test it.
We kindly ask you to [sponsor our work](/sponsor) when using this extension in production. We kindly ask you to [sponsor our work](/sponsor) when using this extension in production.
::: :::
::: warning Use with Collaboration
This extension requires the [`Collaboration`](/api/extensions/collaboration) extension.
:::
## Installation ## Installation
```bash ```bash
# with npm # with npm
@ -23,6 +19,8 @@ npm install @tiptap/extension-collaboration-cursor
yarn add @tiptap/extension-collaboration-cursor yarn add @tiptap/extension-collaboration-cursor
``` ```
This extension requires the [`Collaboration`](/api/extensions/collaboration) extension.
## Settings ## Settings
| Option | Type | Default | Description | | Option | Type | Default | Description |
| -------- | ---------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------- | ---------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@ -42,4 +40,6 @@ yarn add @tiptap/extension-collaboration-cursor
:::warning Public :::warning Public
The content of this editor is shared with other users. The content of this editor is shared with other users.
::: :::
<demo name="Extensions/CollaborationCursor" :show-source="false" />
<demo name="Extensions/CollaborationCursor" highlight="11,39-45" /> <demo name="Extensions/CollaborationCursor" highlight="11,39-45" />

View File

@ -48,4 +48,5 @@ yarn add @tiptap/extension-collaboration yjs y-websocket
:::warning Public :::warning Public
The content of this editor is shared with other users. The content of this editor is shared with other users.
::: :::
<demo name="Extensions/Collaboration" :show-source="false" />
<demo name="Extensions/Collaboration" highlight="10,27-28,35-37,44" /> <demo name="Extensions/Collaboration" highlight="10,27-28,35-37,44" />

View File

@ -5,10 +5,6 @@
This extension enables you to set the font family in the editor. It uses the [`TextStyle`](/api/marks/text-style) mark, which renders a `<span>` tag. The font family is applied as inline style, for example `<span style="font-family: Arial">`. This extension enables you to set the font family in the editor. It uses the [`TextStyle`](/api/marks/text-style) mark, which renders a `<span>` tag. The font family is applied as inline style, for example `<span style="font-family: Arial">`.
## Installation ## Installation
::: warning Use with TextStyle
This extension requires the [`TextStyle`](/api/marks/text-style) mark.
:::
```bash ```bash
# with npm # with npm
npm install @tiptap/extension-text-style @tiptap/extension-font-family npm install @tiptap/extension-text-style @tiptap/extension-font-family
@ -17,6 +13,8 @@ npm install @tiptap/extension-text-style @tiptap/extension-font-family
yarn add @tiptap/extension-text-style @tiptap/extension-font-family yarn add @tiptap/extension-text-style @tiptap/extension-font-family
``` ```
This extension requires the [`TextStyle`](/api/marks/text-style) mark.
## Settings ## Settings
| Option | Type | Default | Description | | Option | Type | Default | Description |
| ------ | ------- | --------------- | ------------------------------------------------------------------------ | | ------ | ------- | --------------- | ------------------------------------------------------------------------ |

View File

@ -7,10 +7,6 @@ This extension enables you to use bullet lists in the editor. They are rendered
Type <code>*&nbsp;</code>, <code>-&nbsp;</code> or <code>+&nbsp;</code> at the beginning of a new line and it will magically transform to a bullet list. Type <code>*&nbsp;</code>, <code>-&nbsp;</code> or <code>+&nbsp;</code> at the beginning of a new line and it will magically transform to a bullet list.
## Installation ## Installation
::: warning Use with ListItem
This extension requires the [`ListItem`](/api/nodes/list-item) node.
:::
```bash ```bash
# with npm # with npm
npm install @tiptap/extension-bullet-list @tiptap/extension-list-item npm install @tiptap/extension-bullet-list @tiptap/extension-list-item
@ -19,6 +15,8 @@ npm install @tiptap/extension-bullet-list @tiptap/extension-list-item
yarn add @tiptap/extension-bullet-list @tiptap/extension-list-item yarn add @tiptap/extension-bullet-list @tiptap/extension-list-item
``` ```
This extension requires the [`ListItem`](/api/nodes/list-item) node.
## Settings ## Settings
| Option | Type | Default | Description | | Option | Type | Default | Description |
| -------------- | -------- | ------- | --------------------------------------------------------------------- | | -------------- | -------- | ------- | --------------------------------------------------------------------- |

View File

@ -1,7 +1,7 @@
# Emoji # Emoji
:::pro Fund the development ♥ :::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). 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 TODO

View File

@ -1,7 +1,7 @@
# Hashtag # Hashtag
:::pro Fund the development ♥ :::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). 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 TODO

View File

@ -5,10 +5,6 @@
The ListItem extension adds support for the `<li>` HTML tag. Its used for bullet lists and ordered lists and cant really be used without them. The ListItem extension adds support for the `<li>` HTML tag. Its used for bullet lists and ordered lists and cant really be used without them.
## Installation ## Installation
::: warning Use with BulletList and/or OrderedList
This extension requires the [`BulletList`](/api/nodes/bullet-list) or [`OrderedList`](/api/nodes/ordered-list) node.
:::
```bash ```bash
# with npm # with npm
npm install @tiptap/extension-list-item npm install @tiptap/extension-list-item
@ -17,6 +13,8 @@ npm install @tiptap/extension-list-item
yarn add @tiptap/extension-list-item yarn add @tiptap/extension-list-item
``` ```
This extension requires the [`BulletList`](/api/nodes/bullet-list) or [`OrderedList`](/api/nodes/ordered-list) node.
## Settings ## Settings
| Option | Type | Default | Description | | Option | Type | Default | Description |
| -------------- | -------- | ------- | --------------------------------------------------------------------- | | -------------- | -------- | ------- | --------------------------------------------------------------------- |

View File

@ -7,10 +7,6 @@ This extension enables you to use ordered lists in the editor. They are rendered
Type <code>1.&nbsp;</code> (or any other number followed by a dot) at the beginning of a new line and it will magically transform to a ordered list. Type <code>1.&nbsp;</code> (or any other number followed by a dot) at the beginning of a new line and it will magically transform to a ordered list.
## Installation ## Installation
::: warning Use with ListItem
This extension requires the [`ListItem`](/api/nodes/list-item) node.
:::
```bash ```bash
# with npm # with npm
npm install @tiptap/extension-ordered-list @tiptap/extension-list-item npm install @tiptap/extension-ordered-list @tiptap/extension-list-item
@ -19,6 +15,8 @@ npm install @tiptap/extension-ordered-list @tiptap/extension-list-item
yarn add @tiptap/extension-ordered-list @tiptap/extension-list-item yarn add @tiptap/extension-ordered-list @tiptap/extension-list-item
``` ```
This extension requires the [`ListItem`](/api/nodes/list-item) node.
## Settings ## Settings
| Option | Type | Default | Description | | Option | Type | Default | Description |
| -------------- | -------- | ------- | --------------------------------------------------------------------- | | -------------- | -------- | ------- | --------------------------------------------------------------------- |

View File

@ -5,10 +5,6 @@
Dont try to use tables without table cells. It wont be fun. Dont try to use tables without table cells. It wont be fun.
## Installation ## Installation
::: warning Use with Table, TableRow and TableHeader
This extension requires the [`Table`](/api/nodes/table), [`TableRow`](/api/nodes/table-row) and [`TableHeader`](/api/nodes/table-header) nodes.
:::
```bash ```bash
# with npm # with npm
npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell
@ -17,6 +13,8 @@ npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extensio
yarn add @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell yarn add @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell
``` ```
This extension requires the [`Table`](/api/nodes/table), [`TableRow`](/api/nodes/table-row) and [`TableHeader`](/api/nodes/table-header) nodes.
## Source code ## Source code
[packages/extension-table-cell/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-table-cell/) [packages/extension-table-cell/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-table-cell/)

View File

@ -21,10 +21,6 @@ TableRow.extend({
``` ```
## Installation ## Installation
::: warning Use with Table, TableRow and TableCell
This extension requires the [`Table`](/api/nodes/table), [`TableRow`](/api/nodes/table-row) and [`TableCell`](/api/nodes/table-cell) nodes.
:::
```bash ```bash
# with npm # with npm
npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell
@ -33,6 +29,8 @@ npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extensio
yarn add @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell yarn add @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell
``` ```
This extension requires the [`Table`](/api/nodes/table), [`TableRow`](/api/nodes/table-row) and [`TableCell`](/api/nodes/table-cell) nodes.
## Source code ## Source code
[packages/extension-table-header/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-table-header/) [packages/extension-table-header/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-table-header/)

View File

@ -5,10 +5,6 @@
Whats a table without rows? Add this extension to make your tables usable. Whats a table without rows? Add this extension to make your tables usable.
## Installation ## Installation
::: warning Use with Table, TableHeader and TableCell
This extension requires the [`Table`](/api/nodes/table), [`TableHeader`](/api/nodes/table-header) and [`TableCell`](/api/nodes/table-cell) nodes.
:::
```bash ```bash
# with npm # with npm
npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell
@ -17,6 +13,8 @@ npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extensio
yarn add @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell yarn add @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell
``` ```
This extension requires the [`Table`](/api/nodes/table), [`TableHeader`](/api/nodes/table-header) and [`TableCell`](/api/nodes/table-cell) nodes.
## Source code ## Source code
[packages/extension-table-row/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-table-row/) [packages/extension-table-row/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-table-row/)

View File

@ -7,10 +7,6 @@ Nothing is as much fun as a good old HTML table. The `Table` extension enables y
Dont forget to add a `spacer.gif`. (Just joking. If you dont know what that is, dont listen.) Dont forget to add a `spacer.gif`. (Just joking. If you dont know what that is, dont listen.)
## Installation ## Installation
::: warning Use with TableRow, TableHeader and TableCell
This extension requires the [`TableRow`](/api/nodes/table-row), [`TableHeader`](/api/nodes/table-header) and [`TableCell`](/api/nodes/table-cell) nodes.
:::
```bash ```bash
# with npm # with npm
npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell
@ -19,6 +15,8 @@ npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extensio
yarn add @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell yarn add @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell
``` ```
This extension requires the [`TableRow`](/api/nodes/table-row), [`TableHeader`](/api/nodes/table-header) and [`TableCell`](/api/nodes/table-cell) nodes.
## Settings ## Settings
| Option | Type | Default | Description | | Option | Type | Default | Description |
| ----------------------- | --------- | ----------- | --------------------------------------------------------------------- | | ----------------------- | --------- | ----------- | --------------------------------------------------------------------- |

View File

@ -7,10 +7,6 @@ This extension renders a task item list element, which is a `<li>` tag with a `d
This extension doesnt require any JavaScript framework, its based on plain JavaScript. This extension doesnt require any JavaScript framework, its based on plain JavaScript.
## Installation ## Installation
::: warning Use with TaskList
This extension requires the [`TaskList`](/api/nodes/task-list) node.
:::
```bash ```bash
# With npm # With npm
npm install @tiptap/extension-task-list @tiptap/extension-task-item npm install @tiptap/extension-task-list @tiptap/extension-task-item
@ -19,6 +15,8 @@ npm install @tiptap/extension-task-list @tiptap/extension-task-item
yarn add @tiptap/extension-task-list @tiptap/extension-task-item yarn add @tiptap/extension-task-list @tiptap/extension-task-item
``` ```
This extension requires the [`TaskList`](/api/nodes/task-list) node.
## Settings ## Settings
| Option | Type | Default | Description | | Option | Type | Default | Description |
| -------------- | -------- | ------- | --------------------------------------------------------------------- | | -------------- | -------- | ------- | --------------------------------------------------------------------- |

View File

@ -7,10 +7,6 @@ This extension enables you to use task lists in the editor. They are rendered as
Type <code>[ ]&nbsp;</code> or <code>[x]&nbsp;</code> at the beginning of a new line and it will magically transform to a task list. Type <code>[ ]&nbsp;</code> or <code>[x]&nbsp;</code> at the beginning of a new line and it will magically transform to a task list.
## Installation ## Installation
::: warning Use with TaskItem
This extension requires the [`TaskItem`](/api/nodes/task-item) extension.
:::
```bash ```bash
# with npm # with npm
npm install @tiptap/extension-task-list @tiptap/extension-task-item npm install @tiptap/extension-task-list @tiptap/extension-task-item
@ -19,6 +15,8 @@ npm install @tiptap/extension-task-list @tiptap/extension-task-item
yarn add @tiptap/extension-task-list @tiptap/extension-task-item yarn add @tiptap/extension-task-list @tiptap/extension-task-item
``` ```
This extension requires the [`TaskItem`](/api/nodes/task-item) extension.
## Settings ## Settings
| Option | Type | Default | Description | | Option | Type | Default | Description |
| -------------- | -------- | ------- | --------------------------------------------------------------------- | | -------------- | -------- | ------- | --------------------------------------------------------------------- |

View File

@ -3,13 +3,11 @@ Congratulations! Youve found our playground with a list of experiments. Be aw
## New ## New
* [Linter](/experiments/linter) * [Linter](/experiments/linter)
* [Annotation](/experiments/annotation)
* [Comments](/experiments/comments)
* [Color](/experiments/color)
* [Commands](/experiments/commands)
* [Embeds](/experiments/embeds)
* [Multiple editors](/experiments/multiple-editors) * [Multiple editors](/experiments/multiple-editors)
* [Details](/experiments/details) * [@tiptap/extension-slash-command?](/experiments/commands)
* [@tiptap/extension-iframe?](/experiments/embeds)
* [@tiptap/extension-toggle-list?](/experiments/details)
* [@tiptap/extension-collaboration-annotation](/experiments/collaboration-annotation)
## Waiting for approval ## Waiting for approval
* [@tiptap/extension-placeholder](/experiments/placeholder) * [@tiptap/extension-placeholder](/experiments/placeholder)

View File

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

View File

@ -0,0 +1,42 @@
# CollaborationAnnotation
[![Version](https://img.shields.io/npm/v/@tiptap/extension-collaboration-annotation.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-collaboration-annotation)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-collaboration-annotation.svg)](https://npmcharts.com/compare/@tiptap/extension-collaboration-annotation?minimal=true)
⚠️ Experiment
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.
<!-- :::pro Pro Extension
We kindly ask you to [sponsor our work](/sponsor) when using this extension in production.
::: -->
## Installation
```bash
# with npm
npm install @tiptap/extension-collaboration-annotation
# with Yarn
yarn add @tiptap/extension-collaboration-annotation
```
This extension requires the [`Collaboration`](/api/extensions/collaboration) extension.
## Settings
| Option | Type | Default | Description |
| -------- | -------- | ----------- | ---------------------------------------------------------------------------------- |
| document | `Object` | `null` | An initialized Y.js document. |
| field | `String` | `'default'` | Name of a Y.js map, can be changed to sync multiple fields with one Y.js document. |
| map | `Object` | `null` | A raw Y.js map, can be used instead of `document` and `field`. |
## Commands
| Command | Parameters | Description |
| ---------------- | ---------- | ------------------------------------------------------------------------- |
| addAnnotation | data | Adds an annotation to the current selection, takes a string or an object. |
| updateAnnotation | id, data | Update the data thats associated with an annotation. |
| deleteAnnotation | id | Remove an annotation. |
## Source code
[packages/extension-collaboration-annotation/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-collaboration-annotation/)
## Usage
<demo name="Experiments/CollaborationAnnotation" />

View File

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

View File

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

View File

@ -1,7 +1,7 @@
# Accessibility # Accessibility
:::pro Fund the development ♥ :::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). 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 ## toc

View File

@ -37,8 +37,27 @@ This will do the following:
5. make the text editable (but thats the default anyway), and 5. make the text editable (but thats the default anyway), and
6. disable the loading of [the default CSS](https://github.com/ueberdosis/tiptap-next/tree/main/packages/core/src/style.ts) (which is not much anyway). 6. disable the loading of [the default CSS](https://github.com/ueberdosis/tiptap-next/tree/main/packages/core/src/style.ts) (which is not much anyway).
## Configure extensions ## Nodes, marks and extensions
A lot of the extension can be configured, too. Add an `.configure()` to the extension and pass an object to it. The following example will disable the default heading levels 4, 5 and 6: Most features are packed into [nodes](/api/nodes), [marks](/api/marks) and [extensions](/api/extensions). Import what you need and pass them as an Array to the editor and you are good to go. Here is the minimal setup with only three extensions:
```js
import { Editor } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
new Editor({
element: document.querySelector('.element'),
extensions: [
Document,
Paragraph,
Text,
],
})
```
### Configure an extensions
Most extensions can be configured. Add a `.configure()` to pass an object to it. The following example will disable the default heading levels 4, 5 and 6:
```js ```js
import { Editor } from '@tiptap/core' import { Editor } from '@tiptap/core'
@ -60,4 +79,57 @@ new Editor({
}) })
``` ```
Have a look at the documentation of the extension youre using to learn more about their settings. Have a look at the documentation of the extension you use to learn more about their settings.
### Default extensions
We have put together a few of the most common extensions and provide a `defaultExtensions()` helper to load them. Here is how you to use that:
```js
import { Editor, defaultExtensions } from '@tiptap/starter-kit'
new Editor({
extensions: defaultExtensions(),
})
```
And you can even pass configuration for all default extensions as an object. Just prefix the configuration with the extension name:
```js
import { Editor, defaultExtensions } from '@tiptap/starter-kit'
new Editor({
extensions: defaultExtensions({
heading: {
levels: [1, 2, 3]
},
}),
})
```
The `defaultExtensions()` function returns an array, so if you want to load them and add some custom extensions you could write it like that:
```js
import { Editor, defaultExtensions } from '@tiptap/starter-kit'
import Strike from '@tiptap/extension-strike'
new Editor({
extensions: [
...defaultExtensions(),
Strike,
],
})
```
Dont want to load a specific extension? Just filter it out:
```js
import { Editor, defaultExtensions } from '@tiptap/starter-kit'
new Editor({
extensions: [
...defaultExtensions().filter(extension => extension.config.name !== 'history'),
]
})
```
Youll probably see something like that in collaborative editing examples. The [`Collaboration`](/api/extensions/collaboration) comes with its own history extension, you need to remove the default [`History`](/api/extensions/history) extension to avoid conflicts.

View File

@ -6,7 +6,7 @@
One of the strength of tiptap is its extendability. You dont depend on the provided extensions, its intended to extend the editor to your liking. With custom extensions you can add new content types and new functionalities, on top of what already exists or from scratch. One of the strength of tiptap is its extendability. You dont depend on the provided extensions, its intended to extend the editor to your liking. With custom extensions you can add new content types and new functionalities, on top of what already exists or from scratch.
## Customize existing extensions ## Customize existing extensions
Lets say you want to change the keyboard shortcuts for the bullet list. You should start by looking at [the source code of the `BulletList` extension](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-bullet-list/index.ts) and find the part you would like to change. In that case, the keyboard shortcut, and just that. Lets say you want to change the keyboard shortcuts for the bullet list. You should start by looking at [the source code of the `BulletList` extension](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-bullet-list/src/bullet-list.ts) and find the part you would like to change. In that case, the keyboard shortcut, and just that.
Every extension has an `extend()` method, which takes an object with everything you want to change or add to it. For the bespoken example, your code could like that: Every extension has an `extend()` method, which takes an object with everything you want to change or add to it. For the bespoken example, your code could like that:

View File

@ -146,3 +146,32 @@ export default {
</node-view-wrapper> </node-view-wrapper>
</template> </template>
``` ```
## Reference
### dom: ?dom.Node
> The outer DOM node that represents the document node. When not given, the default strategy is used to create a DOM node.
### contentDOM: ?dom.Node
> The DOM node that should hold the node's content. Only meaningful if the node view also defines a dom property and if its node type is not a leaf node type. When this is present, ProseMirror will take care of rendering the node's children into it. When it is not present, the node view itself is responsible for rendering (or deciding not to render) its child nodes.
### update: ?fn(node: Node, decorations: [Decoration]) → bool
> When given, this will be called when the view is updating itself. It will be given a node (possibly of a different type), and an array of active decorations (which are automatically drawn, and the node view may ignore if it isn't interested in them), and should return true if it was able to update to that node, and false otherwise. If the node view has a contentDOM property (or no dom property), updating its child nodes will be handled by ProseMirror.
### selectNode: ?fn()
> Can be used to override the way the node's selected status (as a node selection) is displayed.
### deselectNode: ?fn()
> When defining a selectNode method, you should also provide a deselectNode method to remove the effect again.
### setSelection: ?fn(anchor: number, head: number, root: dom.Document)
> This will be called to handle setting the selection inside the node. The anchor and head positions are relative to the start of the node. By default, a DOM selection will be created between the DOM positions corresponding to those positions, but if you override it you can do something else.
### stopEvent: ?fn(event: dom.Event) → bool
> Can be used to prevent the editor view from trying to handle some or all DOM events that bubble up from the node view. Events for which this returns true are not handled by the editor.
### ignoreMutation: ?fn(dom.MutationRecord) → bool
> Called when a DOM mutation or a selection change happens within the view. When the change is a selection change, the record will have a type property of "selection" (which doesn't occur for native mutation records). Return false if the editor should re-read the selection or re-parse the range around the mutation, true if it can safely be ignored.
### destroy: ?fn()
> Called when the node view is removed from the editor or the whole editor is destroyed.

View File

@ -63,7 +63,7 @@ editor.isActive({ textAlign: 'right' })
If your selection spans multiple nodes or marks, or only part of the selection has a mark, `isActive()` will return `false` and indicate nothing is active. That is how it is supposed to be, because it allows people to apply a new node or mark to that selection right-away. If your selection spans multiple nodes or marks, or only part of the selection has a mark, `isActive()` will return `false` and indicate nothing is active. That is how it is supposed to be, because it allows people to apply a new node or mark to that selection right-away.
## Icons ## Icons
Most editor toolbars use icons for their buttons. In some of our demos, we use the open source icon set [Remix Icon](https://remixicon.com/), thats free to use. But its totally up to you what you use. Here are a few icon sets you can consider: Most editor toolbars use icons for their buttons. In some of our demos, we use the open-source icon set [Remix Icon](https://remixicon.com/), thats free to use. But its totally up to you what you use. Here are a few icon sets you can consider:
* [Remix Icon](https://remixicon.com/#editor) * [Remix Icon](https://remixicon.com/#editor)
* [Font Awesome](https://fontawesome.com/icons?c=editors) * [Font Awesome](https://fontawesome.com/icons?c=editors)

View File

@ -7,4 +7,4 @@ The following guide describes how to integrate tiptap with your [Next.js](https:
TODO TODO
<demo name="React" mode="react" /> <demo name="React" />

View File

@ -7,4 +7,4 @@ The following guide describes how to integrate tiptap with your [React](https://
TODO TODO
<demo name="React" mode="react" /> <demo name="React" />

View File

@ -10,7 +10,7 @@ title: Headless WYSIWYG Text Editor
tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*. tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*.
Create exactly the rich text editor you want out of customizable building blocks. tiptap comes with sensible defaults, a lot of extensions and a friendly API to customize every aspect. Its backed by a welcoming community, open source, and free. Create exactly the rich text editor you want out of customizable building blocks. tiptap comes with sensible defaults, a lot of extensions and a friendly API to customize every aspect. Its backed by a welcoming community, open-source, and free.
## Example ## Example
<demo name="Examples/CollaborativeEditing" :show-source="false" inline /> <demo name="Examples/CollaborativeEditing" :show-source="false" inline />

View File

@ -3,16 +3,16 @@
## Introduction ## Introduction
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. 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.
Give back to the open source community and [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 ## Your benefits as a sponsor
* Give back to the open source community * Give back to the open-source community
* Get early access to private repositories * Get early access to private repositories
* Ensure the further maintenace and development of tiptap * Ensure the further maintenace and development of tiptap
* Your issues and pull requests get a `sponsor ♥` label * Your issues and pull requests get a `sponsor ♥` label
* Get a sponsor badge in all your comments on GitHub * Get a sponsor badge in all your comments on GitHub
* Show support in your GitHub profile * Show support in your GitHub profile
* Receive monthly reports about our open source work * Receive monthly reports about our open-source work
Does that sound good? [Sponsor us on GitHub!](https://github.com/sponsors/ueberdosis) Does that sound good? [Sponsor us on GitHub!](https://github.com/sponsors/ueberdosis)

View File

@ -35,7 +35,7 @@
Headless Headless
</h3> </h3>
<p> <p>
We dont tell you what a menu should look like or where it should be rendered in the DOM. Thats why tiptap is headless and comes without any CSS. You are in full control over markup, styling and behaviour. Its headless and comes without any CSS. You are in full control over markup, styling and behaviour.
</p> </p>
<div> <div>
<btn type="tertiary" icon="arrow-right" to="/guide/styling"> <btn type="tertiary" icon="arrow-right" to="/guide/styling">
@ -49,7 +49,7 @@
Framework-agnostic Framework-agnostic
</h3> </h3>
<p> <p>
No matter what framework you use, youll enjoy tiptap. Out of the box, it works with plain JavaScript and Vue.js, but its also possible to use it in <g-link to="/installation/react">React</g-link>, <g-link to="/installation/svelte">Svelte</g-link> and others. Out of the box, tiptap works with plain JavaScript and Vue.js, but its also possible to use it in <g-link to="/installation/react">React</g-link>, <g-link to="/installation/svelte">Svelte</g-link> and others.
</p> </p>
<div> <div>
<btn type="tertiary" icon="arrow-right" to="/installation"> <btn type="tertiary" icon="arrow-right" to="/installation">
@ -63,7 +63,7 @@
TypeScript TypeScript
</h3> </h3>
<p> <p>
tiptap 2 is written in TypeScript. That helps to find bugs early and gives a nice autocomplete for the API (if your IDE supports that) on top of the extensive human written documentation. TypeScript helps to find bugs early and gives you a nice autocomplete for the API on top of the extensive human written documentation.
</p> </p>
<div> <div>
<btn type="tertiary" icon="arrow-right" to="/guide/typescript"> <btn type="tertiary" icon="arrow-right" to="/guide/typescript">
@ -77,7 +77,7 @@
Collaborative Collaborative
</h3> </h3>
<p> <p>
Real-time collaboration, syncing between different devices and working offline used to be hard. We provide everything you need to keep everything in sync, conflict-free with the power of <g-link to="https://github.com/yjs/yjs">Y.js</g-link>. Real-time collaboration, syncing between different devices and working offline isnt hard anymore. Keep everything in sync with the magic of <g-link to="https://github.com/yjs/yjs">Y.js</g-link>.
</p> </p>
<div> <div>
<btn type="tertiary" icon="arrow-right" to="/guide/collaborative-editing"> <btn type="tertiary" icon="arrow-right" to="/guide/collaborative-editing">
@ -91,7 +91,7 @@
Community Community
</h3> </h3>
<p> <p>
Over the years, a lovely community has grown around tiptap. Theres so much content shared, so many people helping out in issues and a ton of community extensions, youll be surprised how much that can help. Theres so much content shared, so many people helping out in issues and a ton of community extensions, youll be surprised how much that all can help.
</p> </p>
<div> <div>
<btn type="tertiary" icon="arrow-right" to="https://github.com/ueberdosis/tiptap-next"> <btn type="tertiary" icon="arrow-right" to="https://github.com/ueberdosis/tiptap-next">
@ -108,7 +108,7 @@
Quickstart Quickstart
</h2> </h2>
<p> <p>
For quick demos or to give it just a spin, grab the latest build from a CDN. Here is a quick example to get you started with tiptap: For quick demos or to give it just a spin, grab the latest build from a CDN. Here is an example to get you started with tiptap:
</p> </p>
<!-- eslint-disable --> <!-- eslint-disable -->
<prism language="html">&lt;!DOCTYPE html&gt; <prism language="html">&lt;!DOCTYPE html&gt;
@ -134,7 +134,7 @@
<!-- eslint-enable --> <!-- eslint-enable -->
<div> <div>
<btn type="tertiary" icon="arrow-right" to="/installation"> <btn type="tertiary" icon="arrow-right" to="/installation">
Learn More Learn more
</btn> </btn>
</div> </div>
</div> </div>

View File

@ -18,21 +18,21 @@ import OrderedList, { OrderedListOptions } from '@tiptap/extension-ordered-list'
import ListItem, { ListItemOptions } from '@tiptap/extension-list-item' import ListItem, { ListItemOptions } from '@tiptap/extension-list-item'
export function defaultExtensions(options?: Partial<{ export function defaultExtensions(options?: Partial<{
dropursor: DropcursorOptions, dropursor: Partial<DropcursorOptions>,
paragraph: ParagraphOptions, paragraph: Partial<ParagraphOptions>,
history: HistoryOptions, history: Partial<HistoryOptions>,
bold: BoldOptions, bold: Partial<BoldOptions>,
italic: ItalicOptions, italic: Partial<ItalicOptions>,
code: CodeOptions, code: Partial<CodeOptions>,
codeBlock: CodeBlockOptions, codeBlock: Partial<CodeBlockOptions>,
heading: HeadingOptions, heading: Partial<HeadingOptions>,
hardBreak: HardBreakOptions, hardBreak: Partial<HardBreakOptions>,
strike: StrikeOptions, strike: Partial<StrikeOptions>,
blockquote: BlockquoteOptions, blockquote: Partial<BlockquoteOptions>,
horizontalRule: HorizontalRuleOptions, horizontalRule: Partial<HorizontalRuleOptions>,
bulletList: BulletListOptions, bulletList: Partial<BulletListOptions>,
orderedList: OrderedListOptions, orderedList: Partial<OrderedListOptions>,
listItem: ListItemOptions, listItem: Partial<ListItemOptions>,
}>) { }>) {
return [ return [
Document, Document,