mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-07 09:25:29 +08:00
feat(core): normalize setContent and insertContentAt (#4895)
When `parseOptions.whitespace === 'full' or parseOptions.whitespace === true` setting content will no longer strip whitespaces on setContent
This commit is contained in:
parent
2198991fda
commit
81d3b8a671
@ -41,4 +41,54 @@ context('/src/Commands/InsertContent/React/', () => {
|
||||
cy.get('.tiptap').should('contain.html', '<pre><code>foo\nbar</code></pre>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep newlines and tabs', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.insertContent('<p>Hello\n\tworld\n\t\thow\n\t\t\tnice.\ntest\tOK</p>')
|
||||
cy.get('.tiptap').should('contain.html', '<p>Hello\n\tworld\n\t\thow\n\t\t\tnice.\ntest\tOK</p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep newlines and tabs', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.insertContent('<h1>Tiptap</h1>\n<p><strong>Hello World</strong></p>')
|
||||
cy.get('.tiptap').should('contain.html', '<h1>Tiptap</h1><p><strong>Hello World</strong></p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow inserting nothing', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.insertContent('')
|
||||
cy.get('.tiptap').should('contain.html', '')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow inserting a partial HTML tag', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.insertContent('<p>foo')
|
||||
cy.get('.tiptap').should('contain.html', '<p>foo</p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow inserting an incomplete HTML tag', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.insertContent('foo<p')
|
||||
cy.get('.tiptap').should('contain.html', '<p>foo<p</p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow inserting a list', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.insertContent('<ul><li>ABC</li><li>123</li></ul>')
|
||||
cy.get('.tiptap').should('contain.html', '<ul><li><p>ABC</p></li><li><p>123</p></li></ul>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove newlines and tabs when parseOptions.preserveWhitespace=false', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.insertContent('\n<h1>Tiptap</h1><p><strong>Hello\n World</strong>\n</p>\n', { parseOptions: { preserveWhitespace: false } })
|
||||
cy.get('.tiptap').should('contain.html', '<h1>Tiptap</h1><p><strong>Hello World</strong></p>')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
0
demos/src/Commands/SetContent/React/index.html
Normal file
0
demos/src/Commands/SetContent/React/index.html
Normal file
31
demos/src/Commands/SetContent/React/index.jsx
Normal file
31
demos/src/Commands/SetContent/React/index.jsx
Normal file
@ -0,0 +1,31 @@
|
||||
import './styles.scss'
|
||||
|
||||
import { Color } from '@tiptap/extension-color'
|
||||
import ListItem from '@tiptap/extension-list-item'
|
||||
import Mentions from '@tiptap/extension-mention'
|
||||
import TextStyle from '@tiptap/extension-text-style'
|
||||
import { EditorProvider } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React from 'react'
|
||||
|
||||
const extensions = [
|
||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||
TextStyle.configure({ types: [ListItem.name] }),
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
keepMarks: true,
|
||||
},
|
||||
orderedList: {
|
||||
keepMarks: true,
|
||||
},
|
||||
}),
|
||||
Mentions,
|
||||
]
|
||||
|
||||
const content = ''
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<EditorProvider extensions={extensions} content={content}></EditorProvider>
|
||||
)
|
||||
}
|
160
demos/src/Commands/SetContent/React/index.spec.js
Normal file
160
demos/src/Commands/SetContent/React/index.spec.js
Normal file
@ -0,0 +1,160 @@
|
||||
context('/src/Commands/SetContent/React/', () => {
|
||||
before(() => {
|
||||
cy.visit('/src/Commands/SetContent/React/')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.get('.tiptap').type('{selectall}{backspace}')
|
||||
})
|
||||
|
||||
it('should insert raw text content', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('Hello World.')
|
||||
cy.get('.tiptap').should('contain.html', '<p>Hello World.</p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should emit updates', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
let updateCount = 0
|
||||
const callback = () => {
|
||||
updateCount += 1
|
||||
}
|
||||
|
||||
editor.on('update', callback)
|
||||
// emit an update
|
||||
editor.commands.setContent('Hello World.', true)
|
||||
expect(updateCount).to.equal(1)
|
||||
|
||||
updateCount = 0
|
||||
// do not emit an update
|
||||
editor.commands.setContent('Hello World again.', false)
|
||||
expect(updateCount).to.equal(0)
|
||||
editor.off('update', callback)
|
||||
})
|
||||
})
|
||||
|
||||
it('should insert more complex html content', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<h1>Welcome to Tiptap</h1><p>This is a paragraph.</p><ul><li><p>List Item A</p></li><li><p>List Item B</p><ul><li><p>Subchild</p></li></ul></li></ul>')
|
||||
cy.get('.tiptap').should('contain.html', '<h1>Welcome to Tiptap</h1><p>This is a paragraph.</p><ul><li><p>List Item A</p></li><li><p>List Item B</p><ul><li><p>Subchild</p></li></ul></li></ul>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove newlines and tabs', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<p>Hello\n\tworld\n\t\thow\n\t\t\tnice.</p>')
|
||||
cy.get('.tiptap').should('contain.html', '<p>Hello world how nice.</p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep newlines and tabs when preserveWhitespace = full', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<p>Hello\n\tworld\n\t\thow\n\t\t\tnice.</p>', false, { preserveWhitespace: 'full' })
|
||||
cy.get('.tiptap').should('contain.html', '<p>Hello\n\tworld\n\t\thow\n\t\t\tnice.</p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should overwrite existing content', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<p>Initial Content</p>')
|
||||
cy.get('.tiptap').should('contain.html', '<p>Initial Content</p>')
|
||||
})
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<p>Overwritten Content</p>')
|
||||
cy.get('.tiptap').should('contain.html', '<p>Overwritten Content</p>')
|
||||
})
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('Content without tags')
|
||||
cy.get('.tiptap').should('contain.html', '<p>Content without tags</p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should insert mentions', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<p><span data-type="mention" data-id="1" data-label="John Doe">@John Doe</span></p>')
|
||||
cy.get('.tiptap').should('contain.html', '<span data-type="mention" data-id="1" data-label="John Doe" contenteditable="false">@John Doe</span>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove newlines and tabs between html fragments', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<h1>Tiptap</h1>\n\t<p><strong>Hello World</strong></p>')
|
||||
cy.get('.tiptap').should('contain.html', '<h1>Tiptap</h1><p><strong>Hello World</strong></p>')
|
||||
})
|
||||
})
|
||||
|
||||
// TODO I'm not certain about this behavior and what it should do...
|
||||
// This exists in insertContentAt as well
|
||||
it('should keep newlines and tabs between html fragments when preserveWhitespace = full', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<h1>Tiptap</h1>\n\t<p><strong>Hello World</strong></p>', false, { preserveWhitespace: 'full' })
|
||||
cy.get('.tiptap').should('contain.html', '<h1>Tiptap</h1><p>\n\t</p><p><strong>Hello World</strong></p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow inserting nothing', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('')
|
||||
cy.get('.tiptap').should('contain.html', '')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow inserting nothing when preserveWhitespace = full', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('', false, { preserveWhitespace: 'full' })
|
||||
cy.get('.tiptap').should('contain.html', '')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow inserting a partial HTML tag', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<p>foo')
|
||||
cy.get('.tiptap').should('contain.html', '<p>foo</p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow inserting a partial HTML tag when preserveWhitespace = full', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<p>foo', false, { preserveWhitespace: 'full' })
|
||||
cy.get('.tiptap').should('contain.html', '<p>foo</p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('will remove an incomplete HTML tag', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('foo<p')
|
||||
cy.get('.tiptap').should('contain.html', '<p>foo</p>')
|
||||
})
|
||||
})
|
||||
|
||||
// TODO I'm not certain about this behavior and what it should do...
|
||||
// This exists in insertContentAt as well
|
||||
it('should allow inserting an incomplete HTML tag when preserveWhitespace = full', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('foo<p', false, { preserveWhitespace: 'full' })
|
||||
cy.get('.tiptap').should('contain.html', '<p>foo<p</p>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow inserting a list', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<ul><li>ABC</li><li>123</li></ul>')
|
||||
cy.get('.tiptap').should('contain.html', '<ul><li><p>ABC</p></li><li><p>123</p></li></ul>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow inserting a list when preserveWhitespace = full', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('<ul><li>ABC</li><li>123</li></ul>', false, { preserveWhitespace: 'full' })
|
||||
cy.get('.tiptap').should('contain.html', '<ul><li><p>ABC</p></li><li><p>123</p></li></ul>')
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove newlines and tabs when parseOptions.preserveWhitespace=false', () => {
|
||||
cy.get('.tiptap').then(([{ editor }]) => {
|
||||
editor.commands.setContent('\n<h1>Tiptap</h1><p><strong>Hello\n World</strong>\n</p>\n', false, { preserveWhitespace: false })
|
||||
cy.get('.tiptap').should('contain.html', '<h1>Tiptap</h1><p><strong>Hello World</strong></p>')
|
||||
})
|
||||
})
|
||||
})
|
56
demos/src/Commands/SetContent/React/styles.scss
Normal file
56
demos/src/Commands/SetContent/React/styles.scss
Normal file
@ -0,0 +1,56 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
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;
|
||||
padding: 0;
|
||||
background: none;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid rgba(#0D0D0D, 0.1);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid rgba(#0D0D0D, 0.1);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
@ -84,11 +84,6 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
|
||||
return false
|
||||
}
|
||||
|
||||
// don’t dispatch an empty fragment because this can lead to strange errors
|
||||
if (content.toString() === '<>') {
|
||||
return true
|
||||
}
|
||||
|
||||
let { from, to } = typeof position === 'number' ? { from: position, to: position } : { from: position.from, to: position.to }
|
||||
|
||||
let isOnlyTextContent = true
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm/model'
|
||||
import { ParseOptions } from '@tiptap/pm/model'
|
||||
|
||||
import { createDocument } from '../helpers/createDocument.js'
|
||||
import { Content, RawCommands } from '../types.js'
|
||||
@ -44,22 +44,34 @@ declare module '@tiptap/core' {
|
||||
}
|
||||
}
|
||||
|
||||
export const setContent: RawCommands['setContent'] = (content, emitUpdate = false, parseOptions = {}, options = {}) => ({ tr, editor, dispatch }) => {
|
||||
export const setContent: RawCommands['setContent'] = (content, emitUpdate = false, parseOptions = {}, options = {}) => ({
|
||||
editor, tr, dispatch, commands,
|
||||
}) => {
|
||||
const { doc } = tr
|
||||
|
||||
let document: Fragment | ProseMirrorNode
|
||||
|
||||
try {
|
||||
document = createDocument(content, editor.schema, parseOptions, {
|
||||
// This is to keep backward compatibility with the previous behavior
|
||||
// TODO remove this in the next major version
|
||||
if (parseOptions.preserveWhitespace !== 'full') {
|
||||
const document = createDocument(content, editor.schema, parseOptions, {
|
||||
errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck,
|
||||
})
|
||||
} catch (e) {
|
||||
return false
|
||||
|
||||
if (dispatch) {
|
||||
tr.replaceWith(0, doc.content.size, document).setMeta('preventUpdate', !emitUpdate)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
tr.replaceWith(0, doc.content.size, document).setMeta('preventUpdate', !emitUpdate)
|
||||
tr.setMeta('preventUpdate', !emitUpdate)
|
||||
}
|
||||
|
||||
return true
|
||||
return commands.insertContentAt(
|
||||
{ from: 0, to: doc.content.size },
|
||||
content,
|
||||
{
|
||||
parseOptions,
|
||||
errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user