From 144266036110e39cb78f7ef081391d1bc7cf794a Mon Sep 17 00:00:00 2001 From: Dominik Biedebach Date: Thu, 21 Sep 2023 10:37:48 +0200 Subject: [PATCH 1/5] fix(core): fix insertContentAt keeping new lines in html content --- demos/src/Issues/2720/React/index.html | 0 demos/src/Issues/2720/React/index.jsx | 249 ++++++++++++++++++ demos/src/Issues/2720/React/index.spec.js | 143 ++++++++++ demos/src/Issues/2720/React/styles.scss | 56 ++++ packages/core/src/commands/insertContentAt.ts | 1 + .../core/src/helpers/createNodeFromContent.ts | 1 + 6 files changed, 450 insertions(+) create mode 100644 demos/src/Issues/2720/React/index.html create mode 100644 demos/src/Issues/2720/React/index.jsx create mode 100644 demos/src/Issues/2720/React/index.spec.js create mode 100644 demos/src/Issues/2720/React/styles.scss diff --git a/demos/src/Issues/2720/React/index.html b/demos/src/Issues/2720/React/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/demos/src/Issues/2720/React/index.jsx b/demos/src/Issues/2720/React/index.jsx new file mode 100644 index 0000000000..098cfe4a91 --- /dev/null +++ b/demos/src/Issues/2720/React/index.jsx @@ -0,0 +1,249 @@ +import './styles.scss' + +import { Color } from '@tiptap/extension-color' +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' + +const htmlContent = `

Tiptap

+

Hello World

` + +const textContent = `Hello World +This is content with a new line. Is this working? + +Lets see if multiple new lines are inserted correctly + +Test` + +const MenuBar = () => { + const { editor } = useCurrentEditor() + + if (!editor) { + return null + } + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +const extensions = [ + Color.configure({ types: [TextStyle.name, ListItem.name] }), + TextStyle.configure({ types: [ListItem.name] }), + StarterKit.configure({ + bulletList: { + keepMarks: true, + keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help + }, + orderedList: { + keepMarks: true, + keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help + }, + }), +] + +const content = ` +

+ Hi there, +

+

+ this is a basic example of tiptap. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists: +

+ +

+ Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block: +

+
body {
+display: none;
+}
+

+ I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too. +

+
+ Wow, that’s amazing. Good work, boy! 👏 +
+ — Mom +
+` + +export default () => { + return ( + } extensions={extensions} content={content}> + ) +} diff --git a/demos/src/Issues/2720/React/index.spec.js b/demos/src/Issues/2720/React/index.spec.js new file mode 100644 index 0000000000..b23e72c17e --- /dev/null +++ b/demos/src/Issues/2720/React/index.spec.js @@ -0,0 +1,143 @@ +context('/src/Examples/Default/React/', () => { + before(() => { + cy.visit('/src/Examples/Default/React/') + }) + + beforeEach(() => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('

Example Text

') + cy.get('.tiptap').type('{selectall}') + }) + }) + + it('should apply the paragraph style when the keyboard shortcut is pressed', () => { + cy.get('.tiptap h1').should('exist') + cy.get('.tiptap p').should('not.exist') + + cy.get('.tiptap') + .trigger('keydown', { modKey: true, altKey: true, key: '0' }) + .find('p') + .should('contain', 'Example Text') + }) + + const buttonMarks = [ + { label: 'bold', tag: 'strong' }, + { label: 'italic', tag: 'em' }, + { label: 'strike', tag: 's' }, + ] + + buttonMarks.forEach(m => { + it(`should disable ${m.label} when the code tag is enabled for cursor`, () => { + cy.get('.tiptap').type('{selectall}Hello world') + cy.get('button').contains('code').click() + cy.get('button').contains(m.label).should('be.disabled') + }) + + it(`should enable ${m.label} when the code tag is disabled for cursor`, () => { + cy.get('.tiptap').type('{selectall}Hello world') + cy.get('button').contains('code').click() + cy.get('button').contains('code').click() + cy.get('button').contains(m.label).should('not.be.disabled') + }) + + it(`should disable ${m.label} when the code tag is enabled for selection`, () => { + cy.get('.tiptap').type('{selectall}Hello world{selectall}') + cy.get('button').contains('code').click() + cy.get('button').contains(m.label).should('be.disabled') + }) + + it(`should enable ${m.label} when the code tag is disabled for selection`, () => { + cy.get('.tiptap').type('{selectall}Hello world{selectall}') + cy.get('button').contains('code').click() + cy.get('button').contains('code').click() + cy.get('button').contains(m.label).should('not.be.disabled') + }) + + it(`should apply ${m.label} when the button is pressed`, () => { + cy.get('.tiptap').type('{selectall}Hello world') + cy.get('button').contains('paragraph').click() + cy.get('.tiptap').type('{selectall}') + cy.get('button').contains(m.label).click() + cy.get(`.tiptap ${m.tag}`).should('exist').should('have.text', 'Hello world') + }) + }) + + it('should clear marks when the button is pressed', () => { + cy.get('.tiptap').type('{selectall}Hello world') + cy.get('button').contains('paragraph').click() + cy.get('.tiptap').type('{selectall}') + cy.get('button').contains('bold').click() + cy.get('.tiptap strong').should('exist').should('have.text', 'Hello world') + cy.get('button').contains('clear marks').click() + cy.get('.tiptap strong').should('not.exist') + }) + + it('should clear nodes when the button is pressed', () => { + cy.get('.tiptap').type('{selectall}Hello world') + cy.get('button').contains('bullet list').click() + cy.get('.tiptap ul').should('exist').should('have.text', 'Hello world') + cy.get('.tiptap').type('{enter}A second item{enter}A third item{selectall}') + cy.get('button').contains('clear nodes').click() + cy.get('.tiptap ul').should('not.exist') + cy.get('.tiptap p').should('have.length', 3) + }) + + const buttonNodes = [ + { label: 'h1', tag: 'h1' }, + { label: 'h2', tag: 'h2' }, + { label: 'h3', tag: 'h3' }, + { label: 'h4', tag: 'h4' }, + { label: 'h5', tag: 'h5' }, + { label: 'h6', tag: 'h6' }, + { label: 'bullet list', tag: 'ul' }, + { label: 'ordered list', tag: 'ol' }, + { label: 'code block', tag: 'pre code' }, + { label: 'blockquote', tag: 'blockquote' }, + ] + + buttonNodes.forEach(n => { + it(`should set ${n.label} when the button is pressed`, () => { + cy.get('button').contains('paragraph').click() + cy.get('.tiptap').type('{selectall}Hello world{selectall}') + + cy.get('button').contains(n.label).click() + cy.get(`.tiptap ${n.tag}`).should('exist').should('have.text', 'Hello world') + cy.get('button').contains(n.label).click() + cy.get(`.tiptap ${n.tag}`).should('not.exist') + }) + }) + + it('should add a hr when on the same line as a node', () => { + cy.get('.tiptap').type('{rightArrow}') + cy.get('button').contains('horizontal rule').click() + cy.get('.tiptap hr').should('exist') + cy.get('.tiptap h1').should('exist') + }) + + it('should add a hr when on a new line', () => { + cy.get('.tiptap').type('{rightArrow}{enter}') + cy.get('button').contains('horizontal rule').click() + cy.get('.tiptap hr').should('exist') + cy.get('.tiptap h1').should('exist') + }) + + it('should add a br', () => { + cy.get('.tiptap').type('{rightArrow}') + cy.get('button').contains('hard break').click() + cy.get('.tiptap h1 br').should('exist') + }) + + it('should undo', () => { + cy.get('.tiptap').type('{selectall}{backspace}') + cy.get('button').contains('undo').click() + cy.get('.tiptap').should('contain', 'Hello world') + }) + + it('should redo', () => { + cy.get('.tiptap').type('{selectall}{backspace}') + cy.get('button').contains('undo').click() + cy.get('.tiptap').should('contain', 'Hello world') + cy.get('button').contains('redo').click() + cy.get('.tiptap').should('not.contain', 'Hello world') + }) +}) diff --git a/demos/src/Issues/2720/React/styles.scss b/demos/src/Issues/2720/React/styles.scss new file mode 100644 index 0000000000..4d2b2c81ea --- /dev/null +++ b/demos/src/Issues/2720/React/styles.scss @@ -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; + } +} diff --git a/packages/core/src/commands/insertContentAt.ts b/packages/core/src/commands/insertContentAt.ts index 68589c5a22..69f917625c 100644 --- a/packages/core/src/commands/insertContentAt.ts +++ b/packages/core/src/commands/insertContentAt.ts @@ -89,6 +89,7 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value, tr.insertText(value as string, from, to) } } else { + console.log(content) tr.replaceWith(from, to, content) } diff --git a/packages/core/src/helpers/createNodeFromContent.ts b/packages/core/src/helpers/createNodeFromContent.ts index b6a7e74895..2870e2ee14 100644 --- a/packages/core/src/helpers/createNodeFromContent.ts +++ b/packages/core/src/helpers/createNodeFromContent.ts @@ -40,6 +40,7 @@ export function createNodeFromContent( } if (typeof content === 'string') { + content = content.replace('\n', '') // we need to remove new lines since the parser will add breaks const parser = DOMParser.fromSchema(schema) return options.slice From 68b52e0732ee46ff9471292c48794de0371bca16 Mon Sep 17 00:00:00 2001 From: Dominik Biedebach Date: Thu, 21 Sep 2023 10:51:16 +0200 Subject: [PATCH 2/5] test(core): add tests --- demos/src/Issues/2720/React/index.jsx | 8 +- demos/src/Issues/2720/React/index.spec.js | 142 ++-------------------- 2 files changed, 14 insertions(+), 136 deletions(-) diff --git a/demos/src/Issues/2720/React/index.jsx b/demos/src/Issues/2720/React/index.jsx index 098cfe4a91..585d5fe3d7 100644 --- a/demos/src/Issues/2720/React/index.jsx +++ b/demos/src/Issues/2720/React/index.jsx @@ -13,9 +13,7 @@ const htmlContent = `

Tiptap

const textContent = `Hello World This is content with a new line. Is this working? -Lets see if multiple new lines are inserted correctly - -Test` +Lets see if multiple new lines are inserted correctly` const MenuBar = () => { const { editor } = useCurrentEditor() @@ -26,10 +24,10 @@ const MenuBar = () => { return ( <> - - - - - - - - - - - - - - - - - - - - - - - - ) } @@ -200,45 +40,14 @@ const extensions = [ StarterKit.configure({ bulletList: { keepMarks: true, - keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help }, orderedList: { keepMarks: true, - keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help }, }), ] -const content = ` -

- Hi there, -

-

- this is a basic example of tiptap. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists: -

- -

- Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block: -

-
body {
-display: none;
-}
-

- I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too. -

-
- Wow, that’s amazing. Good work, boy! 👏 -
- — Mom -
-` +const content = '' export default () => { return ( From ca6546f26a203beba580d9214e86aafb28c1de15 Mon Sep 17 00:00:00 2001 From: Dominik Biedebach Date: Thu, 21 Sep 2023 12:03:01 +0200 Subject: [PATCH 5/5] fix(core): fix replacement on multiple breaks --- demos/src/Issues/2720/React/index.jsx | 16 +++++++++++----- demos/src/Issues/2720/React/index.spec.js | 6 +++--- .../core/src/helpers/createNodeFromContent.ts | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/demos/src/Issues/2720/React/index.jsx b/demos/src/Issues/2720/React/index.jsx index 8216926fdb..ac574f71d6 100644 --- a/demos/src/Issues/2720/React/index.jsx +++ b/demos/src/Issues/2720/React/index.jsx @@ -7,13 +7,19 @@ import { EditorProvider, useCurrentEditor } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import React from 'react' -const htmlContent = `

Tiptap

-

Hello World

` +const htmlContent = ` +

Tiptap

+

Hello World

+

This is a paragraph
with a break.

+

And this is some additional string content.

+` const textContent = `Hello World This is content with a new line. Is this working? -Lets see if multiple new lines are inserted correctly` +Lets see if multiple new lines are inserted correctly + +And more lines` const MenuBar = () => { const { editor } = useCurrentEditor() @@ -24,10 +30,10 @@ const MenuBar = () => { return ( <> - - diff --git a/demos/src/Issues/2720/React/index.spec.js b/demos/src/Issues/2720/React/index.spec.js index 354963e099..6035fd14a5 100644 --- a/demos/src/Issues/2720/React/index.spec.js +++ b/demos/src/Issues/2720/React/index.spec.js @@ -8,14 +8,14 @@ context('/src/Issues/2720/React/', () => { }) it('should insert html content correctly', () => { - cy.get('button[testId="html-content"]').click() + cy.get('button[data-test-id="html-content"]').click() // check if the content html is correct - cy.get('.tiptap').should('contain.html', '

Tiptap

Hello World

') + cy.get('.tiptap').should('contain.html', '

Tiptap

Hello World

This is a paragraph
with a break.

And this is some additional string content.

') }) it('should insert text content correctly', () => { - cy.get('button[testId="text-content"]').click() + cy.get('button[data-test-id="text-content"]').click() // check if the content html is correct cy.get('.tiptap').should('contain.html', 'Hello World\nThis is content with a new line. Is this working?\n\nLets see if multiple new lines are inserted correctly') diff --git a/packages/core/src/helpers/createNodeFromContent.ts b/packages/core/src/helpers/createNodeFromContent.ts index 2870e2ee14..7b161d67ec 100644 --- a/packages/core/src/helpers/createNodeFromContent.ts +++ b/packages/core/src/helpers/createNodeFromContent.ts @@ -40,7 +40,7 @@ export function createNodeFromContent( } if (typeof content === 'string') { - content = content.replace('\n', '') // we need to remove new lines since the parser will add breaks + content = content.split('\n').map(part => part.trim()).join('') // we need to remove new lines since the parser will add breaks const parser = DOMParser.fromSchema(schema) return options.slice