Merge branch 'main' into feature/vue-node-views
This commit is contained in:
22
docs/src/demos/Examples/Basic/index.spec.js
Normal file
22
docs/src/demos/Examples/Basic/index.spec.js
Normal file
@@ -0,0 +1,22 @@
|
||||
context('/examples/basic', () => {
|
||||
before(() => {
|
||||
cy.visit('/examples/basic')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<h1>Example Text</h1>')
|
||||
editor.commands.selectAll()
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply the paragraph style when the keyboard shortcut is pressed', () => {
|
||||
cy.get('.ProseMirror h1').should('exist')
|
||||
cy.get('.ProseMirror p').should('not.exist')
|
||||
|
||||
cy.get('.ProseMirror')
|
||||
.trigger('keydown', { modKey: true, altKey: true, key: '0' })
|
||||
.find('p')
|
||||
.should('contain', 'Example Text')
|
||||
})
|
||||
})
|
||||
@@ -2,4 +2,6 @@ context('/examples/collaborative-editing', () => {
|
||||
before(() => {
|
||||
cy.visit('/examples/collaborative-editing')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
context('/examples/collaborative-editing-ws', () => {
|
||||
before(() => {
|
||||
cy.visit('/examples/collaborative-editing-ws')
|
||||
})
|
||||
})
|
||||
7
docs/src/demos/Examples/Formatting/index.spec.js
Normal file
7
docs/src/demos/Examples/Formatting/index.spec.js
Normal file
@@ -0,0 +1,7 @@
|
||||
context('/examples/formatting', () => {
|
||||
before(() => {
|
||||
cy.visit('/examples/formatting')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
7
docs/src/demos/Examples/Links/index.spec.js
Normal file
7
docs/src/demos/Examples/Links/index.spec.js
Normal file
@@ -0,0 +1,7 @@
|
||||
context('/examples/links', () => {
|
||||
before(() => {
|
||||
cy.visit('/examples/links')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
@@ -2,4 +2,6 @@ context('/examples/minimalist', () => {
|
||||
before(() => {
|
||||
cy.visit('/examples/minimalist')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
|
||||
@@ -2,4 +2,6 @@ context('/examples/todo-app', () => {
|
||||
before(() => {
|
||||
cy.visit('/examples/todo-app')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
|
||||
7
docs/src/demos/Examples/VModel/index.spec.js
Normal file
7
docs/src/demos/Examples/VModel/index.spec.js
Normal file
@@ -0,0 +1,7 @@
|
||||
context('/examples/v-model', () => {
|
||||
before(() => {
|
||||
cy.visit('/examples/v-model')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<div class="editor">
|
||||
<editor-content :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor, EditorContent } from '@tiptap/vue'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import Bold from '@tiptap/extension-bold'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
content: '<p>I’m running tiptap with Vue.js. This demo is interactive, try to edit the text.</p>',
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Bold,
|
||||
],
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -2,4 +2,6 @@ context('/api/extensions/collaboration', () => {
|
||||
before(() => {
|
||||
cy.visit('/api/extensions/collaboration')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
|
||||
@@ -2,4 +2,6 @@ context('/api/extensions/collaboration-cursor', () => {
|
||||
before(() => {
|
||||
cy.visit('/api/extensions/collaboration-cursor')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
|
||||
@@ -2,4 +2,6 @@ context('/examples/dropcursor', () => {
|
||||
before(() => {
|
||||
cy.visit('/examples/dropcursor')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
|
||||
@@ -2,4 +2,6 @@ context('/api/extensions/font-family', () => {
|
||||
before(() => {
|
||||
cy.visit('/api/extensions/font-family')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
|
||||
@@ -2,4 +2,6 @@ context('/examples/gapcursor', () => {
|
||||
before(() => {
|
||||
cy.visit('/examples/gapcursor')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
|
||||
@@ -81,4 +81,32 @@ context('/api/extensions/text-align', () => {
|
||||
.find('p')
|
||||
.should('have.css', 'text-align', 'left')
|
||||
})
|
||||
|
||||
it('aligns the text left when pressing the keyboard shortcut', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.trigger('keydown', { modKey: true, shiftKey: true, key: 'l' })
|
||||
.find('p')
|
||||
.should('have.css', 'text-align', 'left')
|
||||
})
|
||||
|
||||
it('aligns the text center when pressing the keyboard shortcut', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.trigger('keydown', { modKey: true, shiftKey: true, key: 'e' })
|
||||
.find('p')
|
||||
.should('have.css', 'text-align', 'center')
|
||||
})
|
||||
|
||||
it('aligns the text right when pressing the keyboard shortcut', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.trigger('keydown', { modKey: true, shiftKey: true, key: 'r' })
|
||||
.find('p')
|
||||
.should('have.css', 'text-align', 'right')
|
||||
})
|
||||
|
||||
it('aligns the text justified when pressing the keyboard shortcut', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.trigger('keydown', { modKey: true, shiftKey: true, key: 'j' })
|
||||
.find('p')
|
||||
.should('have.css', 'text-align', 'justify')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="editor">
|
||||
<button @click="editor.chain().focus().unsetAllMarks().run()">
|
||||
clear formatting
|
||||
</button>
|
||||
<button @click="editor.chain().focus().undo().run()">
|
||||
undo
|
||||
</button>
|
||||
<button @click="editor.chain().focus().redo().run()">
|
||||
redo
|
||||
</button>
|
||||
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
|
||||
bold
|
||||
</button>
|
||||
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
|
||||
italic
|
||||
</button>
|
||||
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }">
|
||||
h1
|
||||
</button>
|
||||
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }">
|
||||
h2
|
||||
</button>
|
||||
</div>
|
||||
<editor-content :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { EditorContent } from '@tiptap/vue'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import History from '@tiptap/extension-history'
|
||||
import Bold from '@tiptap/extension-bold'
|
||||
import Italic from '@tiptap/extension-italic'
|
||||
import Code from '@tiptap/extension-code'
|
||||
import CodeBlock from '@tiptap/extension-code-block'
|
||||
import Heading from '@tiptap/extension-heading'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
content: '<h2>Hey there!</h2><p>This editor is based on Prosemirror, fully extendable and headless. You can easily add custom nodes as Vue components.</p>',
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
CodeBlock,
|
||||
History,
|
||||
Bold,
|
||||
Italic,
|
||||
Code,
|
||||
Heading,
|
||||
],
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<editor-content :editor="editor" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor, EditorContent, defaultExtensions } from '@tiptap/vue-starter-kit'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
content: '<p>I’m running tiptap with Vue.js. 🎉</p>',
|
||||
extensions: defaultExtensions(),
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -47,31 +47,23 @@ context('/api/marks/link', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const validUrls = [
|
||||
'https://example.com',
|
||||
'https://example.com/with-path',
|
||||
'http://example.com/with-http',
|
||||
'https://www.example.com/with-www',
|
||||
'https://www.example.com/with-numbers-123',
|
||||
'https://www.example.com/with-parameters?var=true',
|
||||
'https://www.example.com/with-multiple-parameters?var=true&foo=bar',
|
||||
'https://www.example.com/with-spaces?var=true&foo=bar+3',
|
||||
// TODO: 'https://www.example.com/with,comma',
|
||||
// TODO: 'https://www.example.com/with(brackets)',
|
||||
// TODO: 'https://www.example.com/with!exclamation!marks',
|
||||
'http://thelongestdomainnameintheworldandthensomeandthensomemoreandmore.com/',
|
||||
'https://example.longtopleveldomain',
|
||||
'https://example-with-dashes.com',
|
||||
]
|
||||
|
||||
validUrls.forEach(url => {
|
||||
it(`url should be detected: ${url}`, () => {
|
||||
cy.get('.ProseMirror').paste({ pastePayload: url, pasteType: 'text/plain' })
|
||||
.find('a')
|
||||
.should('contain', url)
|
||||
.should('have.attr', 'href', url)
|
||||
})
|
||||
it('detects a pasted URL', () => {
|
||||
cy.get('.ProseMirror').paste({ pastePayload: 'https://example.com', pasteType: 'text/plain' })
|
||||
.find('a')
|
||||
.should('contain', 'https://example.com')
|
||||
.should('have.attr', 'href', 'https://example.com')
|
||||
})
|
||||
|
||||
// TODO: Test invalid URLs
|
||||
it('correctly detects multiple pasted URLs', () => {
|
||||
cy.get('.ProseMirror').paste({ pastePayload: 'https://example1.com, https://example2.com/foobar, (http://example3.com/foobar)', pasteType: 'text/plain' })
|
||||
|
||||
cy.get('.ProseMirror').find('a[href="https://example1.com"]')
|
||||
.should('contain', 'https://example1.com')
|
||||
|
||||
cy.get('.ProseMirror').find('a[href="https://example2.com/foobar"]')
|
||||
.should('contain', 'https://example2.com/foobar')
|
||||
|
||||
cy.get('.ProseMirror').find('a[href="http://example3.com/foobar"]')
|
||||
.should('contain', 'http://example3.com/foobar')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -61,3 +61,16 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.ProseMirror {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #68CEF8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,4 +2,6 @@ context('/api/marks/text-style', () => {
|
||||
before(() => {
|
||||
cy.visit('/api/marks/text-style')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
|
||||
@@ -79,6 +79,13 @@ context('/api/nodes/bullet-list', () => {
|
||||
.should('contain', 'Paragraph')
|
||||
})
|
||||
|
||||
it('should make the paragraph a bullet list keyboard shortcut is pressed', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.trigger('keydown', { modKey: true, shiftKey: true, key: '8' })
|
||||
.find('ul li')
|
||||
.should('contain', 'Example Text')
|
||||
})
|
||||
|
||||
it('should make a bullet list from an asterisk', () => {
|
||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||
editor.commands.clearContent()
|
||||
|
||||
@@ -86,7 +86,28 @@ context('/api/nodes/heading', () => {
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('should make a heading from the default markdown shortcut', () => {
|
||||
it('should make the paragraph a h1 keyboard shortcut is pressed', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.trigger('keydown', { modKey: true, altKey: true, key: '1' })
|
||||
.find('h1')
|
||||
.should('contain', 'Example Text')
|
||||
})
|
||||
|
||||
it('should make the paragraph a h2 keyboard shortcut is pressed', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.trigger('keydown', { modKey: true, altKey: true, key: '2' })
|
||||
.find('h2')
|
||||
.should('contain', 'Example Text')
|
||||
})
|
||||
|
||||
it('should make the paragraph a h3 keyboard shortcut is pressed', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.trigger('keydown', { modKey: true, altKey: true, key: '3' })
|
||||
.find('h3')
|
||||
.should('contain', 'Example Text')
|
||||
})
|
||||
|
||||
it('should make a h1 from the default markdown shortcut', () => {
|
||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||
editor.commands.clearContent()
|
||||
})
|
||||
@@ -96,4 +117,26 @@ context('/api/nodes/heading', () => {
|
||||
.find('h1')
|
||||
.should('contain', 'Headline')
|
||||
})
|
||||
|
||||
it('should make a h2 from the default markdown shortcut', () => {
|
||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||
editor.commands.clearContent()
|
||||
})
|
||||
|
||||
cy.get('.ProseMirror')
|
||||
.type('## Headline')
|
||||
.find('h2')
|
||||
.should('contain', 'Headline')
|
||||
})
|
||||
|
||||
it('should make a h3 from the default markdown shortcut', () => {
|
||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||
editor.commands.clearContent()
|
||||
})
|
||||
|
||||
cy.get('.ProseMirror')
|
||||
.type('### Headline')
|
||||
.find('h3')
|
||||
.should('contain', 'Headline')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,4 +2,48 @@ context('/api/nodes/list-item', () => {
|
||||
before(() => {
|
||||
cy.visit('/api/nodes/list-item')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<ul><li>Example Text</li></ul>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should add a new list item on Enter', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.type('{enter}2nd Item')
|
||||
|
||||
cy.get('.ProseMirror')
|
||||
.find('li:nth-child(1)')
|
||||
.should('contain', 'Example Text')
|
||||
|
||||
cy.get('.ProseMirror')
|
||||
.find('li:nth-child(2)')
|
||||
.should('contain', '2nd Item')
|
||||
})
|
||||
|
||||
it('should sink the list item on Tab', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.type('{enter}')
|
||||
.trigger('keydown', { key: 'Tab' })
|
||||
|
||||
cy.get('.ProseMirror').type('2nd Level')
|
||||
|
||||
cy.get('.ProseMirror')
|
||||
.find('li:nth-child(1) li')
|
||||
.should('contain', '2nd Level')
|
||||
})
|
||||
|
||||
it('should lift the list item on Shift+Tab', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.type('{enter}')
|
||||
.trigger('keydown', { key: 'Tab' })
|
||||
.trigger('keydown', { shiftKey: true, key: 'Tab' })
|
||||
|
||||
cy.get('.ProseMirror').type('1st Level')
|
||||
|
||||
cy.get('.ProseMirror')
|
||||
.find('li:nth-child(2)')
|
||||
.should('contain', '1st Level')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,3 +71,50 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.ProseMirror {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: rgba(#616161, 0.1);
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #0D0D0D;
|
||||
color: #FFF;
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
code {
|
||||
color: inherit;
|
||||
background: none;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid rgba(#0D0D0D, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -61,6 +61,13 @@ context('/api/nodes/ordered-list', () => {
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('should make the paragraph an ordered list keyboard shortcut is pressed', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.trigger('keydown', { modKey: true, shiftKey: true, key: '7' })
|
||||
.find('ol li')
|
||||
.should('contain', 'Example Text')
|
||||
})
|
||||
|
||||
it('should leave the list with double enter', () => {
|
||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||
editor.commands.clearContent()
|
||||
|
||||
@@ -2,4 +2,6 @@ context('/api/nodes/task-item', () => {
|
||||
before(() => {
|
||||
cy.visit('/api/nodes/task-item')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
|
||||
@@ -61,6 +61,13 @@ context('/api/nodes/task-list', () => {
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('should make the paragraph a task list when the keyboard shortcut is pressed', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.trigger('keydown', { modKey: true, shiftKey: true, key: 'l' })
|
||||
.find('ul li')
|
||||
.should('contain', 'Example Text')
|
||||
})
|
||||
|
||||
it('should leave the list with double enter', () => {
|
||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||
editor.commands.clearContent()
|
||||
|
||||
5
docs/src/demos/Overview/Installation/index.spec.js
Normal file
5
docs/src/demos/Overview/Installation/index.spec.js
Normal file
@@ -0,0 +1,5 @@
|
||||
context('/overview/installation', () => {
|
||||
before(() => {
|
||||
cy.visit('/overview/installation')
|
||||
})
|
||||
})
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div class="editor">
|
||||
<div class="menubar" v-if="editor">
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
>
|
||||
Bold
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<editor-content :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor, EditorContent, defaultExtensions } from '@tiptap/vue-starter-kit'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
content: '<p>Hi! 👋 I’m a text editor with just one button. Select some text and press the button to see what it does. Yes, it’s marking text <strong>bold</strong>. Amazing, isn’t it?</p>',
|
||||
extensions: defaultExtensions(),
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -48,7 +48,7 @@ Most of the core extensions register their own keyboard shortcuts. Depending on
|
||||
| Center align | `Control` `Shift` `E` | `Cmd` `Shift` `E` |
|
||||
| Right align | `Control` `Shift` `R` | `Cmd` `Shift` `R` |
|
||||
| Justify | `Control` `Shift` `J` | `Cmd` `Shift` `J` |
|
||||
| Task list | `Control` `Shift` `L` | `Cmd` `Shift` `L` |
|
||||
| Task list | `Control` `Shift` `L` | `Cmd` `Shift` `L` (TODO: Conflict!) |
|
||||
| Code block | `Control` `Alt` `C` | `Cmd` `Alt` `C` |
|
||||
|
||||
<!--| Toggle task| `Control` `Enter` | `Cmd` `Enter` | -->
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Create your editor
|
||||
# Create a new toolbar
|
||||
|
||||
## toc
|
||||
|
||||
## Introduction
|
||||
TODO
|
||||
|
||||
<!-- ## Introduction
|
||||
In its simplest version tiptap comes very raw. There is no menu, no buttons, no styling. That’s intended. See tiptap as your building blocks to build exactly the editor you would like to have.
|
||||
|
||||
## Adding a menu
|
||||
@@ -24,4 +26,4 @@ Note that `Document`, `Paragraph` and `Text` are required. Otherwise you won’t
|
||||
|
||||
<demo name="Guide/BuildYourEditor" highlight="10-13,30-33" />
|
||||
|
||||
That’s also the place where you can register custom extensions, which you or someone else built for tiptap.
|
||||
That’s also the place where you can register custom extensions, which you or someone else built for tiptap. -->
|
||||
|
||||
@@ -55,7 +55,38 @@ To actually start using tiptap, you’ll need to add a new component to your app
|
||||
|
||||
This is the fastest way to get tiptap up and running with Vue. It will give you a very basic version of tiptap, without any buttons. No worries, you will be able to add more functionality soon.
|
||||
|
||||
<demo name="Guide/GettingStarted" />
|
||||
```html
|
||||
<template>
|
||||
<editor-content :editor="editor" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor, EditorContent, defaultExtensions } from '@tiptap/vue-starter-kit'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
content: '<p>I’m running tiptap with Vue.js. 🎉</p>',
|
||||
extensions: defaultExtensions(),
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 5. Add it to your app
|
||||
Now, let’s replace the content of `src/App.vue` with the following example code to use our new `Tiptap` component in our app.
|
||||
|
||||
@@ -57,8 +57,34 @@ It’s also amazing for bug reports. Try to recreate a bug there and share it wi
|
||||
* [Vue.js/tiptap on CodeSandbox](https://codesandbox.io/s/tiptap-issue-template-b83rr?file=/src/components/Tiptap.vue)
|
||||
|
||||
|
||||
## Option 4: CDN
|
||||
To pull in tiptap for quick demos or just giving it a spin, grab the latest build via CDN:
|
||||
## Option 4: CDN (Draft)
|
||||
To pull in tiptap for quick demos or just giving it a spin, grab the latest build via CDN. We use two different provides:
|
||||
|
||||
### Skypack (ES Modules)
|
||||
Skypack enables you to use ES modules, which should be supported in all modern browsers. The packages are smaller, that’s great, too. So here is how to use it:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<div class="element"></div>
|
||||
<script type="module">
|
||||
import { Editor } from 'https://cdn.skypack.dev/@tiptap/core?min'
|
||||
import { defaultExtensions } from 'https://cdn.skypack.dev/@tiptap/starter-kit?min'
|
||||
const editor = new Editor({
|
||||
element: document.querySelector('.element'),
|
||||
extensions: defaultExtensions(),
|
||||
content: '<p>Your content.</p>',
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Unpkg (UMD, deprecated)
|
||||
We also have an UMD build on unpkg. Those UMD builds are larger, but should work even in older browsers. As tiptap doesn’t work in older browsers anyway, we tend to remove those builds. What do you think? Anyway, here‘s how you can use it:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
@@ -81,4 +107,3 @@ To pull in tiptap for quick demos or just giving it a spin, grab the latest buil
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import getRenderedAttributes from './getRenderedAttributes'
|
||||
import isEmptyObject from './isEmptyObject'
|
||||
import injectExtensionAttributesToParseRule from './injectExtensionAttributesToParseRule'
|
||||
import callOrReturn from './callOrReturn'
|
||||
import mergeAttributes from './mergeAttributes'
|
||||
|
||||
function cleanUpSchemaItem<T>(data: T) {
|
||||
return Object.fromEntries(Object.entries(data).filter(([key, value]) => {
|
||||
@@ -51,10 +50,7 @@ export default function getSchema(extensions: Extensions): Schema {
|
||||
if (extension.config.renderHTML) {
|
||||
schema.toDOM = node => (extension.config.renderHTML as Function)?.bind(context)({
|
||||
node,
|
||||
HTMLAttributes: mergeAttributes(
|
||||
extension.options.HTMLAttributes,
|
||||
getRenderedAttributes(node, extensionAttributes),
|
||||
),
|
||||
HTMLAttributes: getRenderedAttributes(node, extensionAttributes),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,10 +79,7 @@ export default function getSchema(extensions: Extensions): Schema {
|
||||
if (extension.config.renderHTML) {
|
||||
schema.toDOM = mark => (extension.config.renderHTML as Function)?.bind(context)({
|
||||
mark,
|
||||
HTMLAttributes: mergeAttributes(
|
||||
extension.options.HTMLAttributes,
|
||||
getRenderedAttributes(mark, extensionAttributes),
|
||||
),
|
||||
HTMLAttributes: getRenderedAttributes(mark, extensionAttributes),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Command, Node } from '@tiptap/core'
|
||||
import { Command, Node, mergeAttributes } from '@tiptap/core'
|
||||
import { wrappingInputRule } from 'prosemirror-inputrules'
|
||||
|
||||
export interface BlockquoteOptions {
|
||||
@@ -29,7 +29,7 @@ const Blockquote = Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['blockquote', HTMLAttributes, 0]
|
||||
return ['blockquote', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Mark,
|
||||
markInputRule,
|
||||
markPasteRule,
|
||||
mergeAttributes,
|
||||
} from '@tiptap/core'
|
||||
|
||||
export interface BoldOptions {
|
||||
@@ -40,7 +41,7 @@ const Bold = Mark.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['strong', HTMLAttributes, 0]
|
||||
return ['strong', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Command, Node } from '@tiptap/core'
|
||||
import { Command, Node, mergeAttributes } from '@tiptap/core'
|
||||
import { wrappingInputRule } from 'prosemirror-inputrules'
|
||||
|
||||
export interface BulletListOptions {
|
||||
@@ -27,7 +27,7 @@ const BulletList = Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['ul', HTMLAttributes, 0]
|
||||
return ['ul', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -69,7 +69,7 @@ const CodeBlock = Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['pre', ['code', HTMLAttributes, 0]]
|
||||
return ['pre', this.options.HTMLAttributes, ['code', HTMLAttributes, 0]]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Mark,
|
||||
markInputRule,
|
||||
markPasteRule,
|
||||
mergeAttributes,
|
||||
} from '@tiptap/core'
|
||||
|
||||
export interface CodeOptions {
|
||||
@@ -30,7 +31,7 @@ const Code = Mark.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['code', HTMLAttributes, 0]
|
||||
return ['code', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Extension } from '@tiptap/core'
|
||||
import {
|
||||
redo, undo, ySyncPlugin, yUndoPlugin,
|
||||
redo,
|
||||
undo,
|
||||
ySyncPlugin,
|
||||
yUndoPlugin,
|
||||
} from 'y-prosemirror'
|
||||
|
||||
export interface CollaborationOptions {
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import { Command, Node } from '@tiptap/core'
|
||||
import { Command, Node, mergeAttributes } from '@tiptap/core'
|
||||
import { exitCode } from 'prosemirror-commands'
|
||||
|
||||
export interface HardBreakOptions {
|
||||
HTMLAttributes: {
|
||||
[key: string]: any
|
||||
},
|
||||
}
|
||||
|
||||
const HardBreak = Node.create({
|
||||
name: 'hardBreak',
|
||||
|
||||
defaultOptions: <HardBreakOptions>{
|
||||
languageClassPrefix: 'language-',
|
||||
HTMLAttributes: {},
|
||||
},
|
||||
|
||||
inline: true,
|
||||
|
||||
group: 'inline',
|
||||
@@ -17,7 +28,7 @@ const HardBreak = Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['br', HTMLAttributes]
|
||||
return ['br', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Command, Node } from '@tiptap/core'
|
||||
import { Command, Node, mergeAttributes } from '@tiptap/core'
|
||||
import { textblockTypeInputRule } from 'prosemirror-inputrules'
|
||||
|
||||
type Level = 1 | 2 | 3 | 4 | 5 | 6
|
||||
@@ -47,7 +47,7 @@ const Heading = Node.create({
|
||||
? node.attrs.level
|
||||
: this.options.levels[0]
|
||||
|
||||
return [`h${level}`, HTMLAttributes, 0]
|
||||
return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Mark,
|
||||
markInputRule,
|
||||
markPasteRule,
|
||||
mergeAttributes,
|
||||
} from '@tiptap/core'
|
||||
|
||||
export interface HighlightOptions {
|
||||
@@ -53,7 +54,7 @@ const Highlight = Mark.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['mark', HTMLAttributes, 0]
|
||||
return ['mark', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Command, Node, nodeInputRule } from '@tiptap/core'
|
||||
import {
|
||||
Command,
|
||||
Node,
|
||||
nodeInputRule,
|
||||
mergeAttributes,
|
||||
} from '@tiptap/core'
|
||||
|
||||
export interface HorizontalRuleOptions {
|
||||
HTMLAttributes: {
|
||||
@@ -22,7 +27,7 @@ const HorizontalRule = Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['hr', HTMLAttributes]
|
||||
return ['hr', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Command, Node, nodeInputRule } from '@tiptap/core'
|
||||
import {
|
||||
Command,
|
||||
Node,
|
||||
nodeInputRule,
|
||||
mergeAttributes,
|
||||
} from '@tiptap/core'
|
||||
|
||||
export interface ImageOptions {
|
||||
inline: boolean,
|
||||
@@ -50,7 +55,7 @@ const Image = Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['img', HTMLAttributes]
|
||||
return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Mark,
|
||||
markInputRule,
|
||||
markPasteRule,
|
||||
mergeAttributes,
|
||||
} from '@tiptap/core'
|
||||
|
||||
export interface ItalicOptions {
|
||||
@@ -39,7 +40,7 @@ const Italic = Mark.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['em', HTMLAttributes, 0]
|
||||
return ['em', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Command, Mark, markPasteRule } from '@tiptap/core'
|
||||
import {
|
||||
Command,
|
||||
Mark,
|
||||
markPasteRule,
|
||||
mergeAttributes,
|
||||
} from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
|
||||
export interface LinkOptions {
|
||||
@@ -8,7 +13,8 @@ export interface LinkOptions {
|
||||
},
|
||||
}
|
||||
|
||||
export const pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/()]*)/gi
|
||||
export const pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi
|
||||
export const pasteRegexWithBrackets = /(?:\()https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/()]*)(?:\))/gi
|
||||
|
||||
const Link = Mark.create({
|
||||
name: 'link',
|
||||
@@ -41,7 +47,7 @@ const Link = Mark.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['a', HTMLAttributes, 0]
|
||||
return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
@@ -70,6 +76,7 @@ const Link = Mark.create({
|
||||
addPasteRules() {
|
||||
return [
|
||||
markPasteRule(pasteRegex, this.type, (url: string) => ({ href: url })),
|
||||
markPasteRule(pasteRegexWithBrackets, this.type, (url: string) => ({ href: url })),
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Node } from '@tiptap/core'
|
||||
import { Node, mergeAttributes } from '@tiptap/core'
|
||||
|
||||
export interface ListItemOptions {
|
||||
HTMLAttributes: {
|
||||
@@ -26,7 +26,7 @@ const ListItem = Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['li', HTMLAttributes, 0]
|
||||
return ['li', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Command, Node } from '@tiptap/core'
|
||||
import { Command, Node, mergeAttributes } from '@tiptap/core'
|
||||
import { wrappingInputRule } from 'prosemirror-inputrules'
|
||||
|
||||
export interface OrderedListOptions {
|
||||
@@ -45,8 +45,8 @@ const OrderedList = Node.create({
|
||||
const { start, ...attributesWithoutStart } = HTMLAttributes
|
||||
|
||||
return start === 1
|
||||
? ['ol', attributesWithoutStart, 0]
|
||||
: ['ol', HTMLAttributes, 0]
|
||||
? ['ol', mergeAttributes(this.options.HTMLAttributes, attributesWithoutStart), 0]
|
||||
: ['ol', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Command, Node } from '@tiptap/core'
|
||||
import { Command, Node, mergeAttributes } from '@tiptap/core'
|
||||
|
||||
export interface ParagraphOptions {
|
||||
HTMLAttributes: {
|
||||
@@ -24,7 +24,7 @@ const Paragraph = Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['p', HTMLAttributes, 0]
|
||||
return ['p', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Mark,
|
||||
markInputRule,
|
||||
markPasteRule,
|
||||
mergeAttributes,
|
||||
} from '@tiptap/core'
|
||||
|
||||
export interface StrikeOptions {
|
||||
@@ -39,7 +40,7 @@ const Strike = Mark.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['s', HTMLAttributes, 0]
|
||||
return ['s', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -48,7 +48,11 @@ const TaskItem = Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['li', mergeAttributes(HTMLAttributes, { 'data-type': 'taskItem' }), 0]
|
||||
return ['li', mergeAttributes(
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes,
|
||||
{ 'data-type': 'taskItem' },
|
||||
), 0]
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import { Command, Mark, getMarkAttributes } from '@tiptap/core'
|
||||
import {
|
||||
Command,
|
||||
Mark,
|
||||
getMarkAttributes,
|
||||
mergeAttributes,
|
||||
} from '@tiptap/core'
|
||||
|
||||
export interface TextStyleOptions {
|
||||
HTMLAttributes: {
|
||||
[key: string]: any
|
||||
},
|
||||
}
|
||||
|
||||
const TextStyle = Mark.create({
|
||||
name: 'textStyle',
|
||||
|
||||
defaultOptions: <TextStyleOptions>{
|
||||
HTMLAttributes: {},
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
@@ -21,7 +36,7 @@ const TextStyle = Mark.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['span', HTMLAttributes, 0]
|
||||
return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Command, Mark } from '@tiptap/core'
|
||||
import { Command, Mark, mergeAttributes } from '@tiptap/core'
|
||||
|
||||
export interface UnderlineOptions {
|
||||
HTMLAttributes: {
|
||||
@@ -25,7 +25,7 @@ const Underline = Mark.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['u', HTMLAttributes, 0]
|
||||
return ['u', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
|
||||
@@ -2,10 +2,51 @@
|
||||
|
||||
import { pasteRegex } from '@tiptap/extension-link'
|
||||
|
||||
describe('link regex test', () => {
|
||||
describe('link paste rules', () => {
|
||||
const validUrls = [
|
||||
'https://example.com',
|
||||
'https://example.com/with-path',
|
||||
'http://example.com/with-http',
|
||||
'https://www.example.com/with-www',
|
||||
'https://www.example.com/with-numbers-123',
|
||||
'https://www.example.com/with-parameters?var=true',
|
||||
'https://www.example.com/with-multiple-parameters?var=true&foo=bar',
|
||||
'https://www.example.com/with-spaces?var=true&foo=bar+3',
|
||||
'https://www.example.com/with,comma',
|
||||
'https://www.example.com/with(brackets)',
|
||||
'https://www.example.com/with!exclamation!marks',
|
||||
'http://thelongestdomainnameintheworldandthensomeandthensomemoreandmore.com/',
|
||||
'https://example.longtopleveldomain',
|
||||
'https://example-with-dashes.com',
|
||||
'https://example-with-dashes.com',
|
||||
]
|
||||
|
||||
it('paste regex matches url', () => {
|
||||
expect('https://www.example.com/with-spaces?var=true&foo=bar+3').to.match(pasteRegex)
|
||||
validUrls.forEach(url => {
|
||||
it(`paste regex matches url: ${url}`, {
|
||||
// every second test fails, but the second try succeeds
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 2,
|
||||
},
|
||||
}, () => {
|
||||
// TODO: Check the regex capture group to see *what* is matched
|
||||
expect(url).to.match(pasteRegex)
|
||||
})
|
||||
})
|
||||
|
||||
const invalidUrls = [
|
||||
'ftp://www.example.com',
|
||||
]
|
||||
|
||||
invalidUrls.forEach(url => {
|
||||
it(`paste regex doesn’t match url: ${url}`, {
|
||||
// every second test fails, but the second try succeeds
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 2,
|
||||
},
|
||||
}, () => {
|
||||
expect(url).to.not.match(pasteRegex)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user