Skip to content

Commit

Permalink
Block API: Define granular hooks for block filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth authored and gziolo committed Nov 14, 2017
1 parent e5bdcd1 commit 7f3a223
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 50 deletions.
1 change: 1 addition & 0 deletions blocks/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export {
getDefaultBlockName,
getBlockType,
getBlockTypes,
hasBlockSupport,
} from './registration';
20 changes: 20 additions & 0 deletions blocks/api/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,23 @@ export function getBlockType( name ) {
export function getBlockTypes() {
return Object.values( blocks );
}

/**
* Returns true if the block defines support for a feature, or false otherwise
*
* @param {(String|Object)} nameOrType Block name or type object
* @param {String} feature Feature to test
* @param {Boolean} defaultSupports Whether feature is supported by
* default if not explicitly defined
* @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 );
}
3 changes: 2 additions & 1 deletion blocks/api/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Component, createElement, renderToString, cloneElement, Children } from
* Internal dependencies
*/
import { getBlockType, getUnknownTypeHandlerName } from './registration';
import { applyFilters } from '../hooks';

/**
* Returns the block's default classname from its name
Expand Down Expand Up @@ -55,7 +56,7 @@ export function getSaveContent( blockType, attributes ) {
return element;
}

const extraProps = {};
const extraProps = applyFilters( 'getSaveContent.extraProps', {}, blockType, attributes );
if ( !! className ) {
const updatedClassName = classnames(
className,
Expand Down
64 changes: 64 additions & 0 deletions blocks/api/test/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getDefaultBlockName,
getBlockType,
getBlockTypes,
hasBlockSupport,
} from '../registration';

describe( 'blocks', () => {
Expand Down Expand Up @@ -272,4 +273,67 @@ describe( 'blocks', () => {
] );
} );
} );

describe( 'hasBlockSupport', () => {
it( 'should return false if block has no supports', () => {
registerBlockType( 'core/test-block', defaultBlockSettings );

expect( hasBlockSupport( 'core/test-block', 'foo' ) ).toBe( false );
} );

it( 'should return false if block does not define support by name', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
supports: {
bar: true,
},
} );

expect( hasBlockSupport( 'core/test-block', 'foo' ) ).toBe( false );
} );

it( 'should return custom default supports if block does not define support by name', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
supports: {
bar: true,
},
} );

expect( hasBlockSupport( 'core/test-block', 'foo', true ) ).toBe( true );
} );

it( 'should return true if block type supports', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
supports: {
foo: true,
},
} );

expect( hasBlockSupport( 'core/test-block', 'foo' ) ).toBe( true );
} );

it( 'should return true if block author defines unsupported but truthy value', () => {
registerBlockType( 'core/test-block', {
...defaultBlockSettings,
supports: {
foo: 'hmmm',
},
} );

expect( hasBlockSupport( 'core/test-block', 'foo' ) ).toBe( true );
} );

it( 'should handle block settings object as argument to test', () => {
const settings = {
...defaultBlockSettings,
supports: {
foo: true,
},
};

expect( hasBlockSupport( settings, 'foo' ) ).toBe( true );
} );
} );
} );
3 changes: 2 additions & 1 deletion blocks/block-edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Internal dependencies
*/
import { getBlockType } from '../api';
import { applyFilters } from '../hooks';

function BlockEdit( props ) {
const { name, ...editProps } = props;
Expand All @@ -19,7 +20,7 @@ function BlockEdit( props ) {
Edit = blockType.edit || blockType.save;
}

return <Edit { ...editProps } />;
return applyFilters( 'BlockEdit', <Edit { ...editProps } />, props );
}

export default BlockEdit;
113 changes: 67 additions & 46 deletions blocks/hooks/anchor.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { assign, get } from 'lodash';
import { assign } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { source } from '../api';
import { source, hasBlockSupport } from '../api';
import InspectorControls from '../inspector-controls';

/**
Expand All @@ -22,57 +22,78 @@ import InspectorControls from '../inspector-controls';
*/
const ANCHOR_REGEX = /[\s#]/g;

export default function anchor( settings ) {
if ( ! get( settings.supports, 'anchor' ) ) {
return settings;
/**
* Filters registered block settings, extending attributes with anchor using ID
* of the first node
*
* @param {Object} settings Original block settings
* @return {Object} Filtered block settings
*/
export function addAttribute( settings ) {
if ( hasBlockSupport( settings, 'anchor' ) ) {
// Use Lodash's assign to gracefully handle if attributes are undefined
settings.attributes = assign( settings.attributes, {
anchor: {
type: 'string',
source: source.attr( '*', 'id' ),
},
} );
}

// Extend attributes with anchor determined by ID on the first node, using
// assign to gracefully handle if original attributes are undefined.
assign( settings.attributes, {
anchor: {
type: 'string',
source: source.attr( '*', 'id' ),
},
} );
return settings;
}

// Override the default edit UI to include a new block inspector control
// for assigning the anchor ID
const { edit: Edit } = settings;
settings.edit = function( props ) {
return [
<Edit key="edit" { ...props } />,
props.focus && (
<InspectorControls key="inspector">
<InspectorControls.TextControl
label={ __( 'HTML Anchor' ) }
help={ __( 'Anchors lets you link directly to a section on a page.' ) }
value={ props.attributes.anchor || '' }
onChange={ ( nextValue ) => {
nextValue = nextValue.replace( ANCHOR_REGEX, '-' );
/**
* Override the default edit UI to include a new block inspector control for
* assigning the anchor ID, if block supports anchor
*
* @param {Element} element Original edit element
* @param {Object} props Props passed to BlockEdit
* @return {Element} Filtered edit element
*/
export function addInspectorControl( element, props ) {
if ( hasBlockSupport( props.name, 'anchor' ) && props.focus ) {
element = [
cloneElement( element, { key: 'edit' } ),
<InspectorControls key="inspector">
<InspectorControls.TextControl
label={ __( 'HTML Anchor' ) }
help={ __( 'Anchors lets you link directly to a section on a page.' ) }
value={ props.attributes.anchor || '' }
onChange={ ( nextValue ) => {
nextValue = nextValue.replace( ANCHOR_REGEX, '-' );

props.setAttributes( {
anchor: nextValue,
} );
} } />
</InspectorControls>
),
props.setAttributes( {
anchor: nextValue,
} );
} } />
</InspectorControls>,
];
};
}

// Override the default block serialization to clone the returned element,
// injecting the attribute ID.
const { save } = settings;
settings.save = function( { attributes } ) {
const { anchor: id } = attributes;
return element;
}

let result = save( ...arguments );
if ( 'string' !== typeof result && id ) {
result = cloneElement( result, { id } );
}
/**
* Override props assigned to save component to inject anchor ID, if block
* supports anchor. This is only applied if the block's save result is an
* element and not a markup string.
*
* @param {Object} extraProps Additional props applied to save element
* @param {Object} blockType Block type
* @param {Object} attributes Current block attributes
* @return {Object} Filtered props applied to save element
*/
export function addSaveProps( extraProps, blockType, attributes ) {
if ( hasBlockSupport( blockType, 'anchor' ) ) {
extraProps.id = attributes.anchor;
}

return result;
};
return extraProps;
}

return settings;
export default function anchor( { addFilter } ) {
addFilter( 'registerBlockType', 'core\anchor-attribute', addAttribute );
addFilter( 'BlockEdit', 'core\anchor-inspector-control', addInspectorControl );
addFilter( 'getSaveContent.extraProps', 'core\anchor-save-props', addSaveProps );
}
6 changes: 4 additions & 2 deletions blocks/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import createHooks from '@wordpress/hooks';
*/
import anchor from './anchor';

const hooks = createHooks();

const {
addAction,
addFilter,
Expand All @@ -23,7 +25,7 @@ const {
didFilter,
hasAction,
hasFilter,
} = createHooks();
} = hooks;

export {
addAction,
Expand All @@ -42,4 +44,4 @@ export {
hasFilter,
};

addFilter( 'registerBlockType', 'core\supports-anchor', anchor );
anchor( hooks );
79 changes: 79 additions & 0 deletions blocks/hooks/test/anchor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* External dependencies
*/
import { noop } from 'lodash';

/**
* External dependencies
*/
import createHooks from '@wordpress/hooks';

/**
* Internal dependencies
*/
import anchor from '../anchor';

describe( 'anchor', () => {
const hooks = createHooks();

let blockSettings;
beforeEach( () => {
anchor( hooks );

blockSettings = {
save: noop,
category: 'common',
title: 'block title',
};
} );

afterEach( () => {
hooks.removeAllFilters( 'registerBlockType' );
hooks.removeAllFilters( 'getSaveContent.extraProps' );
} );

describe( 'addAttribute()', () => {
const addAttribute = hooks.applyFilters.bind( null, 'registerBlockType' );

it( 'should do nothing if the block settings do not define anchor support', () => {
const settings = addAttribute( blockSettings );

expect( settings.attributes ).toBe( undefined );
} );

it( 'should assign a new anchor attribute', () => {
const settings = addAttribute( {
...blockSettings,
supports: {
anchor: true,
},
} );

expect( settings.attributes ).toHaveProperty( 'anchor' );
} );
} );

describe( 'addSaveProps', () => {
const addSaveProps = hooks.applyFilters.bind( null, 'getSaveContent.extraProps' );

it( 'should do nothing if the block settings do not define anchor support', () => {
const attributes = { anchor: 'foo' };
const extraProps = addSaveProps( blockSettings, attributes );

expect( extraProps ).not.toHaveProperty( 'id' );
} );

it( 'should inject anchor attribute ID', () => {
const attributes = { anchor: 'foo' };
blockSettings = {
...blockSettings,
supports: {
anchor: true,
},
};
const extraProps = addSaveProps( {}, blockSettings, attributes );

expect( extraProps.id ).toBe( 'foo' );
} );
} );
} );

0 comments on commit 7f3a223

Please sign in to comment.