mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-07 17:43:49 +08:00
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:
parent
763af10c7b
commit
12bb31a099
5
.changeset/lazy-needles-train.md
Normal file
5
.changeset/lazy-needles-train.md
Normal 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
|
@ -1,11 +1,12 @@
|
|||||||
import './styles.scss'
|
import './styles.scss'
|
||||||
|
|
||||||
import { Color } from '@tiptap/extension-color'
|
import { Color } from '@tiptap/extension-color'
|
||||||
|
import { Image } from '@tiptap/extension-image'
|
||||||
import ListItem from '@tiptap/extension-list-item'
|
import ListItem from '@tiptap/extension-list-item'
|
||||||
import TextStyle from '@tiptap/extension-text-style'
|
import TextStyle from '@tiptap/extension-text-style'
|
||||||
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
|
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import React from 'react'
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
const htmlContent = `
|
const htmlContent = `
|
||||||
<h1><a href="https://tiptap.dev/">Tiptap</a></h1>
|
<h1><a href="https://tiptap.dev/">Tiptap</a></h1>
|
||||||
@ -24,6 +25,12 @@ And more lines`
|
|||||||
const MenuBar = () => {
|
const MenuBar = () => {
|
||||||
const { editor } = useCurrentEditor()
|
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) {
|
if (!editor) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -40,12 +47,14 @@ const MenuBar = () => {
|
|||||||
<button data-test-id="text-content" onClick={() => editor.chain().insertContent(textContent).focus().run()}>
|
<button data-test-id="text-content" onClick={() => editor.chain().insertContent(textContent).focus().run()}>
|
||||||
Insert text content
|
Insert text content
|
||||||
</button>
|
</button>
|
||||||
|
<button data-test-id="image-content" onClick={insertImage}>Insert image</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
|
Image,
|
||||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||||
TextStyle.configure({ types: [ListItem.name] }),
|
TextStyle.configure({ types: [ListItem.name] }),
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
|
@ -11,7 +11,7 @@ context('/src/Commands/InsertContent/React/', () => {
|
|||||||
cy.get('button[data-test-id="html-content"]').click()
|
cy.get('button[data-test-id="html-content"]').click()
|
||||||
|
|
||||||
// check if the content html is correct
|
// 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', () => {
|
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
5864
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -71,6 +71,7 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
|
|||||||
}
|
}
|
||||||
|
|
||||||
let content: Fragment | ProseMirrorNode
|
let content: Fragment | ProseMirrorNode
|
||||||
|
const { selection } = editor.state
|
||||||
|
|
||||||
try {
|
try {
|
||||||
content = createNodeFromContent(value, editor.schema, {
|
content = createNodeFromContent(value, editor.schema, {
|
||||||
@ -140,6 +141,13 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
|
|||||||
} else {
|
} else {
|
||||||
newContent = content
|
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)
|
tr.replaceWith(from, to, newContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,21 +52,11 @@ export const HorizontalRule = Node.create<HorizontalRuleOptions>({
|
|||||||
setHorizontalRule:
|
setHorizontalRule:
|
||||||
() => ({ chain, state }) => {
|
() => ({ chain, state }) => {
|
||||||
const { selection } = state
|
const { selection } = state
|
||||||
const { $from: $originFrom, $to: $originTo } = selection
|
const { $to: $originTo } = selection
|
||||||
|
|
||||||
const currentChain = chain()
|
const currentChain = chain()
|
||||||
|
|
||||||
if ($originFrom.parentOffset === 0) {
|
if (isNodeSelection(selection)) {
|
||||||
currentChain.insertContentAt(
|
|
||||||
{
|
|
||||||
from: Math.max($originFrom.pos - 1, 0),
|
|
||||||
to: $originTo.pos,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: this.name,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else if (isNodeSelection(selection)) {
|
|
||||||
currentChain.insertContentAt($originTo.pos, {
|
currentChain.insertContentAt($originTo.pos, {
|
||||||
type: this.name,
|
type: this.name,
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user