From 6904c703bca7750bf128ae66ba93836acd7a3b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Mon, 3 Jul 2023 16:19:21 +0300 Subject: [PATCH 1/7] RichText/Footnotes: make getRichTextValues work with InnerBlocks.Content --- .../src/components/rich-text/content.js | 99 ++++++++++++++----- 1 file changed, 74 insertions(+), 25 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/content.js b/packages/block-editor/src/components/rich-text/content.js index dfd206a1ddb7e..1de8ef5d7d76a 100644 --- a/packages/block-editor/src/components/rich-text/content.js +++ b/packages/block-editor/src/components/rich-text/content.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { RawHTML } from '@wordpress/element'; +import { RawHTML, StrictMode, Fragment } from '@wordpress/element'; import { children as childrenSource, getSaveElement, @@ -45,41 +45,90 @@ export const Content = ( { value, tagName: Tag, multiline, ...props } ) => { Content.__unstableIsRichTextContent = {}; -function findContent( blocks, richTextValues = [] ) { - if ( ! Array.isArray( blocks ) ) { - blocks = [ blocks ]; - } +function _getSaveElement( { name, attributes, innerBlocks } ) { + return getSaveElement( + name, + attributes, + innerBlocks.map( _getSaveElement ) + ); +} - for ( const block of blocks ) { - if ( - block?.type?.__unstableIsRichTextContent === - Content.__unstableIsRichTextContent - ) { - richTextValues.push( block.props.value ); - continue; - } +function renderChildren( children ) { + const values = []; + children = Array.isArray( children ) ? children : [ children ]; - if ( block?.props?.children ) { - findContent( block.props.children, richTextValues ); + for ( let i = 0; i < children.length; i++ ) { + const child = children[ i ]; + const value = renderElement( child ); + if ( value ) { + if ( Array.isArray( value ) ) { + values.push( ...value ); + } else { + values.push( value ); + } } } - return richTextValues; + return values; } -function _getSaveElement( { name, attributes, innerBlocks } ) { - return getSaveElement( - name, - attributes, - innerBlocks.map( _getSaveElement ) - ); +function renderComponent( Component, props ) { + return renderElement( new Component( props ).render() ); +} + +function renderElement( element ) { + if ( null === element || undefined === element || false === element ) { + return; + } + + if ( Array.isArray( element ) ) { + return renderChildren( element ); + } + + switch ( typeof element ) { + case 'string': + case 'number': + return; + } + + const { type, props } = element; + + if ( + type.__unstableIsRichTextContent === Content.__unstableIsRichTextContent + ) { + return props.value; + } + + switch ( type ) { + case StrictMode: + case Fragment: + return renderChildren( props.children ); + case RawHTML: + return; + } + + switch ( typeof type ) { + case 'string': + if ( typeof props.children !== 'undefined' ) { + return renderChildren( props.children ); + } + return; + case 'function': + if ( + type.prototype && + typeof type.prototype.render === 'function' + ) { + return renderComponent( type, props ); + } + + return renderElement( type( props ) ); + } } export function getRichTextValues( blocks = [] ) { getBlockProps.skipFilters = true; - const values = findContent( - ( Array.isArray( blocks ) ? blocks : [ blocks ] ).map( _getSaveElement ) - ); + const saveElement = blocks.map( ( block ) => _getSaveElement( block ) ); + const values = renderElement( saveElement ); getBlockProps.skipFilters = false; return values; } From 43e28d5604fd43fd75cfb86252db876c6c60740e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Mon, 3 Jul 2023 21:41:03 +0300 Subject: [PATCH 2/7] Fix innerBlocks --- .../src/components/rich-text/content.js | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/content.js b/packages/block-editor/src/components/rich-text/content.js index 1de8ef5d7d76a..afa6d588f9937 100644 --- a/packages/block-editor/src/components/rich-text/content.js +++ b/packages/block-editor/src/components/rich-text/content.js @@ -13,6 +13,7 @@ import deprecated from '@wordpress/deprecated'; * Internal dependencies */ import { getMultilineTag } from './utils'; +import InnerBlocks from '../inner-blocks'; export const Content = ( { value, tagName: Tag, multiline, ...props } ) => { // Handle deprecated `children` and `node` sources. @@ -45,14 +46,6 @@ export const Content = ( { value, tagName: Tag, multiline, ...props } ) => { Content.__unstableIsRichTextContent = {}; -function _getSaveElement( { name, attributes, innerBlocks } ) { - return getSaveElement( - name, - attributes, - innerBlocks.map( _getSaveElement ) - ); -} - function renderChildren( children ) { const values = []; children = Array.isArray( children ) ? children : [ children ]; @@ -105,6 +98,8 @@ function renderElement( element ) { return renderChildren( props.children ); case RawHTML: return; + case InnerBlocks.Content: + return getValuesForBlocks( renderElement.innerBlocks ); } switch ( typeof type ) { @@ -125,10 +120,27 @@ function renderElement( element ) { } } +function getValuesForBlocks( blocks ) { + const values = []; + for ( let i = 0; i < blocks.length; i++ ) { + const { name, attributes, innerBlocks } = blocks[ i ]; + const saveElement = getSaveElement( name, attributes ); + renderElement.innerBlocks = innerBlocks; + const value = renderElement( saveElement ); + if ( value ) { + if ( Array.isArray( value ) ) { + values.push( ...value ); + } else { + values.push( value ); + } + } + } + return values; +} + export function getRichTextValues( blocks = [] ) { getBlockProps.skipFilters = true; - const saveElement = blocks.map( ( block ) => _getSaveElement( block ) ); - const values = renderElement( saveElement ); + const values = getValuesForBlocks( blocks ); getBlockProps.skipFilters = false; return values; } From 423588a2b0a05ec25537bf0a8346572aea350938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Mon, 3 Jul 2023 22:57:22 +0300 Subject: [PATCH 3/7] Clean up --- .../src/components/rich-text/content.js | 65 ++++++------------- 1 file changed, 20 insertions(+), 45 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/content.js b/packages/block-editor/src/components/rich-text/content.js index afa6d588f9937..ebbd088127432 100644 --- a/packages/block-editor/src/components/rich-text/content.js +++ b/packages/block-editor/src/components/rich-text/content.js @@ -44,38 +44,21 @@ export const Content = ( { value, tagName: Tag, multiline, ...props } ) => { return content; }; -Content.__unstableIsRichTextContent = {}; - -function renderChildren( children ) { - const values = []; +function addValuesForElements( children, ...args ) { children = Array.isArray( children ) ? children : [ children ]; for ( let i = 0; i < children.length; i++ ) { - const child = children[ i ]; - const value = renderElement( child ); - if ( value ) { - if ( Array.isArray( value ) ) { - values.push( ...value ); - } else { - values.push( value ); - } - } + addValuesForElement( children[ i ], ...args ); } - - return values; -} - -function renderComponent( Component, props ) { - return renderElement( new Component( props ).render() ); } -function renderElement( element ) { +function addValuesForElement( element, ...args ) { if ( null === element || undefined === element || false === element ) { return; } if ( Array.isArray( element ) ) { - return renderChildren( element ); + return addValuesForElements( element, ...args ); } switch ( typeof element ) { @@ -86,26 +69,24 @@ function renderElement( element ) { const { type, props } = element; - if ( - type.__unstableIsRichTextContent === Content.__unstableIsRichTextContent - ) { - return props.value; - } - switch ( type ) { case StrictMode: case Fragment: - return renderChildren( props.children ); + return addValuesForElements( props.children, ...args ); case RawHTML: return; case InnerBlocks.Content: - return getValuesForBlocks( renderElement.innerBlocks ); + return addValuesForBlocks( ...args ); + case Content: + const [ values ] = args; + values.push( props.value ); + return; } switch ( typeof type ) { case 'string': if ( typeof props.children !== 'undefined' ) { - return renderChildren( props.children ); + return addValuesForElements( props.children, ...args ); } return; case 'function': @@ -113,34 +94,28 @@ function renderElement( element ) { type.prototype && typeof type.prototype.render === 'function' ) { - return renderComponent( type, props ); + return addValuesForElement( + new type( props ).render(), + ...args + ); } - return renderElement( type( props ) ); + return addValuesForElement( type( props ), ...args ); } } -function getValuesForBlocks( blocks ) { - const values = []; +function addValuesForBlocks( values, blocks ) { for ( let i = 0; i < blocks.length; i++ ) { const { name, attributes, innerBlocks } = blocks[ i ]; const saveElement = getSaveElement( name, attributes ); - renderElement.innerBlocks = innerBlocks; - const value = renderElement( saveElement ); - if ( value ) { - if ( Array.isArray( value ) ) { - values.push( ...value ); - } else { - values.push( value ); - } - } + addValuesForElement( saveElement, values, innerBlocks ); } - return values; } export function getRichTextValues( blocks = [] ) { getBlockProps.skipFilters = true; - const values = getValuesForBlocks( blocks ); + const values = []; + addValuesForBlocks( values, blocks ); getBlockProps.skipFilters = false; return values; } From 526154ce9db5fe4e2aae5b55a7ece62652919dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Tue, 4 Jul 2023 09:45:37 +0300 Subject: [PATCH 4/7] Add e2e tests --- .../specs/editor/various/footnotes.spec.js | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js index e65eee6d3d08e..053f99d3fb8f7 100644 --- a/test/e2e/specs/editor/various/footnotes.spec.js +++ b/test/e2e/specs/editor/various/footnotes.spec.js @@ -182,4 +182,101 @@ 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.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/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, + }, + ] ); + } ); } ); From d006b5ec180651779e04924ff80fc826ad8c9983 Mon Sep 17 00:00:00 2001 From: Ella Date: Fri, 7 Jul 2023 12:19:34 +0200 Subject: [PATCH 5/7] Polish --- .../src/components/rich-text/content.js | 85 +---------------- .../rich-text/get-rich-text-values.js | 95 +++++++++++++++++++ packages/block-editor/src/private-apis.js | 2 +- 3 files changed, 98 insertions(+), 84 deletions(-) create mode 100644 packages/block-editor/src/components/rich-text/get-rich-text-values.js diff --git a/packages/block-editor/src/components/rich-text/content.js b/packages/block-editor/src/components/rich-text/content.js index ebbd088127432..9762582f86f14 100644 --- a/packages/block-editor/src/components/rich-text/content.js +++ b/packages/block-editor/src/components/rich-text/content.js @@ -1,19 +1,14 @@ /** * WordPress dependencies */ -import { RawHTML, StrictMode, Fragment } from '@wordpress/element'; -import { - children as childrenSource, - getSaveElement, - __unstableGetBlockProps as getBlockProps, -} from '@wordpress/blocks'; +import { RawHTML } from '@wordpress/element'; +import { children as childrenSource } from '@wordpress/blocks'; import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ import { getMultilineTag } from './utils'; -import InnerBlocks from '../inner-blocks'; export const Content = ( { value, tagName: Tag, multiline, ...props } ) => { // Handle deprecated `children` and `node` sources. @@ -43,79 +38,3 @@ export const Content = ( { value, tagName: Tag, multiline, ...props } ) => { return content; }; - -function addValuesForElements( children, ...args ) { - children = Array.isArray( children ) ? children : [ children ]; - - for ( let i = 0; i < children.length; i++ ) { - addValuesForElement( children[ i ], ...args ); - } -} - -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 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/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 0000000000000..4ecee9b76530e --- /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 1200dee367d24..453fbd7ce63eb 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'; From a4406161ab21223621de1119a360ef4abd1bd6f7 Mon Sep 17 00:00:00 2001 From: Ella Date: Fri, 7 Jul 2023 22:11:10 +0200 Subject: [PATCH 6/7] Fix tests after rebase --- test/e2e/specs/editor/various/footnotes.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js index 053f99d3fb8f7..f4e1c0d8bf7d4 100644 --- a/test/e2e/specs/editor/various/footnotes.spec.js +++ b/test/e2e/specs/editor/various/footnotes.spec.js @@ -203,7 +203,7 @@ test.describe( 'Footnotes', () => { { name: 'core/list-item', attributes: { - content: `1*`, + content: `1*`, }, }, ], @@ -243,7 +243,7 @@ test.describe( 'Footnotes', () => { { cells: [ { - content: `1*`, + content: `1*`, tag: 'td', }, { From 8f9041afd4eae17e6d969c7162976f94a309b14b Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <150562+mcsf@users.noreply.github.com> Date: Mon, 10 Jul 2023 10:42:42 +0100 Subject: [PATCH 7/7] Update test/e2e/specs/editor/various/footnotes.spec.js --- test/e2e/specs/editor/various/footnotes.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js index f4e1c0d8bf7d4..1f2fd33f23f73 100644 --- a/test/e2e/specs/editor/various/footnotes.spec.js +++ b/test/e2e/specs/editor/various/footnotes.spec.js @@ -186,7 +186,6 @@ test.describe( 'Footnotes', () => { 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.showBlockToolbar(); await editor.clickBlockToolbarButton( 'More' ); await page.locator( 'button:text("Footnote")' ).click();