diff --git a/blocks/api/factory.js b/blocks/api/factory.js index bb27df7e56d09..c5514147f121c 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -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 */ @@ -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 + } ); +} diff --git a/blocks/api/index.js b/blocks/api/index.js index 2d43c110f7069..be8351e5ca643 100644 --- a/blocks/api/index.js +++ b/blocks/api/index.js @@ -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'; diff --git a/blocks/api/test/factory.js b/blocks/api/test/factory.js index d8c5accc5fcbe..cb1a0265738d5 100644 --- a/blocks/api/test/factory.js +++ b/blocks/api/test/factory.js @@ -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', { @@ -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(); + } ); + } ); } ); diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index 512c197e0c999..dd9d3137d540a 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -52,5 +52,38 @@ registerBlock( 'core/heading', { style={ align ? { textAlign: align } : null } dangerouslySetInnerHTML={ { __html: content } } /> ); + }, + + transforms: { + 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 + }; + } + } + ] } } ); diff --git a/blocks/library/list/index.js b/blocks/library/list/index.js index 8caae01983ddd..7b8112444a4f0 100644 --- a/blocks/library/list/index.js +++ b/blocks/library/list/index.js @@ -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' ), @@ -14,7 +14,7 @@ registerBlock( 'core/list', { attributes: { listType: prop( 'ol,ul', 'nodeName' ), - items: query.query( 'li', { + items: query( 'li', { value: html() } ) }, diff --git a/editor/components/block-switcher/index.js b/editor/components/block-switcher/index.js new file mode 100644 index 0000000000000..e93df85dfd589 --- /dev/null +++ b/editor/components/block-switcher/index.js @@ -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 ( +