From a51e77349a34853a7651ef404bffe3becf5e576f Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco Date: Thu, 17 Mar 2022 16:41:50 +0100 Subject: [PATCH] [Mobile] - Drag & drop blocks - Fetch and share blocks layout size and position coordinates (#39089) * Mobile - Block list - Extract block list context into a separate file and add support to store the blocks layouts data and coordinates. * Mobile - Block list - Adds block list item cell to get the onLayout data and use updateBlocksLayouts to store it. It is needed to use CellRendererComponent to be able to get the right position coordinates * Mobile - Block list - Store block layouts data for inner blocks in a deep level * Mobile - BlockList ItemCell - Destructuring props * Mobile - BlockListContext - Rename findByRootId to findBlockLayoutByClientId * Mobile - BlockListContext - Rename deleteByClientId to deleteBlockLayoutByClientId * Mobile - BlockListContext - Store default context and use it for initialization * Mobile - BlockListContext - Add param's docs * Mobile - Block list context - Export findBlockLayoutByClientId * Mobile - Block list context - Update comments * Mobile - Block list context - Unit tests * Mobile - Block list context - update unit tests --- .../block-list/block-list-context.native.js | 131 +++++++++ .../block-list/block-list-item-cell.native.js | 42 +++ .../src/components/block-list/index.native.js | 40 ++- .../test/block-list-context.native.js | 253 ++++++++++++++++++ .../fixtures/block-list-context.native.js | 79 ++++++ 5 files changed, 537 insertions(+), 8 deletions(-) create mode 100644 packages/block-editor/src/components/block-list/block-list-context.native.js create mode 100644 packages/block-editor/src/components/block-list/block-list-item-cell.native.js create mode 100644 packages/block-editor/src/components/block-list/test/block-list-context.native.js create mode 100644 packages/block-editor/src/components/block-list/test/fixtures/block-list-context.native.js 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 0000000000000..95385b480b3d1 --- /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 0000000000000..c399643a63399 --- /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 ada27b4ebd9fb..f1dc31da0e91e 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 0000000000000..13fd0b1e42cb6 --- /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 0000000000000..af74c07ec8e0a --- /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, +};