Skip to content

Commit

Permalink
fix(core): isNodeEmpty no longer considers attributes for it's checks (
Browse files Browse the repository at this point in the history
  • Loading branch information
nperez0111 authored Jul 25, 2024
1 parent cc3497e commit b012471
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 4 deletions.
6 changes: 6 additions & 0 deletions .changeset/smart-rockets-divide.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions packages/core/src/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
38 changes: 34 additions & 4 deletions packages/core/src/helpers/isNodeEmpty.ts
Original file line number Diff line number Diff line change
@@ -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 (node.content.childCount === 0) {
return true
}

if (!defaultContent) {
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 changes: 183 additions & 0 deletions tests/cypress/integration/core/isNodeEmpty.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})

0 comments on commit b012471

Please sign in to comment.