Fix/insert content at block insertions (#5651)

* fix(core): dont split text nodes when insert block nodes at start of text

* chore: added changeset

* removed duplicated logic from horizontal rule
This commit is contained in:
bdbch 2024-09-26 14:44:49 +02:00 committed by GitHub
parent 763af10c7b
commit 12bb31a099
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1074 additions and 4847 deletions

View File

@ -0,0 +1,5 @@
---
"@tiptap/core": major
---
`insertContent` and `insertContentAt` commands should not split text nodes like paragraphs into multiple nodes when the inserted content is at the beginning of the text to avoid empty nodes being created

View File

@ -1,11 +1,12 @@
import './styles.scss'
import { Color } from '@tiptap/extension-color'
import { Image } from '@tiptap/extension-image'
import ListItem from '@tiptap/extension-list-item'
import TextStyle from '@tiptap/extension-text-style'
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'
import React, { useCallback } from 'react'
const htmlContent = `
<h1><a href="https://tiptap.dev/">Tiptap</a></h1>
@ -24,6 +25,12 @@ And more lines`
const MenuBar = () => {
const { editor } = useCurrentEditor()
const insertImage = useCallback(() => {
const url = prompt('Enter an image URL')
editor.chain().insertContent(`<img src="${url}" alt="Example image" />`).focus().run()
}, [editor])
if (!editor) {
return null
}
@ -40,12 +47,14 @@ const MenuBar = () => {
<button data-test-id="text-content" onClick={() => editor.chain().insertContent(textContent).focus().run()}>
Insert text content
</button>
<button data-test-id="image-content" onClick={insertImage}>Insert image</button>
</div>
</div>
)
}
const extensions = [
Image,
Color.configure({ types: [TextStyle.name, ListItem.name] }),
TextStyle.configure({ types: [ListItem.name] }),
StarterKit.configure({

View File

@ -11,7 +11,7 @@ context('/src/Commands/InsertContent/React/', () => {
cy.get('button[data-test-id="html-content"]').click()
// check if the content html is correct
cy.get('.tiptap').should('contain.html', '<h1>Tiptap</h1><p><strong>Hello World</strong></p><p>This is a paragraph<br>with a break.</p><p>And this is some additional string content.</p>')
cy.get('.tiptap').should('contain.html', '<h1><a target="_blank" rel="noopener noreferrer nofollow" href="https://tiptap.dev/">Tiptap</a></h1><p><strong>Hello World</strong></p><p>This is a paragraph<br>with a break.</p><p>And this is some additional string content.</p>')
})
it('should keep spaces inbetween tags in html content', () => {
@ -91,4 +91,21 @@ context('/src/Commands/InsertContent/React/', () => {
})
})
it('should split content when image is inserted inbetween text', () => {
cy.get('.tiptap').then(([{ editor }]) => {
editor.commands.insertContent('<p>HelloWorld</p>')
editor.commands.setTextSelection(6)
editor.commands.insertContent('<img src="https://example.image/1" alt="This is an example" />')
cy.get('.tiptap').should('contain.html', '<p>Hello</p><img src="https://example.image/1" alt="This is an example" contenteditable="false" draggable="true"><p>World</p>')
})
})
it('should not split content when image is inserted at beginning of text', () => {
cy.get('.tiptap').then(([{ editor }]) => {
editor.commands.insertContent('<p>HelloWorld</p>')
editor.commands.setTextSelection(1)
editor.commands.insertContent('<img src="https://example.image/1" alt="This is an example" />')
cy.get('.tiptap').should('contain.html', '<img src="https://example.image/1" alt="This is an example" contenteditable="false" draggable="true"><p>HelloWorld</p>')
})
})
})

5864
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -71,6 +71,7 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
}
let content: Fragment | ProseMirrorNode
const { selection } = editor.state
try {
content = createNodeFromContent(value, editor.schema, {
@ -140,6 +141,13 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
} else {
newContent = content
const fromSelectionAtStart = selection.$from.parentOffset === 0
const isTextSelection = selection.$from.node().isText || selection.$from.node().isTextblock
if (fromSelectionAtStart && isTextSelection) {
from = Math.max(0, from - 1)
}
tr.replaceWith(from, to, newContent)
}

View File

@ -52,21 +52,11 @@ export const HorizontalRule = Node.create<HorizontalRuleOptions>({
setHorizontalRule:
() => ({ chain, state }) => {
const { selection } = state
const { $from: $originFrom, $to: $originTo } = selection
const { $to: $originTo } = selection
const currentChain = chain()
if ($originFrom.parentOffset === 0) {
currentChain.insertContentAt(
{
from: Math.max($originFrom.pos - 1, 0),
to: $originTo.pos,
},
{
type: this.name,
},
)
} else if (isNodeSelection(selection)) {
if (isNodeSelection(selection)) {
currentChain.insertContentAt($originTo.pos, {
type: this.name,
})