From 5824af3b975873d9ecb3afe61692c98e98ee31de Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 18 Dec 2017 15:40:14 -0500 Subject: [PATCH 1/6] Block API: Add getBlockSupport API For retrieving argument-based block supports --- blocks/api/index.js | 1 + blocks/api/registration.js | 29 ++++++++++++++++++-------- blocks/api/test/registration.js | 36 +++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/blocks/api/index.js b/blocks/api/index.js index 7698a50c40d6ff..3b16a7f5ec066a 100644 --- a/blocks/api/index.js +++ b/blocks/api/index.js @@ -24,6 +24,7 @@ export { getDefaultBlockName, getBlockType, getBlockTypes, + getBlockSupport, hasBlockSupport, isReusableBlock, } from './registration'; diff --git a/blocks/api/registration.js b/blocks/api/registration.js index 2bf044d7ae182f..48f4e8e3202b96 100644 --- a/blocks/api/registration.js +++ b/blocks/api/registration.js @@ -227,6 +227,26 @@ export function getBlockTypes() { return Object.values( blocks ); } +/** + * Returns the block support value for a feature, if defined. + * + * @param {(string|Object)} nameOrType Block name or type object + * @param {string} feature Feature to retrieve + * @param {*} defaultSupports Default value to return if not + * explicitly defined + * @return {?*} Block support value + */ +export function getBlockSupport( nameOrType, feature, defaultSupports ) { + const blockType = 'string' === typeof nameOrType ? + getBlockType( nameOrType ) : + nameOrType; + + return get( blockType, [ + 'supports', + feature, + ], defaultSupports ); +} + /** * Returns true if the block defines support for a feature, or false otherwise. * @@ -238,14 +258,7 @@ export function getBlockTypes() { * @return {boolean} Whether block supports feature. */ export function hasBlockSupport( nameOrType, feature, defaultSupports ) { - const blockType = 'string' === typeof nameOrType ? - getBlockType( nameOrType ) : - nameOrType; - - return !! get( blockType, [ - 'supports', - feature, - ], defaultSupports ); + return !! getBlockSupport( nameOrType, feature, defaultSupports ); } /** diff --git a/blocks/api/test/registration.js b/blocks/api/test/registration.js index f8cf2aa8fb8f42..92685c8baa748a 100644 --- a/blocks/api/test/registration.js +++ b/blocks/api/test/registration.js @@ -17,6 +17,7 @@ import { getDefaultBlockName, getBlockType, getBlockTypes, + getBlockSupport, hasBlockSupport, isReusableBlock, } from '../registration'; @@ -378,6 +379,41 @@ describe( 'blocks', () => { } ); } ); + describe( 'getBlockSupport', () => { + it( 'should return undefined if block has no supports', () => { + registerBlockType( 'core/test-block', { + ...defaultBlockSettings, + supports: { + bar: true, + }, + } ); + + expect( getBlockSupport( 'core/test-block', 'foo' ) ).toBe( undefined ); + } ); + + it( 'should return block supports value', () => { + registerBlockType( 'core/test-block', { + ...defaultBlockSettings, + supports: { + bar: true, + }, + } ); + + expect( getBlockSupport( 'core/test-block', 'bar' ) ).toBe( true ); + } ); + + it( 'should return custom default supports if block does not define support by name', () => { + registerBlockType( 'core/test-block', { + ...defaultBlockSettings, + supports: { + bar: true, + }, + } ); + + expect( getBlockSupport( 'core/test-block', 'foo', true ) ).toBe( true ); + } ); + } ); + describe( 'hasBlockSupport', () => { it( 'should return false if block has no supports', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); From 5300a565e765547b255e2e65753e1d179f955aa1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 18 Dec 2017 15:40:30 -0500 Subject: [PATCH 2/6] Block List: Support wrapper props via incoming prop --- editor/components/block-list/block.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 1faf219674d35a..2d91732ac18495 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -471,9 +471,12 @@ export class BlockListBlock extends Component { const { onMouseLeave, onReplace } = this.props; // Determine whether the block has props to apply to the wrapper. - let wrapperProps; + let wrapperProps = this.props.wrapperProps; if ( blockType.getEditWrapperProps ) { - wrapperProps = blockType.getEditWrapperProps( block.attributes ); + wrapperProps = { + ...wrapperProps, + ...blockType.getEditWrapperProps( block.attributes ), + }; } // Disable reasons: From 0058e5abd829706ea475c3f3e2efc8b5da4a049c Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 18 Dec 2017 15:40:41 -0500 Subject: [PATCH 3/6] Hooks: Add block hook for alignment --- blocks/hooks/align.js | 144 ++++++++++++++++++++++ blocks/hooks/index.js | 1 + blocks/hooks/test/align.js | 243 +++++++++++++++++++++++++++++++++++++ 3 files changed, 388 insertions(+) create mode 100644 blocks/hooks/align.js create mode 100644 blocks/hooks/test/align.js diff --git a/blocks/hooks/align.js b/blocks/hooks/align.js new file mode 100644 index 00000000000000..e805a50574fc28 --- /dev/null +++ b/blocks/hooks/align.js @@ -0,0 +1,144 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { assign, includes } from 'lodash'; + +/** + * WordPress dependencies + */ +import { getWrapperDisplayName } from '@wordpress/element'; +import { addFilter } from '@wordpress/hooks'; +import BlockControls from '../block-controls'; +import BlockAlignmentToolbar from '../block-alignment-toolbar'; + +/** + * Internal dependencies + */ +import { getBlockSupport, hasBlockSupport } from '../api'; + +/** + * Filters registered block settings, extending attributes to include `align`. + * + * @param {Object} settings Original block settings + * @return {Object} Filtered block settings + */ +export function addAttribute( settings ) { + if ( hasBlockSupport( settings, 'align' ) ) { + // Use Lodash's assign to gracefully handle if attributes are undefined + settings.attributes = assign( settings.attributes, { + align: { + type: 'string', + }, + } ); + } + + return settings; +} + +/** + * Returns an array of valid alignments for a block type depending on its + * defined supports. Returns an empty array if block does not support align. + * + * @param {String} blockName Block name to check + * @return {String[]} Valid alignments for block + */ +export function getBlockValidAlignments( blockName ) { + // Explicitly defined array set of valid alignments + const blockAlign = getBlockSupport( blockName, 'align' ); + if ( Array.isArray( blockAlign ) ) { + return blockAlign; + } + + const validAlignments = []; + if ( true === blockAlign ) { + // `true` includes all alignments... + validAlignments.push( 'left', 'center', 'right' ); + + // ...including wide alignments unless explicitly `false`. + if ( hasBlockSupport( blockName, 'wideAlign', true ) ) { + validAlignments.push( 'wide', 'full' ); + } + } + + return validAlignments; +} + +/** + * Override the default edit UI to include new toolbar controls for block + * alignment, if block defines support. + * + * @param {Function} BlockEdit Original component + * @return {Function} Wrapped component + */ +export function withToolbarControls( BlockEdit ) { + const WrappedBlockEdit = ( props ) => { + const validAlignments = getBlockValidAlignments( props.name ); + + const updateAlignment = ( nextAlign ) => props.setAttributes( { align: nextAlign } ); + + return [ + validAlignments.length > 0 && props.focus && ( + + + + ), + , + ]; + }; + WrappedBlockEdit.displayName = getWrapperDisplayName( BlockEdit, 'customClassName' ); + + return WrappedBlockEdit; +} + +/** + * Override the default block element to add alignment wrapper props. + * + * @param {Function} BlockListBlock Original component + * @return {Function} Wrapped component + */ +export function withAlign( BlockListBlock ) { + const WrappedComponent = ( props ) => { + const { align } = props.block.attributes; + const validAlignments = getBlockValidAlignments( props.block.name ); + + let wrapperProps = props.wrapperProps; + if ( includes( validAlignments, align ) ) { + wrapperProps = { ...wrapperProps, 'data-align': align }; + } + + return ; + }; + + WrappedComponent.displayName = getWrapperDisplayName( BlockListBlock, 'align' ); + + return WrappedComponent; +} + +/** + * Override props assigned to save component to inject alignment class name if + * block supports it. + * + * @param {Object} props Additional props applied to save element + * @param {Object} blockType Block type + * @param {Object} attributes Block attributes + * @return {Object} Filtered props applied to save element + */ +export function addAssignedAlign( props, blockType, attributes ) { + const { align } = attributes; + + if ( includes( getBlockValidAlignments( blockType ), align ) ) { + props.className = classnames( `align${ align }`, props.className ); + } + + return props; +} + +addFilter( 'blocks.registerBlockType', 'core/align/addAttribute', addAttribute ); +addFilter( 'editor.BlockListBlock', 'core/align/withAlign', withAlign ); +addFilter( 'blocks.BlockEdit', 'core/align/withToolbarControls', withToolbarControls ); +addFilter( 'blocks.getSaveContent.extraProps', 'core/align/extraProps', addAssignedAlign ); + diff --git a/blocks/hooks/index.js b/blocks/hooks/index.js index 4bc4cebbf3119e..0055037fa07f23 100644 --- a/blocks/hooks/index.js +++ b/blocks/hooks/index.js @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import './align'; import './anchor'; import './custom-class-name'; import './deprecated'; diff --git a/blocks/hooks/test/align.js b/blocks/hooks/test/align.js new file mode 100644 index 00000000000000..a7b02aa871fc6b --- /dev/null +++ b/blocks/hooks/test/align.js @@ -0,0 +1,243 @@ +/** + * External dependencies + */ +import { mount } from 'enzyme'; +import { noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import { + getBlockTypes, + registerBlockType, + unregisterBlockType, +} from '../../api'; +import { + getBlockValidAlignments, + withToolbarControls, + withAlign, + addAssignedAlign, +} from '../align'; + +describe( 'align', () => { + const blockSettings = { + save: noop, + category: 'common', + title: 'block title', + }; + + afterEach( () => { + getBlockTypes().forEach( block => { + unregisterBlockType( block.name ); + } ); + } ); + + describe( 'addAttribute()', () => { + const filterRegisterBlockType = applyFilters.bind( null, 'blocks.registerBlockType' ); + + it( 'should do nothing if the block settings does not define align support', () => { + const settings = filterRegisterBlockType( blockSettings ); + + expect( settings.attributes ).toBeUndefined(); + } ); + + it( 'should assign a new align attribute', () => { + const settings = filterRegisterBlockType( { + ...blockSettings, + supports: { + align: true, + }, + } ); + + expect( settings.attributes ).toHaveProperty( 'align' ); + } ); + } ); + + describe( 'getBlockValidAlignments()', () => { + it( 'should return an empty array if block does not define align support', () => { + registerBlockType( 'core/foo', blockSettings ); + const validAlignments = getBlockValidAlignments( 'core/foo' ); + + expect( validAlignments ).toEqual( [] ); + } ); + + it( 'should return all custom align set', () => { + registerBlockType( 'core/foo', { + ...blockSettings, + supports: { + align: [ 'left', 'right' ], + }, + } ); + const validAlignments = getBlockValidAlignments( 'core/foo' ); + + expect( validAlignments ).toEqual( [ 'left', 'right' ] ); + } ); + + it( 'should return all aligns if block defines align support', () => { + registerBlockType( 'core/foo', { + ...blockSettings, + supports: { + align: true, + }, + } ); + const validAlignments = getBlockValidAlignments( 'core/foo' ); + + expect( validAlignments ).toEqual( [ 'left', 'center', 'right', 'wide', 'full' ] ); + } ); + + it( 'should return all aligns except wide if wide align explicitly false', () => { + registerBlockType( 'core/foo', { + ...blockSettings, + supports: { + align: true, + wideAlign: false, + }, + } ); + const validAlignments = getBlockValidAlignments( 'core/foo' ); + + expect( validAlignments ).toEqual( [ 'left', 'center', 'right' ] ); + } ); + } ); + + describe( 'withToolbarControls', () => { + it( 'should do nothing if no valid alignments', () => { + registerBlockType( 'core/foo', blockSettings ); + + const EnhancedComponent = withToolbarControls( ( { wrapperProps } ) => ( +
+ ) ); + + const wrapper = mount( + + ); + + expect( wrapper.children() ).toHaveLength( 1 ); + } ); + + it( 'should render toolbar controls if valid alignments', () => { + registerBlockType( 'core/foo', { + ...blockSettings, + supports: { + align: true, + wideAlign: false, + }, + } ); + + const EnhancedComponent = withToolbarControls( ( { wrapperProps } ) => ( +
+ ) ); + + const wrapper = mount( + + ); + + expect( wrapper.children() ).toHaveLength( 2 ); + } ); + } ); + + describe( 'withAlign', () => { + it( 'should render with wrapper props', () => { + registerBlockType( 'core/foo', { + ...blockSettings, + supports: { + align: true, + wideAlign: false, + }, + } ); + + const EnhancedComponent = withAlign( ( { wrapperProps } ) => ( +
+ ) ); + + const wrapper = mount( + + ); + + expect( wrapper.childAt( 0 ).prop( 'wrapperProps' ) ).toEqual( { + 'data-align': 'left', + } ); + } ); + + it( 'should not render invalid align', () => { + registerBlockType( 'core/foo', { + ...blockSettings, + supports: { + align: true, + wideAlign: false, + }, + } ); + + const EnhancedComponent = withAlign( ( { wrapperProps } ) => ( +
+ ) ); + + const wrapper = mount( + + ); + + expect( wrapper.childAt( 0 ).prop( 'wrapperProps' ) ).toBeUndefined(); + } ); + } ); + + describe( 'addAssignedAlign', () => { + it( 'should do nothing if block does not support align', () => { + registerBlockType( 'core/foo', blockSettings ); + + const props = addAssignedAlign( { + className: 'foo', + }, 'core/foo', { + align: 'wide', + } ); + + expect( props ).toEqual( { + className: 'foo', + } ); + } ); + + it( 'should do add align classname if block supports align', () => { + registerBlockType( 'core/foo', { + ...blockSettings, + supports: { + align: true, + }, + } ); + + const props = addAssignedAlign( { + className: 'foo', + }, 'core/foo', { + align: 'wide', + } ); + + expect( props ).toEqual( { + className: 'alignwide foo', + } ); + } ); + } ); +} ); From 9bf6e72a29d625060844bfdee3621b6ccc6916d7 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 18 Dec 2017 15:40:52 -0500 Subject: [PATCH 4/6] Blocks: Port Audio block to use align supports --- blocks/hooks/align.js | 4 ++-- blocks/library/audio/index.js | 22 +++++----------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/blocks/hooks/align.js b/blocks/hooks/align.js index e805a50574fc28..6ecde224f4f750 100644 --- a/blocks/hooks/align.js +++ b/blocks/hooks/align.js @@ -40,8 +40,8 @@ export function addAttribute( settings ) { * Returns an array of valid alignments for a block type depending on its * defined supports. Returns an empty array if block does not support align. * - * @param {String} blockName Block name to check - * @return {String[]} Valid alignments for block + * @param {string} blockName Block name to check + * @return {string[]} Valid alignments for block */ export function getBlockValidAlignments( blockName ) { // Explicitly defined array set of valid alignments diff --git a/blocks/library/audio/index.js b/blocks/library/audio/index.js index c47b248296bb32..13e0ccb9b9e483 100644 --- a/blocks/library/audio/index.js +++ b/blocks/library/audio/index.js @@ -17,7 +17,6 @@ import './editor.scss'; import MediaUpload from '../../media-upload'; import RichText from '../../rich-text'; import BlockControls from '../../block-controls'; -import BlockAlignmentToolbar from '../../block-alignment-toolbar'; export const name = 'core/audio'; @@ -37,9 +36,6 @@ export const settings = { selector: 'audio', attribute: 'src', }, - align: { - type: 'string', - }, caption: { type: 'array', source: 'children', @@ -50,11 +46,8 @@ export const settings = { }, }, - getEditWrapperProps( attributes ) { - const { align } = attributes; - if ( 'left' === align || 'right' === align || 'wide' === align || 'full' === align ) { - return { 'data-align': align }; - } + supports: { + align: true, }, edit: class extends Component { @@ -69,10 +62,9 @@ export const settings = { }; } render() { - const { align, caption, id } = this.props.attributes; + const { caption, id } = this.props.attributes; const { setAttributes, isSelected } = this.props; const { editing, className, src } = this.state; - const updateAlignment = ( nextAlign ) => setAttributes( { align: nextAlign } ); const switchToEditing = () => { this.setState( { editing: true } ); }; @@ -95,10 +87,6 @@ export const settings = { }; const controls = isSelected && ( - +
From ebfa9890efcee2c4ade5962df3fcfbb47a9305f1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 19 Dec 2017 13:46:48 -0500 Subject: [PATCH 5/6] Hooks: Pass valid alignments to rendered align toolbar --- blocks/hooks/align.js | 1 + 1 file changed, 1 insertion(+) diff --git a/blocks/hooks/align.js b/blocks/hooks/align.js index 6ecde224f4f750..73fa7719b913f8 100644 --- a/blocks/hooks/align.js +++ b/blocks/hooks/align.js @@ -83,6 +83,7 @@ export function withToolbarControls( BlockEdit ) {
), From 06a301d74a8e15060a93c0b3cdcfd30fdb603664 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 16 Feb 2018 10:54:16 +0100 Subject: [PATCH 6/6] Blocks: Small tweaks to supports align property --- blocks/hooks/align.js | 8 ++++---- blocks/hooks/test/align.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/blocks/hooks/align.js b/blocks/hooks/align.js index 73fa7719b913f8..c323fb57073de0 100644 --- a/blocks/hooks/align.js +++ b/blocks/hooks/align.js @@ -78,8 +78,8 @@ export function withToolbarControls( BlockEdit ) { const updateAlignment = ( nextAlign ) => props.setAttributes( { align: nextAlign } ); return [ - validAlignments.length > 0 && props.focus && ( - + validAlignments.length > 0 && props.isSelected && ( + , ]; }; - WrappedBlockEdit.displayName = getWrapperDisplayName( BlockEdit, 'customClassName' ); + WrappedBlockEdit.displayName = getWrapperDisplayName( BlockEdit, 'align' ); return WrappedBlockEdit; } @@ -141,5 +141,5 @@ export function addAssignedAlign( props, blockType, attributes ) { addFilter( 'blocks.registerBlockType', 'core/align/addAttribute', addAttribute ); addFilter( 'editor.BlockListBlock', 'core/align/withAlign', withAlign ); addFilter( 'blocks.BlockEdit', 'core/align/withToolbarControls', withToolbarControls ); -addFilter( 'blocks.getSaveContent.extraProps', 'core/align/extraProps', addAssignedAlign ); +addFilter( 'blocks.getSaveContent.extraProps', 'core/align/addAssignedAlign', addAssignedAlign ); diff --git a/blocks/hooks/test/align.js b/blocks/hooks/test/align.js index a7b02aa871fc6b..9a14ae843abf76 100644 --- a/blocks/hooks/test/align.js +++ b/blocks/hooks/test/align.js @@ -116,7 +116,7 @@ describe( 'align', () => { ); @@ -140,7 +140,7 @@ describe( 'align', () => { );