Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add insertContent logic to setContent #4895

Merged
merged 9 commits into from
Jun 25, 2024
7 changes: 7 additions & 0 deletions demos/src/Commands/InsertContent/React/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,11 @@ context('/src/Commands/InsertContent/React/', () => {
cy.get('.tiptap').should('contain.html', '<pre><code>foo\nbar</code></pre>')
})
})

it('should keep newlines and tabulators', () => {
cy.get('.tiptap').then(([{ editor }]) => {
editor.commands.insertContent('<p>Hello\n\tworld\n\t\thow\n\t\t\tnice.</p>')
cy.get('.tiptap').should('contain.html', '<p>Hello\n\tworld\n\t\thow\n\t\t\tnice.</p>')
})
})
})
Empty file.
31 changes: 31 additions & 0 deletions demos/src/Commands/SetContent/React/index.jsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
72 changes: 72 additions & 0 deletions demos/src/Commands/SetContent/React/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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 keep 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\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>')
})
})
})
56 changes: 56 additions & 0 deletions demos/src/Commands/SetContent/React/styles.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 0 additions & 5 deletions packages/core/src/commands/insertContentAt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,6 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
},
})

// don’t dispatch an empty fragment because this can lead to strange errors
if (content.toString() === '<>') {
return true
}

Comment on lines -87 to -91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was removed because commands.clearContent relies on setContent to be able to reset the document to the initial schema. This was early exiting and causing a no-op making clearContent not actually clear anything.

This is very old code that wasn't very well documented but after some testing, I'm confident in this not "leading to strange errors". insertContentAt is much more robust than when this was written.

I tested inserting empty content into a node and a block and it essentially resulted in a no-op.

let { from, to } = typeof position === 'number' ? { from: position, to: position } : { from: position.from, to: position.to }

let isOnlyTextContent = true
Expand Down
16 changes: 9 additions & 7 deletions packages/core/src/commands/setContent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ParseOptions } from '@tiptap/pm/model'

import { createDocument } from '../helpers/createDocument.js'
import { Content, RawCommands } from '../types.js'

declare module '@tiptap/core' {
Expand Down Expand Up @@ -35,13 +34,16 @@ declare module '@tiptap/core' {
}
}

export const setContent: RawCommands['setContent'] = (content, emitUpdate = false, parseOptions = {}) => ({ tr, editor, dispatch }) => {
export const setContent: RawCommands['setContent'] = (content, emitUpdate = false, parseOptions = {}) => ({
tr, commands,
}) => {
const { doc } = tr
const document = createDocument(content, editor.schema, parseOptions)

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 },
)
}