Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block Switcher: Adding the transformations API and the block switcher #429

Merged
merged 4 commits into from
Apr 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion blocks/api/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@
* External dependencies
*/
import uuid from 'uuid/v4';
import { get } from 'lodash';

/**
* Internal dependencies
*/
import { getBlockSettings } from './registration';

/**
* Returns a block object given its type and attributes
*
* @param {Object} blockType BlockType
* @param {String} blockType BlockType
* @param {Object} attributes Block attributes
* @return {Object} Block object
*/
Expand All @@ -17,3 +23,31 @@ export function createBlock( blockType, attributes = {} ) {
attributes
};
}

/**
* Switch Block Type and returns the updated block
*
* @param {Object} block Block object
* @param {string} blockType BlockType
* @return {Object?} Block object
*/
export function switchToBlockType( block, blockType ) {
// 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', [] );
const transformationsTo = get( sourceSettings, 'transforms.to', [] );
const transformation =
transformationsTo.find( t => t.blocks.indexOf( blockType ) !== -1 ) ||
transformationsFrom.find( t => t.blocks.indexOf( block.blockType ) !== -1 );

if ( ! transformation ) {
return null;
}

return Object.assign( {
uid: block.uid,
attributes: transformation.transform( block.attributes ),
blockType
} );
}
2 changes: 1 addition & 1 deletion blocks/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import * as query from 'hpq';

export { query };
export { createBlock } from './factory';
export { createBlock, switchToBlockType } from './factory';
export { default as parse } from './parser';
export { default as serialize } from './serializer';
export { getCategories } from './categories';
Expand Down
97 changes: 96 additions & 1 deletion blocks/api/test/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ import { expect } from 'chai';
/**
* Internal dependencies
*/
import { createBlock } from '../factory';
import { createBlock, switchToBlockType } from '../factory';
import { getBlocks, unregisterBlock, setUnknownTypeHandler, registerBlock } from '../registration';

describe( 'block factory', () => {
afterEach( () => {
setUnknownTypeHandler( undefined );
getBlocks().forEach( ( block ) => {
unregisterBlock( block.slug );
} );
} );

describe( 'createBlock()', () => {
it( 'should create a block given its blockType and attributes', () => {
const block = createBlock( 'core/test-block', {
Expand All @@ -22,4 +30,91 @@ describe( 'block factory', () => {
expect( block.uid ).to.be.a( 'string' );
} );
} );

describe( 'switchBlockType()', () => {
it( 'should switch the blockType of a block using the "transform form"', () => {
registerBlock( 'core/updated-text-block', {
transforms: {
from: [ {
blocks: [ 'core/text-block' ],
transform: ( { value } ) => {
return {
value: 'chicken ' + value
};
}
} ]
}
} );
registerBlock( 'core/text-block', {} );

const block = {
uid: 1,
blockType: 'core/text-block',
attributes: {
value: 'ribs'
}
};

const updateBlock = switchToBlockType( block, 'core/updated-text-block' );

expect( updateBlock ).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"', () => {
registerBlock( 'core/updated-text-block', {} );
registerBlock( 'core/text-block', {
transforms: {
to: [ {
blocks: [ 'core/updated-text-block' ],
transform: ( { value } ) => {
return {
value: 'chicken ' + value
};
}
} ]
}
} );

const block = {
uid: 1,
blockType: 'core/text-block',
attributes: {
value: 'ribs'
}
};

const updateBlock = switchToBlockType( block, 'core/updated-text-block' );

expect( updateBlock ).to.eql( {
uid: 1,
blockType: 'core/updated-text-block',
attributes: {
value: 'chicken ribs'
}
} );
} );

it( 'should return null if no transformation is found', () => {
registerBlock( 'core/updated-text-block', {} );
registerBlock( 'core/text-block', {} );

const block = {
uid: 1,
blockType: 'core/text-block',
attributes: {
value: 'ribs'
}
};

const updateBlock = switchToBlockType( block, 'core/updated-text-block' );

expect( updateBlock ).to.be.null();
} );
} );
} );
33 changes: 33 additions & 0 deletions blocks/library/heading/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,38 @@ registerBlock( 'core/heading', {
style={ align ? { textAlign: align } : null }
dangerouslySetInnerHTML={ { __html: content } } />
);
},

transforms: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Thinking about conventions of property order, I quite like in a typical React component the render is usually the last member of the class, and had thought we could do similar with edit / save, having other properties occur earlier. Do you think there's any value in this or another convention?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I always forget about this convention sorry. Will fix later

from: [
{
type: 'block',
blocks: [ 'core/text' ],
transform: ( { content, align } ) => {
if ( Array.isArray( content ) ) {
// TODO this appears to always be true?
// TODO reject the switch if more than one paragraph
content = content[ 0 ];
}
return {
tag: 'H2',
content,
align
};
}
}
],
to: [
{
type: 'block',
blocks: [ 'core/text' ],
transform: ( { content, align } ) => {
return {
content,
align
};
}
}
]
}
} );
6 changes: 3 additions & 3 deletions blocks/library/list/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
* Internal dependencies
*/
import './style.scss';
import { registerBlock, query } from 'api';
import { registerBlock, query as hpq } from 'api';
import Editable from 'components/editable';

const { html, prop } = query;
const { html, prop, query } = hpq;

registerBlock( 'core/list', {
title: wp.i18n.__( 'List' ),
Expand All @@ -14,7 +14,7 @@ registerBlock( 'core/list', {

attributes: {
listType: prop( 'ol,ul', 'nodeName' ),
items: query.query( 'li', {
items: query( 'li', {
value: html()
} )
},
Expand Down
98 changes: 98 additions & 0 deletions editor/components/block-switcher/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* External dependencies
*/
import { connect } from 'react-redux';
import { uniq, get, reduce } from 'lodash';

/**
* Internal dependencies
*/
import './style.scss';
import IconButton from 'components/icon-button';

class BlockSwitcher extends wp.element.Component {
constructor() {
super( ...arguments );
this.toggleMenu = this.toggleMenu.bind( this );
this.state = {
open: false
};
}

toggleMenu() {
this.setState( {
open: ! this.state.open
} );
}

switchBlockType( blockType ) {
return () => {
this.setState( {
open: false
} );
this.props.onTransform( this.props.block, blockType );
};
}

render() {
const blockSettings = wp.blocks.getBlockSettings( this.props.block.blockType );
const blocksToBeTransformedFrom = reduce( wp.blocks.getBlocks(), ( memo, block ) => {
const transformFrom = get( block, 'transforms.from', [] );
const transformation = transformFrom.find( t => t.blocks.indexOf( this.props.block.blockType ) !== -1 );
return transformation ? memo.concat( [ block.slug ] ) : memo;
}, [] );
const blocksToBeTransformedTo = get( blockSettings, 'transforms.to', [] )
.reduce( ( memo, transformation ) => memo.concat( transformation.blocks ), [] );
const allowedBlocks = uniq( blocksToBeTransformedFrom.concat( blocksToBeTransformedTo ) )
.reduce( ( memo, blockType ) => {
const block = wp.blocks.getBlockSettings( blockType );
return !! block ? memo.concat( block ) : memo;
}, [] );

if ( ! allowedBlocks.length ) {
return null;
}

return (
<div className="editor-block-switcher">
<IconButton
className="editor-block-switcher__toggle"
icon={ blockSettings.icon }
onClick={ this.toggleMenu }
>
<div className="editor-block-switcher__arrow" />
</IconButton>
{ this.state.open &&
<div className="editor-block-switcher__menu">
<div className="editor-block-switcher__menu-arrow" />
{ allowedBlocks.map( ( { slug, title, icon } ) => (
<IconButton
key={ slug }
onClick={ this.switchBlockType( slug ) }
className="editor-block-switcher__menu-item"
icon={ icon }
>
{ title }
</IconButton>
) ) }
</div>
}
</div>
);
}
}

export default connect(
( state, ownProps ) => ( {
block: state.blocks.byUid[ ownProps.uid ]
} ),
( dispatch, ownProps ) => ( {
onTransform( block, blockType ) {
dispatch( {
type: 'SWITCH_BLOCK_TYPE',
uid: ownProps.uid,
block: wp.blocks.switchToBlockType( block, blockType )
} );
}
} )
)( BlockSwitcher );
Loading