Merge branch 'main' into feature/vue-node-views

This commit is contained in:
Philipp Kühn
2020-11-25 10:00:13 +01:00
58 changed files with 483 additions and 275 deletions

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

View File

@@ -2,4 +2,6 @@ context('/examples/collaborative-editing', () => {
before(() => { before(() => {
cy.visit('/examples/collaborative-editing') cy.visit('/examples/collaborative-editing')
}) })
// TODO: Write tests
}) })

View File

@@ -1,5 +0,0 @@
context('/examples/collaborative-editing-ws', () => {
before(() => {
cy.visit('/examples/collaborative-editing-ws')
})
})

View File

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

View File

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

View File

@@ -2,4 +2,6 @@ context('/examples/minimalist', () => {
before(() => { before(() => {
cy.visit('/examples/minimalist') cy.visit('/examples/minimalist')
}) })
// TODO: Write tests
}) })

View File

@@ -2,4 +2,6 @@ context('/examples/todo-app', () => {
before(() => { before(() => {
cy.visit('/examples/todo-app') cy.visit('/examples/todo-app')
}) })
// TODO: Write tests
}) })

View File

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

View File

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

View File

@@ -2,4 +2,6 @@ context('/api/extensions/collaboration', () => {
before(() => { before(() => {
cy.visit('/api/extensions/collaboration') cy.visit('/api/extensions/collaboration')
}) })
// TODO: Write tests
}) })

View File

@@ -2,4 +2,6 @@ context('/api/extensions/collaboration-cursor', () => {
before(() => { before(() => {
cy.visit('/api/extensions/collaboration-cursor') cy.visit('/api/extensions/collaboration-cursor')
}) })
// TODO: Write tests
}) })

View File

@@ -2,4 +2,6 @@ context('/examples/dropcursor', () => {
before(() => { before(() => {
cy.visit('/examples/dropcursor') cy.visit('/examples/dropcursor')
}) })
// TODO: Write tests
}) })

View File

@@ -2,4 +2,6 @@ context('/api/extensions/font-family', () => {
before(() => { before(() => {
cy.visit('/api/extensions/font-family') cy.visit('/api/extensions/font-family')
}) })
// TODO: Write tests
}) })

View File

@@ -2,4 +2,6 @@ context('/examples/gapcursor', () => {
before(() => { before(() => {
cy.visit('/examples/gapcursor') cy.visit('/examples/gapcursor')
}) })
// TODO: Write tests
}) })

View File

@@ -81,4 +81,32 @@ context('/api/extensions/text-align', () => {
.find('p') .find('p')
.should('have.css', 'text-align', 'left') .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')
})
}) })

View File

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

View File

@@ -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>Im running tiptap with Vue.js. 🎉</p>',
extensions: defaultExtensions(),
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@@ -47,31 +47,23 @@ context('/api/marks/link', () => {
}) })
}) })
const validUrls = [ it('detects a pasted URL', () => {
'https://example.com', cy.get('.ProseMirror').paste({ pastePayload: 'https://example.com', pasteType: 'text/plain' })
'https://example.com/with-path', .find('a')
'http://example.com/with-http', .should('contain', 'https://example.com')
'https://www.example.com/with-www', .should('have.attr', 'href', 'https://example.com')
'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)
})
}) })
// 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')
})
}) })

View File

@@ -61,3 +61,16 @@ export default {
}, },
} }
</script> </script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
a {
color: #68CEF8;
}
}
</style>

View File

@@ -2,4 +2,6 @@ context('/api/marks/text-style', () => {
before(() => { before(() => {
cy.visit('/api/marks/text-style') cy.visit('/api/marks/text-style')
}) })
// TODO: Write tests
}) })

View File

@@ -79,6 +79,13 @@ context('/api/nodes/bullet-list', () => {
.should('contain', 'Paragraph') .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', () => { it('should make a bullet list from an asterisk', () => {
cy.get('.ProseMirror').then(([{ editor }]) => { cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent() editor.commands.clearContent()

View File

@@ -86,7 +86,28 @@ context('/api/nodes/heading', () => {
.should('not.exist') .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 }]) => { cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent() editor.commands.clearContent()
}) })
@@ -96,4 +117,26 @@ context('/api/nodes/heading', () => {
.find('h1') .find('h1')
.should('contain', 'Headline') .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

@@ -2,4 +2,48 @@ context('/api/nodes/list-item', () => {
before(() => { before(() => {
cy.visit('/api/nodes/list-item') 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')
})
}) })

View File

@@ -71,3 +71,50 @@ export default {
}, },
} }
</script> </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>

View File

@@ -61,6 +61,13 @@ context('/api/nodes/ordered-list', () => {
.should('not.exist') .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', () => { it('should leave the list with double enter', () => {
cy.get('.ProseMirror').then(([{ editor }]) => { cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent() editor.commands.clearContent()

View File

@@ -2,4 +2,6 @@ context('/api/nodes/task-item', () => {
before(() => { before(() => {
cy.visit('/api/nodes/task-item') cy.visit('/api/nodes/task-item')
}) })
// TODO: Write tests
}) })

View File

@@ -61,6 +61,13 @@ context('/api/nodes/task-list', () => {
.should('not.exist') .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', () => { it('should leave the list with double enter', () => {
cy.get('.ProseMirror').then(([{ editor }]) => { cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.clearContent() editor.commands.clearContent()

View File

@@ -0,0 +1,5 @@
context('/overview/installation', () => {
before(() => {
cy.visit('/overview/installation')
})
})

View File

@@ -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! 👋 Im a text editor with just one button. Select some text and press the button to see what it does. Yes, its marking text <strong>bold</strong>. Amazing, isnt it?</p>',
extensions: defaultExtensions(),
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@@ -48,7 +48,7 @@ Most of the core extensions register their own keyboard shortcuts. Depending on
| Center align | `Control`&nbsp;`Shift`&nbsp;`E` | `Cmd`&nbsp;`Shift`&nbsp;`E` | | Center align | `Control`&nbsp;`Shift`&nbsp;`E` | `Cmd`&nbsp;`Shift`&nbsp;`E` |
| Right align | `Control`&nbsp;`Shift`&nbsp;`R` | `Cmd`&nbsp;`Shift`&nbsp;`R` | | Right align | `Control`&nbsp;`Shift`&nbsp;`R` | `Cmd`&nbsp;`Shift`&nbsp;`R` |
| Justify | `Control`&nbsp;`Shift`&nbsp;`J` | `Cmd`&nbsp;`Shift`&nbsp;`J` | | Justify | `Control`&nbsp;`Shift`&nbsp;`J` | `Cmd`&nbsp;`Shift`&nbsp;`J` |
| Task list | `Control`&nbsp;`Shift`&nbsp;`L` | `Cmd`&nbsp;`Shift`&nbsp;`L` | | Task list | `Control`&nbsp;`Shift`&nbsp;`L` | `Cmd`&nbsp;`Shift`&nbsp;`L` (TODO: Conflict!) |
| Code block | `Control`&nbsp;`Alt`&nbsp;`C` | `Cmd`&nbsp;`Alt`&nbsp;`C` | | Code block | `Control`&nbsp;`Alt`&nbsp;`C` | `Cmd`&nbsp;`Alt`&nbsp;`C` |
<!--| Toggle task| `Control`&nbsp;`Enter` | `Cmd`&nbsp;`Enter` | --> <!--| Toggle task| `Control`&nbsp;`Enter` | `Cmd`&nbsp;`Enter` | -->

View File

@@ -1,8 +1,10 @@
# Create your editor # Create a new toolbar
## toc ## toc
## Introduction TODO
<!-- ## Introduction
In its simplest version tiptap comes very raw. There is no menu, no buttons, no styling. Thats intended. See tiptap as your building blocks to build exactly the editor you would like to have. In its simplest version tiptap comes very raw. There is no menu, no buttons, no styling. Thats intended. See tiptap as your building blocks to build exactly the editor you would like to have.
## Adding a menu ## Adding a menu
@@ -24,4 +26,4 @@ Note that `Document`, `Paragraph` and `Text` are required. Otherwise you wont
<demo name="Guide/BuildYourEditor" highlight="10-13,30-33" /> <demo name="Guide/BuildYourEditor" highlight="10-13,30-33" />
Thats also the place where you can register custom extensions, which you or someone else built for tiptap. Thats also the place where you can register custom extensions, which you or someone else built for tiptap. -->

View File

@@ -55,7 +55,38 @@ To actually start using tiptap, youll 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. 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>Im running tiptap with Vue.js. 🎉</p>',
extensions: defaultExtensions(),
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
```
## 5. Add it to your app ## 5. Add it to your app
Now, lets replace the content of `src/App.vue` with the following example code to use our new `Tiptap` component in our app. Now, lets replace the content of `src/App.vue` with the following example code to use our new `Tiptap` component in our app.

View File

@@ -57,8 +57,34 @@ Its 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) * [Vue.js/tiptap on CodeSandbox](https://codesandbox.io/s/tiptap-issue-template-b83rr?file=/src/components/Tiptap.vue)
## Option 4: CDN ## Option 4: CDN (Draft)
To pull in tiptap for quick demos or just giving it a spin, grab the latest build via CDN: 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, thats 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 doesnt work in older browsers anyway, we tend to remove those builds. What do you think? Anyway, heres how you can use it:
```html ```html
<!doctype 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> </body>
</html> </html>
``` ```

View File

@@ -6,7 +6,6 @@ import getRenderedAttributes from './getRenderedAttributes'
import isEmptyObject from './isEmptyObject' import isEmptyObject from './isEmptyObject'
import injectExtensionAttributesToParseRule from './injectExtensionAttributesToParseRule' import injectExtensionAttributesToParseRule from './injectExtensionAttributesToParseRule'
import callOrReturn from './callOrReturn' import callOrReturn from './callOrReturn'
import mergeAttributes from './mergeAttributes'
function cleanUpSchemaItem<T>(data: T) { function cleanUpSchemaItem<T>(data: T) {
return Object.fromEntries(Object.entries(data).filter(([key, value]) => { return Object.fromEntries(Object.entries(data).filter(([key, value]) => {
@@ -51,10 +50,7 @@ export default function getSchema(extensions: Extensions): Schema {
if (extension.config.renderHTML) { if (extension.config.renderHTML) {
schema.toDOM = node => (extension.config.renderHTML as Function)?.bind(context)({ schema.toDOM = node => (extension.config.renderHTML as Function)?.bind(context)({
node, node,
HTMLAttributes: mergeAttributes( HTMLAttributes: getRenderedAttributes(node, extensionAttributes),
extension.options.HTMLAttributes,
getRenderedAttributes(node, extensionAttributes),
),
}) })
} }
@@ -83,10 +79,7 @@ export default function getSchema(extensions: Extensions): Schema {
if (extension.config.renderHTML) { if (extension.config.renderHTML) {
schema.toDOM = mark => (extension.config.renderHTML as Function)?.bind(context)({ schema.toDOM = mark => (extension.config.renderHTML as Function)?.bind(context)({
mark, mark,
HTMLAttributes: mergeAttributes( HTMLAttributes: getRenderedAttributes(mark, extensionAttributes),
extension.options.HTMLAttributes,
getRenderedAttributes(mark, extensionAttributes),
),
}) })
} }

View File

@@ -1,4 +1,4 @@
import { Command, Node } from '@tiptap/core' import { Command, Node, mergeAttributes } from '@tiptap/core'
import { wrappingInputRule } from 'prosemirror-inputrules' import { wrappingInputRule } from 'prosemirror-inputrules'
export interface BlockquoteOptions { export interface BlockquoteOptions {
@@ -29,7 +29,7 @@ const Blockquote = Node.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['blockquote', HTMLAttributes, 0] return ['blockquote', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -3,6 +3,7 @@ import {
Mark, Mark,
markInputRule, markInputRule,
markPasteRule, markPasteRule,
mergeAttributes,
} from '@tiptap/core' } from '@tiptap/core'
export interface BoldOptions { export interface BoldOptions {
@@ -40,7 +41,7 @@ const Bold = Mark.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['strong', HTMLAttributes, 0] return ['strong', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -1,4 +1,4 @@
import { Command, Node } from '@tiptap/core' import { Command, Node, mergeAttributes } from '@tiptap/core'
import { wrappingInputRule } from 'prosemirror-inputrules' import { wrappingInputRule } from 'prosemirror-inputrules'
export interface BulletListOptions { export interface BulletListOptions {
@@ -27,7 +27,7 @@ const BulletList = Node.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['ul', HTMLAttributes, 0] return ['ul', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -69,7 +69,7 @@ const CodeBlock = Node.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['pre', ['code', HTMLAttributes, 0]] return ['pre', this.options.HTMLAttributes, ['code', HTMLAttributes, 0]]
}, },
addCommands() { addCommands() {

View File

@@ -3,6 +3,7 @@ import {
Mark, Mark,
markInputRule, markInputRule,
markPasteRule, markPasteRule,
mergeAttributes,
} from '@tiptap/core' } from '@tiptap/core'
export interface CodeOptions { export interface CodeOptions {
@@ -30,7 +31,7 @@ const Code = Mark.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['code', HTMLAttributes, 0] return ['code', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -1,6 +1,9 @@
import { Extension } from '@tiptap/core' import { Extension } from '@tiptap/core'
import { import {
redo, undo, ySyncPlugin, yUndoPlugin, redo,
undo,
ySyncPlugin,
yUndoPlugin,
} from 'y-prosemirror' } from 'y-prosemirror'
export interface CollaborationOptions { export interface CollaborationOptions {

View File

@@ -1,9 +1,20 @@
import { Command, Node } from '@tiptap/core' import { Command, Node, mergeAttributes } from '@tiptap/core'
import { exitCode } from 'prosemirror-commands' import { exitCode } from 'prosemirror-commands'
export interface HardBreakOptions {
HTMLAttributes: {
[key: string]: any
},
}
const HardBreak = Node.create({ const HardBreak = Node.create({
name: 'hardBreak', name: 'hardBreak',
defaultOptions: <HardBreakOptions>{
languageClassPrefix: 'language-',
HTMLAttributes: {},
},
inline: true, inline: true,
group: 'inline', group: 'inline',
@@ -17,7 +28,7 @@ const HardBreak = Node.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['br', HTMLAttributes] return ['br', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
}, },
addCommands() { addCommands() {

View File

@@ -1,4 +1,4 @@
import { Command, Node } from '@tiptap/core' import { Command, Node, mergeAttributes } from '@tiptap/core'
import { textblockTypeInputRule } from 'prosemirror-inputrules' import { textblockTypeInputRule } from 'prosemirror-inputrules'
type Level = 1 | 2 | 3 | 4 | 5 | 6 type Level = 1 | 2 | 3 | 4 | 5 | 6
@@ -47,7 +47,7 @@ const Heading = Node.create({
? node.attrs.level ? node.attrs.level
: this.options.levels[0] : this.options.levels[0]
return [`h${level}`, HTMLAttributes, 0] return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -3,6 +3,7 @@ import {
Mark, Mark,
markInputRule, markInputRule,
markPasteRule, markPasteRule,
mergeAttributes,
} from '@tiptap/core' } from '@tiptap/core'
export interface HighlightOptions { export interface HighlightOptions {
@@ -53,7 +54,7 @@ const Highlight = Mark.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['mark', HTMLAttributes, 0] return ['mark', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -1,4 +1,9 @@
import { Command, Node, nodeInputRule } from '@tiptap/core' import {
Command,
Node,
nodeInputRule,
mergeAttributes,
} from '@tiptap/core'
export interface HorizontalRuleOptions { export interface HorizontalRuleOptions {
HTMLAttributes: { HTMLAttributes: {
@@ -22,7 +27,7 @@ const HorizontalRule = Node.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['hr', HTMLAttributes] return ['hr', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
}, },
addCommands() { addCommands() {

View File

@@ -1,4 +1,9 @@
import { Command, Node, nodeInputRule } from '@tiptap/core' import {
Command,
Node,
nodeInputRule,
mergeAttributes,
} from '@tiptap/core'
export interface ImageOptions { export interface ImageOptions {
inline: boolean, inline: boolean,
@@ -50,7 +55,7 @@ const Image = Node.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['img', HTMLAttributes] return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
}, },
addCommands() { addCommands() {

View File

@@ -3,6 +3,7 @@ import {
Mark, Mark,
markInputRule, markInputRule,
markPasteRule, markPasteRule,
mergeAttributes,
} from '@tiptap/core' } from '@tiptap/core'
export interface ItalicOptions { export interface ItalicOptions {
@@ -39,7 +40,7 @@ const Italic = Mark.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['em', HTMLAttributes, 0] return ['em', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -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' import { Plugin, PluginKey } from 'prosemirror-state'
export interface LinkOptions { 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({ const Link = Mark.create({
name: 'link', name: 'link',
@@ -41,7 +47,7 @@ const Link = Mark.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['a', HTMLAttributes, 0] return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {
@@ -70,6 +76,7 @@ const Link = Mark.create({
addPasteRules() { addPasteRules() {
return [ return [
markPasteRule(pasteRegex, this.type, (url: string) => ({ href: url })), markPasteRule(pasteRegex, this.type, (url: string) => ({ href: url })),
markPasteRule(pasteRegexWithBrackets, this.type, (url: string) => ({ href: url })),
] ]
}, },

View File

@@ -1,4 +1,4 @@
import { Node } from '@tiptap/core' import { Node, mergeAttributes } from '@tiptap/core'
export interface ListItemOptions { export interface ListItemOptions {
HTMLAttributes: { HTMLAttributes: {
@@ -26,7 +26,7 @@ const ListItem = Node.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['li', HTMLAttributes, 0] return ['li', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addKeyboardShortcuts() { addKeyboardShortcuts() {

View File

@@ -1,4 +1,4 @@
import { Command, Node } from '@tiptap/core' import { Command, Node, mergeAttributes } from '@tiptap/core'
import { wrappingInputRule } from 'prosemirror-inputrules' import { wrappingInputRule } from 'prosemirror-inputrules'
export interface OrderedListOptions { export interface OrderedListOptions {
@@ -45,8 +45,8 @@ const OrderedList = Node.create({
const { start, ...attributesWithoutStart } = HTMLAttributes const { start, ...attributesWithoutStart } = HTMLAttributes
return start === 1 return start === 1
? ['ol', attributesWithoutStart, 0] ? ['ol', mergeAttributes(this.options.HTMLAttributes, attributesWithoutStart), 0]
: ['ol', HTMLAttributes, 0] : ['ol', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -1,4 +1,4 @@
import { Command, Node } from '@tiptap/core' import { Command, Node, mergeAttributes } from '@tiptap/core'
export interface ParagraphOptions { export interface ParagraphOptions {
HTMLAttributes: { HTMLAttributes: {
@@ -24,7 +24,7 @@ const Paragraph = Node.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['p', HTMLAttributes, 0] return ['p', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -3,6 +3,7 @@ import {
Mark, Mark,
markInputRule, markInputRule,
markPasteRule, markPasteRule,
mergeAttributes,
} from '@tiptap/core' } from '@tiptap/core'
export interface StrikeOptions { export interface StrikeOptions {
@@ -39,7 +40,7 @@ const Strike = Mark.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['s', HTMLAttributes, 0] return ['s', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -48,7 +48,11 @@ const TaskItem = Node.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['li', mergeAttributes(HTMLAttributes, { 'data-type': 'taskItem' }), 0] return ['li', mergeAttributes(
this.options.HTMLAttributes,
HTMLAttributes,
{ 'data-type': 'taskItem' },
), 0]
}, },
addKeyboardShortcuts() { addKeyboardShortcuts() {

View File

@@ -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({ const TextStyle = Mark.create({
name: 'textStyle', name: 'textStyle',
defaultOptions: <TextStyleOptions>{
HTMLAttributes: {},
},
parseHTML() { parseHTML() {
return [ return [
{ {
@@ -21,7 +36,7 @@ const TextStyle = Mark.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['span', HTMLAttributes, 0] return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -1,4 +1,4 @@
import { Command, Mark } from '@tiptap/core' import { Command, Mark, mergeAttributes } from '@tiptap/core'
export interface UnderlineOptions { export interface UnderlineOptions {
HTMLAttributes: { HTMLAttributes: {
@@ -25,7 +25,7 @@ const Underline = Mark.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['u', HTMLAttributes, 0] return ['u', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {

View File

@@ -2,10 +2,51 @@
import { pasteRegex } from '@tiptap/extension-link' 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', () => { validUrls.forEach(url => {
expect('https://www.example.com/with-spaces?var=true&foo=bar+3').to.match(pasteRegex) 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 doesnt match url: ${url}`, {
// every second test fails, but the second try succeeds
retries: {
runMode: 2,
openMode: 2,
},
}, () => {
expect(url).to.not.match(pasteRegex)
})
})
}) })