-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(core): isNodeEmpty no longer considers attributes for it's checks (…
- Loading branch information
1 parent
cc3497e
commit b012471
Showing
4 changed files
with
225 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) | ||
}) |