Skip to content

Commit

Permalink
fix emptyTextBlock detection to handle leaf nodes too (#5838)
Browse files Browse the repository at this point in the history
* fix: #4327

* merge forked PR

---------

Co-authored-by: Tony Hallett <[email protected]>
  • Loading branch information
bdbch and tonyhallett authored Nov 30, 2024
1 parent ca6269e commit d9b6ef5
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/five-lobsters-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tiptap/extension-floating-menu": patch
---

Fixed an issue that cause the floating menu empty-node check to not respect leaf nodes that didn't count into a nodes text content
26 changes: 26 additions & 0 deletions demos/src/Examples/Issue4327/React/foo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { mergeAttributes, Node } from '@tiptap/core'

export default Node.create({
name: 'foo',

group: 'inline',

inline: true,

parseHTML() {
return [
{
tag: 'span',
getAttrs: node => (node as HTMLElement).hasAttribute('data-foo') && null,
},
]
},

renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes({ 'data-foo': '', HTMLAttributes }), 'foo']
},

renderText() {
return 'foo'
},
})
Empty file.
27 changes: 27 additions & 0 deletions demos/src/Examples/Issue4327/React/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import './styles.scss'

import { EditorContent, FloatingMenu, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'

import Foo from './foo.js'

export default () => {
const editor = useEditor({
extensions: [
StarterKit, Foo,
],
content: `
<p><span data-foo=''>foo</span></p>
`,
})

return (
<>
{editor && <FloatingMenu editor={editor} tippyOptions={{ duration: 100 }}>
<div>Hello</div>
</FloatingMenu>}
<EditorContent editor={editor} />
</>
)
}
10 changes: 10 additions & 0 deletions demos/src/Examples/Issue4327/React/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.tiptap {
> * + * {
margin-top: 0.75em;
}

ul,
ol {
padding: 0 1rem;
}
}
26 changes: 26 additions & 0 deletions demos/src/Examples/Issue4327/foo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { mergeAttributes, Node } from '@tiptap/core'

export default Node.create({
name: 'foo',

group: 'inline',

inline: true,

parseHTML() {
return [
{
tag: 'span',
getAttrs: node => (node as HTMLElement).hasAttribute('data-foo') && null,
},
]
},

renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes({ 'data-foo': '', HTMLAttributes }), 'foo']
},

renderText() {
return 'foo'
},
})
42 changes: 40 additions & 2 deletions demos/src/Extensions/FloatingMenu/React/index.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
import './styles.scss'

import { EditorContent, FloatingMenu, useEditor } from '@tiptap/react'
import {
EditorContent, FloatingMenu, mergeAttributes,
Node, useEditor,
} from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React, { useEffect } from 'react'

const Foo = Node.create({
name: 'foo',

group: 'inline',

inline: true,

parseHTML() {
return [
{
tag: 'span',
getAttrs: node => node.hasAttribute('data-foo') && null,
},
]
},

renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes({ 'data-foo': '', HTMLAttributes }), 'foo']
},

renderText() {
return 'foo'
},

addCommands() {
return {
insertFoo: () => ({ commands }) => {
return commands.insertContent({ type: this.name })
},
}
},
})

export default () => {
const editor = useEditor({
extensions: [
StarterKit,
Foo,
],
content: `
<p>
Expand All @@ -32,9 +69,10 @@ export default () => {
<input type="checkbox" checked={isEditable} onChange={() => setIsEditable(!isEditable)} />
Editable
</label>
<button data-testid="insert-foo" onClick={() => editor.chain().insertFoo().focus().run()}>Insert Foo</button>
</div>
{editor && <FloatingMenu editor={editor} tippyOptions={{ duration: 100 }}>
<div className="floating-menu">
<div data-testid="floating-menu" className="floating-menu">
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
Expand Down
35 changes: 35 additions & 0 deletions demos/src/Extensions/FloatingMenu/React/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
context('/src/Extensions/FloatingMenu/React/', () => {
before(() => {
cy.visit('/src/Extensions/FloatingMenu/React/')
})

it('should not render a floating menu on non-empty nodes', () => {
cy.get('.tiptap').then(([{ editor }]) => {
editor.chain().setContent('<p>Example Text</p>').focus().run()
const floatingMenu = cy.get('[data-testID="floating-menu"]')

floatingMenu.should('not.exist')
})
})

it('should render a floating menu on empty nodes', () => {
cy.get('.tiptap').then(([{ editor }]) => {
editor.chain().setContent('<p></p>').focus().run()
const floatingMenu = cy.get('[data-testID="floating-menu"]')

floatingMenu.should('exist')
})
})

it('should not render a floating menu when a leaf node is inserted', () => {
cy.get('.tiptap').then(([{ editor }]) => {
editor.chain().setContent('<p></p>').focus().run()

cy.get('[data-testID="insert-foo"]').click()

const floatingMenu = cy.get('[data-testID="floating-menu"]')

floatingMenu.should('not.exist')
})
})
})
12 changes: 10 additions & 2 deletions packages/extension-floating-menu/src/floating-menu-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Editor, posToDOMRect } from '@tiptap/core'
import {
Editor, getText, getTextSerializersFromSchema, posToDOMRect,
} from '@tiptap/core'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state'
import { EditorView } from '@tiptap/pm/view'
import tippy, { Instance, Props } from 'tippy.js'
Expand Down Expand Up @@ -64,11 +67,16 @@ export class FloatingMenuView {

public tippyOptions?: Partial<Props>

private getTextContent(node:ProseMirrorNode) {
return getText(node, { textSerializers: getTextSerializersFromSchema(this.editor.schema) })
}

public shouldShow: Exclude<FloatingMenuPluginProps['shouldShow'], null> = ({ view, state }) => {
const { selection } = state
const { $anchor, empty } = selection
const isRootDepth = $anchor.depth === 1
const isEmptyTextBlock = $anchor.parent.isTextblock && !$anchor.parent.type.spec.code && !$anchor.parent.textContent

const isEmptyTextBlock = $anchor.parent.isTextblock && !$anchor.parent.type.spec.code && !$anchor.parent.textContent && $anchor.parent.childCount === 0 && !this.getTextContent($anchor.parent)

if (
!view.hasFocus()
Expand Down
18 changes: 18 additions & 0 deletions tests/cypress/integration/Issue4327/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// <reference types="cypress" />

import { Editor } from '@tiptap/core'

interface EditorElement extends HTMLElement {
editor: Editor
}
context('/cypress/integration/Issue4327/React/', () => {
before(() => {
cy.visit('/src/Examples/Issue4327/React/')
})
it('should not show menu when node has renderText returning text with length > 0', () => {
cy.get('.tiptap').then(([editorElement]) => {
(editorElement as EditorElement).editor.commands.focus()
}).get('.ProseMirror-focused').get('#app')
.should('not.have.descendants', '[data-tippy-root]')
})
})

0 comments on commit d9b6ef5

Please sign in to comment.