mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-01-06 02:17:49 +08:00
fix emptyTextBlock detection to handle leaf nodes too (#5838)
* fix: #4327 * merge forked PR --------- Co-authored-by: Tony Hallett <tonyhallett74@gmail.com>
This commit is contained in:
parent
ca6269e928
commit
d9b6ef5ce2
5
.changeset/five-lobsters-sing.md
Normal file
5
.changeset/five-lobsters-sing.md
Normal file
@ -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
demos/src/Examples/Issue4327/React/foo.ts
Normal file
26
demos/src/Examples/Issue4327/React/foo.ts
Normal file
@ -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'
|
||||||
|
},
|
||||||
|
})
|
0
demos/src/Examples/Issue4327/React/index.html
Normal file
0
demos/src/Examples/Issue4327/React/index.html
Normal file
27
demos/src/Examples/Issue4327/React/index.tsx
Normal file
27
demos/src/Examples/Issue4327/React/index.tsx
Normal file
@ -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
demos/src/Examples/Issue4327/React/styles.scss
Normal file
10
demos/src/Examples/Issue4327/React/styles.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.tiptap {
|
||||||
|
> * + * {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
26
demos/src/Examples/Issue4327/foo.ts
Normal file
26
demos/src/Examples/Issue4327/foo.ts
Normal file
@ -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'
|
||||||
|
},
|
||||||
|
})
|
@ -1,13 +1,50 @@
|
|||||||
import './styles.scss'
|
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 StarterKit from '@tiptap/starter-kit'
|
||||||
import React, { useEffect } from 'react'
|
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 () => {
|
export default () => {
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit,
|
||||||
|
Foo,
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<p>
|
<p>
|
||||||
@ -32,9 +69,10 @@ export default () => {
|
|||||||
<input type="checkbox" checked={isEditable} onChange={() => setIsEditable(!isEditable)} />
|
<input type="checkbox" checked={isEditable} onChange={() => setIsEditable(!isEditable)} />
|
||||||
Editable
|
Editable
|
||||||
</label>
|
</label>
|
||||||
|
<button data-testid="insert-foo" onClick={() => editor.chain().insertFoo().focus().run()}>Insert Foo</button>
|
||||||
</div>
|
</div>
|
||||||
{editor && <FloatingMenu editor={editor} tippyOptions={{ duration: 100 }}>
|
{editor && <FloatingMenu editor={editor} tippyOptions={{ duration: 100 }}>
|
||||||
<div className="floating-menu">
|
<div data-testid="floating-menu" className="floating-menu">
|
||||||
<button
|
<button
|
||||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||||
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
|
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
|
||||||
|
35
demos/src/Extensions/FloatingMenu/React/index.spec.js
Normal file
35
demos/src/Extensions/FloatingMenu/React/index.spec.js
Normal file
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -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 { EditorState, Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
import { EditorView } from '@tiptap/pm/view'
|
import { EditorView } from '@tiptap/pm/view'
|
||||||
import tippy, { Instance, Props } from 'tippy.js'
|
import tippy, { Instance, Props } from 'tippy.js'
|
||||||
@ -64,11 +67,16 @@ export class FloatingMenuView {
|
|||||||
|
|
||||||
public tippyOptions?: Partial<Props>
|
public tippyOptions?: Partial<Props>
|
||||||
|
|
||||||
|
private getTextContent(node:ProseMirrorNode) {
|
||||||
|
return getText(node, { textSerializers: getTextSerializersFromSchema(this.editor.schema) })
|
||||||
|
}
|
||||||
|
|
||||||
public shouldShow: Exclude<FloatingMenuPluginProps['shouldShow'], null> = ({ view, state }) => {
|
public shouldShow: Exclude<FloatingMenuPluginProps['shouldShow'], null> = ({ view, state }) => {
|
||||||
const { selection } = state
|
const { selection } = state
|
||||||
const { $anchor, empty } = selection
|
const { $anchor, empty } = selection
|
||||||
const isRootDepth = $anchor.depth === 1
|
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 (
|
if (
|
||||||
!view.hasFocus()
|
!view.hasFocus()
|
||||||
|
18
tests/cypress/integration/Issue4327/index.spec.ts
Normal file
18
tests/cypress/integration/Issue4327/index.spec.ts
Normal file
@ -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]')
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user