From 66a8d575c13fda2d9bc239decc405b01a5fa726d Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 2 Nov 2017 14:52:38 -0400 Subject: [PATCH 1/3] Extensibility: Define anchor behavior as filtered blocks support --- blocks/api/factory.js | 5 -- blocks/api/parser.js | 7 +- blocks/api/registration.js | 11 ++- blocks/api/serializer.js | 4 - blocks/api/test/factory.js | 24 ------ blocks/api/test/parser.js | 20 ----- blocks/api/test/serializer.js | 14 ---- blocks/hooks/anchor.js | 78 +++++++++++++++++++ blocks/hooks/index.js | 45 +++++++++++ blocks/index.js | 1 + blocks/library/heading/index.js | 4 +- docs/block-api.md | 11 +-- .../block-inspector/advanced-controls.js | 58 +------------- package-lock.json | 23 +++--- package.json | 1 + 15 files changed, 162 insertions(+), 144 deletions(-) create mode 100644 blocks/hooks/anchor.js create mode 100644 blocks/hooks/index.js diff --git a/blocks/api/factory.js b/blocks/api/factory.js index 796c60fac2309..7beaf4b596163 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -46,11 +46,6 @@ export function createBlock( name, blockAttributes = {} ) { return result; }, {} ); - // Keep the anchor if the block supports it - if ( blockType.supportAnchor && blockAttributes.anchor ) { - attributes.anchor = blockAttributes.anchor; - } - // Keep the className if the block supports it if ( blockType.className !== false && blockAttributes.className ) { attributes.className = blockAttributes.className; diff --git a/blocks/api/parser.js b/blocks/api/parser.js index ff78f55f0b8fa..159f8c9ecdb7b 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { parse as hpqParse, attr } from 'hpq'; +import { parse as hpqParse } from 'hpq'; import { mapValues, reduce, pickBy } from 'lodash'; /** @@ -148,11 +148,6 @@ export function getBlockAttributes( blockType, innerHTML, attributes ) { return result; }, {} ); - // If the block supports anchor, parse the id - if ( blockType.supportAnchor ) { - blockAttributes.anchor = hpqParse( innerHTML, attr( '*', 'id' ) ); - } - // If the block supports a custom className parse it if ( blockType.className !== false && attributes && attributes.className ) { blockAttributes.className = attributes.className; diff --git a/blocks/api/registration.js b/blocks/api/registration.js index ed6c4fa1a436c..e216fdc9de315 100644 --- a/blocks/api/registration.js +++ b/blocks/api/registration.js @@ -10,6 +10,11 @@ import { get, isFunction, some } from 'lodash'; */ import { getCategories } from './categories'; +/** + * Internal dependencies + */ +import { applyFilters } from '../hooks'; + /** * Block settings keyed by block name. * @@ -113,13 +118,15 @@ export function registerBlockType( name, settings ) { if ( ! settings.icon ) { settings.icon = 'block-default'; } - const block = blocks[ name ] = { + settings = { name, attributes: get( window._wpBlocksAttributes, name ), ...settings, }; - return block; + settings = applyFilters( 'registerBlockType', settings, name ); + + return blocks[ name ] = settings; } /** diff --git a/blocks/api/serializer.js b/blocks/api/serializer.js index 2c6980810e9c3..de67f4203e0a4 100644 --- a/blocks/api/serializer.js +++ b/blocks/api/serializer.js @@ -65,10 +65,6 @@ export function getSaveContent( blockType, attributes ) { extraProps.className = updatedClassName; } - if ( blockType.supportAnchor && attributes.anchor ) { - extraProps.id = attributes.anchor; - } - return cloneElement( element, extraProps ); }; const contentWithClassname = Children.map( saveContent, addAdvancedAttributes ); diff --git a/blocks/api/test/factory.js b/blocks/api/test/factory.js index d5a71698c29b1..893d1ec59e74f 100644 --- a/blocks/api/test/factory.js +++ b/blocks/api/test/factory.js @@ -57,30 +57,6 @@ describe( 'block factory', () => { expect( typeof block.uid ).toBe( 'string' ); } ); - it( 'should keep the anchor if the block supports it', () => { - registerBlockType( 'core/test-block', { - attributes: { - align: { - type: 'string', - }, - }, - save: noop, - category: 'common', - title: 'test block', - supportAnchor: true, - } ); - const block = createBlock( 'core/test-block', { - align: 'left', - anchor: 'chicken', - } ); - - expect( block.attributes ).toEqual( { - anchor: 'chicken', - align: 'left', - } ); - expect( block.isValid ).toBe( true ); - } ); - it( 'should keep the className if the block supports it', () => { registerBlockType( 'core/test-block', { attributes: {}, diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 9d248cf6501fd..cd3cf20177b3f 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -157,26 +157,6 @@ describe( 'block parser', () => { } ); } ); - it( 'should parse the anchor if the block supports it', () => { - const blockType = { - attributes: { - content: { - type: 'string', - source: text( 'div' ), - }, - }, - supportAnchor: true, - }; - - const innerHTML = '
Ribs
'; - const attrs = {}; - - expect( getBlockAttributes( blockType, innerHTML, attrs ) ).toEqual( { - content: 'Ribs', - anchor: 'chicken', - } ); - } ); - it( 'should parse the className if the block supports it', () => { const blockType = { attributes: {}, diff --git a/blocks/api/test/serializer.js b/blocks/api/test/serializer.js index 7a64ebd33e3e7..71f116fd77eec 100644 --- a/blocks/api/test/serializer.js +++ b/blocks/api/test/serializer.js @@ -121,20 +121,6 @@ describe( 'block serializer', () => { expect( saved ).toBe( '
Bananas
' ); } ); - - it( 'should add an id if the block supports anchors', () => { - const saved = getSaveContent( - { - save: ( { attributes } ) => createElement( 'div', null, attributes.fruit ), - supportAnchor: true, - name: 'myplugin/fruit', - className: false, - }, - { fruit: 'Bananas', anchor: 'my-fruit' } - ); - - expect( saved ).toBe( '
Bananas
' ); - } ); } ); describe( 'component save', () => { diff --git a/blocks/hooks/anchor.js b/blocks/hooks/anchor.js new file mode 100644 index 0000000000000..0423b6d5c0f79 --- /dev/null +++ b/blocks/hooks/anchor.js @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import { assign, get } from 'lodash'; + +/** + * WordPress dependencies + */ +import { cloneElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { source } from '../api'; +import InspectorControls from '../inspector-controls'; + +/** + * Regular expression matching invalid anchor characters for replacement. + * + * @type {RegExp} + */ +const ANCHOR_REGEX = /[\s#]/g; + +export default function anchor( settings ) { + if ( ! get( settings.supports, 'anchor' ) ) { + return settings; + } + + // 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' ), + }, + } ); + + // 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 [ + , + props.focus && ( + + { + nextValue = nextValue.replace( ANCHOR_REGEX, '-' ); + + props.setAttributes( { + anchor: nextValue, + } ); + } } /> + + ), + ]; + }; + + // 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; + + let result = save( ...arguments ); + if ( 'string' !== typeof result && id ) { + result = cloneElement( result, { id } ); + } + + return result; + }; + + return settings; +} diff --git a/blocks/hooks/index.js b/blocks/hooks/index.js new file mode 100644 index 0000000000000..01d81b80af69e --- /dev/null +++ b/blocks/hooks/index.js @@ -0,0 +1,45 @@ +/** + * WordPress dependencies + */ +import createHooks from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import anchor from './anchor'; + +const { + addAction, + addFilter, + removeAction, + removeFilter, + removeAllActions, + removeAllFilters, + doAction, + applyFilters, + doingAction, + doingFilter, + didAction, + didFilter, + hasAction, + hasFilter, +} = createHooks(); + +export { + addAction, + addFilter, + removeAction, + removeFilter, + removeAllActions, + removeAllFilters, + doAction, + applyFilters, + doingAction, + doingFilter, + didAction, + didFilter, + hasAction, + hasFilter, +}; + +addFilter( 'registerBlockType', 'core\supports-anchor', anchor ); diff --git a/blocks/index.js b/blocks/index.js index faebbaa6c03fb..713067cc06181 100644 --- a/blocks/index.js +++ b/blocks/index.js @@ -13,6 +13,7 @@ import './library'; // Blocks are inferred from the HTML source of a post through a parsing mechanism // and then stored as objects in state, from which it is then rendered for editing. export * from './api'; +export * from './hooks'; export { default as AlignmentToolbar } from './alignment-toolbar'; export { default as BlockAlignmentToolbar } from './block-alignment-toolbar'; export { default as BlockControls } from './block-controls'; diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index 5e4f01dc44d5a..d61c2024565bc 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -29,7 +29,9 @@ registerBlockType( 'core/heading', { className: false, - supportAnchor: true, + supports: { + anchor: true, + }, attributes: { content: { diff --git a/docs/block-api.md b/docs/block-api.md index fe66d6dcec01a..1e230e9e3f4bd 100644 --- a/docs/block-api.md +++ b/docs/block-api.md @@ -119,16 +119,17 @@ Whether a block can only be used once per post. useOnce: true, ``` -#### supportAnchor (optional) +#### supports (optional) -* **Type:** `Bool` -* **Default:** `false` +* **Type:** `Object` + +Optional block extended support features. The following options are supported, and should be specified as a boolean `true` or `false` value: -Anchors let you link directly to a specific block on a page. This property adds a field to define an id for the block and a button to copy the direct link. +- `anchor` (default `false`): Anchors let you link directly to a specific block on a page. This property adds a field to define an id for the block and a button to copy the direct link. ```js // Add the support for an anchor link. -supportAnchor: true, +anchor: true, ``` #### supportHTML (optional) diff --git a/editor/components/block-inspector/advanced-controls.js b/editor/components/block-inspector/advanced-controls.js index 6df4d92c4da4a..70f4df8b41871 100644 --- a/editor/components/block-inspector/advanced-controls.js +++ b/editor/components/block-inspector/advanced-controls.js @@ -9,29 +9,19 @@ import { connect } from 'react-redux'; import { Component } from '@wordpress/element'; import { getBlockType, InspectorControls } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; -import { ClipboardButton, Tooltip, PanelBody } from '@wordpress/components'; +import { PanelBody } from '@wordpress/components'; /** * Internal Dependencies */ import { updateBlockAttributes } from '../../actions'; import { getSelectedBlock, getCurrentPost } from '../../selectors'; -import { filterURLForDisplay } from '../../utils/url'; - -/** - * Internal constants - */ -const ANCHOR_REGEX = /[\s#]/g; class BlockInspectorAdvancedControls extends Component { constructor() { super( ...arguments ); - this.state = { - showCopyConfirmation: false, - }; - this.onCopy = this.onCopy.bind( this ); + this.setClassName = this.setClassName.bind( this ); - this.setAnchor = this.setAnchor.bind( this ); } setClassName( className ) { @@ -39,32 +29,10 @@ class BlockInspectorAdvancedControls extends Component { setAttributes( selectedBlock.uid, { className } ); } - setAnchor( anchor ) { - const { selectedBlock, setAttributes } = this.props; - setAttributes( selectedBlock.uid, { anchor: anchor.replace( ANCHOR_REGEX, '-' ) } ); - } - - componentWillUnmout() { - clearTimeout( this.dismissCopyConfirmation ); - } - - onCopy() { - this.setState( { - showCopyConfirmation: true, - } ); - - clearTimeout( this.dismissCopyConfirmation ); - this.dismissCopyConfirmation = setTimeout( () => { - this.setState( { - showCopyConfirmation: false, - } ); - }, 4000 ); - } - render() { - const { selectedBlock, post } = this.props; + const { selectedBlock } = this.props; const blockType = getBlockType( selectedBlock.name ); - if ( false === blockType.className && ! blockType.supportAnchor ) { + if ( false === blockType.className ) { return null; } @@ -80,24 +48,6 @@ class BlockInspectorAdvancedControls extends Component { value={ selectedBlock.attributes.className || '' } onChange={ this.setClassName } /> } - { blockType.supportAnchor && -
- - { !! post.link && !! selectedBlock.attributes.anchor && -
- - -
{ this.state.showCopyConfirmation ? __( 'Copied!' ) : __( 'Copy Link' ) }
-
-
-
- } -
- } ); } diff --git a/package-lock.json b/package-lock.json index bcf8a6f01fc1b..890bbe5dfcebe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,6 +78,11 @@ "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-0.1.0-beta.1.tgz", "integrity": "sha512-PatjEn3GbOVLQPG3K0p7aLR6A0ggCfWa2AUK+T9t6J1oPLa3d8Kt39h/zkfd7hyn9FV7DItMk4ud8VmDthHcvA==" }, + "@wordpress/hooks": { + "version": "0.1.0-beta.5", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-0.1.0-beta.5.tgz", + "integrity": "sha512-RlNjbZRx249jZVeXKCwbvEb7Fk6UdZmB0OxBYlRVVW7ANWzgWo+I/fIoIht5gilg0iIF/XlTyXLTAEqHMUFGKw==" + }, "@wordpress/url": { "version": "0.1.0-beta.1", "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-0.1.0-beta.1.tgz", @@ -1367,7 +1372,7 @@ "base64-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", - "integrity": "sha1-qRlH2h9KUW6jjltOwOw3c2deCIY=", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", "dev": true }, "bcrypt-pbkdf": { @@ -1409,7 +1414,7 @@ "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha1-LN4J617jQfSEdGuwMJsyU7GxRC8=", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", "dev": true }, "boolbase": { @@ -1738,7 +1743,7 @@ "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", "dev": true, "requires": { "inherits": "2.0.3", @@ -2205,7 +2210,7 @@ "crypto-browserify": { "version": "3.11.1", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.1.tgz", - "integrity": "sha1-lIlF78Z1ekANbl5a9HGU0QBkJ58=", + "integrity": "sha512-Na7ZlwCOqoaW5RwUK1WpXws2kv8mNhWdTlzob0UXulk6G9BDbyiJaGTYBIX61Ozn9l1EPPJpICZb4DaOpT9NlQ==", "dev": true, "requires": { "browserify-cipher": "1.0.0", @@ -4791,7 +4796,7 @@ "hash.js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha1-NA3tvmKQGHFRweodd3o0SJNd+EY=", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", "dev": true, "requires": { "inherits": "2.0.3", @@ -6617,7 +6622,7 @@ "json-loader": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha1-3KFKcCNf+C8KyaOr62DTN6NlGF0=", + "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", "dev": true }, "json-schema": { @@ -8349,7 +8354,7 @@ "randombytes": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", - "integrity": "sha1-3ACaJGuNCaF3tLegrne8Vw9LG3k=", + "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", "dev": true, "requires": { "safe-buffer": "5.1.1" @@ -9431,7 +9436,7 @@ "stream-http": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", - "integrity": "sha1-QKBQ7I3DtTsz2ZCUFcAsC/Gr+60=", + "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", "dev": true, "requires": { "builtin-status-codes": "3.0.0", @@ -9737,7 +9742,7 @@ "timers-browserify": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.4.tgz", - "integrity": "sha1-lspT9LeUpefA4b18yIo3Ipj6AeY=", + "integrity": "sha512-uZYhyU3EX8O7HQP+J9fTVYwsq90Vr68xPEFo7yrVImIxYvHgukBEgOB/SgGoorWVTzGM/3Z+wUNnboA4M8jWrg==", "dev": true, "requires": { "setimmediate": "1.0.5" diff --git a/package.json b/package.json index d9a050e1aa86f..4f4f08d532553 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ ], "dependencies": { "@wordpress/a11y": "0.1.0-beta.1", + "@wordpress/hooks": "0.1.0-beta.5", "@wordpress/url": "0.1.0-beta.1", "classnames": "2.2.5", "clipboard": "1.7.1", From e5bdcd178485321536129c80a363e9e5dccce27d Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 8 Nov 2017 09:37:23 -0500 Subject: [PATCH 2/3] Blocks: Extract common behaviors for block edit rendering --- blocks/block-edit/index.js | 25 +++++++++++++ blocks/block-edit/test/index.js | 56 +++++++++++++++++++++++++++++ blocks/index.js | 1 + editor/modes/visual-editor/block.js | 18 ++-------- 4 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 blocks/block-edit/index.js create mode 100644 blocks/block-edit/test/index.js diff --git a/blocks/block-edit/index.js b/blocks/block-edit/index.js new file mode 100644 index 0000000000000..c07252ed08abe --- /dev/null +++ b/blocks/block-edit/index.js @@ -0,0 +1,25 @@ +/** + * Internal dependencies + */ +import { getBlockType } from '../api'; + +function BlockEdit( props ) { + const { name, ...editProps } = props; + const blockType = getBlockType( name ); + + if ( ! blockType ) { + return null; + } + + // `edit` and `save` are functions or components describing the markup + // with which a block is displayed. If `blockType` is valid, assign + // them preferencially as the render value for the block. + let Edit; + if ( blockType ) { + Edit = blockType.edit || blockType.save; + } + + return ; +} + +export default BlockEdit; diff --git a/blocks/block-edit/test/index.js b/blocks/block-edit/test/index.js new file mode 100644 index 0000000000000..c24648e6f4795 --- /dev/null +++ b/blocks/block-edit/test/index.js @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; +import { noop } from 'lodash'; + +/** + * Internal dependencies + */ +import BlockEdit from '../'; +import { + registerBlockType, + unregisterBlockType, + getBlockTypes, +} from '../../api'; + +describe( 'BlockEdit', () => { + afterEach( () => { + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); + } ); + + it( 'should return null if block type not defined', () => { + const wrapper = shallow( ); + + expect( wrapper.type() ).toBe( null ); + } ); + + it( 'should use edit implementation of block', () => { + const edit = () =>
; + registerBlockType( 'core/test-block', { + save: noop, + category: 'common', + title: 'block title', + edit, + } ); + + const wrapper = shallow( ); + + expect( wrapper.type() ).toBe( edit ); + } ); + + it( 'should use save implementation of block as fallback', () => { + const save = () =>
; + registerBlockType( 'core/test-block', { + save, + category: 'common', + title: 'block title', + } ); + + const wrapper = shallow( ); + + expect( wrapper.type() ).toBe( save ); + } ); +} ); diff --git a/blocks/index.js b/blocks/index.js index 713067cc06181..ef56387023922 100644 --- a/blocks/index.js +++ b/blocks/index.js @@ -18,6 +18,7 @@ export { default as AlignmentToolbar } from './alignment-toolbar'; export { default as BlockAlignmentToolbar } from './block-alignment-toolbar'; export { default as BlockControls } from './block-controls'; export { default as BlockDescription } from './block-description'; +export { default as BlockEdit } from './block-edit'; export { default as BlockIcon } from './block-icon'; export { default as ColorPalette } from './color-palette'; export { default as Editable } from './editable'; diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index 530810bbe9c69..02cb53d21b423 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -10,7 +10,7 @@ import { has, partial, reduce, size } from 'lodash'; */ import { Component, createElement } from '@wordpress/element'; import { keycodes } from '@wordpress/utils'; -import { getBlockType, getBlockDefaultClassname, createBlock } from '@wordpress/blocks'; +import { getBlockType, BlockEdit, getBlockDefaultClassname, createBlock } from '@wordpress/blocks'; import { __, sprintf } from '@wordpress/i18n'; /** @@ -318,20 +318,7 @@ class VisualEditorBlock extends Component { // translators: %s: Type of block (i.e. Text, Image etc) const blockLabel = sprintf( __( 'Block: %s' ), blockType.title ); // The block as rendered in the editor is composed of general block UI - // (mover, toolbar, wrapper) and the display of the block content, which - // is referred to as . - let BlockEdit; - // `edit` and `save` are functions or components describing the markup - // with which a block is displayed. If `blockType` is valid, assign - // them preferencially as the render value for the block. - if ( blockType ) { - BlockEdit = blockType.edit || blockType.save; - } - - // Should `BlockEdit` return as null, we have nothing to display for the block. - if ( ! BlockEdit ) { - return null; - } + // (mover, toolbar, wrapper) and the display of the block content. // Generate the wrapper class names handling the different states of the block. const { isHovered, isSelected, isMultiSelected, isFirstMultiSelected, focus } = this.props; @@ -389,6 +376,7 @@ class VisualEditorBlock extends Component { { isValid && mode === 'visual' && ( Date: Wed, 8 Nov 2017 10:05:16 -0500 Subject: [PATCH 3/3] Block API: Define granular hooks for block filtering --- blocks/api/index.js | 1 + blocks/api/registration.js | 20 ++++++ blocks/api/serializer.js | 3 +- blocks/api/test/registration.js | 64 ++++++++++++++++++ blocks/block-edit/index.js | 3 +- blocks/hooks/anchor.js | 113 +++++++++++++++++++------------- blocks/hooks/index.js | 6 +- blocks/hooks/test/anchor.js | 79 ++++++++++++++++++++++ 8 files changed, 239 insertions(+), 50 deletions(-) create mode 100644 blocks/hooks/test/anchor.js diff --git a/blocks/api/index.js b/blocks/api/index.js index 30a3dadb11f0a..4cb6c022af85d 100644 --- a/blocks/api/index.js +++ b/blocks/api/index.js @@ -19,4 +19,5 @@ export { getDefaultBlockName, getBlockType, getBlockTypes, + hasBlockSupport, } from './registration'; diff --git a/blocks/api/registration.js b/blocks/api/registration.js index e216fdc9de315..b220d9778f931 100644 --- a/blocks/api/registration.js +++ b/blocks/api/registration.js @@ -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 ); +} diff --git a/blocks/api/serializer.js b/blocks/api/serializer.js index de67f4203e0a4..4c88e5275fb11 100644 --- a/blocks/api/serializer.js +++ b/blocks/api/serializer.js @@ -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 @@ -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, diff --git a/blocks/api/test/registration.js b/blocks/api/test/registration.js index 82ecf564af60e..4411d93276e60 100644 --- a/blocks/api/test/registration.js +++ b/blocks/api/test/registration.js @@ -17,6 +17,7 @@ import { getDefaultBlockName, getBlockType, getBlockTypes, + hasBlockSupport, } from '../registration'; describe( 'blocks', () => { @@ -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 ); + } ); + } ); } ); diff --git a/blocks/block-edit/index.js b/blocks/block-edit/index.js index c07252ed08abe..ee1cb389cb47b 100644 --- a/blocks/block-edit/index.js +++ b/blocks/block-edit/index.js @@ -2,6 +2,7 @@ * Internal dependencies */ import { getBlockType } from '../api'; +import { applyFilters } from '../hooks'; function BlockEdit( props ) { const { name, ...editProps } = props; @@ -19,7 +20,7 @@ function BlockEdit( props ) { Edit = blockType.edit || blockType.save; } - return ; + return applyFilters( 'BlockEdit', , props ); } export default BlockEdit; diff --git a/blocks/hooks/anchor.js b/blocks/hooks/anchor.js index 0423b6d5c0f79..00274520932e8 100644 --- a/blocks/hooks/anchor.js +++ b/blocks/hooks/anchor.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { assign, get } from 'lodash'; +import { assign } from 'lodash'; /** * WordPress dependencies @@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { source } from '../api'; +import { source, hasBlockSupport } from '../api'; import InspectorControls from '../inspector-controls'; /** @@ -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 [ - , - props.focus && ( - - { - 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' } ), + + { + nextValue = nextValue.replace( ANCHOR_REGEX, '-' ); - props.setAttributes( { - anchor: nextValue, - } ); - } } /> - - ), + props.setAttributes( { + anchor: nextValue, + } ); + } } /> + , ]; - }; + } - // 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 ); } diff --git a/blocks/hooks/index.js b/blocks/hooks/index.js index 01d81b80af69e..1a61cfddb51f1 100644 --- a/blocks/hooks/index.js +++ b/blocks/hooks/index.js @@ -8,6 +8,8 @@ import createHooks from '@wordpress/hooks'; */ import anchor from './anchor'; +const hooks = createHooks(); + const { addAction, addFilter, @@ -23,7 +25,7 @@ const { didFilter, hasAction, hasFilter, -} = createHooks(); +} = hooks; export { addAction, @@ -42,4 +44,4 @@ export { hasFilter, }; -addFilter( 'registerBlockType', 'core\supports-anchor', anchor ); +anchor( hooks ); diff --git a/blocks/hooks/test/anchor.js b/blocks/hooks/test/anchor.js new file mode 100644 index 0000000000000..1fcecf44d6a0d --- /dev/null +++ b/blocks/hooks/test/anchor.js @@ -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' ); + } ); + } ); +} );