Skip to content

Commit

Permalink
fix: clear nodes when cursor at start of empty isolating parent (#3943)
Browse files Browse the repository at this point in the history
* fix: clear nodes when cursor at start of empty isolating parent

* fix: dont break backspace behavior when childCount is over 1

* fix: check if parent is textblock

* fix: add strict pos check for parent isolating pos

* demo: add isolation clear demo
  • Loading branch information
bdbch authored Apr 5, 2023
1 parent d2c0d04 commit 7278ee2
Show file tree
Hide file tree
Showing 6 changed files with 443 additions and 1 deletion.
Empty file.
216 changes: 216 additions & 0 deletions demos/src/Experiments/IsolatingClear/React/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import './styles.scss'

import { Color } from '@tiptap/extension-color'
import ListItem from '@tiptap/extension-list-item'
import TextStyle from '@tiptap/extension-text-style'
import { EditorContent, Node, useEditor } from '@tiptap/react'
import React from 'react'

import { content } from '../content'

const WrapperBlock = Node.create({
name: 'wrapperBlock',

group: 'block',

isolating: true,

content: 'block*',

parseHTML() {
return [{ tag: 'div[data-wrapper-block]' }]
},

renderHTML({ HTMLAttributes }) {
return ['div', { ...HTMLAttributes, 'data-wrapper-block': true }, 0]
},
})

const MenuBar = ({ editor }) => {
if (!editor) {
return null
}

return (
<>
<button
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleBold()
.run()
}
className={editor.isActive('bold') ? 'is-active' : ''}
>
bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleItalic()
.run()
}
className={editor.isActive('italic') ? 'is-active' : ''}
>
italic
</button>
<button
onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleStrike()
.run()
}
className={editor.isActive('strike') ? 'is-active' : ''}
>
strike
</button>
<button
onClick={() => editor.chain().focus().toggleCode().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleCode()
.run()
}
className={editor.isActive('code') ? 'is-active' : ''}
>
code
</button>
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
clear marks
</button>
<button onClick={() => editor.chain().focus().clearNodes().run()}>
clear nodes
</button>
<button
onClick={() => editor.chain().focus().setParagraph().run()}
className={editor.isActive('paragraph') ? 'is-active' : ''}
>
paragraph
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
>
h1
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
>
h2
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}
>
h3
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''}
>
h4
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''}
>
h5
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''}
>
h6
</button>
<button
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive('bulletList') ? 'is-active' : ''}
>
bullet list
</button>
<button
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={editor.isActive('orderedList') ? 'is-active' : ''}
>
ordered list
</button>
<button
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={editor.isActive('codeBlock') ? 'is-active' : ''}
>
code block
</button>
<button
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={editor.isActive('blockquote') ? 'is-active' : ''}
>
blockquote
</button>
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
horizontal rule
</button>
<button onClick={() => editor.chain().focus().setHardBreak().run()}>
hard break
</button>
<button
onClick={() => editor.chain().focus().undo().run()}
disabled={
!editor.can()
.chain()
.focus()
.undo()
.run()
}
>
undo
</button>
<button
onClick={() => editor.chain().focus().redo().run()}
disabled={
!editor.can()
.chain()
.focus()
.redo()
.run()
}
>
redo
</button>
<button
onClick={() => editor.chain().focus().setColor('#958DF1').run()}
className={editor.isActive('textStyle', { color: '#958DF1' }) ? 'is-active' : ''}
>
purple
</button>
</>
)
}

export default () => {
const editor = useEditor({
extensions: [
WrapperBlock,
Color.configure({ types: [TextStyle.name, ListItem.name] }),
TextStyle.configure({ types: [ListItem.name] }),
],
content,
})

return (
<div>
<MenuBar editor={editor} />
<EditorContent editor={editor} />
</div>
)
}
143 changes: 143 additions & 0 deletions demos/src/Experiments/IsolatingClear/React/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
context('/src/Examples/Default/React/', () => {
before(() => {
cy.visit('/src/Examples/Default/React/')
})

beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<h1>Example Text</h1>')
cy.get('.ProseMirror').type('{selectall}')
})
})

it('should apply the paragraph style when the keyboard shortcut is pressed', () => {
cy.get('.ProseMirror h1').should('exist')
cy.get('.ProseMirror p').should('not.exist')

cy.get('.ProseMirror')
.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('.ProseMirror').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('.ProseMirror').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('.ProseMirror').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('.ProseMirror').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('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click()
cy.get('.ProseMirror').type('{selectall}')
cy.get('button').contains(m.label).click()
cy.get(`.ProseMirror ${m.tag}`).should('exist').should('have.text', 'Hello world')
})
})

it('should clear marks when the button is pressed', () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click()
cy.get('.ProseMirror').type('{selectall}')
cy.get('button').contains('bold').click()
cy.get('.ProseMirror strong').should('exist').should('have.text', 'Hello world')
cy.get('button').contains('clear marks').click()
cy.get('.ProseMirror strong').should('not.exist')
})

it('should clear nodes when the button is pressed', () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('bullet list').click()
cy.get('.ProseMirror ul').should('exist').should('have.text', 'Hello world')
cy.get('.ProseMirror').type('{enter}A second item{enter}A third item{selectall}')
cy.get('button').contains('clear nodes').click()
cy.get('.ProseMirror ul').should('not.exist')
cy.get('.ProseMirror 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('.ProseMirror').type('{selectall}Hello world{selectall}')

cy.get('button').contains(n.label).click()
cy.get(`.ProseMirror ${n.tag}`).should('exist').should('have.text', 'Hello world')
cy.get('button').contains(n.label).click()
cy.get(`.ProseMirror ${n.tag}`).should('not.exist')
})
})

it('should add a hr when on the same line as a node', () => {
cy.get('.ProseMirror').type('{rightArrow}')
cy.get('button').contains('horizontal rule').click()
cy.get('.ProseMirror hr').should('exist')
cy.get('.ProseMirror h1').should('exist')
})

it('should add a hr when on a new line', () => {
cy.get('.ProseMirror').type('{rightArrow}{enter}')
cy.get('button').contains('horizontal rule').click()
cy.get('.ProseMirror hr').should('exist')
cy.get('.ProseMirror h1').should('exist')
})

it('should add a br', () => {
cy.get('.ProseMirror').type('{rightArrow}')
cy.get('button').contains('hard break').click()
cy.get('.ProseMirror h1 br').should('exist')
})

it('should undo', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
cy.get('button').contains('undo').click()
cy.get('.ProseMirror').should('contain', 'Hello world')
})

it('should redo', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
cy.get('button').contains('undo').click()
cy.get('.ProseMirror').should('contain', 'Hello world')
cy.get('button').contains('redo').click()
cy.get('.ProseMirror').should('not.contain', 'Hello world')
})
})
Loading

0 comments on commit 7278ee2

Please sign in to comment.