diff --git a/gutenberg b/gutenberg index bcd13aa20c37d2..08897864660d68 160000 --- a/gutenberg +++ b/gutenberg @@ -1 +1 @@ -Subproject commit bcd13aa20c37d28e0b7d9a3f810f4b8004fb5023 +Subproject commit 08897864660d68efb226c19b685699992598b791 diff --git a/src/app/AppContainer.js b/src/app/AppContainer.js index 17fcba40fd2f38..1689e6fbfd7d8d 100644 --- a/src/app/AppContainer.js +++ b/src/app/AppContainer.js @@ -10,6 +10,7 @@ import { deleteBlockAction, createBlockAction, parseBlocksAction, + mergeBlocksAction, } from '../store/actions'; import MainApp from './MainApp'; @@ -41,6 +42,9 @@ const mapDispatchToProps = ( dispatch, ownProps ) => { parseBlocksAction: ( html ) => { dispatch( parseBlocksAction( html ) ); }, + mergeBlocksAction: ( blockOneClientId, blockTwoClientId, block ) => { + dispatch( mergeBlocksAction( blockOneClientId, blockTwoClientId, block ) ); + }, }; }; diff --git a/src/block-management/block-holder.js b/src/block-management/block-holder.js index 955b029fdcc5fd..6d05be9dbadacd 100644 --- a/src/block-management/block-holder.js +++ b/src/block-management/block-holder.js @@ -20,6 +20,7 @@ type PropsType = BlockType & { onToolbarButtonPressed: ( button: number, clientId: string ) => void, onBlockHolderPressed: ( clientId: string ) => void, insertBlocksAfter: ( blocks: Array ) => void, + mergeBlocks: ( forward: boolean ) => void, }; type StateType = { @@ -71,6 +72,7 @@ export default class BlockHolder extends React.Component { this.props.onChange( this.props.clientId, { ...this.props.attributes, ...attrs } ) } insertBlocksAfter={ this.props.insertBlocksAfter } + mergeBlocks={ this.props.mergeBlocks } isSelected={ this.props.focused } style={ style } /> diff --git a/src/block-management/block-manager.js b/src/block-management/block-manager.js index 08f6d905b9907d..717e556b615bf4 100644 --- a/src/block-management/block-manager.js +++ b/src/block-management/block-manager.js @@ -14,7 +14,7 @@ import BlockPicker from './block-picker'; import HTMLTextInput from '../components/html-text-input'; // Gutenberg imports -import { createBlock } from '@wordpress/blocks'; +import { createBlock, getBlockType, switchToBlockType } from '@wordpress/blocks'; export type BlockListType = { onChange: ( clientId: string, attributes: mixed ) => void, @@ -24,6 +24,7 @@ export type BlockListType = { deleteBlockAction: string => mixed, createBlockAction: ( string, BlockType, string ) => mixed, parseBlocksAction: string => mixed, + mergeBlocksAction: ( string, string, BlockType ) => mixed, blocks: Array, aztechtml: string, refresh: boolean, @@ -154,6 +155,73 @@ export default class BlockManager extends React.Component this.props.focusBlockAction( newBlock.clientId ); // this not working atm } + mergeBlocks( clientId: string, forward: boolean ) { + // find currently focused block + const focusedItemIndex = this.getDataSourceIndexFromClientId( clientId ); + // Do nothing when it's the first block and backspace is pressed + // Do nothing when it's the last block and delete is pressed + if ( + ( ! forward && focusedItemIndex === 0 ) || + ( forward && ! focusedItemIndex === this.state.dataSource.size() - 1 ) + ) { + return; + } + + let blockA = null; + let blockB = null; + if ( forward ) { + blockA = this.state.dataSource.get( focusedItemIndex ); + blockB = this.state.dataSource.get( focusedItemIndex + 1 ); + } else { + blockA = this.state.dataSource.get( focusedItemIndex - 1 ); + blockB = this.state.dataSource.get( focusedItemIndex ); + } + + const blockType = getBlockType( blockA.name ); + + // Only focus the previous block if it's not mergeable + if ( ! blockType.merge ) { + // TO DO: move the focus to the prev block + return; + } + + // TODO: there is a problem with block transformation + // let's stop here until we fix the problems with block transformation routine + if ( blockA.name !== blockB.name ) { + return; + } + + // We can only merge blocks with similar types + // thus, we transform the block to merge first + const blocksWithTheSameType = blockA.name === blockB.name ? + [ blockB ] : + switchToBlockType( blockB, blockA.name ); + + // If the block types can not match, do nothing + if ( ! blocksWithTheSameType || ! blocksWithTheSameType.length ) { + return; + } + + // Calling the merge to update the attributes and remove the block to be merged + const updatedAttributes = blockType.merge( + blockA.attributes, + blocksWithTheSameType[ 0 ].attributes + ); + + const newBlock = { + ...blockA, + attributes: { + ...blockA.attributes, + ...updatedAttributes, + }, + }; + newBlock.focused = true; + + // set it into the datasource, and use the same object instance to send it to props/redux + this.state.dataSource.splice( this.getDataSourceIndexFromClientId( blockA.clientId ), 2, newBlock ); + this.props.mergeBlocksAction( blockA.clientId, blockB.clientId, newBlock ); + } + onChange( clientId: string, attributes: mixed ) { // Update Redux store this.props.onChange( clientId, attributes ); @@ -270,6 +338,9 @@ export default class BlockManager extends React.Component insertBlocksAfter={ ( blocks ) => this.insertBlocksAfter.bind( this )( value.item.clientId, blocks ) } + mergeBlocks={ ( forward = false ) => + this.mergeBlocks.bind( this )( value.item.clientId, forward ) + } { ...value.item } /> { this.state.blockTypePickerVisible && value.item.focused && insertHere } diff --git a/src/store/actions/ActionTypes.js b/src/store/actions/ActionTypes.js index 63a9c57bf74ed0..a01e0f25f9aa3f 100644 --- a/src/store/actions/ActionTypes.js +++ b/src/store/actions/ActionTypes.js @@ -12,5 +12,6 @@ export default { DELETE: 'BLOCK_DELETE_ACTION', CREATE: 'BLOCK_CREATE_ACTION', PARSE: 'BLOCK_PARSE_ACTION', + MERGE: 'BLOCKS_MERGE_ACTION', }, }; diff --git a/src/store/actions/index.js b/src/store/actions/index.js index a4698c4c025131..f0babe0539924a 100644 --- a/src/store/actions/index.js +++ b/src/store/actions/index.js @@ -23,6 +23,13 @@ export type ParseActionType = string => { html: string, }; +export type BlocksActionType = ( string, string, BlockType ) => { + type: $Values, + blockOneClientId: string, + blockTwoClientId: string, + block: BlockType, +}; + export function updateBlockAttributes( clientId: string, attributes: mixed ) { return { type: ActionTypes.BLOCK.UPDATE_ATTRIBUTES, @@ -62,3 +69,10 @@ export const parseBlocksAction: ParseActionType = ( html ) => ( { type: ActionTypes.BLOCK.PARSE, html, } ); + +export const mergeBlocksAction: BlocksActionType = ( blockOneClientId, blockTwoClientId, block ) => ( { + type: ActionTypes.BLOCK.MERGE, + blockOneClientId, + blockTwoClientId, + block, +} ); diff --git a/src/store/reducers/index.js b/src/store/reducers/index.js index 1593fb9be3c86b..b55475dffd65c6 100644 --- a/src/store/reducers/index.js +++ b/src/store/reducers/index.js @@ -122,6 +122,11 @@ export const reducer = ( const parsed = parse( action.html ); return { blocks: parsed, refresh: ! state.refresh, fullparse: true }; } + case ActionTypes.BLOCK.MERGE: { + const index = findBlockIndex( blocks, action.blockOneClientId ); + blocks.splice( index, 2, action.block ); + return { blocks: blocks, refresh: ! state.refresh }; + } default: return state; }