add node demos

This commit is contained in:
Philipp Kühn
2021-08-25 18:15:10 +02:00
parent 502b533f60
commit 7dd4e38af5
51 changed files with 2925 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/Blockquote', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,104 @@
context('/demos/Nodes/Blockquote', () => {
before(() => {
cy.visit('/demos/Nodes/Blockquote')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p>')
cy.get('.ProseMirror').type('{selectall}')
})
})
it('should parse blockquote tags correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<blockquote><p>Example Text</p></blockquote>')
expect(editor.getHTML()).to.eq('<blockquote><p>Example Text</p></blockquote>')
})
})
it('should parse blockquote tags without paragraphs correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<blockquote>Example Text</blockquote>')
expect(editor.getHTML()).to.eq('<blockquote><p>Example Text</p></blockquote>')
})
})
it('the button should make the selected line a blockquote', () => {
cy.get('.ProseMirror blockquote')
.should('not.exist')
cy.get('button:first')
.click()
cy.get('.ProseMirror')
.find('blockquote')
.should('contain', 'Example Text')
})
it('the button should wrap all nodes in one blockquote', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p><p>Example Text</p>')
cy.get('.ProseMirror').type('{selectall}')
})
cy.get('button:first')
.click()
cy.get('.ProseMirror')
.find('blockquote')
.should('have.length', 1)
})
it('the button should toggle the blockquote', () => {
cy.get('.ProseMirror blockquote')
.should('not.exist')
cy.get('button:first')
.click()
cy.get('.ProseMirror')
.find('blockquote')
.should('contain', 'Example Text')
cy.get('.ProseMirror')
.type('{selectall}')
cy.get('button:first')
.click()
cy.get('.ProseMirror blockquote')
.should('not.exist')
})
it('should make the selected line a blockquote when the keyboard shortcut is pressed', () => {
cy.get('.ProseMirror')
.trigger('keydown', { shiftKey: true, modKey: true, key: 'b' })
.find('blockquote')
.should('contain', 'Example Text')
})
it('should toggle the blockquote when the keyboard shortcut is pressed', () => {
cy.get('.ProseMirror blockquote')
.should('not.exist')
cy.get('.ProseMirror')
.trigger('keydown', { shiftKey: true, modKey: true, key: 'b' })
.find('blockquote')
.should('contain', 'Example Text')
cy.get('.ProseMirror')
.type('{selectall}')
.trigger('keydown', { shiftKey: true, modKey: true, key: 'b' })
cy.get('.ProseMirror blockquote')
.should('not.exist')
})
it('should make a blockquote from markdown shortcuts', () => {
cy.get('.ProseMirror')
.type('> Quote')
.find('blockquote')
.should('contain', 'Quote')
})
})

View File

@@ -0,0 +1,64 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }">
blockquote
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Blockquote from '@tiptap/extension-blockquote'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
Blockquote,
],
content: `
<blockquote>
Life is like riding a bycicle. To keep your balance, you must keep moving.
</blockquote>
<p>Albert Einstein</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
}
</style>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/BulletList', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,152 @@
context('/demos/Nodes/BulletList', () => {
before(() => {
cy.visit('/demos/Nodes/BulletList')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p>')
cy.get('.ProseMirror').type('{selectall}')
})
})
it('should parse unordered lists correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<ul><li><p>Example Text</p></li></ul>')
expect(editor.getHTML()).to.eq('<ul><li><p>Example Text</p></li></ul>')
})
})
it('should parse unordered lists without paragraphs correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<ul><li>Example Text</li></ul>')
expect(editor.getHTML()).to.eq('<ul><li><p>Example Text</p></li></ul>')
})
})
it('the button should make the selected line a bullet list item', () => {
cy.get('.ProseMirror ul')
.should('not.exist')
cy.get('.ProseMirror ul li')
.should('not.exist')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror')
.find('ul')
.should('contain', 'Example Text')
cy.get('.ProseMirror')
.find('ul li')
.should('contain', 'Example Text')
})
it('the button should toggle the bullet list', () => {
cy.get('.ProseMirror ul')
.should('not.exist')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror')
.find('ul')
.should('contain', 'Example Text')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror ul')
.should('not.exist')
})
it('should leave the list with double enter', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
cy.get('.ProseMirror')
.type('- List Item 1{enter}{enter}Paragraph')
cy.get('.ProseMirror')
.find('li')
.its('length')
.should('eq', 1)
cy.get('.ProseMirror')
.find('p')
.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()
})
cy.get('.ProseMirror')
.type('* List Item 1{enter}List Item 2')
cy.get('.ProseMirror')
.find('li:nth-child(1)')
.should('contain', 'List Item 1')
cy.get('.ProseMirror')
.find('li:nth-child(2)')
.should('contain', 'List Item 2')
})
it('should make a bullet list from a dash', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
cy.get('.ProseMirror')
.type('- List Item 1{enter}List Item 2')
cy.get('.ProseMirror')
.find('li:nth-child(1)')
.should('contain', 'List Item 1')
cy.get('.ProseMirror')
.find('li:nth-child(2)')
.should('contain', 'List Item 2')
})
it('should make a bullet list from a plus', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
cy.get('.ProseMirror')
.type('+ List Item 1{enter}List Item 2')
cy.get('.ProseMirror')
.find('li:nth-child(1)')
.should('contain', 'List Item 1')
cy.get('.ProseMirror')
.find('li:nth-child(2)')
.should('contain', 'List Item 2')
})
it('should remove the bullet list after pressing backspace', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
cy.get('.ProseMirror')
.type('* {backspace}Example')
cy.get('.ProseMirror')
.find('p')
.should('contain', '* Example')
})
})

View File

@@ -0,0 +1,66 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
bullet list
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import BulletList from '@tiptap/extension-bullet-list'
import ListItem from '@tiptap/extension-list-item'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
BulletList,
ListItem,
],
content: `
<ul>
<li>A list item</li>
<li>And another one</li>
</ul>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
}
</style>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/CodeBlock', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,185 @@
context('/demos/Nodes/CodeBlock', () => {
before(() => {
cy.visit('/demos/Nodes/CodeBlock')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p>')
cy.get('.ProseMirror').type('{selectall}')
})
})
it('should parse code blocks correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<pre><code>Example Text</code></pre>')
expect(editor.getHTML()).to.eq('<pre><code>Example Text</code></pre>')
})
})
it('should parse code blocks with language correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<pre><code class="language-css">Example Text</code></pre>')
expect(editor.getHTML()).to.eq('<pre><code class="language-css">Example Text</code></pre>')
})
})
it('the button should make the selected line a code block', () => {
cy.get('button:first')
.click()
cy.get('.ProseMirror')
.find('pre')
.should('contain', 'Example Text')
})
it('the button should toggle the code block', () => {
cy.get('button:first')
.click()
cy.get('.ProseMirror')
.find('pre')
.should('contain', 'Example Text')
cy.get('.ProseMirror')
.type('{selectall}')
cy.get('button:first')
.click()
cy.get('.ProseMirror pre')
.should('not.exist')
})
it('the keyboard shortcut should make the selected line a code block', () => {
cy.get('.ProseMirror')
.trigger('keydown', { modKey: true, altKey: true, key: 'c' })
.find('pre')
.should('contain', 'Example Text')
})
it('the keyboard shortcut should toggle the code block', () => {
cy.get('.ProseMirror')
.trigger('keydown', { modKey: true, altKey: true, key: 'c' })
.find('pre')
.should('contain', 'Example Text')
cy.get('.ProseMirror')
.type('{selectall}')
.trigger('keydown', { modKey: true, altKey: true, key: 'c' })
cy.get('.ProseMirror pre')
.should('not.exist')
})
it('should parse the language from a HTML code block', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<pre><code class="language-css">body { display: none; }</code></pre>')
cy.get('.ProseMirror')
.find('pre>code.language-css')
.should('have.length', 1)
})
})
it('should make a code block from backtick markdown shortcuts', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
cy.get('.ProseMirror')
.type('``` Code')
.find('pre>code')
.should('contain', 'Code')
})
})
it('should make a code block from tilde markdown shortcuts', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
cy.get('.ProseMirror')
.type('~~~ Code')
.find('pre>code')
.should('contain', 'Code')
})
})
it('should make a code block for js with backticks', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
cy.get('.ProseMirror')
.type('```js Code')
.find('pre>code.language-js')
.should('contain', 'Code')
})
})
it('should make a code block for js with tildes', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
cy.get('.ProseMirror')
.type('~~~js Code')
.find('pre>code.language-js')
.should('contain', 'Code')
})
})
it('reverts the markdown shortcut when pressing backspace', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
cy.get('.ProseMirror')
.type('``` {backspace}')
cy.get('.ProseMirror pre')
.should('not.exist')
})
})
it('removes the code block when pressing backspace', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
cy.get('.ProseMirror pre')
.should('not.exist')
cy.get('.ProseMirror')
.type('Paragraph{enter}``` A{backspace}{backspace}')
cy.get('.ProseMirror pre')
.should('not.exist')
})
})
it('removes the code block when pressing backspace, even with blank lines', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
cy.get('.ProseMirror pre')
.should('not.exist')
cy.get('.ProseMirror')
.type('Paragraph{enter}{enter}``` A{backspace}{backspace}')
cy.get('.ProseMirror pre')
.should('not.exist')
})
})
it('removes the code block when pressing backspace, even at start of document', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
cy.get('.ProseMirror pre')
.should('not.exist')
cy.get('.ProseMirror')
.type('``` A{leftArrow}{backspace}')
cy.get('.ProseMirror pre')
.should('not.exist')
})
})
})

View File

@@ -0,0 +1,87 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }">
code block
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import CodeBlock from '@tiptap/extension-code-block'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
CodeBlock,
],
content: `
<p>
Thats a boring paragraph followed by a fenced code block:
</p>
<pre><code>for (var i=1; i <= 20; i++)
{
if (i % 15 == 0)
console.log("FizzBuzz");
else if (i % 3 == 0)
console.log("Fizz");
else if (i % 5 == 0)
console.log("Buzz");
else
console.log(i);
}</code></pre>
<p>
Press Command/Ctrl + Enter to leave the fenced code block and continue typing in boring paragraphs.
</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
}
</style>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/CodeBlockLowlight', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,149 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }">
code block
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
// load all highlight.js languages
import lowlight from 'lowlight'
// load specific languages only
// import lowlight from 'lowlight/lib/core'
// import javascript from 'highlight.js/lib/languages/javascript'
// lowlight.registerLanguage('javascript', javascript)
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
CodeBlockLowlight.configure({
lowlight,
}),
],
content: `
<p>
Thats a boring paragraph followed by a fenced code block:
</p>
<pre><code class="language-javascript">for (var i=1; i <= 20; i++)
{
if (i % 15 == 0)
console.log("FizzBuzz");
else if (i % 3 == 0)
console.log("Fizz");
else if (i % 5 == 0)
console.log("Buzz");
else
console.log(i);
}</code></pre>
<p>
Press Command/Ctrl + Enter to leave the fenced code block and continue typing in boring paragraphs.
</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
.hljs-comment,
.hljs-quote {
color: #616161;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #F98181;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #FBBC88;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #B9F18D;
}
.hljs-title,
.hljs-section {
color: #FAF594;
}
.hljs-keyword,
.hljs-selector-tag {
color: #70CFF8;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}
}
</style>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/Document', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
context('/demos/Nodes/Document', () => {
before(() => {
cy.visit('/demos/Nodes/Document')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p></p>')
})
})
it('should return the document in as json', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
const json = editor.getJSON()
expect(json).to.deep.equal({
type: 'doc',
content: [
{
type: 'paragraph',
},
],
})
})
})
})

View File

@@ -0,0 +1,41 @@
<template>
<div v-if="editor">
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
],
content: `
<p>The Document extension is required. Though, you can write your own implementation, e. g. to give it custom name.</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/HardBreak', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,58 @@
context('/demos/Nodes/HardBreak', () => {
before(() => {
cy.visit('/demos/Nodes/HardBreak')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p>')
})
})
it('should parse hard breaks correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example<br>Text</p>')
expect(editor.getHTML()).to.eq('<p>Example<br>Text</p>')
})
})
it('should parse hard breaks with self-closing tag correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example<br />Text</p>')
expect(editor.getHTML()).to.eq('<p>Example<br>Text</p>')
})
})
it('the button should add a line break', () => {
cy.get('.ProseMirror br')
.should('not.exist')
cy.get('button:first')
.click()
cy.get('.ProseMirror br')
.should('exist')
})
it('the default keyboard shortcut should add a line break', () => {
cy.get('.ProseMirror br')
.should('not.exist')
cy.get('.ProseMirror')
.trigger('keydown', { shiftKey: true, key: 'Enter' })
cy.get('.ProseMirror br')
.should('exist')
})
it('the alternative keyboard shortcut should add a line break', () => {
cy.get('.ProseMirror br')
.should('not.exist')
cy.get('.ProseMirror')
.trigger('keydown', { modKey: true, key: 'Enter' })
cy.get('.ProseMirror br')
.should('exist')
})
})

View File

@@ -0,0 +1,56 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().setHardBreak().run()">
hardBreak
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import HardBreak from '@tiptap/extension-hard-break'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
HardBreak,
],
content: `
<p>
This<br>
is<br>
a<br>
single<br>
paragraph<br>
with<br>
line<br>
breaks.
</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/Heading', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,142 @@
context('/demos/Nodes/Heading', () => {
before(() => {
cy.visit('/demos/Nodes/Heading')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p>')
cy.get('.ProseMirror').type('{selectall}')
})
})
const headings = [
'<h1>Example Text</h1>',
'<h2>Example Text</h2>',
'<h3>Example Text</h3>',
]
headings.forEach(html => {
it(`should parse headings correctly (${html})`, () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent(html)
expect(editor.getHTML()).to.eq(html)
})
})
})
it('should omit disabled heading levels', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<h4>Example Text</h4>')
expect(editor.getHTML()).to.eq('<p>Example Text</p>')
})
})
it('the button should make the selected line a h1', () => {
cy.get('.ProseMirror h1')
.should('not.exist')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror')
.find('h1')
.should('contain', 'Example Text')
})
it('the button should make the selected line a h2', () => {
cy.get('.ProseMirror h2')
.should('not.exist')
cy.get('button:nth-child(2)')
.click()
cy.get('.ProseMirror')
.find('h2')
.should('contain', 'Example Text')
})
it('the button should make the selected line a h3', () => {
cy.get('.ProseMirror h3')
.should('not.exist')
cy.get('button:nth-child(3)')
.click()
cy.get('.ProseMirror')
.find('h3')
.should('contain', 'Example Text')
})
it('the button should toggle the heading', () => {
cy.get('.ProseMirror h1')
.should('not.exist')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror')
.find('h1')
.should('contain', 'Example Text')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror h1')
.should('not.exist')
})
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()
})
cy.get('.ProseMirror')
.type('# Headline')
.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')
})
})

View File

@@ -0,0 +1,58 @@
<template>
<div v-if="editor">
<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>
<button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }">
h3
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Heading from '@tiptap/extension-heading'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
Heading.configure({
levels: [1, 2, 3],
}),
],
content: `
<h1>This is a 1st level heading</h1>
<h2>This is a 2nd level heading</h2>
<h3>This is a 3rd level heading</h3>
<h4>This 4th level heading will be converted to a paragraph, because levels are configured to be only 1, 2 or 3.</h4>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/HorizontalRule', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,66 @@
context('/demos/Nodes/HorizontalRule', () => {
before(() => {
cy.visit('/demos/Nodes/HorizontalRule')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p>')
})
})
it('should parse horizontal rules correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p><hr>')
expect(editor.getHTML()).to.eq('<p>Example Text</p><hr>')
})
})
it('should parse horizontal rules with self-closing tag correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p><hr />')
expect(editor.getHTML()).to.eq('<p>Example Text</p><hr>')
})
})
it('the button should add a horizontal rule', () => {
cy.get('.ProseMirror hr')
.should('not.exist')
cy.get('button:first')
.click()
cy.get('.ProseMirror hr')
.should('exist')
})
it('the default markdown shortcut should add a horizontal rule', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
cy.get('.ProseMirror hr')
.should('not.exist')
cy.get('.ProseMirror')
.type('---')
cy.get('.ProseMirror hr')
.should('exist')
})
})
it('the alternative markdown shortcut should add a horizontal rule', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
cy.get('.ProseMirror hr')
.should('not.exist')
cy.get('.ProseMirror')
.type('___ ')
cy.get('.ProseMirror hr')
.should('exist')
})
})
})

View File

@@ -0,0 +1,51 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().setHorizontalRule().run()">
horizontalRule
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import HorizontalRule from '@tiptap/extension-horizontal-rule'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
HorizontalRule,
],
content: `
<p>This is a paragraph.</p>
<hr>
<p>And this is another paragraph.</p>
<hr>
<p>But between those paragraphs are horizontal rules.</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/Image', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
context('/demos/Nodes/Image', () => {
before(() => {
cy.visit('/demos/Nodes/Image')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p>')
cy.get('.ProseMirror').type('{selectall}')
})
})
it('should add an img tag with the correct URL', () => {
cy.window().then(win => {
cy.stub(win, 'prompt').returns('foobar.png')
cy.get('button:first')
.click()
cy.window().its('prompt').should('be.called')
cy.get('.ProseMirror')
.find('img')
.should('have.attr', 'src', 'foobar.png')
})
})
})

View File

@@ -0,0 +1,78 @@
<template>
<div v-if="editor">
<button @click="addImage">
image
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Image from '@tiptap/extension-image'
import Dropcursor from '@tiptap/extension-dropcursor'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
methods: {
addImage() {
const url = window.prompt('URL')
if (url) {
this.editor.chain().focus().setImage({ src: url }).run()
}
},
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
Image,
Dropcursor,
],
content: `
<p>This is a basic example of implementing images. Drag to re-order.</p>
<img src="https://source.unsplash.com/8xznAGy4HcY/800x400" />
<img src="https://source.unsplash.com/K9QHL52rE2k/800x400" />
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
img {
max-width: 100%;
height: auto;
&.ProseMirror-selectednode {
outline: 3px solid #68CEF8;
}
}
}
</style>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/ListItem', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,49 @@
context('/demos/Nodes/ListItem', () => {
before(() => {
cy.visit('/demos/Nodes/ListItem')
})
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')
})
})

View File

@@ -0,0 +1,86 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
bullet list
</button>
<button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }">
ordered list
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import BulletList from '@tiptap/extension-bullet-list'
import OrderedList from '@tiptap/extension-ordered-list'
import ListItem from '@tiptap/extension-list-item'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
BulletList,
OrderedList,
ListItem,
],
content: `
<p>
I like lists. Lets add one:
</p>
<ul>
<li>This is a bullet list.</li>
<li>And it has three list items.</li>
<li>Here is the third one.</li>
</ul>
<p>
Do you want to see one more? I bet! Here is another one:
</p>
<ol>
<li>Thats a different list, actually its an ordered list.</li>
<li>It also has three list items.</li>
<li>And all of them are numbered.</li>
</ol>
<p>
Lists would be nothing without list items.
</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<div class="items">
<button
class="item"
:class="{ 'is-selected': index === selectedIndex }"
v-for="(item, index) in items"
:key="index"
@click="selectItem(index)"
>
{{ item }}
</button>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
command: {
type: Function,
required: true,
},
},
data() {
return {
selectedIndex: 0,
}
},
watch: {
items() {
this.selectedIndex = 0
},
},
methods: {
onKeyDown({ event }) {
if (event.key === 'ArrowUp') {
this.upHandler()
return true
}
if (event.key === 'ArrowDown') {
this.downHandler()
return true
}
if (event.key === 'Enter') {
this.enterHandler()
return true
}
return false
},
upHandler() {
this.selectedIndex = ((this.selectedIndex + this.items.length) - 1) % this.items.length
},
downHandler() {
this.selectedIndex = (this.selectedIndex + 1) % this.items.length
},
enterHandler() {
this.selectItem(this.selectedIndex)
},
selectItem(index) {
const item = this.items[index]
if (item) {
this.command({ id: item })
}
},
},
}
</script>
<style lang="scss">
.items {
position: relative;
border-radius: 0.25rem;
background: white;
color: rgba(black, 0.8);
overflow: hidden;
font-size: 0.9rem;
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.1),
0px 10px 20px rgba(0, 0, 0, 0.1),
;
}
.item {
display: block;
width: 100%;
text-align: left;
background: transparent;
border: none;
padding: 0.2rem 0.5rem;
&.is-selected,
&:hover {
color: #A975FF;
background: rgba(#A975FF, 0.1);
}
}
</style>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/Mention', source)
</script>
</body>
</html>

View File

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

View File

@@ -0,0 +1,121 @@
<template>
<div v-if="editor">
<editor-content :editor="editor" />
</div>
</template>
<script>
import tippy from 'tippy.js'
import { Editor, EditorContent, VueRenderer } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Mention from '@tiptap/extension-mention'
import MentionList from './MentionList.vue'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
Mention.configure({
HTMLAttributes: {
class: 'mention',
},
suggestion: {
items: query => {
return [
'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10)
},
render: () => {
let component
let popup
return {
onStart: props => {
component = new VueRenderer(MentionList, {
// using vue 2:
// parent: this,
// propsData: props,
props,
editor: props.editor,
})
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
})
},
onUpdate(props) {
component.updateProps(props)
popup[0].setProps({
getReferenceClientRect: props.clientRect,
})
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup[0].hide()
return true
}
return component.ref?.onKeyDown(props)
},
onExit() {
popup[0].destroy()
component.destroy()
},
}
},
},
}),
],
content: `
<p>Hi everyone! Dont forget the daily stand up at 8 AM.</p>
<p><span data-mention data-id="Jennifer Grey"></span> Would you mind to share what youve been working on lately? We fear not much happened since Dirty Dancing.
<p><span data-mention data-id="Winona Ryder"></span> <span data-mention data-id="Axl Rose"></span> Lets go through your most important points quickly.</p>
<p>I have a meeting with <span data-mention data-id="Christina Applegate"></span> and dont want to come late.</p>
<p> Thanks, your big boss</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
}
.mention {
color: #A975FF;
background-color: rgba(#A975FF, 0.1);
border-radius: 0.3rem;
padding: 0.1rem 0.3rem;
}
</style>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/OrderedList', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,131 @@
context('/demos/Nodes/OrderedList', () => {
before(() => {
cy.visit('/demos/Nodes/OrderedList')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p>')
cy.get('.ProseMirror').type('{selectall}')
})
})
it('should parse ordered lists correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<ol><li><p>Example Text</p></li></ol>')
expect(editor.getHTML()).to.eq('<ol><li><p>Example Text</p></li></ol>')
})
})
it('should parse ordered lists without paragraphs correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<ol><li>Example Text</li></ol>')
expect(editor.getHTML()).to.eq('<ol><li><p>Example Text</p></li></ol>')
})
})
it('the button should make the selected line a ordered list item', () => {
cy.get('.ProseMirror ol')
.should('not.exist')
cy.get('.ProseMirror ol li')
.should('not.exist')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror')
.find('ol')
.should('contain', 'Example Text')
cy.get('.ProseMirror')
.find('ol li')
.should('contain', 'Example Text')
})
it('the button should toggle the ordered list', () => {
cy.get('.ProseMirror ol')
.should('not.exist')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror')
.find('ol')
.should('contain', 'Example Text')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror ol')
.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()
})
cy.get('.ProseMirror')
.type('1. List Item 1{enter}{enter}Paragraph')
cy.get('.ProseMirror')
.find('li')
.its('length')
.should('eq', 1)
cy.get('.ProseMirror')
.find('p')
.should('contain', 'Paragraph')
})
it('should make a ordered list from a number', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
cy.get('.ProseMirror')
.type('1. List Item 1{enter}List Item 2')
cy.get('.ProseMirror')
.find('li:nth-child(1)')
.should('contain', 'List Item 1')
cy.get('.ProseMirror')
.find('li:nth-child(2)')
.should('contain', 'List Item 2')
})
it('should make a ordered list from a number other than number one', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
cy.get('.ProseMirror')
.type('2. List Item 1{enter}List Item 2')
cy.get('.ProseMirror')
.find('ol')
.should('have.attr', 'start', '2')
})
it('should remove the ordered list after pressing backspace', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
cy.get('.ProseMirror')
.type('1. {backspace}Example')
cy.get('.ProseMirror')
.find('p')
.should('contain', '1. Example')
})
})

View File

@@ -0,0 +1,71 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }">
ordered list
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import OrderedList from '@tiptap/extension-ordered-list'
import ListItem from '@tiptap/extension-list-item'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
OrderedList,
ListItem,
],
content: `
<ol>
<li>A list item</li>
<li>And another one</li>
</ol>
<ol start="5">
<li>This item starts at 5</li>
<li>And another one</li>
</ol>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
}
</style>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/Paragraph', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,58 @@
context('/demos/Nodes/Paragraph', () => {
before(() => {
cy.visit('/demos/Nodes/Paragraph')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
})
it('should parse paragraphs correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p>')
expect(editor.getHTML()).to.eq('<p>Example Text</p>')
editor.commands.setContent('<p><x-unknown>Example Text</x-unknown></p>')
expect(editor.getHTML()).to.eq('<p>Example Text</p>')
editor.commands.setContent('<p style="display: block;">Example Text</p>')
expect(editor.getHTML()).to.eq('<p>Example Text</p>')
})
})
it('text should be wrapped in a paragraph by default', () => {
cy.get('.ProseMirror')
.type('Example Text')
.find('p')
.should('contain', 'Example Text')
})
it('enter should make a new paragraph', () => {
cy.get('.ProseMirror')
.type('First Paragraph{enter}Second Paragraph')
.find('p')
.should('have.length', 2)
cy.get('.ProseMirror')
.find('p:first')
.should('contain', 'First Paragraph')
cy.get('.ProseMirror')
.find('p:nth-child(2)')
.should('contain', 'Second Paragraph')
})
it('backspace should remove the second paragraph', () => {
cy.get('.ProseMirror')
.type('{enter}')
.find('p')
.should('have.length', 2)
cy.get('.ProseMirror')
.type('{backspace}')
.find('p')
.should('have.length', 1)
})
})

View File

@@ -0,0 +1,41 @@
<template>
<div v-if="editor">
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
],
content: `
<p>The Paragraph extension is not required, but its very likely you want to use it. Its needed to write paragraphs of text. 🤓</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/Table', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,77 @@
context('/demos/Nodes/Table', () => {
before(() => {
cy.visit('/demos/Nodes/Table')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
})
it('creates a table (1x1)', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.insertTable({ cols: 1, rows: 1, withHeaderRow: false })
cy.get('.ProseMirror').find('td').its('length').should('eq', 1)
cy.get('.ProseMirror').find('tr').its('length').should('eq', 1)
})
})
it('creates a table (3x1)', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.insertTable({ cols: 3, rows: 1, withHeaderRow: false })
cy.get('.ProseMirror').find('td').its('length').should('eq', 3)
cy.get('.ProseMirror').find('tr').its('length').should('eq', 1)
})
})
it('creates a table (1x3)', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.insertTable({ cols: 1, rows: 3, withHeaderRow: false })
cy.get('.ProseMirror').find('td').its('length').should('eq', 3)
cy.get('.ProseMirror').find('tr').its('length').should('eq', 3)
})
})
it('creates a table with header row (1x3)', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.insertTable({ cols: 1, rows: 3, withHeaderRow: true })
cy.get('.ProseMirror').find('th').its('length').should('eq', 1)
cy.get('.ProseMirror').find('td').its('length').should('eq', 2)
cy.get('.ProseMirror').find('tr').its('length').should('eq', 3)
})
})
it('creates a table with correct defaults (3x3, th)', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.insertTable()
cy.get('.ProseMirror').find('th').its('length').should('eq', 3)
cy.get('.ProseMirror').find('td').its('length').should('eq', 6)
cy.get('.ProseMirror').find('tr').its('length').should('eq', 3)
})
})
it('generates correct markup for a table (1x1)', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.insertTable({ cols: 1, rows: 1, withHeaderRow: false })
const html = editor.getHTML()
expect(html).to.equal('<table><tbody><tr><td colspan="1" rowspan="1"><p></p></td></tr></tbody></table>')
})
})
it('generates correct markup for a table (1x1, th)', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.insertTable({ cols: 1, rows: 1, withHeaderRow: true })
const html = editor.getHTML()
expect(html).to.equal('<table><tbody><tr><th colspan="1" rowspan="1"><p></p></th></tr></tbody></table>')
})
})
})

View File

@@ -0,0 +1,181 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()">
insertTable
</button>
<button @click="editor.chain().focus().addColumnBefore().run()">
addColumnBefore
</button>
<button @click="editor.chain().focus().addColumnAfter().run()">
addColumnAfter
</button>
<button @click="editor.chain().focus().deleteColumn().run()">
deleteColumn
</button>
<button @click="editor.chain().focus().addRowBefore().run()">
addRowBefore
</button>
<button @click="editor.chain().focus().addRowAfter().run()">
addRowAfter
</button>
<button @click="editor.chain().focus().deleteRow().run()">
deleteRow
</button>
<button @click="editor.chain().focus().deleteTable().run()">
deleteTable
</button>
<button @click="editor.chain().focus().mergeCells().run()">
mergeCells
</button>
<button @click="editor.chain().focus().splitCell().run()">
splitCell
</button>
<button @click="editor.chain().focus().toggleHeaderColumn().run()">
toggleHeaderColumn
</button>
<button @click="editor.chain().focus().toggleHeaderRow().run()">
toggleHeaderRow
</button>
<button @click="editor.chain().focus().toggleHeaderCell().run()">
toggleHeaderCell
</button>
<button @click="editor.chain().focus().mergeOrSplit().run()">
mergeOrSplit
</button>
<button @click="editor.chain().focus().setCellAttribute('colspan', 2).run()">
setCellAttribute
</button>
<button @click="editor.chain().focus().fixTables().run()">
fixTables
</button>
<button @click="editor.chain().focus().goToNextCell().run()">
goToNextCell
</button>
<button @click="editor.chain().focus().goToPreviousCell().run()">
goToPreviousCell
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Table from '@tiptap/extension-table'
import TableRow from '@tiptap/extension-table-row'
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import Gapcursor from '@tiptap/extension-gapcursor'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
Gapcursor,
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
TableCell,
],
content: `
<table>
<tbody>
<tr>
<th>Name</th>
<th colspan="3">Description</th>
</tr>
<tr>
<td>Cyndi Lauper</td>
<td>singer</td>
<td>songwriter</td>
<td>actress</td>
</tr>
</tbody>
</table>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
.ProseMirror {
table {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0;
overflow: hidden;
td,
th {
min-width: 1em;
border: 2px solid #ced4da;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box;
position: relative;
> * {
margin-bottom: 0;
}
}
th {
font-weight: bold;
text-align: left;
background-color: #f1f3f5;
}
.selectedCell:after {
z-index: 2;
position: absolute;
content: "";
left: 0; right: 0; top: 0; bottom: 0;
background: rgba(200, 200, 255, 0.4);
pointer-events: none;
}
.column-resize-handle {
position: absolute;
right: -2px;
top: 0;
bottom: -2px;
width: 4px;
background-color: #adf;
pointer-events: none;
}
}
}
.tableWrapper {
padding: 1rem 0;
overflow-x: auto;
}
.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
</style>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/TaskItem', source)
</script>
</body>
</html>

View File

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

View File

@@ -0,0 +1,65 @@
<template>
<div v-if="editor">
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
TaskList,
TaskItem,
],
content: `
<ul data-type="taskList">
<li data-type="taskItem" data-checked="true">A list item</li>
<li data-type="taskItem" data-checked="false">And another one</li>
</ul>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
ul[data-type="taskList"] {
list-style: none;
padding: 0;
li {
display: flex;
align-items: center;
> label {
flex: 0 0 auto;
margin-right: 0.5rem;
}
}
}
</style>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/TaskList', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,126 @@
context('/demos/Nodes/TaskList', () => {
before(() => {
cy.visit('/demos/Nodes/TaskList')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p>')
cy.get('.ProseMirror').type('{selectall}')
})
})
it('should parse unordered lists correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<ul data-type="taskList"><li data-checked="true" data-type="taskItem"><p>Example Text</p></li></ul>')
expect(editor.getHTML()).to.eq('<ul data-type="taskList"><li data-checked="true" data-type="taskItem"><p>Example Text</p></li></ul>')
})
})
it('should parse unordered lists without paragraphs correctly', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<ul data-type="taskList"><li data-checked="false" data-type="taskItem">Example Text</li></ul>')
expect(editor.getHTML()).to.eq('<ul data-type="taskList"><li data-checked="false" data-type="taskItem"><p>Example Text</p></li></ul>')
})
})
it('the button should make the selected line a task list item', () => {
cy.get('.ProseMirror ul')
.should('not.exist')
cy.get('.ProseMirror ul li')
.should('not.exist')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror')
.find('ul[data-type="taskList"]')
.should('contain', 'Example Text')
cy.get('.ProseMirror')
.find('ul[data-type="taskList"] li')
.should('contain', 'Example Text')
})
it('the button should toggle the task list', () => {
cy.get('.ProseMirror ul')
.should('not.exist')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror')
.find('ul[data-type="taskList"]')
.should('contain', 'Example Text')
cy.get('button:nth-child(1)')
.click()
cy.get('.ProseMirror ul')
.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: '9' })
.find('ul li')
.should('contain', 'Example Text')
})
it('should leave the list with double enter', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
cy.get('.ProseMirror')
.type('[ ] List Item 1{enter}{enter}Paragraph')
cy.get('.ProseMirror')
.find('li')
.its('length')
.should('eq', 1)
cy.get('.ProseMirror')
.find('p')
.should('contain', 'Paragraph')
})
it('should make a task list from square brackets', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
cy.get('.ProseMirror')
.type('[ ] List Item 1{enter}List Item 2')
cy.get('.ProseMirror')
.find('li:nth-child(1)')
.should('contain', 'List Item 1')
.should('have.attr', 'data-checked', 'false')
cy.get('.ProseMirror')
.find('li:nth-child(2)')
.should('contain', 'List Item 2')
.should('have.attr', 'data-checked', 'false')
})
it('should make a task list from checked square brackets', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
cy.get('.ProseMirror')
.type('[x] List Item 1{enter}List Item 2')
cy.get('.ProseMirror')
.find('li:nth-child(1)')
.should('contain', 'List Item 1')
.should('have.attr', 'data-checked', 'true')
cy.get('.ProseMirror')
.find('li:nth-child(2)')
.should('contain', 'List Item 2')
.should('have.attr', 'data-checked', 'false')
})
})

View File

@@ -0,0 +1,69 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().toggleTaskList().run()" :class="{ 'is-active': editor.isActive('taskList') }">
task list
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
TaskList,
TaskItem,
],
content: `
<ul data-type="taskList">
<li data-type="taskItem" data-checked="true">A list item</li>
<li data-type="taskItem" data-checked="false">And another one</li>
</ul>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
ul[data-type="taskList"] {
list-style: none;
padding: 0;
li {
display: flex;
align-items: center;
> label {
flex: 0 0 auto;
margin-right: 0.5rem;
}
}
}
</style>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Nodes/Text', source)
</script>
</body>
</html>

View File

@@ -0,0 +1,18 @@
context('/demos/Nodes/Text', () => {
before(() => {
cy.visit('/demos/Nodes/Text')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent()
})
})
it('text should be wrapped in a paragraph by default', () => {
cy.get('.ProseMirror')
.type('Example Text')
.find('p')
.should('contain', 'Example Text')
})
})

View File

@@ -0,0 +1,41 @@
<template>
<div v-if="editor">
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
],
content: `
<p>The Text extension is required, at least if you want to have text in your text editor and thats very likely.</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>