Skip to content

Commit b012471

Browse files
authoredJul 25, 2024··
fix(core): isNodeEmpty no longer considers attributes for it's checks (#5393)
1 parent cc3497e commit b012471

File tree

4 files changed

+225
-4
lines changed

4 files changed

+225
-4
lines changed
 

‎.changeset/smart-rockets-divide.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@tiptap/core": patch
3+
"@tiptap/extension-placeholder": patch
4+
---
5+
6+
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

‎packages/core/src/Editor.ts

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { isFunction } from './utilities/isFunction.js'
3939

4040
export * as extensions from './extensions/index.js'
4141

42+
// @ts-ignore
4243
export interface TiptapEditorHTMLElement extends HTMLElement {
4344
editor?: Editor
4445
}
@@ -340,6 +341,7 @@ export class Editor extends EventEmitter<EditorEvents> {
340341

341342
// Let’s store the editor instance in the DOM element.
342343
// So we’ll have access to it for tests.
344+
// @ts-ignore
343345
const dom = this.view.dom as TiptapEditorHTMLElement
344346

345347
dom.editor = this
+34-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,41 @@
11
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
22

3-
export function isNodeEmpty(node: ProseMirrorNode): boolean {
4-
const defaultContent = node.type.createAndFill(node.attrs)
3+
/**
4+
* Returns true if the given node is empty.
5+
* When `checkChildren` is true (default), it will also check if all children are empty.
6+
*/
7+
export function isNodeEmpty(
8+
node: ProseMirrorNode,
9+
{ checkChildren }: { checkChildren: boolean } = { checkChildren: true },
10+
): boolean {
11+
if (node.isText) {
12+
return !node.text
13+
}
14+
15+
if (node.content.childCount === 0) {
16+
return true
17+
}
518

6-
if (!defaultContent) {
19+
if (node.isLeaf) {
720
return false
821
}
922

10-
return node.eq(defaultContent)
23+
if (checkChildren) {
24+
let hasSameContent = true
25+
26+
node.content.forEach(childNode => {
27+
if (hasSameContent === false) {
28+
// Exit early for perf
29+
return
30+
}
31+
32+
if (!isNodeEmpty(childNode)) {
33+
hasSameContent = false
34+
}
35+
})
36+
37+
return hasSameContent
38+
}
39+
40+
return false
1141
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/// <reference types="cypress" />
2+
3+
import { getSchema, isNodeEmpty } from '@tiptap/core'
4+
import Document from '@tiptap/extension-document'
5+
import Image from '@tiptap/extension-image'
6+
import StarterKit from '@tiptap/starter-kit'
7+
8+
const schema = getSchema([StarterKit])
9+
const modifiedSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'heading block*' })])
10+
const imageSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'image block*' }), Image])
11+
12+
describe('isNodeEmpty', () => {
13+
describe('with default schema', () => {
14+
it('should return false when text has content', () => {
15+
const node = schema.nodeFromJSON({ type: 'text', text: 'Hello world!' })
16+
17+
expect(isNodeEmpty(node)).to.eq(false)
18+
})
19+
20+
it('should return false when a paragraph has text', () => {
21+
const node = schema.nodeFromJSON({
22+
type: 'paragraph',
23+
content: [{ type: 'text', text: 'Hello world!' }],
24+
})
25+
26+
expect(isNodeEmpty(node)).to.eq(false)
27+
})
28+
29+
it('should return true when a paragraph has no content', () => {
30+
const node = schema.nodeFromJSON({
31+
type: 'paragraph',
32+
content: [],
33+
})
34+
35+
expect(isNodeEmpty(node)).to.eq(true)
36+
})
37+
38+
it('should return true when a paragraph has additional attrs & no content', () => {
39+
const node = schema.nodeFromJSON({
40+
type: 'paragraph',
41+
content: [],
42+
attrs: {
43+
id: 'test',
44+
},
45+
})
46+
47+
expect(isNodeEmpty(node)).to.eq(true)
48+
})
49+
50+
it('should return true when a paragraph has additional marks & no content', () => {
51+
const node = schema.nodeFromJSON({
52+
type: 'paragraph',
53+
content: [],
54+
attrs: {
55+
id: 'test',
56+
},
57+
marks: [{ type: 'bold' }],
58+
})
59+
60+
expect(isNodeEmpty(node)).to.eq(true)
61+
})
62+
63+
it('should return false when a document has text', () => {
64+
const node = schema.nodeFromJSON({
65+
type: 'doc',
66+
content: [
67+
{
68+
type: 'paragraph',
69+
content: [{ type: 'text', text: 'Hello world!' }],
70+
},
71+
],
72+
})
73+
74+
expect(isNodeEmpty(node)).to.eq(false)
75+
})
76+
it('should return true when a document has an empty paragraph', () => {
77+
const node = schema.nodeFromJSON({
78+
type: 'doc',
79+
content: [
80+
{
81+
type: 'paragraph',
82+
content: [],
83+
},
84+
],
85+
})
86+
87+
expect(isNodeEmpty(node)).to.eq(true)
88+
})
89+
})
90+
91+
describe('with modified schema', () => {
92+
it('should return false when a document has a filled heading', () => {
93+
const node = modifiedSchema.nodeFromJSON({
94+
type: 'doc',
95+
content: [
96+
{
97+
type: 'heading',
98+
content: [
99+
{ type: 'text', text: 'Hello world!' },
100+
],
101+
},
102+
],
103+
})
104+
105+
expect(isNodeEmpty(node)).to.eq(false)
106+
})
107+
108+
it('should return false when a document has a filled paragraph', () => {
109+
const node = modifiedSchema.nodeFromJSON({
110+
type: 'doc',
111+
content: [
112+
{ type: 'heading' },
113+
{
114+
type: 'paragraph',
115+
content: [
116+
{ type: 'text', text: 'Hello world!' },
117+
],
118+
},
119+
],
120+
})
121+
122+
expect(isNodeEmpty(node)).to.eq(false)
123+
})
124+
125+
it('should return true when a document has an empty heading', () => {
126+
const node = modifiedSchema.nodeFromJSON({
127+
type: 'doc',
128+
content: [
129+
{ type: 'heading', content: [] },
130+
{ type: 'paragraph', content: [] },
131+
],
132+
})
133+
134+
expect(isNodeEmpty(node)).to.eq(true)
135+
})
136+
137+
it('should return true when a document has an empty heading with attrs', () => {
138+
const node = modifiedSchema.nodeFromJSON({
139+
type: 'doc',
140+
content: [
141+
{ type: 'heading', content: [], attrs: { level: 2 } },
142+
],
143+
})
144+
145+
expect(isNodeEmpty(node)).to.eq(true)
146+
})
147+
148+
it('should return true when a document has an empty heading & paragraph', () => {
149+
const node = modifiedSchema.nodeFromJSON({
150+
type: 'doc',
151+
content: [
152+
{ type: 'heading', content: [] },
153+
{ type: 'paragraph', content: [] },
154+
],
155+
})
156+
157+
expect(isNodeEmpty(node)).to.eq(true)
158+
})
159+
it('should return true when a document has an empty heading & paragraph with attributes', () => {
160+
const node = modifiedSchema.nodeFromJSON({
161+
type: 'doc',
162+
content: [
163+
{ type: 'heading', content: [], attrs: { id: 'test' } },
164+
{ type: 'paragraph', content: [], attrs: { id: 'test' } },
165+
],
166+
})
167+
168+
expect(isNodeEmpty(node)).to.eq(true)
169+
})
170+
171+
it('can handle an image node', () => {
172+
const node = imageSchema.nodeFromJSON({
173+
type: 'doc',
174+
content: [
175+
{ type: 'image', attrs: { src: 'https://examples.com' } },
176+
{ type: 'heading', content: [] },
177+
],
178+
})
179+
180+
expect(isNodeEmpty(node)).to.eq(true)
181+
})
182+
})
183+
})

0 commit comments

Comments
 (0)
Please sign in to comment.