From 4bf1e34a40144a31072073b027f259c60289d28a Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 5 Aug 2024 17:58:36 +0200 Subject: [PATCH] feat(core): add ignoreWhitespace option to isNodeEmpty --- .changeset/hungry-poems-bake.md | 5 ++ packages/core/src/helpers/isNodeEmpty.ts | 37 +++++++--- .../integration/core/isNodeEmpty.spec.ts | 69 ++++++++++++++----- 3 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 .changeset/hungry-poems-bake.md diff --git a/.changeset/hungry-poems-bake.md b/.changeset/hungry-poems-bake.md new file mode 100644 index 00000000000..b3f05642134 --- /dev/null +++ b/.changeset/hungry-poems-bake.md @@ -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 diff --git a/packages/core/src/helpers/isNodeEmpty.ts b/packages/core/src/helpers/isNodeEmpty.ts index 90d94f6eea9..8d9a764006b 100644 --- a/packages/core/src/helpers/isNodeEmpty.ts +++ b/packages/core/src/helpers/isNodeEmpty.ts @@ -1,13 +1,34 @@ import { Node as ProseMirrorNode } from '@tiptap/pm/model' /** - * Returns true if the given node is empty. - * When `checkChildren` is true (default), it will also check if all children are empty. + * Returns true if the given prosemirror node is empty. */ export function isNodeEmpty( 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 { + 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) { return !node.text } @@ -21,20 +42,20 @@ export function isNodeEmpty( } if (checkChildren) { - let hasSameContent = true + let isContentEmpty = true node.content.forEach(childNode => { - if (hasSameContent === false) { + if (isContentEmpty === false) { // Exit early for perf return } - if (!isNodeEmpty(childNode)) { - hasSameContent = false + if (!isNodeEmpty(childNode, { ignoreWhitespace, checkChildren })) { + isContentEmpty = false } }) - return hasSameContent + return isContentEmpty } return false diff --git a/tests/cypress/integration/core/isNodeEmpty.spec.ts b/tests/cypress/integration/core/isNodeEmpty.spec.ts index 7e1a8e6c809..e4c1dc9462c 100644 --- a/tests/cypress/integration/core/isNodeEmpty.spec.ts +++ b/tests/cypress/integration/core/isNodeEmpty.spec.ts @@ -7,10 +7,49 @@ import Mention from '@tiptap/extension-mention' import StarterKit from '@tiptap/starter-kit' const schema = getSchema([StarterKit, Mention]) -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]) +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('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', () => { it('should return false when text has content', () => { 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', () => { const node = schema.nodeFromJSON({ type: 'paragraph', - content: [{ - type: 'mention', - attrs: { - id: 'Winona Ryder', - label: null, + content: [ + { + type: 'mention', + attrs: { + id: 'Winona Ryder', + label: null, + }, }, - }], + ], }) expect(isNodeEmpty(node)).to.eq(false) @@ -120,9 +161,7 @@ describe('isNodeEmpty', () => { content: [ { type: 'heading', - content: [ - { type: 'text', text: 'Hello world!' }, - ], + content: [{ type: 'text', text: 'Hello world!' }], }, ], }) @@ -137,9 +176,7 @@ describe('isNodeEmpty', () => { { type: 'heading' }, { type: 'paragraph', - content: [ - { type: 'text', text: 'Hello world!' }, - ], + content: [{ type: 'text', text: 'Hello world!' }], }, ], }) @@ -162,9 +199,7 @@ describe('isNodeEmpty', () => { 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 } }, - ], + content: [{ type: 'heading', content: [], attrs: { level: 2 } }], }) expect(isNodeEmpty(node)).to.eq(true)