diff --git a/blocks/api/factory.js b/blocks/api/factory.js index c5514147f121c1..be4668bfe6dbe6 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -2,7 +2,7 @@ * External dependencies */ import uuid from 'uuid/v4'; -import { get } from 'lodash'; +import { get, castArray, findIndex, isObjectLike } from 'lodash'; /** * Internal dependencies @@ -25,14 +25,15 @@ export function createBlock( blockType, attributes = {} ) { } /** - * Switch Block Type and returns the updated block + * Switch a block into one or more blocks of the new block type * * @param {Object} block Block object * @param {string} blockType BlockType - * @return {Object?} Block object + * @return {Array} Block object */ export function switchToBlockType( block, blockType ) { - // Find the right transformation by giving priority to the "to" transformation + // Find the right transformation by giving priority to the "to" + // transformation. const destinationSettings = getBlockSettings( blockType ); const sourceSettings = getBlockSettings( block.blockType ); const transformationsFrom = get( destinationSettings, 'transforms.from', [] ); @@ -41,13 +42,44 @@ export function switchToBlockType( block, blockType ) { transformationsTo.find( t => t.blocks.indexOf( blockType ) !== -1 ) || transformationsFrom.find( t => t.blocks.indexOf( block.blockType ) !== -1 ); + // If no valid transformation, stop. (How did we get here?) if ( ! transformation ) { return null; } - return Object.assign( { - uid: block.uid, - attributes: transformation.transform( block.attributes ), - blockType + let transformationResults = transformation.transform( block.attributes ); + + // Ensure that the transformation function returned an object or an array + // of objects. + if ( ! isObjectLike( transformationResults ) ) { + return null; + } + + // If the transformation function returned a single object, we want to work + // with an array instead. + transformationResults = castArray( transformationResults ); + + // Ensure that every block object returned by the transformation has a + // valid block type. + if ( transformationResults.some( ( result ) => ! getBlockSettings( result.blockType ) ) ) { + return null; + } + + const firstSwitchedBlock = findIndex( transformationResults, ( result ) => result.blockType === blockType ); + + // Ensure that at least one block object returned by the transformation has + // the expected "destination" block type. + if ( firstSwitchedBlock < 0 ) { + return null; + } + + return transformationResults.map( ( result, index ) => { + return { + // The first transformed block whose type matches the "destination" + // type gets to keep the existing block's UID. + uid: index === firstSwitchedBlock ? block.uid : uuid(), + blockType: result.blockType, + attributes: result.attributes + }; } ); } diff --git a/blocks/api/test/factory.js b/blocks/api/test/factory.js index cb1a0265738d59..cf59fbc34f8911 100644 --- a/blocks/api/test/factory.js +++ b/blocks/api/test/factory.js @@ -31,7 +31,7 @@ describe( 'block factory', () => { } ); } ); - describe( 'switchBlockType()', () => { + describe( 'switchToBlockType()', () => { it( 'should switch the blockType of a block using the "transform form"', () => { registerBlock( 'core/updated-text-block', { transforms: { @@ -39,7 +39,10 @@ describe( 'block factory', () => { blocks: [ 'core/text-block' ], transform: ( { value } ) => { return { - value: 'chicken ' + value + blockType: 'core/updated-text-block', + attributes: { + value: 'chicken ' + value + } }; } } ] @@ -55,15 +58,15 @@ describe( 'block factory', () => { } }; - const updateBlock = switchToBlockType( block, 'core/updated-text-block' ); + const updatedBlock = switchToBlockType( block, 'core/updated-text-block' ); - expect( updateBlock ).to.eql( { + expect( updatedBlock ).to.eql( [ { uid: 1, blockType: 'core/updated-text-block', attributes: { value: 'chicken ribs' } - } ); + } ] ); } ); it( 'should switch the blockType of a block using the "transform to"', () => { @@ -74,7 +77,10 @@ describe( 'block factory', () => { blocks: [ 'core/updated-text-block' ], transform: ( { value } ) => { return { - value: 'chicken ' + value + blockType: 'core/updated-text-block', + attributes: { + value: 'chicken ' + value + } }; } } ] @@ -89,15 +95,15 @@ describe( 'block factory', () => { } }; - const updateBlock = switchToBlockType( block, 'core/updated-text-block' ); + const updatedBlock = switchToBlockType( block, 'core/updated-text-block' ); - expect( updateBlock ).to.eql( { + expect( updatedBlock ).to.eql( [ { uid: 1, blockType: 'core/updated-text-block', attributes: { value: 'chicken ribs' } - } ); + } ] ); } ); it( 'should return null if no transformation is found', () => { @@ -112,9 +118,251 @@ describe( 'block factory', () => { } }; - const updateBlock = switchToBlockType( block, 'core/updated-text-block' ); + const updatedBlock = switchToBlockType( block, 'core/updated-text-block' ); + + expect( updatedBlock ).to.be.null(); + } ); + + it( 'should reject transformations that return null', () => { + registerBlock( 'core/updated-text-block', { + transforms: { + from: [ { + blocks: [ 'core/text-block' ], + transform: () => null + } ] + } + } ); + registerBlock( 'core/text-block', {} ); + + const block = { + uid: 1, + blockType: 'core/text-block', + attributes: { + value: 'ribs' + } + }; + + const updatedBlock = switchToBlockType( block, 'core/updated-text-block' ); + + expect( updatedBlock ).to.be.null(); + } ); + + it( 'should reject transformations that return an empty array', () => { + registerBlock( 'core/updated-text-block', { + transforms: { + from: [ { + blocks: [ 'core/text-block' ], + transform: () => [] + } ] + } + } ); + registerBlock( 'core/text-block', {} ); + + const block = { + uid: 1, + blockType: 'core/text-block', + attributes: { + value: 'ribs' + } + }; + + const updatedBlock = switchToBlockType( block, 'core/updated-text-block' ); - expect( updateBlock ).to.be.null(); + expect( updatedBlock ).to.be.null(); + } ); + + it( 'should reject single transformations that do not include block types', () => { + registerBlock( 'core/updated-text-block', { + transforms: { + from: [ { + blocks: [ 'core/text-block' ], + transform: ( { value } ) => { + return { + attributes: { + value: 'chicken ' + value + } + }; + } + } ] + } + } ); + registerBlock( 'core/text-block', {} ); + + const block = { + uid: 1, + blockType: 'core/text-block', + attributes: { + value: 'ribs' + } + }; + + const updatedBlock = switchToBlockType( block, 'core/updated-text-block' ); + + expect( updatedBlock ).to.be.null(); + } ); + + it( 'should reject array transformations that do not include block types', () => { + registerBlock( 'core/updated-text-block', { + transforms: { + from: [ { + blocks: [ 'core/text-block' ], + transform: ( { value } ) => { + return [ + { + blockType: 'core/updated-text-block', + attributes: { + value: 'chicken ' + value + } + }, { + attributes: { + value: 'smoked ' + value + } + } + ]; + } + } ] + } + } ); + registerBlock( 'core/text-block', {} ); + + const block = { + uid: 1, + blockType: 'core/text-block', + attributes: { + value: 'ribs' + } + }; + + const updatedBlock = switchToBlockType( block, 'core/updated-text-block' ); + + expect( updatedBlock ).to.be.null(); + } ); + + it( 'should reject single transformations with unexpected block types', () => { + registerBlock( 'core/updated-text-block', {} ); + registerBlock( 'core/text-block', { + transforms: { + to: [ { + blocks: [ 'core/updated-text-block' ], + transform: ( { value } ) => { + return { + blockType: 'core/text-block', + attributes: { + value: 'chicken ' + value + } + }; + } + } ] + } + } ); + + const block = { + uid: 1, + blockType: 'core/text-block', + attributes: { + value: 'ribs' + } + }; + + const updatedBlock = switchToBlockType( block, 'core/updated-text-block' ); + + expect( updatedBlock ).to.eql( null ); + } ); + + it( 'should reject array transformations with unexpected block types', () => { + registerBlock( 'core/updated-text-block', {} ); + registerBlock( 'core/text-block', { + transforms: { + to: [ { + blocks: [ 'core/updated-text-block' ], + transform: ( { value } ) => { + return [ + { + blockType: 'core/text-block', + attributes: { + value: 'chicken ' + value + } + }, { + blockType: 'core/text-block', + attributes: { + value: 'smoked ' + value + } + } + ]; + } + } ] + } + } ); + + const block = { + uid: 1, + blockType: 'core/text-block', + attributes: { + value: 'ribs' + } + }; + + const updatedBlock = switchToBlockType( block, 'core/updated-text-block' ); + + expect( updatedBlock ).to.eql( null ); + } ); + + it( 'should accept valid array transformations', () => { + registerBlock( 'core/updated-text-block', {} ); + registerBlock( 'core/text-block', { + transforms: { + to: [ { + blocks: [ 'core/updated-text-block' ], + transform: ( { value } ) => { + return [ + { + blockType: 'core/text-block', + attributes: { + value: 'chicken ' + value + } + }, { + blockType: 'core/updated-text-block', + attributes: { + value: 'smoked ' + value + } + } + ]; + } + } ] + } + } ); + + const block = { + uid: 1, + blockType: 'core/text-block', + attributes: { + value: 'ribs' + } + }; + + const updatedBlock = switchToBlockType( block, 'core/updated-text-block' ); + + // Make sure the block UIDs are set as expected: the first + // transformed block whose type matches the "destination" type gets + // to keep the existing block's UID. + expect( updatedBlock ).to.have.lengthOf( 2 ); + expect( updatedBlock[ 0 ].uid ).to.exist().and.not.eql( 1 ); + expect( updatedBlock[ 1 ].uid ).to.eql( 1 ); + updatedBlock[ 0 ].uid = 2; + + expect( updatedBlock ).to.eql( [ { + uid: 2, + blockType: 'core/text-block', + attributes: { + value: 'chicken ribs' + } + }, { + uid: 1, + blockType: 'core/updated-text-block', + attributes: { + value: 'smoked ribs' + } + } ] ); } ); } ); } ); diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index 6d9104bba0a1c4..97cbdf94f97cbf 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -35,15 +35,37 @@ registerBlock( 'core/heading', { { type: 'block', blocks: [ 'core/text' ], - transform: ( { content } ) => { + transform: ( { content, ...attrs } ) => { if ( Array.isArray( content ) ) { - // TODO this appears to always be true? - // TODO reject the switch if more than one paragraph - content = content[ 0 ]; + const heading = { + blockType: 'core/heading', + attributes: { + nodeName: 'H2', + content: content[ 0 ] + } + }; + const blocks = [ heading ]; + + const remainingContent = content.slice( 1 ); + if ( remainingContent.length ) { + const text = { + blockType: 'core/text', + attributes: { + ...attrs, + content: remainingContent + } + }; + blocks.push( text ); + } + + return blocks; } return { - nodeName: 'H2', - content + blockType: 'core/heading', + attributes: { + nodeName: 'H2', + content + } }; } } @@ -54,7 +76,10 @@ registerBlock( 'core/heading', { blocks: [ 'core/text' ], transform: ( { content } ) => { return { - content: [ content ] + blockType: 'core/text', + attributes: { + content + } }; } } diff --git a/blocks/library/quote/index.js b/blocks/library/quote/index.js index 807be717b62807..fc3d9c4f411c71 100644 --- a/blocks/library/quote/index.js +++ b/blocks/library/quote/index.js @@ -40,7 +40,82 @@ registerBlock( 'core/quote', { subscript: variation } ) ), - edit( { attributes, setAttributes, focus, setFocus } ) { + transforms: { + from: [ + { + type: 'block', + blocks: [ 'core/text' ], + transform: ( { content } ) => { + return { + blockType: 'core/quote', + attributes: { + value: content + } + }; + } + }, + { + type: 'block', + blocks: [ 'core/heading' ], + transform: ( { content } ) => { + return { + blockType: 'core/quote', + attributes: { + value: content + } + }; + } + } + ], + to: [ + { + type: 'block', + blocks: [ 'core/text' ], + transform: ( { value, citation } ) => { + return { + blockType: 'core/text', + attributes: { + content: wp.element.concatChildren( value, citation ) + } + }; + } + }, + { + type: 'block', + blocks: [ 'core/heading' ], + transform: ( { value, citation, ...attrs } ) => { + if ( Array.isArray( value ) || citation ) { + const heading = { + blockType: 'core/heading', + attributes: { + nodeName: 'H2', + content: Array.isArray( value ) ? value[ 0 ] : value + } + }; + const quote = { + blockType: 'core/quote', + attributes: { + ...attrs, + citation, + value: Array.isArray( value ) ? value.slice( 1 ) : '' + } + }; + + return [ heading, quote ]; + } + return { + blockType: 'core/heading', + attributes: { + nodeName: 'H2', + content: value + } + }; + } + } + ] + }, + + edit( { attributes, setAttributes, focus, setFocus, mergeWithPrevious } ) { const { value, citation, style = 1 } = attributes; const focusedEditable = focus ? focus.editable || 'value' : null; @@ -55,6 +130,7 @@ registerBlock( 'core/quote', { } focus={ focusedEditable === 'value' ? focus : null } onFocus={ () => setFocus( { editable: 'value' } ) } + onMerge={ mergeWithPrevious } showAlignments /> { ( citation || !! focus ) && ( diff --git a/editor/components/block-switcher/index.js b/editor/components/block-switcher/index.js index c3fb6ce7dd1603..1b7c43ab920f65 100644 --- a/editor/components/block-switcher/index.js +++ b/editor/components/block-switcher/index.js @@ -88,9 +88,9 @@ export default connect( ( dispatch, ownProps ) => ( { onTransform( block, blockType ) { dispatch( { - type: 'SWITCH_BLOCK_TYPE', - uid: ownProps.uid, - block: wp.blocks.switchToBlockType( block, blockType ) + type: 'REPLACE_BLOCKS', + uids: [ ownProps.uid ], + blocks: wp.blocks.switchToBlockType( block, blockType ) } ); } } ) diff --git a/editor/index.js b/editor/index.js index 4a003fdf0de398..3ffae58b51b1e6 100644 --- a/editor/index.js +++ b/editor/index.js @@ -20,8 +20,8 @@ import { createReduxStore } from './state'; export function createEditorInstance( id, post ) { const store = createReduxStore(); store.dispatch( { - type: 'REPLACE_BLOCKS', - blockNodes: wp.blocks.parse( post.content.raw ) + type: 'RESET_BLOCKS', + blocks: wp.blocks.parse( post.content.raw ) } ); wp.element.render( diff --git a/editor/modes/text-editor/index.js b/editor/modes/text-editor/index.js index 91bed6ddad1d76..33b877d1e3c863 100644 --- a/editor/modes/text-editor/index.js +++ b/editor/modes/text-editor/index.js @@ -48,8 +48,8 @@ export default connect( ( dispatch ) => ( { onChange( value ) { dispatch( { - type: 'REPLACE_BLOCKS', - blockNodes: wp.blocks.parse( value ) + type: 'RESET_BLOCKS', + blocks: wp.blocks.parse( value ) } ); } } ) diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index 65c8ae65a58643..b5a1b04f5a8c92 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -64,7 +64,7 @@ class VisualEditorBlock extends wp.element.Component { } mergeWithPrevious() { - const { block, previousBlock, onRemove, onChange, onFocus } = this.props; + const { block, previousBlock, onFocus, replaceBlocks } = this.props; // Do nothing when it's the first block if ( ! previousBlock ) { @@ -80,25 +80,32 @@ class VisualEditorBlock extends wp.element.Component { // We can only merge blocks with similar types // thus, we transform the block to merge first - const blockWithTheSameType = previousBlock.blockType === block.blockType - ? block + const blocksWithTheSameType = previousBlock.blockType === block.blockType + ? [ block ] : wp.blocks.switchToBlockType( block, previousBlock.blockType ); // If the block types can not match, do nothing - if ( ! blockWithTheSameType ) { + if ( ! blocksWithTheSameType || ! blocksWithTheSameType.length ) { return; } // Calling the merge to update the attributes and remove the block to be merged - const updatedAttributes = previousBlockSettings.merge( previousBlock.attributes, blockWithTheSameType.attributes ); + const updatedAttributes = previousBlockSettings.merge( previousBlock.attributes, blocksWithTheSameType[ 0 ].attributes ); + onFocus( previousBlock.uid, { offset: -1 } ); - onChange( previousBlock.uid, { - attributes: { - ...previousBlock.attributes, - ...updatedAttributes - } - } ); - onRemove( block.uid ); + replaceBlocks( + [ previousBlock.uid, block.uid ], + [ + { + ...previousBlock, + attributes: { + ...previousBlock.attributes, + ...updatedAttributes + } + }, + ...blocksWithTheSameType.slice( 1 ) + ] + ); } componentDidUpdate() { @@ -263,6 +270,14 @@ export default connect( type: 'REMOVE_BLOCK', uid } ); + }, + + replaceBlocks( uids, blocks ) { + dispatch( { + type: 'REPLACE_BLOCKS', + uids, + blocks + } ); } } ) )( VisualEditorBlock ); diff --git a/editor/state.js b/editor/state.js index 1ac3ac40dd9f80..6158db4212dba7 100644 --- a/editor/state.js +++ b/editor/state.js @@ -20,8 +20,8 @@ import { combineUndoableReducers } from 'utils/undoable-reducer'; export const blocks = combineUndoableReducers( { byUid( state = {}, action ) { switch ( action.type ) { - case 'REPLACE_BLOCKS': - return keyBy( action.blockNodes, 'uid' ); + case 'RESET_BLOCKS': + return keyBy( action.blocks, 'uid' ); case 'UPDATE_BLOCK': return { @@ -38,11 +38,16 @@ export const blocks = combineUndoableReducers( { [ action.block.uid ]: action.block }; - case 'SWITCH_BLOCK_TYPE': - return { - ...state, - [ action.uid ]: action.block - }; + case 'REPLACE_BLOCKS': + if ( ! action.blocks ) { + return state; + } + return action.blocks.reduce( ( memo, block ) => { + return { + ...memo, + [ block.uid ]: block + }; + }, omit( state, action.uids ) ); case 'REMOVE_BLOCK': return omit( state, action.uid ); @@ -54,8 +59,8 @@ export const blocks = combineUndoableReducers( { let index; let swappedUid; switch ( action.type ) { - case 'REPLACE_BLOCKS': - return action.blockNodes.map( ( { uid } ) => uid ); + case 'RESET_BLOCKS': + return action.blocks.map( ( { uid } ) => uid ); case 'INSERT_BLOCK': const position = action.after ? state.indexOf( action.after ) + 1 : state.length; @@ -91,13 +96,28 @@ export const blocks = combineUndoableReducers( { ...state.slice( index + 2 ) ]; + case 'REPLACE_BLOCKS': + if ( ! action.blocks ) { + return state; + } + index = state.indexOf( action.uids[ 0 ] ); + return state.reduce( ( memo, uid ) => { + if ( uid === action.uids[ 0 ] ) { + return memo.concat( action.blocks.map( ( block ) => block.uid ) ); + } + if ( action.uids.indexOf( uid ) === -1 ) { + memo.push( uid ); + } + return memo; + }, [] ); + case 'REMOVE_BLOCK': return without( state, action.uid ); } return state; } -}, { resetTypes: [ 'REPLACE_BLOCKS' ] } ); +}, { resetTypes: [ 'RESET_BLOCKS' ] } ); /** * Reducer returning selected block state. @@ -153,6 +173,17 @@ export function selectedBlock( state = {}, action ) { ...state, typing: true }; + + case 'REPLACE_BLOCKS': + if ( ! action.blocks || ! action.blocks.length || action.uids.indexOf( state.uid ) === -1 ) { + return state; + } + + return { + uid: action.blocks[ 0 ].uid, + typing: false, + focus: {} + }; } return state; @@ -175,8 +206,16 @@ export function hoveredBlock( state = null, action ) { return null; } break; + case 'START_TYPING': return null; + + case 'REPLACE_BLOCKS': + if ( ! action.blocks || ! action.blocks.length || action.uids.indexOf( state ) === -1 ) { + return state; + } + + return action.blocks[ 0 ].uid; } return state; diff --git a/editor/test/state.js b/editor/test/state.js index 688d6c7e64ae3b..06e6847860ecc5 100644 --- a/editor/test/state.js +++ b/editor/test/state.js @@ -38,8 +38,8 @@ describe( 'state', () => { it( 'should key by replaced blocks uid', () => { const original = blocks( undefined, {} ); const state = blocks( original, { - type: 'REPLACE_BLOCKS', - blockNodes: [ { uid: 'bananas' } ] + type: 'RESET_BLOCKS', + blocks: [ { uid: 'bananas' } ] } ); expect( Object.keys( state.byUid ) ).to.have.lengthOf( 1 ); @@ -49,8 +49,8 @@ describe( 'state', () => { it( 'should return with block updates', () => { const original = blocks( undefined, { - type: 'REPLACE_BLOCKS', - blockNodes: [ { + type: 'RESET_BLOCKS', + blocks: [ { uid: 'kumquat', attributes: {} } ] @@ -70,8 +70,8 @@ describe( 'state', () => { it( 'should insert block', () => { const original = blocks( undefined, { - type: 'REPLACE_BLOCKS', - blockNodes: [ { + type: 'RESET_BLOCKS', + blocks: [ { uid: 'chicken', blockType: 'core/test-block', attributes: {} @@ -90,32 +90,34 @@ describe( 'state', () => { expect( state.order ).to.eql( [ 'chicken', 'ribs' ] ); } ); - it( 'should switch the block', () => { + it( 'should replace the block', () => { const original = blocks( undefined, { - type: 'REPLACE_BLOCKS', - blockNodes: [ { + type: 'RESET_BLOCKS', + blocks: [ { uid: 'chicken', blockType: 'core/test-block', attributes: {} } ] } ); const state = blocks( original, { - type: 'SWITCH_BLOCK_TYPE', - uid: 'chicken', - block: { - uid: 'chicken', + type: 'REPLACE_BLOCKS', + uids: [ 'chicken' ], + blocks: [ { + uid: 'wings', blockType: 'core/freeform' - } + } ] } ); expect( Object.keys( state.byUid ) ).to.have.lengthOf( 1 ); expect( values( state.byUid )[ 0 ].blockType ).to.equal( 'core/freeform' ); + expect( values( state.byUid )[ 0 ].uid ).to.equal( 'wings' ); + expect( state.order ).to.eql( [ 'wings' ] ); } ); it( 'should move the block up', () => { const original = blocks( undefined, { - type: 'REPLACE_BLOCKS', - blockNodes: [ { + type: 'RESET_BLOCKS', + blocks: [ { uid: 'chicken', blockType: 'core/test-block', attributes: {} @@ -135,8 +137,8 @@ describe( 'state', () => { it( 'should not move the first block up', () => { const original = blocks( undefined, { - type: 'REPLACE_BLOCKS', - blockNodes: [ { + type: 'RESET_BLOCKS', + blocks: [ { uid: 'chicken', blockType: 'core/test-block', attributes: {} @@ -156,8 +158,8 @@ describe( 'state', () => { it( 'should move the block down', () => { const original = blocks( undefined, { - type: 'REPLACE_BLOCKS', - blockNodes: [ { + type: 'RESET_BLOCKS', + blocks: [ { uid: 'chicken', blockType: 'core/test-block', attributes: {} @@ -177,8 +179,8 @@ describe( 'state', () => { it( 'should not move the last block down', () => { const original = blocks( undefined, { - type: 'REPLACE_BLOCKS', - blockNodes: [ { + type: 'RESET_BLOCKS', + blocks: [ { uid: 'chicken', blockType: 'core/test-block', attributes: {} @@ -198,8 +200,8 @@ describe( 'state', () => { it( 'should remove the block', () => { const original = blocks( undefined, { - type: 'REPLACE_BLOCKS', - blockNodes: [ { + type: 'RESET_BLOCKS', + blocks: [ { uid: 'chicken', blockType: 'core/test-block', attributes: {} @@ -223,6 +225,33 @@ describe( 'state', () => { } } ); } ); + + it( 'should insert after the specified block uid', () => { + const original = blocks( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'kumquat', + blockType: 'core/test-block', + attributes: {} + }, { + uid: 'loquat', + blockType: 'core/test-block', + attributes: {} + } ] + } ); + + const state = blocks( original, { + type: 'INSERT_BLOCK', + after: 'kumquat', + block: { + uid: 'persimmon', + blockType: 'core/freeform' + } + } ); + + expect( Object.keys( state.byUid ) ).to.have.lengthOf( 3 ); + expect( state.order ).to.eql( [ 'kumquat', 'persimmon', 'loquat' ] ); + } ); } ); describe( 'hoveredBlock()', () => { @@ -245,6 +274,32 @@ describe( 'state', () => { expect( state ).to.be.null(); } ); + + it( 'should replace the hovered block', () => { + const state = hoveredBlock( 'chicken', { + type: 'REPLACE_BLOCKS', + uids: [ 'chicken' ], + blocks: [ { + uid: 'wings', + blockType: 'core/freeform' + } ] + } ); + + expect( state ).to.equal( 'wings' ); + } ); + + it( 'should keep the hovered block', () => { + const state = hoveredBlock( 'chicken', { + type: 'REPLACE_BLOCKS', + uids: [ 'ribs' ], + blocks: [ { + uid: 'wings', + blockType: 'core/freeform' + } ] + } ); + + expect( state ).to.equal( 'chicken' ); + } ); } ); describe( 'selectedBlock()', () => { @@ -386,31 +441,32 @@ describe( 'state', () => { expect( state ).to.eql( { uid: 'ribs', typing: true, focus: { editable: 'citation' } } ); } ); - it( 'should insert after the specified block uid', () => { - const original = blocks( undefined, { + it( 'should replace the selected block', () => { + const original = deepFreeze( { uid: 'chicken', typing: false, focus: { editable: 'citation' } } ); + const state = selectedBlock( original, { type: 'REPLACE_BLOCKS', - blockNodes: [ { - uid: 'kumquat', - blockType: 'core/test-block', - attributes: {} - }, { - uid: 'loquat', - blockType: 'core/test-block', - attributes: {} + uids: [ 'chicken' ], + blocks: [ { + uid: 'wings', + blockType: 'core/freeform' } ] } ); - const state = blocks( original, { - type: 'INSERT_BLOCK', - after: 'kumquat', - block: { - uid: 'persimmon', + expect( state ).to.eql( { uid: 'wings', typing: false, focus: {} } ); + } ); + + it( 'should keep the selected block', () => { + const original = deepFreeze( { uid: 'chicken', typing: false, focus: { editable: 'citation' } } ); + const state = selectedBlock( original, { + type: 'REPLACE_BLOCKS', + uids: [ 'ribs' ], + blocks: [ { + uid: 'wings', blockType: 'core/freeform' - } + } ] } ); - expect( Object.keys( state.byUid ) ).to.have.lengthOf( 3 ); - expect( state.order ).to.eql( [ 'kumquat', 'persimmon', 'loquat' ] ); + expect( state ).to.equal( original ); } ); } ); diff --git a/element/index.js b/element/index.js index b575ae14d36d85..8c8b2762a88755 100644 --- a/element/index.js +++ b/element/index.js @@ -79,7 +79,7 @@ export function renderToString( element ) { export function concatChildren( ...childrens ) { return childrens.reduce( ( memo, children, i ) => { Children.forEach( children, ( child, j ) => { - if ( 'string' !== typeof child ) { + if ( child && 'string' !== typeof child ) { child = cloneElement( child, { key: [ i, j ].join() } );