feat(core): add ignoreWhitespace option to isNodeEmpty (#5446)

This commit is contained in:
Nick Perez 2024-08-06 10:05:50 +02:00 committed by GitHub
parent efb27faf54
commit ae0254db97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 86 additions and 25 deletions

View File

@ -0,0 +1,5 @@
---
"@tiptap/core": minor
---
Add `ignoreWhitespace` option to `isNodeEmpty` to ignore any whitespace and hardbreaks in a node to check for emptiness

View File

@ -1,13 +1,34 @@
import { Node as ProseMirrorNode } from '@tiptap/pm/model' import { Node as ProseMirrorNode } from '@tiptap/pm/model'
/** /**
* Returns true if the given node is empty. * Returns true if the given prosemirror node is empty.
* When `checkChildren` is true (default), it will also check if all children are empty.
*/ */
export function isNodeEmpty( export function isNodeEmpty(
node: ProseMirrorNode, node: ProseMirrorNode,
{ checkChildren }: { checkChildren: boolean } = { checkChildren: true }, {
checkChildren = true,
ignoreWhitespace = false,
}: {
/**
* When true (default), it will also check if all children are empty.
*/
checkChildren?: boolean;
/**
* When true, it will ignore whitespace when checking for emptiness.
*/
ignoreWhitespace?: boolean;
} = {},
): boolean { ): boolean {
if (ignoreWhitespace) {
if (node.type.name === 'hardBreak') {
// Hard breaks are considered empty
return true
}
if (node.isText) {
return /^\s*$/m.test(node.text ?? '')
}
}
if (node.isText) { if (node.isText) {
return !node.text return !node.text
} }
@ -21,20 +42,20 @@ export function isNodeEmpty(
} }
if (checkChildren) { if (checkChildren) {
let hasSameContent = true let isContentEmpty = true
node.content.forEach(childNode => { node.content.forEach(childNode => {
if (hasSameContent === false) { if (isContentEmpty === false) {
// Exit early for perf // Exit early for perf
return return
} }
if (!isNodeEmpty(childNode)) { if (!isNodeEmpty(childNode, { ignoreWhitespace, checkChildren })) {
hasSameContent = false isContentEmpty = false
} }
}) })
return hasSameContent return isContentEmpty
} }
return false return false

View File

@ -7,10 +7,49 @@ import Mention from '@tiptap/extension-mention'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
const schema = getSchema([StarterKit, Mention]) const schema = getSchema([StarterKit, Mention])
const modifiedSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'heading block*' })]) const modifiedSchema = getSchema([
const imageSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'image block*' }), Image]) 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('isNodeEmpty', () => {
describe('ignoreWhitespace=true', () => {
it('should return true when text has only whitespace', () => {
const node = schema.nodeFromJSON({ type: 'text', text: ' \n\t\r\n' })
expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true)
})
it('should return true when a paragraph has only whitespace', () => {
const node = schema.nodeFromJSON({
type: 'paragraph',
content: [{ type: 'text', text: ' \n\t\r\n' }],
})
expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true)
})
it('should return true for a hardbreak', () => {
const node = schema.nodeFromJSON({ type: 'hardBreak' })
expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true)
})
it('should return true when a paragraph has only a hardbreak', () => {
const node = schema.nodeFromJSON({
type: 'paragraph',
content: [{ type: 'hardBreak' }],
})
expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true)
})
})
describe('with default schema', () => { describe('with default schema', () => {
it('should return false when text has content', () => { it('should return false when text has content', () => {
const node = schema.nodeFromJSON({ type: 'text', text: 'Hello world!' }) const node = schema.nodeFromJSON({ type: 'text', text: 'Hello world!' })
@ -39,13 +78,15 @@ describe('isNodeEmpty', () => {
it('should return false when a paragraph has a mention', () => { it('should return false when a paragraph has a mention', () => {
const node = schema.nodeFromJSON({ const node = schema.nodeFromJSON({
type: 'paragraph', type: 'paragraph',
content: [{ content: [
type: 'mention', {
attrs: { type: 'mention',
id: 'Winona Ryder', attrs: {
label: null, id: 'Winona Ryder',
label: null,
},
}, },
}], ],
}) })
expect(isNodeEmpty(node)).to.eq(false) expect(isNodeEmpty(node)).to.eq(false)
@ -120,9 +161,7 @@ describe('isNodeEmpty', () => {
content: [ content: [
{ {
type: 'heading', type: 'heading',
content: [ content: [{ type: 'text', text: 'Hello world!' }],
{ type: 'text', text: 'Hello world!' },
],
}, },
], ],
}) })
@ -137,9 +176,7 @@ describe('isNodeEmpty', () => {
{ type: 'heading' }, { type: 'heading' },
{ {
type: 'paragraph', type: 'paragraph',
content: [ content: [{ type: 'text', text: 'Hello world!' }],
{ type: 'text', text: 'Hello world!' },
],
}, },
], ],
}) })
@ -162,9 +199,7 @@ describe('isNodeEmpty', () => {
it('should return true when a document has an empty heading with attrs', () => { it('should return true when a document has an empty heading with attrs', () => {
const node = modifiedSchema.nodeFromJSON({ const node = modifiedSchema.nodeFromJSON({
type: 'doc', type: 'doc',
content: [ content: [{ type: 'heading', content: [], attrs: { level: 2 } }],
{ type: 'heading', content: [], attrs: { level: 2 } },
],
}) })
expect(isNodeEmpty(node)).to.eq(true) expect(isNodeEmpty(node)).to.eq(true)