diff --git a/packages/block-editor/src/components/rich-text/content.js b/packages/block-editor/src/components/rich-text/content.js index dfd206a1ddb7e9..9762582f86f141 100644 --- a/packages/block-editor/src/components/rich-text/content.js +++ b/packages/block-editor/src/components/rich-text/content.js @@ -2,11 +2,7 @@ * WordPress dependencies */ import { RawHTML } from '@wordpress/element'; -import { - children as childrenSource, - getSaveElement, - __unstableGetBlockProps as getBlockProps, -} from '@wordpress/blocks'; +import { children as childrenSource } from '@wordpress/blocks'; import deprecated from '@wordpress/deprecated'; /** @@ -42,44 +38,3 @@ export const Content = ( { value, tagName: Tag, multiline, ...props } ) => { return content; }; - -Content.__unstableIsRichTextContent = {}; - -function findContent( blocks, richTextValues = [] ) { - if ( ! Array.isArray( blocks ) ) { - blocks = [ blocks ]; - } - - for ( const block of blocks ) { - if ( - block?.type?.__unstableIsRichTextContent === - Content.__unstableIsRichTextContent - ) { - richTextValues.push( block.props.value ); - continue; - } - - if ( block?.props?.children ) { - findContent( block.props.children, richTextValues ); - } - } - - return richTextValues; -} - -function _getSaveElement( { name, attributes, innerBlocks } ) { - return getSaveElement( - name, - attributes, - innerBlocks.map( _getSaveElement ) - ); -} - -export function getRichTextValues( blocks = [] ) { - getBlockProps.skipFilters = true; - const values = findContent( - ( Array.isArray( blocks ) ? blocks : [ blocks ] ).map( _getSaveElement ) - ); - getBlockProps.skipFilters = false; - return values; -} diff --git a/packages/block-editor/src/components/rich-text/get-rich-text-values.js b/packages/block-editor/src/components/rich-text/get-rich-text-values.js new file mode 100644 index 00000000000000..4ecee9b76530e5 --- /dev/null +++ b/packages/block-editor/src/components/rich-text/get-rich-text-values.js @@ -0,0 +1,95 @@ +/** + * WordPress dependencies + */ +import { RawHTML, StrictMode, Fragment } from '@wordpress/element'; +import { + getSaveElement, + __unstableGetBlockProps as getBlockProps, +} from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import InnerBlocks from '../inner-blocks'; +import { Content } from './content'; + +/* + * This function is similar to `@wordpress/element`'s `renderToString` function, + * except that it does not render the elements to a string, but instead collects + * the values of all rich text `Content` elements. + */ +function addValuesForElement( element, ...args ) { + if ( null === element || undefined === element || false === element ) { + return; + } + + if ( Array.isArray( element ) ) { + return addValuesForElements( element, ...args ); + } + + switch ( typeof element ) { + case 'string': + case 'number': + return; + } + + const { type, props } = element; + + switch ( type ) { + case StrictMode: + case Fragment: + return addValuesForElements( props.children, ...args ); + case RawHTML: + return; + case InnerBlocks.Content: + return addValuesForBlocks( ...args ); + case Content: + const [ values ] = args; + values.push( props.value ); + return; + } + + switch ( typeof type ) { + case 'string': + if ( typeof props.children !== 'undefined' ) { + return addValuesForElements( props.children, ...args ); + } + return; + case 'function': + if ( + type.prototype && + typeof type.prototype.render === 'function' + ) { + return addValuesForElement( + new type( props ).render(), + ...args + ); + } + + return addValuesForElement( type( props ), ...args ); + } +} + +function addValuesForElements( children, ...args ) { + children = Array.isArray( children ) ? children : [ children ]; + + for ( let i = 0; i < children.length; i++ ) { + addValuesForElement( children[ i ], ...args ); + } +} + +function addValuesForBlocks( values, blocks ) { + for ( let i = 0; i < blocks.length; i++ ) { + const { name, attributes, innerBlocks } = blocks[ i ]; + const saveElement = getSaveElement( name, attributes ); + addValuesForElement( saveElement, values, innerBlocks ); + } +} + +export function getRichTextValues( blocks = [] ) { + getBlockProps.skipFilters = true; + const values = []; + addValuesForBlocks( values, blocks ); + getBlockProps.skipFilters = false; + return values; +} diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 1200dee367d243..453fbd7ce63eb9 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -4,7 +4,7 @@ import * as globalStyles from './components/global-styles'; import { ExperimentalBlockEditorProvider } from './components/provider'; import { lock } from './lock-unlock'; -import { getRichTextValues } from './components/rich-text/content'; +import { getRichTextValues } from './components/rich-text/get-rich-text-values'; import { kebabCase } from './utils/object'; import ResizableBoxPopover from './components/resizable-box-popover'; import { ComposedPrivateInserter as PrivateInserter } from './components/inserter'; diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js index e65eee6d3d08eb..1f2fd33f23f738 100644 --- a/test/e2e/specs/editor/various/footnotes.spec.js +++ b/test/e2e/specs/editor/various/footnotes.spec.js @@ -182,4 +182,100 @@ test.describe( 'Footnotes', () => { expect( await getFootnotes( page ) ).toMatchObject( [] ); } ); + + test( 'can be inserted in a list', async ( { editor, page } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '* 1' ); + await editor.clickBlockToolbarButton( 'More' ); + await page.locator( 'button:text("Footnote")' ).click(); + + await page.keyboard.type( 'a' ); + + const id1 = await editor.canvas.evaluate( () => { + return document.activeElement.id; + } ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/list', + innerBlocks: [ + { + name: 'core/list-item', + attributes: { + content: `1*`, + }, + }, + ], + }, + { + name: 'core/footnotes', + }, + ] ); + + expect( await getFootnotes( page ) ).toMatchObject( [ + { + content: 'a', + id: id1, + }, + ] ); + } ); + + test( 'can be inserted in a table', async ( { editor, page } ) => { + await editor.insertBlock( { name: 'core/table' } ); + await editor.canvas.click( 'role=button[name="Create Table"i]' ); + await page.keyboard.type( '1' ); + await editor.showBlockToolbar(); + await editor.clickBlockToolbarButton( 'More' ); + await page.locator( 'button:text("Footnote")' ).click(); + + await page.keyboard.type( 'a' ); + + const id1 = await editor.canvas.evaluate( () => { + return document.activeElement.id; + } ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/table', + attributes: { + body: [ + { + cells: [ + { + content: `1*`, + tag: 'td', + }, + { + content: '', + tag: 'td', + }, + ], + }, + { + cells: [ + { + content: '', + tag: 'td', + }, + { + content: '', + tag: 'td', + }, + ], + }, + ], + }, + }, + { + name: 'core/footnotes', + }, + ] ); + + expect( await getFootnotes( page ) ).toMatchObject( [ + { + content: 'a', + id: id1, + }, + ] ); + } ); } );