diff --git a/blocks/api/factory.js b/blocks/api/factory.js index f3fb437c932e9a..e2ffc1468b5058 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -69,8 +69,8 @@ export function createBlock( name, blockAttributes = {} ) { * @param {Boolean} isMultiBlock Array of possible block transformations * @return {Function} Predicate that receives a block type. */ -const isTransformForBlockSource = ( sourceName, isMultiBlock = false ) => ( transform ) => ( - transform.type === 'block' && +const isTransformForBlockSource = ( sourceName, transformType, isMultiBlock = false ) => ( transform ) => ( + transform.type === transformType && transform.blocks.indexOf( sourceName ) !== -1 && ( ! isMultiBlock || transform.isMultiBlock ) ); @@ -83,10 +83,10 @@ const isTransformForBlockSource = ( sourceName, isMultiBlock = false ) => ( tran * @param {Boolean} isMultiBlock Array of possible block transformations * @return {Function} Predicate that receives a block type. */ -const createIsTypeTransformableFrom = ( sourceName, isMultiBlock = false ) => ( type ) => ( +const createIsTypeTransformableFrom = ( sourceName, transformType, isMultiBlock = false ) => ( type ) => ( !! find( get( type, 'transforms.from', [] ), - isTransformForBlockSource( sourceName, isMultiBlock ), + isTransformForBlockSource( sourceName, transformType, isMultiBlock ), ) ); @@ -111,7 +111,7 @@ export function getPossibleBlockTransformations( blocks ) { //compute the block that have a from transformation able to transfer blocks passed as argument. const blocksToBeTransformedFrom = filter( getBlockTypes(), - createIsTypeTransformableFrom( sourceBlockName, isMultiBlock ), + createIsTypeTransformableFrom( sourceBlockName, 'block', isMultiBlock ), ).map( type => type.name ); const blockType = getBlockType( sourceBlockName ); @@ -136,6 +136,22 @@ export function getPossibleBlockTransformations( blocks ) { }, [] ); } +/** + * Gets all possible shortcut transforms based on a block name. + * + * @param {String} name Block name. + * @return {Array} Array of transforms. + */ +export function getPossibleShortcutTransformations( name ) { + const transformsFrom = getBlockTypes() + .reduce( ( acc, blockType ) => [ ...acc, ...get( blockType, 'transforms.from', [] ) ], [] ) + .filter( isTransformForBlockSource( name, 'shortcut', false ) ); + const transformsTo = get( getBlockType( name ), 'transforms.to', [] ) + .filter( ( { type } ) => type === 'shortcut' ); + + return [ ...transformsFrom, ...transformsTo ]; +} + /** * Switch one or more blocks into one or more blocks of the new block type. * diff --git a/blocks/api/index.js b/blocks/api/index.js index 7f0653a133c5b3..b9904d52406720 100644 --- a/blocks/api/index.js +++ b/blocks/api/index.js @@ -1,4 +1,10 @@ -export { createBlock, getPossibleBlockTransformations, switchToBlockType, createReusableBlock } from './factory'; +export { + createBlock, + getPossibleBlockTransformations, + getPossibleShortcutTransformations, + switchToBlockType, + createReusableBlock, +} from './factory'; export { default as parse, getBlockAttributes } from './parser'; export { default as rawHandler } from './raw-handling'; export { diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index c68d85fefdbbb4..a5090164c54086 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -79,6 +79,19 @@ registerBlockType( 'core/heading', { } ); }, }, + ...'23456'.split( '' ).map( ( level ) => ( { + type: 'shortcut', + blocks: [ 'core/paragraph' ], + shortcut: level, + transform( blockAttributes ) { + return blockAttributes.map( ( { content } ) => { + return createBlock( 'core/heading', { + nodeName: `H${ level }`, + content, + } ); + } ); + }, + } ) ), ], to: [ { @@ -90,6 +103,24 @@ registerBlockType( 'core/heading', { } ); }, }, + ...'23456'.split( '' ).map( ( level ) => ( { + type: 'shortcut', + shortcut: level, + transform( blockAttributes ) { + return blockAttributes.map( ( { content, nodeName } ) => { + if ( nodeName === `H${ level }` ) { + return createBlock( 'core/paragraph', { + content, + } ); + } else { + return createBlock( 'core/heading', { + nodeName: `H${ level }`, + content, + } ); + } + } ); + }, + } ) ), ], }, diff --git a/blocks/library/list/index.js b/blocks/library/list/index.js index cec6b4aa69da5d..44cdce96d67a41 100644 --- a/blocks/library/list/index.js +++ b/blocks/library/list/index.js @@ -113,6 +113,19 @@ registerBlockType( 'core/list', { } ); }, }, + ...[ 'OL', 'UL' ].map( ( tag ) => ( { + type: 'shortcut', + blocks: [ 'core/paragraph' ], + shortcut: tag.charAt( 0 ).toLowerCase(), + transform( blockAttributes ) { + const items = blockAttributes.map( ( { content } ) => content ); + const hasItems = ! items.every( isEmpty ); + return createBlock( 'core/list', { + nodeName: tag, + values: hasItems ? items.map( ( content, index ) =>
  • { content }
  • ) : [], + } ); + }, + } ) ), { type: 'block', blocks: [ 'core/quote' ], @@ -175,6 +188,16 @@ registerBlockType( 'core/list', { } ); }, }, + ...[ 'OL', 'UL' ].map( ( tag ) => ( { + type: 'shortcut', + shortcut: tag.charAt( 0 ).toLowerCase(), + transform( blockAttributes ) { + return createBlock( 'core/list', { + nodeName: 'OL', + values: blockAttributes.reduce( ( acc, { values } ) => [ ...acc, ...values ], [] ), + } ); + }, + } ) ), ], }, diff --git a/editor/components/block-list/index.js b/editor/components/block-list/index.js index 19f7b99bffe713..055fd3a5358901 100644 --- a/editor/components/block-list/index.js +++ b/editor/components/block-list/index.js @@ -10,6 +10,10 @@ import { mapValues, sortBy, throttle, + find, + first, + castArray, + every, } from 'lodash'; import scrollIntoView from 'dom-scroll-into-view'; import 'element-closest'; @@ -18,7 +22,8 @@ import 'element-closest'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { serialize } from '@wordpress/blocks'; +import { serialize, getPossibleShortcutTransformations } from '@wordpress/blocks'; +import { keycodes } from '@wordpress/utils'; /** * Internal dependencies @@ -35,9 +40,11 @@ import { getSelectedBlock, isSelectionEnabled, } from '../../store/selectors'; -import { startMultiSelect, stopMultiSelect, multiSelect, selectBlock } from '../../store/actions'; +import { startMultiSelect, stopMultiSelect, multiSelect, selectBlock, replaceBlocks } from '../../store/actions'; import { isInputField } from '../../utils/dom'; +const { isAccess } = keycodes; + class BlockList extends Component { constructor( props ) { super( props ); @@ -53,6 +60,7 @@ class BlockList extends Component { // Browser does not fire `*move` event when the pointer position changes // relative to the document, so fire it with the last known position. this.onScroll = () => this.onPointerMove( { clientY: this.lastClientY } ); + this.onKeyDown = this.onKeyDown.bind( this ); this.lastClientY = 0; this.nodes = {}; @@ -71,6 +79,32 @@ class BlockList extends Component { } componentWillReceiveProps( nextProps ) { + const prevCommonName = this.commonName; + + if ( nextProps.selectedBlock ) { + this.blocks = [ nextProps.selectedBlock ]; + this.commonName = nextProps.selectedBlock.name; + } else if ( nextProps.multiSelectedBlocks.length ) { + this.blocks = nextProps.multiSelectedBlocks; + + const firstName = first( nextProps.multiSelectedBlocks ).name + + if ( every( nextProps.multiSelectedBlocks, ( { name } ) => name === firstName ) ) { + this.commonName = firstName; + } else { + delete this.commonName; + } + } else { + delete this.blocks; + delete this.commonName; + } + + if ( ! this.commonName ) { + delete this.shortcutTransforms; + } else if ( this.commonName !== prevCommonName ) { + this.shortcutTransforms = getPossibleShortcutTransformations( this.commonName ); + } + if ( isEqual( this.props.multiSelectedBlockUids, nextProps.multiSelectedBlockUids ) ) { return; } @@ -214,11 +248,29 @@ class BlockList extends Component { } } + onKeyDown( event ) { + const { onReplace } = this.props; + + if ( ! this.shortcutTransforms ) { + return; + } + + const transform = find( this.shortcutTransforms, ( { shortcut } ) => isAccess( event, shortcut ) ); + + if ( transform ) { + const blocks = castArray( transform.transform( this.blocks.map( ( { attributes } ) => attributes ) ) ); + + onReplace( this.blocks.map( ( { uid } ) => uid ), blocks ); + + return; + } + } + render() { const { blocks, showContextualToolbar } = this.props; return ( -
    +
    { !! blocks.length && } { map( blocks, ( uid ) => (