diff --git a/packages/block-editor/src/components/block-list/block-list-context.native.js b/packages/block-editor/src/components/block-list/block-list-context.native.js new file mode 100644 index 00000000000000..95385b480b3d14 --- /dev/null +++ b/packages/block-editor/src/components/block-list/block-list-context.native.js @@ -0,0 +1,131 @@ +/** + * WordPress dependencies + */ +import { createContext, useContext } from '@wordpress/element'; + +export const DEFAULT_BLOCK_LIST_CONTEXT = { + scrollRef: null, + blocksLayouts: { current: {} }, + findBlockLayoutByClientId, + updateBlocksLayouts, +}; + +const Context = createContext( DEFAULT_BLOCK_LIST_CONTEXT ); +const { Provider, Consumer } = Context; + +/** + * Finds a block's layout data by its client Id. + * + * @param {Object} data Blocks layouts object. + * @param {string} clientId Block's clientId. + * + * @return {Object} Found block layout data. + */ +function findBlockLayoutByClientId( data, clientId ) { + return Object.entries( data ).reduce( ( acc, entry ) => { + const item = entry[ 1 ]; + if ( acc ) { + return acc; + } + if ( item?.clientId === clientId ) { + return item; + } + if ( item?.innerBlocks && Object.keys( item.innerBlocks ).length > 0 ) { + return findBlockLayoutByClientId( item.innerBlocks, clientId ); + } + return null; + }, null ); +} + +/** + * Deletes the layout data of a block by its client Id. + * + * @param {Object} data Blocks layouts object. + * @param {string} clientId Block's clientsId. + * + * @return {Object} Updated data object. + */ +export function deleteBlockLayoutByClientId( data, clientId ) { + return Object.keys( data ).reduce( ( acc, key ) => { + if ( key !== clientId ) { + acc[ key ] = data[ key ]; + } + if ( + data[ key ]?.innerBlocks && + Object.keys( data[ key ].innerBlocks ).length > 0 + ) { + if ( acc[ key ] ) { + acc[ key ].innerBlocks = deleteBlockLayoutByClientId( + data[ key ].innerBlocks, + clientId + ); + } + } + return acc; + }, {} ); +} + +/** + * Updates or deletes a block's layout data in the blocksLayouts object, + * in case of deletion, the layout data is not required. + * + * @param {Object} blocksLayouts Blocks layouts object. + * @param {Object} blockData Block's layout data to add or remove to/from the blockLayouts object. + * @param {string} blockData.clientId Block's clientId. + * @param {?string} blockData.rootClientId Optional. Block's rootClientId. + * @param {?boolean} blockData.shouldRemove Optional. Flag to remove it from the blocksLayout list. + * @param {number} blockData.width Block's width. + * @param {number} blockData.height Block's height. + * @param {number} blockData.x Block's x coordinate (relative to the parent). + * @param {number} blockData.y Block's y coordinate (relative to the parent). + */ + +function updateBlocksLayouts( blocksLayouts, blockData ) { + const { clientId, rootClientId, shouldRemove, ...layoutProps } = blockData; + + if ( clientId && shouldRemove ) { + blocksLayouts.current = deleteBlockLayoutByClientId( + blocksLayouts.current, + clientId + ); + return; + } + + if ( clientId && ! rootClientId ) { + blocksLayouts.current[ clientId ] = { + clientId, + rootClientId, + ...layoutProps, + innerBlocks: { + ...blocksLayouts.current[ clientId ]?.innerBlocks, + }, + }; + } else if ( clientId && rootClientId ) { + const block = findBlockLayoutByClientId( + blocksLayouts.current, + rootClientId + ); + + if ( block ) { + block.innerBlocks[ clientId ] = { + clientId, + rootClientId, + ...layoutProps, + innerBlocks: { + ...block.innerBlocks[ clientId ]?.innerBlocks, + }, + }; + } + } +} + +export { Provider as BlockListProvider, Consumer as BlockListConsumer }; + +/** + * Hook that returns the block list context. + * + * @return {Object} Block list context + */ +export const useBlockListContext = () => { + return useContext( Context ); +}; diff --git a/packages/block-editor/src/components/block-list/block-list-item-cell.native.js b/packages/block-editor/src/components/block-list/block-list-item-cell.native.js new file mode 100644 index 00000000000000..c399643a633996 --- /dev/null +++ b/packages/block-editor/src/components/block-list/block-list-item-cell.native.js @@ -0,0 +1,42 @@ +/** + * External dependencies + */ +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { useEffect, useCallback } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { useBlockListContext } from './block-list-context'; + +function BlockListItemCell( { children, clientId, rootClientId } ) { + const { blocksLayouts, updateBlocksLayouts } = useBlockListContext(); + + useEffect( () => { + return () => { + updateBlocksLayouts( blocksLayouts, { + clientId, + shouldRemove: true, + } ); + }; + }, [] ); + + const onLayout = useCallback( + ( { nativeEvent: { layout } } ) => { + updateBlocksLayouts( blocksLayouts, { + clientId, + rootClientId, + ...layout, + } ); + }, + [ clientId, rootClientId, updateBlocksLayouts ] + ); + + return { children }; +} + +export default BlockListItemCell; diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index ada27b4ebd9fbf..f1dc31da0e91eb 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -25,10 +25,14 @@ import { __ } from '@wordpress/i18n'; import styles from './style.scss'; import BlockListAppender from '../block-list-appender'; import BlockListItem from './block-list-item'; +import BlockListItemCell from './block-list-item-cell'; +import { + BlockListProvider, + BlockListConsumer, + DEFAULT_BLOCK_LIST_CONTEXT, +} from './block-list-context'; import { store as blockEditorStore } from '../../store'; -const BlockListContext = createContext(); - export const OnCaretVerticalPositionChange = createContext(); const stylesMemo = {}; @@ -78,6 +82,9 @@ export class BlockList extends Component { ); this.renderEmptyList = this.renderEmptyList.bind( this ); this.getExtraData = this.getExtraData.bind( this ); + this.getCellRendererComponent = this.getCellRendererComponent.bind( + this + ); this.onLayout = this.onLayout.bind( this ); @@ -154,6 +161,17 @@ export class BlockList extends Component { return this.extraData; } + getCellRendererComponent( { children, item } ) { + const { rootClientId } = this.props; + return ( + + ); + } + onLayout( { nativeEvent } ) { const { layout } = nativeEvent; const { blockWidth } = this.state; @@ -173,17 +191,22 @@ export class BlockList extends Component { const { isRootList } = this.props; // Use of Context to propagate the main scroll ref to its children e.g InnerBlocks. const blockList = isRootList ? ( - + { this.renderList() } - + ) : ( - - { ( ref ) => + + { ( { scrollRef } ) => this.renderList( { - parentScrollRef: ref, + parentScrollRef: scrollRef, } ) } - + ); return ( @@ -279,6 +302,7 @@ export class BlockList extends Component { data={ blockClientIds } keyExtractor={ identity } renderItem={ this.renderItem } + CellRendererComponent={ this.getCellRendererComponent } shouldPreventAutomaticScroll={ this.shouldFlatListPreventAutomaticScroll } diff --git a/packages/block-editor/src/components/block-list/test/block-list-context.native.js b/packages/block-editor/src/components/block-list/test/block-list-context.native.js new file mode 100644 index 00000000000000..13fd0b1e42cb6b --- /dev/null +++ b/packages/block-editor/src/components/block-list/test/block-list-context.native.js @@ -0,0 +1,253 @@ +/** + * External dependencies + */ +import { cloneDeep } from 'lodash'; + +/** + * Internal dependencies + */ +import { + DEFAULT_BLOCK_LIST_CONTEXT, + deleteBlockLayoutByClientId, +} from '../block-list-context.native'; +import { + BLOCKS_LAYOUTS_DATA, + DEEP_NESTED_ID, + GROUP_BLOCK_LAYOUT_DATA, + NESTED_WITH_INNER_BLOCKS_ID, + PARAGRAPH_BLOCK_LAYOUT_DATA, + ROOT_LEVEL_ID, +} from './fixtures/block-list-context.native'; + +describe( 'findBlockLayoutByClientId', () => { + it( "finds a block's layout data at root level", () => { + const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT; + const currentBlockLayouts = BLOCKS_LAYOUTS_DATA; + + const blockRootLevel = findBlockLayoutByClientId( + currentBlockLayouts, + ROOT_LEVEL_ID + ); + + expect( blockRootLevel ).toEqual( + expect.objectContaining( { clientId: ROOT_LEVEL_ID } ) + ); + } ); + + it( "finds a nested block's layout data with inner blocks", () => { + const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT; + const currentBlockLayouts = BLOCKS_LAYOUTS_DATA; + + const nestedBlock = findBlockLayoutByClientId( + currentBlockLayouts, + NESTED_WITH_INNER_BLOCKS_ID + ); + + expect( nestedBlock ).toEqual( + expect.objectContaining( { clientId: NESTED_WITH_INNER_BLOCKS_ID } ) + ); + } ); + + it( "finds a deep nested block's layout data", () => { + const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT; + const currentBlockLayouts = BLOCKS_LAYOUTS_DATA; + + const deepNestedBlock = findBlockLayoutByClientId( + currentBlockLayouts, + DEEP_NESTED_ID + ); + + expect( deepNestedBlock ).toEqual( + expect.objectContaining( { clientId: DEEP_NESTED_ID } ) + ); + } ); +} ); + +describe( 'deleteBlockLayoutByClientId', () => { + it( "deletes a block's layout data at root level", () => { + const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT; + const defaultBlockLayouts = cloneDeep( BLOCKS_LAYOUTS_DATA ); + const currentBlockLayouts = deleteBlockLayoutByClientId( + defaultBlockLayouts, + ROOT_LEVEL_ID + ); + + const findDeletedBlock = findBlockLayoutByClientId( + currentBlockLayouts, + ROOT_LEVEL_ID + ); + + expect( findDeletedBlock ).toBeNull(); + } ); + + it( "deletes a nested block's layout data with inner blocks", () => { + const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT; + const defaultBlockLayouts = cloneDeep( BLOCKS_LAYOUTS_DATA ); + const currentBlockLayouts = deleteBlockLayoutByClientId( + defaultBlockLayouts, + NESTED_WITH_INNER_BLOCKS_ID + ); + + const findDeletedBlock = findBlockLayoutByClientId( + currentBlockLayouts, + NESTED_WITH_INNER_BLOCKS_ID + ); + + expect( findDeletedBlock ).toBeNull(); + } ); + + it( "deletes a deep nested block's layout data", () => { + const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT; + const defaultBlockLayouts = cloneDeep( BLOCKS_LAYOUTS_DATA ); + const currentBlockLayouts = deleteBlockLayoutByClientId( + defaultBlockLayouts, + DEEP_NESTED_ID + ); + + const findDeletedBlock = findBlockLayoutByClientId( + currentBlockLayouts, + DEEP_NESTED_ID + ); + + expect( findDeletedBlock ).toBeNull(); + } ); +} ); + +describe( 'updateBlocksLayouts', () => { + it( "adds a new block's layout data at root level with an empty object", () => { + const { + blocksLayouts, + findBlockLayoutByClientId, + updateBlocksLayouts, + } = DEFAULT_BLOCK_LIST_CONTEXT; + const currentBlockLayouts = cloneDeep( blocksLayouts ); + const BLOCK_CLIENT_ID = PARAGRAPH_BLOCK_LAYOUT_DATA.clientId; + + updateBlocksLayouts( currentBlockLayouts, PARAGRAPH_BLOCK_LAYOUT_DATA ); + + const findAddedBlock = findBlockLayoutByClientId( + currentBlockLayouts.current, + BLOCK_CLIENT_ID + ); + + expect( findAddedBlock ).toEqual( + expect.objectContaining( { + clientId: BLOCK_CLIENT_ID, + rootClientId: undefined, + } ) + ); + } ); + + it( "adds a new block's layout data at root level with inner blocks", () => { + const { + findBlockLayoutByClientId, + updateBlocksLayouts, + } = DEFAULT_BLOCK_LIST_CONTEXT; + const currentBlockLayouts = { + current: cloneDeep( BLOCKS_LAYOUTS_DATA ), + }; + const PARENT_BLOCK_CLIENT_ID = GROUP_BLOCK_LAYOUT_DATA.clientId; + + // Add parent block + updateBlocksLayouts( currentBlockLayouts, GROUP_BLOCK_LAYOUT_DATA ); + + const findAddedParentBlock = findBlockLayoutByClientId( + currentBlockLayouts.current, + PARENT_BLOCK_CLIENT_ID + ); + + expect( findAddedParentBlock ).toEqual( + expect.objectContaining( { clientId: PARENT_BLOCK_CLIENT_ID } ) + ); + + // Add inner block to it's parent + updateBlocksLayouts( currentBlockLayouts, { + ...PARAGRAPH_BLOCK_LAYOUT_DATA, + rootClientId: PARENT_BLOCK_CLIENT_ID, + } ); + + const findAddedInnerBlock = findBlockLayoutByClientId( + currentBlockLayouts.current, + PARAGRAPH_BLOCK_LAYOUT_DATA.clientId + ); + + expect( findAddedInnerBlock ).toEqual( + expect.objectContaining( { + clientId: PARAGRAPH_BLOCK_LAYOUT_DATA.clientId, + rootClientId: PARENT_BLOCK_CLIENT_ID, + } ) + ); + } ); + + it( "adds a new block's layout data at deep level", () => { + const { + findBlockLayoutByClientId, + updateBlocksLayouts, + } = DEFAULT_BLOCK_LIST_CONTEXT; + const currentBlockLayouts = { + current: cloneDeep( BLOCKS_LAYOUTS_DATA ), + }; + + // Add block layout data to it's parents inner blocks + updateBlocksLayouts( currentBlockLayouts, { + ...PARAGRAPH_BLOCK_LAYOUT_DATA, + rootClientId: NESTED_WITH_INNER_BLOCKS_ID, + } ); + + const findAddedInnerBlock = findBlockLayoutByClientId( + currentBlockLayouts.current, + PARAGRAPH_BLOCK_LAYOUT_DATA.clientId + ); + + expect( findAddedInnerBlock ).toEqual( + expect.objectContaining( { + clientId: PARAGRAPH_BLOCK_LAYOUT_DATA.clientId, + rootClientId: NESTED_WITH_INNER_BLOCKS_ID, + } ) + ); + } ); + + it( "deletes a block's layout data at root level", () => { + const { + findBlockLayoutByClientId, + updateBlocksLayouts, + } = DEFAULT_BLOCK_LIST_CONTEXT; + const currentBlockLayouts = { + current: cloneDeep( BLOCKS_LAYOUTS_DATA ), + }; + + updateBlocksLayouts( currentBlockLayouts, { + shouldRemove: true, + clientId: ROOT_LEVEL_ID, + } ); + + const findDeletedBlock = findBlockLayoutByClientId( + currentBlockLayouts.current, + ROOT_LEVEL_ID + ); + + expect( findDeletedBlock ).toBeNull(); + } ); + + it( "deletes a block's layout data at a deep level", () => { + const { + findBlockLayoutByClientId, + updateBlocksLayouts, + } = DEFAULT_BLOCK_LIST_CONTEXT; + const currentBlockLayouts = { + current: cloneDeep( BLOCKS_LAYOUTS_DATA ), + }; + + updateBlocksLayouts( currentBlockLayouts, { + shouldRemove: true, + clientId: DEEP_NESTED_ID, + } ); + + const findDeletedBlock = findBlockLayoutByClientId( + currentBlockLayouts.current, + DEEP_NESTED_ID + ); + + expect( findDeletedBlock ).toBeNull(); + } ); +} ); diff --git a/packages/block-editor/src/components/block-list/test/fixtures/block-list-context.native.js b/packages/block-editor/src/components/block-list/test/fixtures/block-list-context.native.js new file mode 100644 index 00000000000000..af74c07ec8e0ae --- /dev/null +++ b/packages/block-editor/src/components/block-list/test/fixtures/block-list-context.native.js @@ -0,0 +1,79 @@ +export const ROOT_LEVEL_ID = 'e59528f8-fb35-4ec1-aec6-5a065c236fa1'; +export const ROOT_LEVEL_WITH_INNER_BLOCKS_ID = + '72a9220f-4c3d-4b00-bae1-4506513f63d8'; +export const NESTED_WITH_INNER_BLOCKS_ID = + '9f3d1f1e-df85-485d-af63-dc8cb1b93cbc'; +export const DEEP_NESTED_ID = 'abec845a-e4de-43fb-96f7-80dc3d51ad7a'; + +export const BLOCKS_LAYOUTS_DATA = { + [ ROOT_LEVEL_ID ]: { + clientId: ROOT_LEVEL_ID, + width: 390, + height: 54, + x: 0, + y: 83, + innerBlocks: {}, + }, + [ ROOT_LEVEL_WITH_INNER_BLOCKS_ID ]: { + clientId: ROOT_LEVEL_WITH_INNER_BLOCKS_ID, + width: 390, + height: 386, + x: 0, + y: 137, + innerBlocks: { + '62839858-48b0-44ed-b834-1343a1357e54': { + clientId: '62839858-48b0-44ed-b834-1343a1357e54', + rootClientId: ROOT_LEVEL_WITH_INNER_BLOCKS_ID, + width: 390, + height: 54, + x: 0, + y: 0, + innerBlocks: {}, + }, + [ NESTED_WITH_INNER_BLOCKS_ID ]: { + clientId: NESTED_WITH_INNER_BLOCKS_ID, + rootClientId: ROOT_LEVEL_WITH_INNER_BLOCKS_ID, + width: 390, + height: 332, + x: 0, + y: 54, + innerBlocks: { + '435d62a4-afa7-457c-a894-b04390d7b447': { + clientId: '435d62a4-afa7-457c-a894-b04390d7b447', + rootClientId: NESTED_WITH_INNER_BLOCKS_ID, + width: 358, + height: 54, + x: 0, + y: 0, + innerBlocks: {}, + }, + [ DEEP_NESTED_ID ]: { + clientId: DEEP_NESTED_ID, + rootClientId: NESTED_WITH_INNER_BLOCKS_ID, + width: 358, + height: 98, + x: 0, + y: 54, + innerBlocks: {}, + }, + }, + }, + }, + }, +}; + +export const PARAGRAPH_BLOCK_LAYOUT_DATA = { + clientId: '22dda04f-4718-45b2-8cd2-36cedb9eae4d', + width: 390, + height: 98, + x: 0, + y: 83, +}; + +export const GROUP_BLOCK_LAYOUT_DATA = { + clientId: 'e18249d9-ec06-4f54-b71e-6ec59be5213e', + width: 390, + height: 164, + x: 0, + y: 83, +};