mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-11-27 14:59:27 +08:00
fix(core): isNodeEmpty no longer considers attributes for it's checks (#5393)
This commit is contained in:
parent
cc3497efd5
commit
b012471755
6
.changeset/smart-rockets-divide.md
Normal file
6
.changeset/smart-rockets-divide.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"@tiptap/core": patch
|
||||
"@tiptap/extension-placeholder": patch
|
||||
---
|
||||
|
||||
This addresses an issue with `isNodeEmpty` function where it was also comparing node attributes and finding mismatches on actually empty nodes. This helps placeholders find empty content correctly
|
@ -39,6 +39,7 @@ import { isFunction } from './utilities/isFunction.js'
|
||||
|
||||
export * as extensions from './extensions/index.js'
|
||||
|
||||
// @ts-ignore
|
||||
export interface TiptapEditorHTMLElement extends HTMLElement {
|
||||
editor?: Editor
|
||||
}
|
||||
@ -340,6 +341,7 @@ export class Editor extends EventEmitter<EditorEvents> {
|
||||
|
||||
// Let’s store the editor instance in the DOM element.
|
||||
// So we’ll have access to it for tests.
|
||||
// @ts-ignore
|
||||
const dom = this.view.dom as TiptapEditorHTMLElement
|
||||
|
||||
dom.editor = this
|
||||
|
@ -1,11 +1,41 @@
|
||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
|
||||
export function isNodeEmpty(node: ProseMirrorNode): boolean {
|
||||
const defaultContent = node.type.createAndFill(node.attrs)
|
||||
/**
|
||||
* Returns true if the given node is empty.
|
||||
* When `checkChildren` is true (default), it will also check if all children are empty.
|
||||
*/
|
||||
export function isNodeEmpty(
|
||||
node: ProseMirrorNode,
|
||||
{ checkChildren }: { checkChildren: boolean } = { checkChildren: true },
|
||||
): boolean {
|
||||
if (node.isText) {
|
||||
return !node.text
|
||||
}
|
||||
|
||||
if (!defaultContent) {
|
||||
if (node.content.childCount === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (node.isLeaf) {
|
||||
return false
|
||||
}
|
||||
|
||||
return node.eq(defaultContent)
|
||||
if (checkChildren) {
|
||||
let hasSameContent = true
|
||||
|
||||
node.content.forEach(childNode => {
|
||||
if (hasSameContent === false) {
|
||||
// Exit early for perf
|
||||
return
|
||||
}
|
||||
|
||||
if (!isNodeEmpty(childNode)) {
|
||||
hasSameContent = false
|
||||
}
|
||||
})
|
||||
|
||||
return hasSameContent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
183
tests/cypress/integration/core/isNodeEmpty.spec.ts
Normal file
183
tests/cypress/integration/core/isNodeEmpty.spec.ts
Normal file
@ -0,0 +1,183 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { getSchema, isNodeEmpty } from '@tiptap/core'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Image from '@tiptap/extension-image'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
|
||||
const schema = getSchema([StarterKit])
|
||||
const modifiedSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'heading block*' })])
|
||||
const imageSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'image block*' }), Image])
|
||||
|
||||
describe('isNodeEmpty', () => {
|
||||
describe('with default schema', () => {
|
||||
it('should return false when text has content', () => {
|
||||
const node = schema.nodeFromJSON({ type: 'text', text: 'Hello world!' })
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(false)
|
||||
})
|
||||
|
||||
it('should return false when a paragraph has text', () => {
|
||||
const node = schema.nodeFromJSON({
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: 'Hello world!' }],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(false)
|
||||
})
|
||||
|
||||
it('should return true when a paragraph has no content', () => {
|
||||
const node = schema.nodeFromJSON({
|
||||
type: 'paragraph',
|
||||
content: [],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(true)
|
||||
})
|
||||
|
||||
it('should return true when a paragraph has additional attrs & no content', () => {
|
||||
const node = schema.nodeFromJSON({
|
||||
type: 'paragraph',
|
||||
content: [],
|
||||
attrs: {
|
||||
id: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(true)
|
||||
})
|
||||
|
||||
it('should return true when a paragraph has additional marks & no content', () => {
|
||||
const node = schema.nodeFromJSON({
|
||||
type: 'paragraph',
|
||||
content: [],
|
||||
attrs: {
|
||||
id: 'test',
|
||||
},
|
||||
marks: [{ type: 'bold' }],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(true)
|
||||
})
|
||||
|
||||
it('should return false when a document has text', () => {
|
||||
const node = schema.nodeFromJSON({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: 'Hello world!' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(false)
|
||||
})
|
||||
it('should return true when a document has an empty paragraph', () => {
|
||||
const node = schema.nodeFromJSON({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with modified schema', () => {
|
||||
it('should return false when a document has a filled heading', () => {
|
||||
const node = modifiedSchema.nodeFromJSON({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'heading',
|
||||
content: [
|
||||
{ type: 'text', text: 'Hello world!' },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(false)
|
||||
})
|
||||
|
||||
it('should return false when a document has a filled paragraph', () => {
|
||||
const node = modifiedSchema.nodeFromJSON({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{ type: 'heading' },
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{ type: 'text', text: 'Hello world!' },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(false)
|
||||
})
|
||||
|
||||
it('should return true when a document has an empty heading', () => {
|
||||
const node = modifiedSchema.nodeFromJSON({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{ type: 'heading', content: [] },
|
||||
{ type: 'paragraph', content: [] },
|
||||
],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(true)
|
||||
})
|
||||
|
||||
it('should return true when a document has an empty heading with attrs', () => {
|
||||
const node = modifiedSchema.nodeFromJSON({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{ type: 'heading', content: [], attrs: { level: 2 } },
|
||||
],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(true)
|
||||
})
|
||||
|
||||
it('should return true when a document has an empty heading & paragraph', () => {
|
||||
const node = modifiedSchema.nodeFromJSON({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{ type: 'heading', content: [] },
|
||||
{ type: 'paragraph', content: [] },
|
||||
],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(true)
|
||||
})
|
||||
it('should return true when a document has an empty heading & paragraph with attributes', () => {
|
||||
const node = modifiedSchema.nodeFromJSON({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{ type: 'heading', content: [], attrs: { id: 'test' } },
|
||||
{ type: 'paragraph', content: [], attrs: { id: 'test' } },
|
||||
],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(true)
|
||||
})
|
||||
|
||||
it('can handle an image node', () => {
|
||||
const node = imageSchema.nodeFromJSON({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{ type: 'image', attrs: { src: 'https://examples.com' } },
|
||||
{ type: 'heading', content: [] },
|
||||
],
|
||||
})
|
||||
|
||||
expect(isNodeEmpty(node)).to.eq(true)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user